├── .gitmodules ├── src ├── __init__.py ├── fuzzer │ ├── __init__.py │ ├── xfail.py │ ├── runner │ │ ├── __init__.py │ │ ├── multi_runner.py │ │ ├── scenario.py │ │ ├── boa_scenario_runner.py │ │ └── ivy_scenario_runner.py │ ├── mutator │ │ ├── __init__.py │ │ ├── mutations │ │ │ ├── __init__.py │ │ │ ├── module.py │ │ │ ├── base.py │ │ │ ├── for_loop.py │ │ │ ├── function_def.py │ │ │ ├── binop.py │ │ │ ├── subscript.py │ │ │ ├── unaryop.py │ │ │ ├── boolop.py │ │ │ ├── compare.py │ │ │ ├── assign.py │ │ │ ├── if_stmt.py │ │ │ └── int_literal.py │ │ ├── mutation_engine.py │ │ ├── interface_registry.py │ │ ├── base_value_generator.py │ │ ├── literal_generator.py │ │ └── argument_mutator.py │ ├── corpus.py │ ├── trace_types.py │ └── replay_divergence.py ├── ivy │ ├── evm │ │ ├── __init__.py │ │ ├── precompiles.py │ │ ├── evm_callbacks.py │ │ └── evm_structures.py │ ├── builtins │ │ ├── __init__.py │ │ ├── unsafe_math_utils.py │ │ ├── create_utils.py │ │ ├── convert_utils.py │ │ └── builtin_registry.py │ ├── expr │ │ ├── __init__.py │ │ ├── default_values.py │ │ └── clamper.py │ ├── frontend │ │ ├── __init__.py │ │ ├── decoder_utils.py │ │ └── event.py │ ├── constants.py │ ├── __init__.py │ ├── __main__.py │ ├── abi │ │ └── __init__.py │ ├── exceptions.py │ ├── visitor.py │ ├── utils.py │ ├── allocator.py │ ├── variable.py │ ├── context.py │ └── stmt.py └── unparser │ ├── __init__.py │ └── README.md ├── tests ├── __init__.py ├── ivy │ ├── __init__.py │ ├── compiler │ │ ├── __init__.py │ │ └── functional │ │ │ ├── __init__.py │ │ │ ├── builtins │ │ │ ├── __init__.py │ │ │ └── codegen │ │ │ │ ├── __init__.py │ │ │ │ ├── test_unsafe_math.py │ │ │ │ └── test_keccak256.py │ │ │ └── codegen │ │ │ ├── __init__.py │ │ │ ├── builtins │ │ │ ├── __init__.py │ │ │ ├── test_minmax_value.py │ │ │ └── test_minmax.py │ │ │ ├── features │ │ │ ├── __init__.py │ │ │ ├── iteration │ │ │ │ ├── __init__.py │ │ │ │ ├── test_continue.py │ │ │ │ ├── test_break.py │ │ │ │ └── test_range_in.py │ │ │ ├── decorators │ │ │ │ ├── __init__.py │ │ │ │ ├── test_view.py │ │ │ │ └── test_pure.py │ │ │ ├── test_address_balance.py │ │ │ ├── test_string_map_keys.py │ │ │ ├── test_conditionals.py │ │ │ ├── test_reverting.py │ │ │ ├── test_comparison.py │ │ │ ├── test_init.py │ │ │ └── test_short_circuiting.py │ │ │ ├── modules │ │ │ ├── __init__.py │ │ │ ├── test_flag_imports.py │ │ │ ├── test_events.py │ │ │ ├── test_nonreentrant.py │ │ │ ├── test_module_constants.py │ │ │ └── test_interface_imports.py │ │ │ ├── types │ │ │ ├── __init__.py │ │ │ ├── numberz │ │ │ │ ├── __init__.py │ │ │ │ ├── test_modulo.py │ │ │ │ ├── test_isqrt.py │ │ │ │ └── test_exponents.py │ │ │ ├── test_bytes_zero_padding.py │ │ │ ├── test_string_literal.py │ │ │ ├── test_struct.py │ │ │ ├── test_bytes_literal.py │ │ │ └── test_array_indexing.py │ │ │ ├── calling_convention │ │ │ ├── __init__.py │ │ │ ├── test_inleable_functions.py │ │ │ └── test_self_call_struct.py │ │ │ └── storage_variables │ │ │ ├── __init__.py │ │ │ ├── test_storage_variable.py │ │ │ └── test_getters.py │ ├── examples │ │ ├── __init__.py │ │ └── ERC20.py │ ├── example_contracts │ │ ├── example.vy │ │ └── for_loop_example.vy │ └── utils.py ├── unparser │ ├── __init__.py │ └── test_unparser.py └── conftest.py ├── .pre-commit-config.yaml ├── quicktest.sh ├── .env.example ├── pytest.ini ├── .gitignore ├── pyproject.toml ├── setup_dev.sh ├── CLAUDE.md ├── ruff.toml └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fuzzer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ivy/evm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ivy/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ivy/expr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ivy/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/unparser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unparser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/builtins/codegen/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ivy/constants.py: -------------------------------------------------------------------------------- 1 | REENTRANT_KEY = "$.nonreentrant_key" 2 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/numberz/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/calling_convention/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/iteration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/storage_variables/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ivy/example_contracts/example.vy: -------------------------------------------------------------------------------- 1 | struct S: 2 | a: uint256 3 | b: uint256 4 | 5 | @external 6 | def foo() -> uint256: 7 | s: S = S(a=1, b=2) 8 | return s.a -------------------------------------------------------------------------------- /src/unparser/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Unparser 3 | - take Vyper AST and unparse it to original source code 4 | - works on abstract syntax, and as such doesn't support constructs like comments -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.6.4 5 | hooks: 6 | # Run the formatter. 7 | - id: ruff-format 8 | -------------------------------------------------------------------------------- /src/ivy/evm/precompiles.py: -------------------------------------------------------------------------------- 1 | from ivy.types import Address 2 | 3 | 4 | def precompile_identity(data: bytes) -> bytes: 5 | return data 6 | 7 | 8 | PRECOMPILE_REGISTRY = { 9 | Address(0x4): precompile_identity, 10 | } 11 | -------------------------------------------------------------------------------- /src/ivy/__init__.py: -------------------------------------------------------------------------------- 1 | from ivy.frontend.env import Env 2 | 3 | 4 | env = Env.get_singleton() 5 | 6 | 7 | def set_interpreter(interpreter): 8 | env.interpreter = interpreter 9 | 10 | 11 | def set_evm(evm): 12 | env.evm = evm 13 | -------------------------------------------------------------------------------- /quicktest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # examples: 4 | # ./quicktest.sh 5 | # ./quicktest.sh tests/.../mytest.py 6 | 7 | # run pytest but bail out on first error 8 | # useful for dev workflow 9 | pytest -q -s --instafail -x --disable-warnings "$@" 10 | -------------------------------------------------------------------------------- /src/ivy/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import ivy.frontend.loader as itp 4 | 5 | 6 | def main(): 7 | out = itp.load(sys.argv[1]) 8 | # print(out.compiler_data.compilation_target) 9 | print(out.foo()) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /src/ivy/abi/__init__.py: -------------------------------------------------------------------------------- 1 | from .abi_encoder import abi_encode as abi_encode 2 | from .abi_decoder import abi_decode as abi_decode 3 | from .abi_decoder import DecodeError as DecodeError 4 | from .abi_decoder import DecodedValue as DecodedValue 5 | from .abi_encoder import EncodeError as EncodeError 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Example environment configuration for Ivy development 2 | 3 | # Python path configuration (usually not needed if using pip install -e) 4 | # PYTHONPATH=src 5 | 6 | # Vyper test exports directory (if using a custom location) 7 | # VYPER_EXPORTS_DIR=tests/vyper-exports 8 | 9 | # Enable debug logging 10 | # IVY_DEBUG=1 11 | -------------------------------------------------------------------------------- /src/fuzzer/xfail.py: -------------------------------------------------------------------------------- 1 | """Shared types for expected failure annotations.""" 2 | 3 | from dataclasses import dataclass 4 | from typing import Optional 5 | 6 | 7 | @dataclass 8 | class XFailExpectation: 9 | """Records that a failure of ``kind`` is expected, with optional context.""" 10 | 11 | kind: str 12 | reason: Optional[str] = None 13 | -------------------------------------------------------------------------------- /src/fuzzer/runner/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_scenario_runner import BaseScenarioRunner 2 | from .ivy_scenario_runner import IvyScenarioRunner 3 | from .boa_scenario_runner import BoaScenarioRunner 4 | from .scenario import Scenario, create_scenario_from_item 5 | 6 | __all__ = [ 7 | "BaseScenarioRunner", 8 | "IvyScenarioRunner", 9 | "BoaScenarioRunner", 10 | "Scenario", 11 | "create_scenario_from_item", 12 | ] 13 | -------------------------------------------------------------------------------- /tests/ivy/example_contracts/for_loop_example.vy: -------------------------------------------------------------------------------- 1 | 2 | @internal 3 | def bar(c: uint256) -> uint256: 4 | a: DynArray[uint256, 10] = [1, 2, 3] 5 | counter: uint256 = 0 6 | for i: uint256 in a: 7 | counter += i 8 | return counter + c 9 | 10 | 11 | @external 12 | def foo() -> uint256: 13 | a: DynArray[uint256, 10] = [1, 2, 3] 14 | counter: uint256 = 0 15 | for i: uint256 in a: 16 | counter += i 17 | return counter + self.bar(1) -------------------------------------------------------------------------------- /src/fuzzer/mutator/__init__.py: -------------------------------------------------------------------------------- 1 | from .ast_mutator import AstMutator 2 | from .value_mutator import ValueMutator 3 | from .literal_generator import LiteralGenerator 4 | from .base_value_generator import BaseValueGenerator 5 | from .argument_mutator import ArgumentMutator 6 | from .trace_mutator import TraceMutator 7 | 8 | __all__ = [ 9 | "AstMutator", 10 | "ValueMutator", 11 | "LiteralGenerator", 12 | "BaseValueGenerator", 13 | "ArgumentMutator", 14 | "TraceMutator", 15 | ] 16 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | 3 | python_files = test_*.py 4 | testpaths = tests 5 | pythonpath = src 6 | filterwarnings = 7 | ignore::vyper.warnings.VyperWarning 8 | xfail_strict = true 9 | markers = 10 | fuzzing: Run Hypothesis fuzz test suite (deselect with '-m "not fuzzing"') 11 | requires_evm_version(version): Mark tests that require at least a specific EVM version and would throw `EvmVersionException` otherwise 12 | venom_xfail: mark a test case as a regression (expected to fail) under the venom pipeline -------------------------------------------------------------------------------- /src/ivy/evm/evm_callbacks.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | from vyper.semantics.types.module import ModuleT 5 | from vyper.semantics.types.function import ContractFunctionT 6 | 7 | 8 | class EVMCallbacks(ABC): 9 | @abstractmethod 10 | def allocate_variables(self, module_t: ModuleT) -> None: 11 | pass 12 | 13 | @abstractmethod 14 | def execute_init_function(self, func_t: ContractFunctionT) -> Any: 15 | pass 16 | 17 | @abstractmethod 18 | def dispatch(self): 19 | pass 20 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/decorators/test_view.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_constant_test(get_contract): 5 | constant_test = """ 6 | @external 7 | @view 8 | def foo() -> int128: 9 | return 5 10 | """ 11 | 12 | c = get_contract(constant_test) 13 | assert c.foo() == 5 14 | 15 | 16 | def test_transient_test(get_contract): 17 | code = """ 18 | x: transient(uint256) 19 | 20 | @external 21 | @view 22 | def foo() -> uint256: 23 | return self.x 24 | """ 25 | c = get_contract(code) 26 | assert c.foo() == 0 27 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/test_address_balance.py: -------------------------------------------------------------------------------- 1 | def test_constant_address_balance(env, get_contract): 2 | code = """ 3 | a: constant(address) = 0x776Ba14735FF84789320718cf0aa43e91F7A8Ce1 4 | 5 | @external 6 | def foo() -> uint256: 7 | x: uint256 = a.balance 8 | return x 9 | """ 10 | address = "0x776Ba14735FF84789320718cf0aa43e91F7A8Ce1" 11 | d_a = env.deployer 12 | 13 | c = get_contract(code) 14 | 15 | assert c.foo() == 0 16 | 17 | env.set_balance(env.deployer, 1337) 18 | env.message_call(address, value=1337) 19 | 20 | assert c.foo() == 1337 21 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/builtins/test_minmax_value.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from vyper.semantics.types import DecimalT, IntegerT 4 | 5 | 6 | @pytest.mark.parametrize("typ", sorted(IntegerT.all() + (DecimalT(),))) 7 | @pytest.mark.parametrize("op", ("min_value", "max_value")) 8 | def test_minmax_value(get_contract, op, typ): 9 | code = f""" 10 | @external 11 | def foo() -> {typ}: 12 | return {op}({typ}) 13 | """ 14 | c = get_contract(code) 15 | 16 | lo, hi = typ.int_bounds 17 | if op == "min_value": 18 | assert c.foo() == lo 19 | elif op == "max_value": 20 | assert c.foo() == hi 21 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/test_string_map_keys.py: -------------------------------------------------------------------------------- 1 | def test_string_map_keys(get_contract): 2 | code = """ 3 | f:HashMap[String[1], bool] 4 | @external 5 | def test() -> bool: 6 | a:String[1] = "a" 7 | b:String[1] = "b" 8 | self.f[a] = True 9 | return self.f[b] # should return False 10 | """ 11 | c = get_contract(code) 12 | c.test() 13 | assert c.test() is False 14 | 15 | 16 | def test_string_map_keys_literals(get_contract): 17 | code = """ 18 | f:HashMap[String[1], bool] 19 | @external 20 | def test() -> bool: 21 | self.f["a"] = True 22 | return self.f["b"] # should return False 23 | """ 24 | c = get_contract(code) 25 | assert c.test() is False 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | env/ 9 | venv/ 10 | ENV/ 11 | env.bak/ 12 | venv.bak/ 13 | *.egg-info/ 14 | dist/ 15 | build/ 16 | *.egg 17 | 18 | # Virtualenv 19 | .venv/ 20 | .uv/ 21 | .uvtemp/ 22 | 23 | # Logs 24 | *.log 25 | 26 | # Unit test / coverage reports 27 | htmlcov/ 28 | .tox/ 29 | .nox/ 30 | coverage.xml 31 | *.cover 32 | .hypothesis/ 33 | .cache/ 34 | nosetests.xml 35 | pytest_cache/ 36 | *.coveragerc 37 | 38 | # Jupyter Notebook checkpoints 39 | .ipynb_checkpoints 40 | 41 | # System files 42 | .DS_Store 43 | Thumbs.db 44 | 45 | # PyCharm files 46 | ivy/.idea/*.xml 47 | .idea/ 48 | 49 | tests/vyper-exports 50 | 51 | reports/ 52 | -------------------------------------------------------------------------------- /tests/ivy/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import decimal 3 | import os 4 | 5 | from vyper import ast as vy_ast 6 | from vyper.semantics.analysis.constant_folding import constant_fold 7 | from vyper.utils import DECIMAL_EPSILON, round_towards_zero 8 | 9 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 10 | 11 | 12 | @contextlib.contextmanager 13 | def working_directory(directory): 14 | tmp = os.getcwd() 15 | try: 16 | os.chdir(directory) 17 | yield 18 | finally: 19 | os.chdir(tmp) 20 | 21 | 22 | def parse_and_fold(source_code): 23 | ast = vy_ast.parse_to_ast(source_code) 24 | constant_fold(ast) 25 | return ast 26 | 27 | 28 | def decimal_to_int(*args): 29 | s = decimal.Decimal(*args) 30 | return round_towards_zero(s / DECIMAL_EPSILON) 31 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from ..strategy import StrategyRegistry 4 | from . import ( 5 | module, 6 | int_literal, 7 | function_def, 8 | binop, 9 | if_stmt, 10 | assign, 11 | compare, 12 | unaryop, 13 | boolop, 14 | subscript, 15 | for_loop, 16 | ) 17 | 18 | 19 | def register_all(registry: StrategyRegistry) -> None: 20 | module.register(registry) 21 | int_literal.register(registry) 22 | function_def.register(registry) 23 | binop.register(registry) 24 | if_stmt.register(registry) 25 | assign.register(registry) 26 | compare.register(registry) 27 | unaryop.register(registry) 28 | boolop.register(registry) 29 | subscript.register(registry) 30 | for_loop.register(registry) 31 | -------------------------------------------------------------------------------- /src/ivy/builtins/unsafe_math_utils.py: -------------------------------------------------------------------------------- 1 | from vyper.semantics.types import IntegerT 2 | 3 | 4 | # EVM div semantics as a python function 5 | def evm_div(x, y): 6 | if y == 0: 7 | return 0 8 | sign = -1 if (x * y) < 0 else 1 9 | return sign * (abs(x) // abs(y)) # adapted from py-evm 10 | 11 | 12 | def validate_typs(typs): 13 | assert len(typs) == 2 14 | # TODO is this the right way to compare? 15 | assert typs[0] == typs[1] 16 | typ = typs[0] 17 | assert isinstance(typ, IntegerT) 18 | return typ.bits, typ.is_signed 19 | 20 | 21 | def wrap_value(value, bits, signed): 22 | mod = 2**bits 23 | result = value % mod # Wrap to [0, 2^bits - 1] 24 | if signed and result > 2 ** (bits - 1) - 1: 25 | result -= mod # Adjust to [-2^(bits-1), 2^(bits-1) - 1] 26 | return result 27 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/module.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..strategy import Strategy, StrategyRegistry 6 | from .base import MutationCtx 7 | 8 | 9 | def register(registry: StrategyRegistry) -> None: 10 | registry.register( 11 | Strategy( 12 | name="module.inject_statement", 13 | type_classes=(ast.Module,), 14 | tags=frozenset({"mutation"}), 15 | is_applicable=_can_inject, 16 | weight=lambda **_: 1.0, 17 | run=_inject_statement, 18 | ) 19 | ) 20 | 21 | 22 | def _can_inject(*, ctx: MutationCtx, **_) -> bool: 23 | return len(ctx.node.body) < 30 24 | 25 | 26 | def _inject_statement(*, ctx: MutationCtx, **_) -> ast.Module: 27 | ctx.stmt_gen.inject_statements( 28 | ctx.node.body, ctx.context, ctx.node, depth=0, n_stmts=1 29 | ) 30 | return ctx.node 31 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING, Optional 5 | import random 6 | 7 | from vyper.ast import nodes as ast 8 | from vyper.semantics.types import VyperType 9 | 10 | from ..context import Context 11 | from ..expr_generator import ExprGenerator 12 | from ..stmt_generator import StatementGenerator 13 | from ..function_registry import FunctionRegistry 14 | 15 | if TYPE_CHECKING: 16 | from ..value_mutator import ValueMutator 17 | 18 | 19 | @dataclass 20 | class MutationCtx: 21 | node: ast.VyperNode 22 | rng: random.Random 23 | context: Context 24 | expr_gen: ExprGenerator 25 | stmt_gen: StatementGenerator 26 | function_registry: FunctionRegistry 27 | value_mutator: ValueMutator 28 | inferred_type: Optional[VyperType] = None 29 | parent: Optional[ast.VyperNode] = None 30 | depth: int = 0 31 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/storage_variables/test_storage_variable.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from vyper.exceptions import UndeclaredDefinition 4 | 5 | 6 | def test_permanent_variables_test(get_contract): 7 | permanent_variables_test = """ 8 | struct Var: 9 | a: int128 10 | b: int128 11 | var: Var 12 | 13 | @deploy 14 | def __init__(a: int128, b: int128): 15 | self.var.a = a 16 | self.var.b = b 17 | 18 | @external 19 | def returnMoose() -> int128: 20 | return self.var.a * 10 + self.var.b 21 | """ 22 | 23 | c = get_contract(permanent_variables_test, *[5, 7]) 24 | assert c.returnMoose() == 57 25 | print("Passed init argument and variable member test") 26 | 27 | 28 | def test_missing_global(get_contract): 29 | code = """ 30 | @external 31 | def a() -> int128: 32 | return self.b 33 | """ 34 | 35 | with raises(UndeclaredDefinition): 36 | get_contract(code) 37 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/for_loop.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..strategy import Strategy, StrategyRegistry 6 | from .base import MutationCtx 7 | 8 | 9 | def register(registry: StrategyRegistry) -> None: 10 | registry.register( 11 | Strategy( 12 | name="for.inject_statement", 13 | type_classes=(ast.For,), 14 | tags=frozenset({"mutation", "for"}), 15 | is_applicable=lambda **_: True, 16 | weight=lambda **_: 1.0, 17 | run=_inject_statement, 18 | ) 19 | ) 20 | 21 | 22 | def _inject_statement(*, ctx: MutationCtx, **_) -> ast.For: 23 | """Inject a statement into the for loop body.""" 24 | # Scope is already created by the visitor before mutation 25 | ctx.stmt_gen.inject_statements( 26 | ctx.node.body, ctx.context, ctx.node, depth=1, n_stmts=1 27 | ) 28 | return ctx.node 29 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/function_def.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..strategy import Strategy, StrategyRegistry 6 | from .base import MutationCtx 7 | 8 | 9 | def register(registry: StrategyRegistry) -> None: 10 | registry.register( 11 | Strategy( 12 | name="function.inject_statement", 13 | type_classes=(ast.FunctionDef,), 14 | tags=frozenset({"mutation", "function"}), 15 | is_applicable=_has_body, 16 | weight=lambda **_: 1.0, 17 | run=_inject_statement, 18 | ) 19 | ) 20 | 21 | 22 | def _has_body(*, ctx: MutationCtx, **_) -> bool: 23 | return len(ctx.node.body) > 0 24 | 25 | 26 | def _inject_statement(*, ctx: MutationCtx, **_) -> ast.FunctionDef: 27 | ctx.stmt_gen.inject_statements( 28 | ctx.node.body, ctx.context, ctx.node, depth=0, n_stmts=1 29 | ) 30 | return ctx.node 31 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/modules/test_flag_imports.py: -------------------------------------------------------------------------------- 1 | def test_import_flag_types(make_input_bundle, get_contract): 2 | lib1 = """ 3 | import lib2 4 | 5 | flag Roles: 6 | ADMIN 7 | USER 8 | 9 | enum Roles2: 10 | ADMIN 11 | USER 12 | 13 | role: Roles 14 | role2: Roles2 15 | role3: lib2.Roles3 16 | """ 17 | lib2 = """ 18 | flag Roles3: 19 | ADMIN 20 | USER 21 | NOBODY 22 | """ 23 | contract = """ 24 | import lib1 25 | 26 | initializes: lib1 27 | 28 | @external 29 | def bar(r: lib1.Roles, r2: lib1.Roles2, r3: lib1.lib2.Roles3) -> bool: 30 | lib1.role = r 31 | lib1.role2 = r2 32 | lib1.role3 = r3 33 | assert lib1.role == lib1.Roles.ADMIN 34 | assert lib1.role2 == lib1.Roles2.USER 35 | assert lib1.role3 == lib1.lib2.Roles3.NOBODY 36 | return True 37 | """ 38 | 39 | input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2}) 40 | c = get_contract(contract, input_bundle=input_bundle) 41 | assert c.bar(1, 2, 4) is True 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ivy" 3 | version = "0.1.0" 4 | description = "An AST interpreter for Vyper with a custom EVM backend" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "vyper @ git+https://github.com/vyperlang/vyper.git@v0.4.3", 9 | "eth-utils==4.1.1", 10 | "rlp==4.0.1", 11 | ] 12 | 13 | [project.optional-dependencies] 14 | dev = [ 15 | "titanoboa @ git+https://github.com/cyberthirst/titanoboa.git@fuzzing", 16 | "ruff>=0.6.4", 17 | "pyright>=1.1.0", 18 | "pre-commit>=3.8.0", 19 | "pytest==7.4.3", 20 | "pytest-cov==4.1.0", 21 | "pytest-xdist>=3.6.1", 22 | "pytest-instafail>=0.5.0", 23 | "hypothesis==6.104.2", 24 | ] 25 | 26 | 27 | [build-system] 28 | requires = ["hatchling"] 29 | build-backend = "hatchling.build" 30 | 31 | [tool.hatch.metadata] 32 | allow-direct-references = true 33 | 34 | [tool.hatch.build.targets.wheel] 35 | sources = ["src"] 36 | 37 | [tool.pyright] 38 | include = ["src"] 39 | pythonVersion = "3.10" 40 | typeCheckingMode = "basic" -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/test_bytes_zero_padding.py: -------------------------------------------------------------------------------- 1 | import hypothesis 2 | import pytest 3 | 4 | 5 | @pytest.fixture(scope="module") 6 | def little_endian_contract(get_contract): 7 | code = """ 8 | @internal 9 | @view 10 | def to_little_endian_64(_value: uint256) -> Bytes[8]: 11 | y: uint256 = 0 12 | x: uint256 = _value 13 | for _: uint256 in range(8): 14 | y = (y << 8) | (x & 255) 15 | x >>= 8 16 | return slice(convert(y, bytes32), 24, 8) 17 | 18 | @external 19 | @view 20 | def get_count(counter: uint256) -> Bytes[24]: 21 | return self.to_little_endian_64(counter) 22 | """ 23 | return get_contract(code) 24 | 25 | 26 | @pytest.mark.fuzzing 27 | @hypothesis.given( 28 | value=hypothesis.strategies.integers(min_value=0, max_value=(2**64 - 1)) 29 | ) 30 | def test_zero_pad_range(little_endian_contract, value): 31 | actual_bytes = value.to_bytes(8, byteorder="little") 32 | contract_bytes = little_endian_contract.get_count(value) 33 | assert contract_bytes == actual_bytes 34 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/test_conditionals.py: -------------------------------------------------------------------------------- 1 | def test_conditional_return_code(get_contract): 2 | conditional_return_code = """ 3 | @external 4 | def foo(i: bool) -> int128: 5 | if i: 6 | return 5 7 | else: 8 | assert 2 != 0 9 | return 7 10 | """ 11 | 12 | c = get_contract(conditional_return_code) 13 | assert c.foo(True) == 5 14 | assert c.foo(False) == 7 15 | 16 | print("Passed conditional return tests") 17 | 18 | 19 | def test_single_branch_underflow_public(get_contract): 20 | code = """ 21 | @external 22 | def doit(): 23 | if False: 24 | raw_call(msg.sender, b"", max_outsize=0, value=0, gas=msg.gas) 25 | """ 26 | c = get_contract(code) 27 | c.doit() 28 | 29 | 30 | def test_single_branch_underflow_private(get_contract): 31 | code = """ 32 | @internal 33 | def priv() -> uint256: 34 | return 1 35 | 36 | @external 37 | def dont_doit(): 38 | if False: 39 | self.priv() 40 | """ 41 | c = get_contract(code) 42 | c.dont_doit() 43 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutation_engine.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | 5 | from vyper.ast import nodes as ast 6 | 7 | from .strategy import StrategyRegistry, StrategySelector, StrategyExecutor 8 | from .mutations.base import MutationCtx 9 | 10 | 11 | class MutationEngine: 12 | def __init__(self, registry: StrategyRegistry, rng: random.Random): 13 | self.registry = registry 14 | self.selector = StrategySelector(rng) 15 | self.executor = StrategyExecutor(self.selector) 16 | 17 | def mutate(self, ctx: MutationCtx) -> ast.VyperNode: 18 | strategies = self.registry.collect( 19 | type_class=type(ctx.node), 20 | include_tags=("mutation",), 21 | context={"ctx": ctx}, 22 | ) 23 | 24 | if not strategies: 25 | return ctx.node 26 | 27 | def _noop(): 28 | return ctx.node 29 | 30 | return self.executor.execute_with_retry( 31 | strategies, 32 | policy="weighted_random", 33 | fallback=_noop, 34 | context={"ctx": ctx}, 35 | ) 36 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/test_string_literal.py: -------------------------------------------------------------------------------- 1 | def test_string_literal_return(get_contract): 2 | code = """ 3 | @external 4 | def test() -> String[100]: 5 | return "hello world!" 6 | 7 | 8 | @external 9 | def testb() -> Bytes[100]: 10 | return b"hello world!" 11 | """ 12 | 13 | c = get_contract(code) 14 | 15 | assert c.test() == "hello world!" 16 | assert c.testb() == b"hello world!" 17 | 18 | 19 | def test_string_convert(get_contract): 20 | code = """ 21 | @external 22 | def testb() -> String[100]: 23 | return convert(b"hello world!", String[100]) 24 | 25 | @external 26 | def testbb() -> String[100]: 27 | return convert(convert("hello world!", Bytes[100]), String[100]) 28 | """ 29 | 30 | c = get_contract(code) 31 | 32 | assert c.testb() == "hello world!" 33 | assert c.testbb() == "hello world!" 34 | 35 | 36 | def test_str_assign(get_contract): 37 | code = """ 38 | @external 39 | def test() -> String[100]: 40 | a: String[100] = "baba black sheep" 41 | return a 42 | """ 43 | 44 | c = get_contract(code) 45 | 46 | assert c.test() == "baba black sheep" 47 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/binop.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..strategy import Strategy, StrategyRegistry 6 | from .base import MutationCtx 7 | 8 | 9 | OP_SWAPS = { 10 | ast.Add: ast.Sub, 11 | ast.Sub: ast.Add, 12 | ast.Mult: ast.FloorDiv, 13 | ast.FloorDiv: ast.Mult, 14 | ast.Mod: ast.FloorDiv, 15 | ast.BitAnd: ast.BitOr, 16 | ast.BitOr: ast.BitAnd, 17 | ast.BitXor: ast.BitAnd, 18 | ast.LShift: ast.RShift, 19 | ast.RShift: ast.LShift, 20 | } 21 | 22 | 23 | def register(registry: StrategyRegistry) -> None: 24 | registry.register( 25 | Strategy( 26 | name="binop.swap_operator", 27 | type_classes=(ast.BinOp,), 28 | tags=frozenset({"mutation", "binop"}), 29 | is_applicable=_can_swap, 30 | weight=lambda **_: 1.0, 31 | run=_swap_operator, 32 | ) 33 | ) 34 | 35 | 36 | def _can_swap(*, ctx: MutationCtx, **_) -> bool: 37 | return type(ctx.node.op) in OP_SWAPS 38 | 39 | 40 | def _swap_operator(*, ctx: MutationCtx, **_) -> ast.BinOp: 41 | op_type = type(ctx.node.op) 42 | ctx.node.op = OP_SWAPS[op_type]() 43 | return ctx.node 44 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/iteration/test_continue.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from vyper.exceptions import StructureException 4 | 5 | 6 | def test_continue1(get_contract): 7 | code = """ 8 | @external 9 | def foo() -> bool: 10 | for i: uint256 in range(2): 11 | continue 12 | return False 13 | return True 14 | """ 15 | c = get_contract(code) 16 | assert c.foo() 17 | 18 | 19 | def test_continue2(get_contract): 20 | code = """ 21 | @external 22 | def foo() -> int128: 23 | x: int128 = 0 24 | for i: int128 in range(3): 25 | x += 1 26 | continue 27 | x -= 1 28 | return x 29 | """ 30 | c = get_contract(code) 31 | assert c.foo() == 3 32 | 33 | 34 | def test_continue3(get_contract): 35 | code = """ 36 | @external 37 | def foo() -> int128: 38 | x: int128 = 0 39 | for i: int128 in range(3): 40 | x += i 41 | continue 42 | return x 43 | """ 44 | c = get_contract(code) 45 | assert c.foo() == 3 46 | 47 | 48 | def test_continue4(get_contract): 49 | code = """ 50 | @external 51 | def foo() -> int128: 52 | x: int128 = 0 53 | for i: int128 in range(6): 54 | if i % 2 == 0: 55 | continue 56 | x += 1 57 | return x 58 | """ 59 | c = get_contract(code) 60 | assert c.foo() == 3 61 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/subscript.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | from vyper.semantics.types import TupleT 5 | 6 | from ..strategy import Strategy, StrategyRegistry 7 | from .base import MutationCtx 8 | 9 | 10 | def register(registry: StrategyRegistry) -> None: 11 | registry.register( 12 | Strategy( 13 | name="subscript.mutate_index", 14 | type_classes=(ast.Subscript,), 15 | tags=frozenset({"mutation", "subscript"}), 16 | is_applicable=_has_int_slice_non_tuple, 17 | weight=lambda **_: 1.0, 18 | run=_mutate_index, 19 | ) 20 | ) 21 | 22 | 23 | def _has_int_slice_non_tuple(*, ctx: MutationCtx, **_) -> bool: 24 | if not isinstance(ctx.node.slice, ast.Int): 25 | return False 26 | # Skip tuples to avoid changing element types 27 | base_type = ( 28 | ctx.node.value._metadata.get("type") 29 | if hasattr(ctx.node.value, "_metadata") 30 | else None 31 | ) 32 | return not isinstance(base_type, TupleT) 33 | 34 | 35 | def _mutate_index(*, ctx: MutationCtx, **_) -> ast.Subscript: 36 | """Mutate the index value.""" 37 | current = ctx.node.slice.value 38 | ctx.node.slice.value = ctx.rng.choice([0, 1, current + 1, current - 1]) 39 | return ctx.node 40 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/decorators/test_pure.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from vyper.exceptions import StateAccessViolation 4 | 5 | 6 | def test_pure_operation(get_contract): 7 | code = """ 8 | @pure 9 | @external 10 | def foo() -> int128: 11 | return 5 12 | """ 13 | c = get_contract(code) 14 | assert c.foo() == 5 15 | 16 | 17 | def test_pure_call(get_contract): 18 | code = """ 19 | @pure 20 | @internal 21 | def _foo() -> int128: 22 | return 5 23 | 24 | @pure 25 | @external 26 | def foo() -> int128: 27 | return self._foo() 28 | """ 29 | c = get_contract(code) 30 | assert c.foo() == 5 31 | 32 | 33 | def test_pure_interface(get_contract): 34 | code1 = """ 35 | @pure 36 | @external 37 | def foo() -> int128: 38 | return 5 39 | """ 40 | code2 = """ 41 | interface Foo: 42 | def foo() -> int128: pure 43 | 44 | @pure 45 | @external 46 | def foo(a: address) -> int128: 47 | return staticcall Foo(a).foo() 48 | """ 49 | c1 = get_contract(code1) 50 | c2 = get_contract(code2) 51 | assert c2.foo(c1.address) == 5 52 | 53 | 54 | def test_type_in_pure(get_contract): 55 | code = """ 56 | @pure 57 | @external 58 | def _convert(x: bytes32) -> uint256: 59 | return convert(x, uint256) 60 | """ 61 | c = get_contract(code) 62 | x = 123456 63 | bs = x.to_bytes(32, "big") 64 | assert x == c._convert(bs) 65 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/unaryop.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..strategy import Strategy, StrategyRegistry 6 | from .base import MutationCtx 7 | 8 | 9 | def register(registry: StrategyRegistry) -> None: 10 | registry.register( 11 | Strategy( 12 | name="unaryop.remove", 13 | type_classes=(ast.UnaryOp,), 14 | tags=frozenset({"mutation", "unaryop"}), 15 | is_applicable=lambda **_: True, 16 | weight=lambda **_: 1.0, 17 | run=_remove_operator, 18 | ) 19 | ) 20 | registry.register( 21 | Strategy( 22 | name="unaryop.double", 23 | type_classes=(ast.UnaryOp,), 24 | tags=frozenset({"mutation", "unaryop"}), 25 | is_applicable=lambda **_: True, 26 | weight=lambda **_: 1.0, 27 | run=_double_operator, 28 | ) 29 | ) 30 | 31 | 32 | def _remove_operator(*, ctx: MutationCtx, **_) -> ast.VyperNode: 33 | """Remove the unary operator, returning just the operand.""" 34 | return ctx.node.operand 35 | 36 | 37 | def _double_operator(*, ctx: MutationCtx, **_) -> ast.UnaryOp: 38 | """Double the operator: -x → -(-x), ~x → ~~x, not x → not not x.""" 39 | inner = ast.UnaryOp(op=ctx.node.op.__class__(), operand=ctx.node.operand) 40 | ctx.node.operand = inner 41 | return ctx.node 42 | -------------------------------------------------------------------------------- /src/ivy/exceptions.py: -------------------------------------------------------------------------------- 1 | from vyper.semantics.types import TupleT, StringT 2 | from vyper.utils import method_id 3 | 4 | 5 | class EVMException(Exception): 6 | pass 7 | 8 | 9 | class StaticCallViolation(EVMException): 10 | pass 11 | 12 | 13 | class GasReference(EVMException): 14 | def __init__(self, message="Gas is not a supported concept in Ivy"): 15 | super().__init__(message) 16 | 17 | 18 | class VyperException(Exception): 19 | pass 20 | 21 | 22 | class Revert(VyperException): 23 | def __init__(self, message="Revert", data=b""): 24 | super().__init__(message) 25 | self.data = data 26 | 27 | def __str__(self): 28 | if self.data: 29 | if len(self.data) >= 4: 30 | error_method_id = method_id("Error(string)") 31 | if error_method_id == self.data[:4]: 32 | from ivy.abi import abi_decode 33 | 34 | ret = abi_decode(TupleT((StringT(2**16),)), self.data[4:]) 35 | return ret[0] 36 | return self.data.hex() 37 | return super().__str__() 38 | 39 | 40 | class Invalid(VyperException): 41 | pass 42 | 43 | 44 | class PayabilityViolation(VyperException): 45 | pass 46 | 47 | 48 | class AccessViolation(Revert): 49 | pass 50 | 51 | 52 | class FunctionNotFound(Revert): 53 | pass 54 | 55 | 56 | class Raise(Revert): 57 | pass 58 | 59 | 60 | class Assert(Revert): 61 | pass 62 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/numberz/test_modulo.py: -------------------------------------------------------------------------------- 1 | from tests.ivy.utils import decimal_to_int 2 | 3 | 4 | def test_modulo(get_contract): 5 | code = """ 6 | @external 7 | def num_modulo_num() -> int128: 8 | return 1 % 2 9 | 10 | @external 11 | def decimal_modulo_decimal() -> decimal: 12 | return 1.5 % .33 13 | 14 | @external 15 | def decimal_modulo_num() -> decimal: 16 | return .5 % 1.0 17 | 18 | 19 | @external 20 | def num_modulo_decimal() -> decimal: 21 | return 1.5 % 1.0 22 | """ 23 | c = get_contract(code) 24 | assert c.num_modulo_num() == 1 25 | assert c.decimal_modulo_decimal() == decimal_to_int(".18") 26 | assert c.decimal_modulo_num() == decimal_to_int(".5") 27 | assert c.num_modulo_decimal() == decimal_to_int(".5") 28 | 29 | 30 | def test_modulo_with_input_of_zero(tx_failed, get_contract): 31 | code = """ 32 | @external 33 | def foo(a: decimal, b: decimal) -> decimal: 34 | return a % b 35 | """ 36 | c = get_contract(code) 37 | with tx_failed(): 38 | c.foo(decimal_to_int("1"), decimal_to_int("0")) 39 | 40 | 41 | def test_literals_vs_evm(get_contract): 42 | code = """ 43 | @external 44 | @view 45 | def foo() -> (int128, int128, int128, int128): 46 | return 5%2, 5%-2, -5%2, -5%-2 47 | 48 | @external 49 | @view 50 | def bar(a: int128) -> bool: 51 | assert -5%2 == a%2 52 | return True 53 | """ 54 | 55 | c = get_contract(code) 56 | assert c.foo() == (1, 1, -1, -1) 57 | assert c.bar(-5) is True 58 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/calling_convention/test_inleable_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test functionality of internal functions which may be inlined 3 | """ 4 | # note for refactor: this may be able to be merged with 5 | # calling_convention/test_internal_call.py 6 | 7 | 8 | def test_call_in_call(get_contract): 9 | code = """ 10 | @internal 11 | def _foo(a: uint256,) -> uint256: 12 | return 1 + a 13 | 14 | @internal 15 | def _foo2() -> uint256: 16 | return 4 17 | 18 | @external 19 | def foo() -> uint256: 20 | return self._foo(self._foo2()) 21 | """ 22 | 23 | c = get_contract(code) 24 | assert c.foo() == 5 25 | 26 | 27 | def test_call_in_call_with_raise(get_contract, tx_failed): 28 | code = """ 29 | @internal 30 | def sum(a: uint256) -> uint256: 31 | if a > 1: 32 | return a + 1 33 | raise 34 | 35 | @internal 36 | def middle(a: uint256) -> uint256: 37 | return self.sum(a) 38 | 39 | @external 40 | def test(a: uint256) -> uint256: 41 | return self.middle(a) 42 | """ 43 | 44 | c = get_contract(code) 45 | 46 | assert c.test(2) == 3 47 | 48 | with tx_failed(): 49 | c.test(0) 50 | 51 | 52 | def test_inliner_with_unused_param(get_contract): 53 | code = """ 54 | data: public(uint256) 55 | 56 | @internal 57 | def _foo(start: uint256, length: uint256): 58 | self.data = start 59 | 60 | @external 61 | def foo(x: uint256, y: uint256): 62 | self._foo(x, y) 63 | """ 64 | 65 | c = get_contract(code) 66 | c.foo(1, 2) 67 | -------------------------------------------------------------------------------- /setup_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Setting up Ivy development environment..." 4 | 5 | # Check Python version 6 | python_version=$(python3 --version 2>&1 | awk '{print $2}' | cut -d. -f1,2) 7 | required_version="3.10" 8 | 9 | if [[ $(echo "$python_version < $required_version" | bc) -eq 1 ]]; then 10 | echo "Error: Python $required_version or higher is required. Current version: $python_version" 11 | exit 1 12 | fi 13 | 14 | # Create virtual environment if it doesn't exist 15 | if [ ! -d "venv" ]; then 16 | echo "Creating virtual environment..." 17 | python3 -m venv venv 18 | fi 19 | 20 | # Activate virtual environment 21 | echo "Activating virtual environment..." 22 | source venv/bin/activate 23 | 24 | # Upgrade pip 25 | echo "Upgrading pip..." 26 | pip install --upgrade pip 27 | 28 | # Install package with dev dependencies 29 | echo "Installing Ivy with development dependencies..." 30 | pip install -e ".[dev]" 31 | 32 | # Run tests to verify installation 33 | echo "Running basic tests to verify installation..." 34 | PYTHONPATH=src pytest tests/ivy/test_e2e.py::test_if_control_flow -xvs 35 | 36 | echo "" 37 | echo "✅ Development environment setup complete!" 38 | echo "" 39 | echo "To activate the virtual environment in the future, run:" 40 | echo " source venv/bin/activate" 41 | echo "" 42 | echo "To run tests:" 43 | echo " pytest" 44 | echo "" 45 | echo "To run with Vyper test exports:" 46 | echo " 1. Copy exports to tests/vyper-exports/" 47 | echo " 2. Run: python examples/differential_testing.py" -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/test_struct.py: -------------------------------------------------------------------------------- 1 | def test_nested_struct(get_contract): 2 | code = """ 3 | struct Animal: 4 | location: address 5 | fur: String[32] 6 | 7 | struct Human: 8 | location: address 9 | animal: Animal 10 | 11 | @external 12 | def modify_nested_struct(_human: Human) -> Human: 13 | human: Human = _human 14 | 15 | # do stuff, edit the structs 16 | # (13 is the length of the result) 17 | human.animal.fur = slice(concat(human.animal.fur, " is great"), 0, 13) 18 | 19 | return human 20 | """ 21 | c = get_contract(code) 22 | addr1 = "0x1234567890123456789012345678901234567890" 23 | addr2 = "0x1234567890123456789012345678900000000000" 24 | # assert c.modify_nested_tuple([addr1, 123], [addr2, 456]) == ([addr1, 124], [addr2, 457]) 25 | assert c.modify_nested_struct((addr1, (addr2, "wool"))) == ( 26 | addr1, 27 | (addr2, "wool is great"), 28 | ) 29 | 30 | 31 | def test_nested_single_struct(get_contract): 32 | code = """ 33 | struct Animal: 34 | fur: String[32] 35 | 36 | struct Human: 37 | animal: Animal 38 | 39 | @external 40 | def modify_nested_single_struct(_human: Human) -> Human: 41 | human: Human = _human 42 | 43 | # do stuff, edit the structs 44 | # (13 is the length of the result) 45 | human.animal.fur = slice(concat(human.animal.fur, " is great"), 0, 13) 46 | 47 | return human 48 | """ 49 | c = get_contract(code) 50 | 51 | assert c.modify_nested_single_struct((("wool",),)) == (("wool is great",),) 52 | -------------------------------------------------------------------------------- /src/ivy/frontend/decoder_utils.py: -------------------------------------------------------------------------------- 1 | from vyper.semantics.types.primitives import _PrimT 2 | 3 | from ivy.types import StaticArray, DynamicArray, Map, Address, Struct, Tuple as IvyTuple 4 | 5 | 6 | def decode_ivy_object(v, typ): 7 | if isinstance(v, (StaticArray, DynamicArray)): 8 | # see __len__ in StaticArray to understand why we use iter 9 | v = list(iter(v)) 10 | if typ_needs_decode(typ.value_type): 11 | v = [decode_ivy_object(x, typ.value_type) for x in v] 12 | elif isinstance(v, Map): 13 | v = dict(v) 14 | if typ_needs_decode(typ.value_type): 15 | v = {k: decode_ivy_object(v, typ.value_type) for k, v in v.items()} 16 | elif isinstance(v, Address): 17 | v = str(v) 18 | elif isinstance(v, Struct): 19 | # Convert struct to dict like boa does 20 | result = {} 21 | for key in v._typ.members.keys(): 22 | value = v[key] 23 | member_typ = v._typ.members[key] 24 | if typ_needs_decode(member_typ): 25 | value = decode_ivy_object(value, member_typ) 26 | result[key] = value 27 | v = result 28 | elif isinstance(v, (tuple, IvyTuple)): 29 | v = list(v) 30 | for i, member_typ in enumerate(typ.member_types): 31 | if typ_needs_decode(member_typ): 32 | v[i] = decode_ivy_object(v[i], member_typ) 33 | v = tuple(v) 34 | # TODO add flag 35 | return v 36 | 37 | 38 | def typ_needs_decode(typ): 39 | if isinstance(typ, _PrimT): 40 | return False 41 | return True 42 | -------------------------------------------------------------------------------- /src/ivy/frontend/event.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Any 3 | 4 | 5 | @dataclass 6 | class Event: 7 | address: str # checksum address 8 | event_type: Any # vyper.semantics.types.user.EventT 9 | event: str # for compatibility with vyper 10 | topics: list[Any] # list of decoded topics 11 | args: list[Any] # list of decoded args 12 | args_obj: Any = field(default=None, init=False) 13 | 14 | def __post_init__(self): 15 | class Args: 16 | pass 17 | 18 | self.args_obj = Args() 19 | for name, value in self.ordered_args(): 20 | setattr(self.args_obj, name, value) 21 | 22 | def ordered_args(self): 23 | # TODO what about raw_log? 24 | t_i = 1 # skip the first topic which is the event_id 25 | a_i = 0 26 | b = [] 27 | # align the evm topic + args lists with the way they appear in the source 28 | # ex. Transfer(indexed address, address, indexed address) 29 | for is_topic, k in zip( 30 | self.event_type.indexed, self.event_type.arguments.keys() 31 | ): 32 | if is_topic: 33 | b.append((k, self.topics[t_i])) 34 | t_i += 1 35 | else: 36 | b.append((k, self.args[a_i])) 37 | a_i += 1 38 | 39 | return b 40 | 41 | def __repr__(self): 42 | b = self.ordered_args() 43 | args = ", ".join(f"{k}={v}" for k, v in b) 44 | return f"{self.event_type.name}({args})" 45 | 46 | 47 | @dataclass 48 | class RawEvent: 49 | event_data: Any 50 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/modules/test_events.py: -------------------------------------------------------------------------------- 1 | def test_module_event(get_contract, make_input_bundle, get_logs): 2 | # log from a module 3 | lib1 = """ 4 | event MyEvent: 5 | pass 6 | 7 | @internal 8 | def foo(): 9 | log MyEvent() 10 | """ 11 | main = """ 12 | import lib1 13 | 14 | @external 15 | def bar(): 16 | lib1.foo() 17 | """ 18 | input_bundle = make_input_bundle({"lib1.vy": lib1}) 19 | c = get_contract(main, input_bundle=input_bundle) 20 | c.bar() 21 | logs = get_logs(c, "MyEvent") 22 | assert len(logs) == 1 23 | 24 | 25 | def test_module_event2(get_contract, make_input_bundle, get_logs): 26 | # log a module event from main contract 27 | lib1 = """ 28 | event MyEvent: 29 | x: uint256 30 | """ 31 | main = """ 32 | import lib1 33 | 34 | @external 35 | def bar(): 36 | log lib1.MyEvent(5) 37 | """ 38 | input_bundle = make_input_bundle({"lib1.vy": lib1}) 39 | c = get_contract(main, input_bundle=input_bundle) 40 | c.bar() 41 | (log,) = get_logs(c, "MyEvent") 42 | assert log.args.x == 5 43 | 44 | 45 | def test_module_event_indexed(get_contract, make_input_bundle, get_logs): 46 | lib1 = """ 47 | event MyEvent: 48 | x: uint256 49 | y: indexed(uint256) 50 | 51 | @internal 52 | def foo(): 53 | log MyEvent(x=5, y=6) 54 | """ 55 | main = """ 56 | import lib1 57 | 58 | @external 59 | def bar(): 60 | lib1.foo() 61 | """ 62 | input_bundle = make_input_bundle({"lib1.vy": lib1}) 63 | c = get_contract(main, input_bundle=input_bundle) 64 | c.bar() 65 | (log,) = get_logs(c, "MyEvent") 66 | assert log.args.x == 5 67 | assert log.args.y == 6 68 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/boolop.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..strategy import Strategy, StrategyRegistry 6 | from .base import MutationCtx 7 | 8 | 9 | def register(registry: StrategyRegistry) -> None: 10 | registry.register( 11 | Strategy( 12 | name="boolop.duplicate_operand", 13 | type_classes=(ast.BoolOp,), 14 | tags=frozenset({"mutation", "boolop"}), 15 | is_applicable=_has_multiple_values, 16 | weight=lambda **_: 1.0, 17 | run=_duplicate_operand, 18 | ) 19 | ) 20 | registry.register( 21 | Strategy( 22 | name="boolop.swap_and_or", 23 | type_classes=(ast.BoolOp,), 24 | tags=frozenset({"mutation", "boolop"}), 25 | is_applicable=lambda **_: True, 26 | weight=lambda **_: 1.0, 27 | run=_swap_and_or, 28 | ) 29 | ) 30 | 31 | 32 | def _has_multiple_values(*, ctx: MutationCtx, **_) -> bool: 33 | return len(ctx.node.values) >= 2 34 | 35 | 36 | def _duplicate_operand(*, ctx: MutationCtx, **_) -> ast.BoolOp: 37 | """Duplicate one operand (a and b → a and a).""" 38 | idx = ctx.rng.randint(0, len(ctx.node.values) - 1) 39 | other_idx = ctx.rng.choice([i for i in range(len(ctx.node.values)) if i != idx]) 40 | ctx.node.values[other_idx] = ctx.node.values[idx] 41 | return ctx.node 42 | 43 | 44 | def _swap_and_or(*, ctx: MutationCtx, **_) -> ast.BoolOp: 45 | """Swap between And and Or.""" 46 | if isinstance(ctx.node.op, ast.And): 47 | ctx.node.op = ast.Or() 48 | else: 49 | ctx.node.op = ast.And() 50 | return ctx.node 51 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/test_reverting.py: -------------------------------------------------------------------------------- 1 | from vyper.utils import method_id 2 | from vyper.semantics.types import TupleT, IntegerT 3 | 4 | from ivy.abi import abi_encode 5 | 6 | 7 | def test_revert_reason(env, tx_failed, get_contract): 8 | reverty_code = """ 9 | @external 10 | def foo(): 11 | data: Bytes[4] = method_id("NoFives()") 12 | raw_revert(data) 13 | """ 14 | 15 | revert_bytes = method_id("NoFives()") 16 | 17 | with tx_failed(text=revert_bytes.hex()): 18 | get_contract(reverty_code).foo() 19 | 20 | 21 | def test_revert_reason_typed(env, tx_failed, get_contract): 22 | reverty_code = """ 23 | @external 24 | def foo(): 25 | val: uint256 = 5 26 | data: Bytes[100] = _abi_encode(val, method_id=method_id("NoFives(uint256)")) 27 | raw_revert(data) 28 | """ 29 | 30 | # revert_bytes = method_id("NoFives(uint256)") + abi.encode("(uint256)", (5,)) 31 | encode_typ = TupleT((IntegerT(False, 256),)) 32 | revert_bytes = ( 33 | method_id("NoFives(uint256)").hex() + abi_encode(encode_typ, (5,)).hex() 34 | ) 35 | 36 | with tx_failed(text=revert_bytes): 37 | get_contract(reverty_code).foo() 38 | 39 | 40 | def test_revert_reason_typed_no_variable(env, tx_failed, get_contract): 41 | reverty_code = """ 42 | @external 43 | def foo(): 44 | val: uint256 = 5 45 | raw_revert(_abi_encode(val, method_id=method_id("NoFives(uint256)"))) 46 | """ 47 | 48 | encode_typ = TupleT((IntegerT(False, 256),)) 49 | revert_bytes = ( 50 | method_id("NoFives(uint256)").hex() + abi_encode(encode_typ, (5,)).hex() 51 | ) 52 | 53 | with tx_failed(text=revert_bytes): 54 | get_contract(reverty_code).foo() 55 | -------------------------------------------------------------------------------- /src/ivy/visitor.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | import copy 4 | 5 | from vyper.ast import nodes as ast 6 | 7 | from ivy.types import _Container 8 | 9 | 10 | class BaseVisitor(ABC): 11 | def visit(self, node): 12 | method_name = f"visit_{type(node).__name__}" 13 | visitor = getattr(self, method_name, self.generic_visit) 14 | return visitor(node) 15 | 16 | def generic_visit(self, node): 17 | raise Exception(f"No visit method for {type(node).__name__}") 18 | 19 | @abstractmethod 20 | def set_variable(self, name: str, value, node: Optional[ast.VyperNode] = None): 21 | pass 22 | 23 | @abstractmethod 24 | def get_variable(self, name: str, node: Optional[ast.VyperNode] = None): 25 | pass 26 | 27 | # pass by value 28 | def deep_copy_visit(self, node): 29 | ret = self.visit(node) 30 | if not isinstance(ret, (_Container, tuple)): 31 | return ret 32 | # TODO can be further optimized 33 | # - can avoid deepcopy in ext func return values, exprs which don't 34 | # retrieve variables, when consumer doesn't modify the value (copy on write), etc 35 | return copy.deepcopy(ret) 36 | 37 | 38 | class BaseClassVisitor(ABC): 39 | @classmethod 40 | def visit(cls, node, *args): 41 | method_name = f"visit_{type(node).__name__}" 42 | visitor = getattr(cls, method_name, cls.generic_visit_class) 43 | return visitor(node, *args) 44 | 45 | def generic(self, node): 46 | raise Exception(f"No visit method for {type(node).__name__}") 47 | 48 | @classmethod 49 | def generic_visit_class(cls, node): 50 | raise Exception(f"No class visit method for {type(node).__name__}") 51 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/compare.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | from vyper.semantics.types.primitives import NumericT 5 | 6 | from ..strategy import Strategy, StrategyRegistry 7 | from .base import MutationCtx 8 | 9 | 10 | def register(registry: StrategyRegistry) -> None: 11 | registry.register( 12 | Strategy( 13 | name="compare.swap_numeric_op", 14 | type_classes=(ast.Compare,), 15 | tags=frozenset({"mutation", "compare"}), 16 | is_applicable=_is_numeric_compare, 17 | weight=lambda **_: 1.0, 18 | run=_swap_numeric_op, 19 | ) 20 | ) 21 | registry.register( 22 | Strategy( 23 | name="compare.toggle_eq", 24 | type_classes=(ast.Compare,), 25 | tags=frozenset({"mutation", "compare"}), 26 | is_applicable=_is_eq_or_neq, 27 | weight=lambda **_: 1.0, 28 | run=_toggle_eq, 29 | ) 30 | ) 31 | 32 | 33 | def _is_numeric_compare(*, ctx: MutationCtx, **_) -> bool: 34 | return ctx.inferred_type is not None and isinstance(ctx.inferred_type, NumericT) 35 | 36 | 37 | def _is_eq_or_neq(*, ctx: MutationCtx, **_) -> bool: 38 | return isinstance(ctx.node.op, (ast.Eq, ast.NotEq)) 39 | 40 | 41 | def _swap_numeric_op(*, ctx: MutationCtx, **_) -> ast.Compare: 42 | ops = [ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.Eq, ast.NotEq] 43 | new_op_type = ctx.rng.choice(ops) 44 | ctx.node.op = new_op_type() 45 | return ctx.node 46 | 47 | 48 | def _toggle_eq(*, ctx: MutationCtx, **_) -> ast.Compare: 49 | if isinstance(ctx.node.op, ast.Eq): 50 | ctx.node.op = ast.NotEq() 51 | else: 52 | ctx.node.op = ast.Eq() 53 | return ctx.node 54 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/assign.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..strategy import Strategy, StrategyRegistry 6 | from .base import MutationCtx 7 | 8 | 9 | def register(registry: StrategyRegistry) -> None: 10 | registry.register( 11 | Strategy( 12 | name="assign.use_var_as_rhs", 13 | type_classes=(ast.Assign,), 14 | tags=frozenset({"mutation", "assign"}), 15 | is_applicable=_has_matching_var, 16 | weight=lambda **_: 1.0, 17 | run=_use_var_as_rhs, 18 | ) 19 | ) 20 | registry.register( 21 | Strategy( 22 | name="assign.generate_new_expr", 23 | type_classes=(ast.Assign,), 24 | tags=frozenset({"mutation", "assign"}), 25 | is_applicable=_has_rhs_type, 26 | weight=lambda **_: 1.0, 27 | run=_generate_new_expr, 28 | ) 29 | ) 30 | 31 | 32 | def _has_rhs_type(*, ctx: MutationCtx, **_) -> bool: 33 | return ctx.inferred_type is not None 34 | 35 | 36 | def _has_matching_var(*, ctx: MutationCtx, **_) -> bool: 37 | return ctx.inferred_type is not None and bool( 38 | ctx.context.find_matching_vars(ctx.inferred_type) 39 | ) 40 | 41 | 42 | def _use_var_as_rhs(*, ctx: MutationCtx, **_) -> ast.Assign: 43 | assert ctx.inferred_type is not None 44 | other_var = ctx.expr_gen.random_var_ref(ctx.inferred_type, ctx.context) 45 | assert other_var is not None 46 | ctx.node.value = other_var 47 | return ctx.node 48 | 49 | 50 | def _generate_new_expr(*, ctx: MutationCtx, **_) -> ast.Assign: 51 | new_expr = ctx.expr_gen.generate(ctx.inferred_type, ctx.context, depth=2) 52 | ctx.node.value = new_expr 53 | return ctx.node 54 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | - Ivy is an AST interpreter for Vyper (EVM smart contract language). 3 | - The project’s purpose is differential fuzzing: compare Vyper-compiled bytecode (executed in Titanoboa/PyEVM) against Ivy. 4 | - Goal: uncover semantic bugs (miscompilations) where compiled bytecode diverges from source-level semantics. 5 | 6 | # Workflow 7 | - Interpreter: executes Vyper AST. 8 | - Contract generator/mutator: produces new inputs by mutating or generating code. 9 | - Exports: JSON traces from Vyper’s test suite, used to validate interpreter correctness and serve as the base corpus for the generator. See `exports-readme.md` if necessary. 10 | - Tests: run with pytest (multicore enabled by default; use 1 core when debugging). 11 | - Key test: `test_replay.py` (replays exports, checks e2e interpreter correctness). 12 | - Run the fuzzer: `python -m src.fuzzer.differential_fuzzer`. 13 | - Filter exports with `TestFilter` (e.g. `include_path("functional/builtins/codegen/test_slice")`). 14 | 15 | # Coding Guidelines 16 | - Minimal changes, no unnecessary abstractions. Reuse existing code where possible. 17 | - Write concise, readable code. Comments only for non-obvious logic. Don't write docs for modules and functions. 18 | - Prioritize throughput but not at the expense of readability. 19 | - Focus strictly on compiler correctness bugs, not lexer/parser bugs. 20 | 21 | # Research Direction 22 | - Apply knowledge from compiler testing (e.g. EMI, Csmith). 23 | - Suggest 2–3 best approaches (with pros/cons) when exploring new techniques/features. 24 | - Do not drift the project outside differential fuzzing. 25 | 26 | # Goals 27 | - Build a continuous fuzzing tool for Vyper dev workflow. 28 | - Aim for a working MVP, not a perfect system. 29 | - Use test modules and debug prints/logging to experiment with new ideas. 30 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/modules/test_nonreentrant.py: -------------------------------------------------------------------------------- 1 | def test_export_nonreentrant(make_input_bundle, get_contract, tx_failed): 2 | lib1 = """ 3 | interface Foo: 4 | def foo() -> uint256: nonpayable 5 | 6 | implements: Foo 7 | 8 | @external 9 | @nonreentrant 10 | def foo() -> uint256: 11 | return 5 12 | """ 13 | main = """ 14 | import lib1 15 | 16 | initializes: lib1 17 | 18 | exports: lib1.foo 19 | 20 | @external 21 | @nonreentrant 22 | def re_enter(): 23 | extcall lib1.Foo(self).foo() # should always throw 24 | 25 | @external 26 | def __default__(): 27 | # sanity: make sure we don't revert due to bad selector 28 | pass 29 | """ 30 | 31 | input_bundle = make_input_bundle({"lib1.vy": lib1}) 32 | 33 | c = get_contract(main, input_bundle=input_bundle) 34 | assert c.foo() == 5 35 | with tx_failed(): 36 | c.re_enter() 37 | 38 | 39 | def test_internal_nonreentrant(make_input_bundle, get_contract, tx_failed): 40 | lib1 = """ 41 | interface Foo: 42 | def foo() -> uint256: nonpayable 43 | 44 | implements: Foo 45 | 46 | @external 47 | def foo() -> uint256: 48 | return self._safe_fn() 49 | 50 | @internal 51 | @nonreentrant 52 | def _safe_fn() -> uint256: 53 | return 10 54 | """ 55 | main = """ 56 | import lib1 57 | 58 | initializes: lib1 59 | 60 | exports: lib1.foo 61 | 62 | @external 63 | @nonreentrant 64 | def re_enter(): 65 | extcall lib1.Foo(self).foo() # should always throw 66 | 67 | @external 68 | def __default__(): 69 | # sanity: make sure we don't revert due to bad selector 70 | pass 71 | """ 72 | 73 | input_bundle = make_input_bundle({"lib1.vy": lib1}) 74 | 75 | c = get_contract(main, input_bundle=input_bundle) 76 | assert c.foo() == 10 77 | with tx_failed(): 78 | c.re_enter() 79 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/test_comparison.py: -------------------------------------------------------------------------------- 1 | # test syntactic comparisons 2 | # most tests under tests/ast/nodes/test_evaluate_compare.py 3 | import pytest 4 | 5 | 6 | def test_3034_verbatim(get_contract): 7 | # test GH issue 3034 exactly 8 | code = """ 9 | @view 10 | @external 11 | def showError(): 12 | adr1: address = 0xFbEEa1C75E4c4465CB2FCCc9c6d6afe984558E20 13 | adr2: address = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 14 | adr3: address = 0xFbEEa1C75E4c4465CB2FCCc9c6d6afe984558E20 15 | assert adr1 in [adr2,adr3], "error in comparison with in statement!" 16 | """ 17 | c = get_contract(code) 18 | c.showError() 19 | 20 | 21 | @pytest.mark.parametrize("invert", (True, False)) 22 | def test_in_list(get_contract, invert): 23 | # test slightly more complicated variations of #3034 24 | INVERT = "not" if invert else "" 25 | code = f""" 26 | SOME_ADDRESS: constant(address) = 0x22cb70ba2EC32347D9e32740fc14b2f3d038Ce8E 27 | @view 28 | @external 29 | def test_in(addr: address) -> bool: 30 | x: address = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 31 | y: address = 0xFbEEa1C75E4c4465CB2FCCc9c6d6afe984558E20 32 | # in list which 33 | return addr {INVERT} in [x, y, SOME_ADDRESS] 34 | """ 35 | c = get_contract(code) 36 | should_in = [ 37 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 38 | "0xFbEEa1C75E4c4465CB2FCCc9c6d6afe984558E20", 39 | "0x22cb70ba2EC32347D9e32740fc14b2f3d038Ce8E", 40 | ] 41 | should_not_in = [ 42 | "0x" + "00" * 20, 43 | "0xfBeeA1C75E4C4465CB2fccC9C6d6AFe984558e21", # y but last bit flipped 44 | ] 45 | for t in should_in: 46 | assert c.test_in(t) is (True if not invert else False) 47 | for t in should_not_in: 48 | assert c.test_in(t) is (True if invert else False) 49 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/calling_convention/test_self_call_struct.py: -------------------------------------------------------------------------------- 1 | from tests.ivy.utils import decimal_to_int 2 | 3 | 4 | def test_call_to_self_struct(env, get_contract): 5 | code = """ 6 | struct MyStruct: 7 | e1: decimal 8 | e2: uint256 9 | 10 | @internal 11 | @view 12 | def get_my_struct(_e1: decimal, _e2: uint256) -> MyStruct: 13 | return MyStruct(e1=_e1, e2=_e2) 14 | 15 | @external 16 | @view 17 | def wrap_get_my_struct_WORKING(_e1: decimal) -> MyStruct: 18 | testing: MyStruct = self.get_my_struct(_e1, block.timestamp) 19 | return testing 20 | 21 | @external 22 | @view 23 | def wrap_get_my_struct_BROKEN(_e1: decimal) -> MyStruct: 24 | return self.get_my_struct(_e1, block.timestamp) 25 | """ 26 | c = get_contract(code) 27 | assert c.wrap_get_my_struct_WORKING(decimal_to_int("0.1")) == ( 28 | decimal_to_int("0.1"), 29 | env.timestamp, 30 | ) 31 | assert c.wrap_get_my_struct_BROKEN(decimal_to_int("0.1")) == ( 32 | decimal_to_int("0.1"), 33 | env.timestamp, 34 | ) 35 | 36 | 37 | def test_call_to_self_struct_2(get_contract): 38 | code = """ 39 | struct MyStruct: 40 | e1: decimal 41 | 42 | @internal 43 | @view 44 | def get_my_struct(_e1: decimal) -> MyStruct: 45 | return MyStruct(e1=_e1) 46 | 47 | @external 48 | @view 49 | def wrap_get_my_struct_WORKING(_e1: decimal) -> MyStruct: 50 | testing: MyStruct = self.get_my_struct(_e1) 51 | return testing 52 | 53 | @external 54 | @view 55 | def wrap_get_my_struct_BROKEN(_e1: decimal) -> MyStruct: 56 | return self.get_my_struct(_e1) 57 | """ 58 | c = get_contract(code) 59 | assert c.wrap_get_my_struct_WORKING(decimal_to_int("0.1")) == ( 60 | decimal_to_int("0.1"), 61 | ) 62 | assert c.wrap_get_my_struct_BROKEN(decimal_to_int("0.1")) == ( 63 | decimal_to_int("0.1"), 64 | ) 65 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/test_init.py: -------------------------------------------------------------------------------- 1 | import vyper 2 | 3 | 4 | def test_basic_init_function(get_contract): 5 | code = """ 6 | val: public(uint256) 7 | 8 | @deploy 9 | def __init__(a: uint256): 10 | self.val = a 11 | """ 12 | 13 | c = get_contract(code, *[123]) 14 | 15 | assert c.val() == 123 16 | 17 | # Make sure the init code does not access calldata 18 | assembly = vyper.compile_code(code, output_formats=["asm"])["asm"].split(" ") 19 | ir_return_idx_start = assembly.index("{") 20 | ir_return_idx_end = assembly.index("}") 21 | 22 | assert "CALLDATALOAD" in assembly 23 | assert ( 24 | "CALLDATACOPY" 25 | not in assembly[:ir_return_idx_start] + assembly[ir_return_idx_end:] 26 | ) 27 | assert ( 28 | "CALLDATALOAD" 29 | not in assembly[:ir_return_idx_start] + assembly[ir_return_idx_end:] 30 | ) 31 | 32 | 33 | def test_init_calls_internal(get_contract, tx_failed): 34 | code = """ 35 | foo: public(uint8) 36 | 37 | @internal 38 | def bar(x: uint256) -> uint8: 39 | return convert(x, uint8) * 7 40 | 41 | @deploy 42 | def __init__(a: uint256): 43 | self.foo = self.bar(a) 44 | 45 | @external 46 | def baz() -> uint8: 47 | return self.bar(convert(self.foo, uint256)) 48 | """ 49 | n = 5 50 | c = get_contract(code, n) 51 | assert c.foo() == n * 7 52 | assert c.baz() == 245 # 5*7*7 53 | 54 | n = 6 55 | c = get_contract(code, n) 56 | assert c.foo() == n * 7 57 | with tx_failed(): 58 | c.baz() 59 | 60 | 61 | # GH issue 3206 62 | def test_nested_internal_call_from_ctor(get_contract): 63 | code = """ 64 | x: uint256 65 | 66 | @deploy 67 | def __init__(): 68 | self.a() 69 | 70 | @internal 71 | def a(): 72 | self.x += 1 73 | self.b() 74 | 75 | @internal 76 | def b(): 77 | self.x += 2 78 | 79 | @external 80 | def test() -> uint256: 81 | return self.x 82 | """ 83 | c = get_contract(code) 84 | assert c.test() == 3 85 | -------------------------------------------------------------------------------- /tests/ivy/examples/ERC20.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ivy.frontend.loader import load 4 | 5 | 6 | def test_ERC20(env): 7 | # deploy contract while passing data to the __init__ function, 8 | # and return user-facing frontend repr of the deployed contract for interaction 9 | contract = load("examples/ERC20.vy", "MyToken", "MTKN", 18, 100 * 10**18) 10 | # interact with contract 11 | # - the methods are exposed as attributes on the contract object 12 | assert contract.name() == "MyToken" 13 | assert contract.symbol() == "MTKN" 14 | assert contract.decimals() == 18 15 | # load 2 accounts from the environment 16 | alice, bob = env.accounts[:2] 17 | # alice doesn't have any tokens, catch error 18 | with pytest.raises(ValueError): 19 | contract.transfer(bob, 10 * 10**18, sender=alice) 20 | # mint tokens to alice & transfer to bob 21 | contract.mint(alice, 100 * 10**18) 22 | contract.transfer(bob, 10 * 10**18, sender=alice) 23 | # get logs 24 | logs = contract.get_logs() 25 | assert len(logs) == 1 26 | # sender and receiver are indexed, so access via topics 27 | assert logs[0].topics[0] == alice and logs[0].topics[1] == bob 28 | # value is not indexed 29 | assert logs[0].args[0] == 10 * 10**18 30 | # check that tokens were transferred 31 | assert contract.balanceOf(alice) == 90 * 10**18 32 | assert contract.balanceOf(bob) == 10 * 10**18 33 | # further contract interaction 34 | contract.approve(bob, 5 * 10**18, sender=alice) 35 | # bob transfers 5 tokens from alice to himself 36 | contract.transferFrom(alice, bob, 5 * 10**18, sender=bob) 37 | assert contract.balanceOf(alice) == 85 * 10**18 38 | assert contract.balanceOf(bob) == 15 * 10**18 39 | # bob transfers 5 tokens from alice to himself 40 | # but he doesn't have enough allowance 41 | with pytest.raises(ValueError): 42 | contract.transferFrom(alice, bob, 5 * 10**18, sender=bob) 43 | # alice's balance remains the same 44 | assert contract.balanceOf(alice) == 85 * 10**18 45 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # Exclude a variety of commonly ignored directories. 2 | exclude = [ 3 | ".bzr", 4 | ".direnv", 5 | ".eggs", 6 | ".git", 7 | ".git-rewrite", 8 | ".hg", 9 | ".ipynb_checkpoints", 10 | ".mypy_cache", 11 | ".nox", 12 | ".pants.d", 13 | ".pyenv", 14 | ".pytest_cache", 15 | ".pytype", 16 | ".ruff_cache", 17 | ".svn", 18 | ".tox", 19 | ".venv", 20 | ".vscode", 21 | "__pypackages__", 22 | "_build", 23 | "buck-out", 24 | "build", 25 | "dist", 26 | "node_modules", 27 | "site-packages", 28 | "venv", 29 | ] 30 | 31 | # Same as Black. 32 | line-length = 88 33 | indent-width = 4 34 | 35 | target-version = "py310" 36 | 37 | src = ["src/ivy" ] 38 | 39 | [lint] 40 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. 41 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or 42 | # McCabe complexity (`C901`) by default. 43 | select = ["E4", "E7", "E9", "F"] 44 | ignore = [] 45 | 46 | # Allow fix for all enabled rules (when `--fix`) is provided. 47 | fixable = ["ALL"] 48 | unfixable = [] 49 | 50 | # Allow unused variables when underscore-prefixed. 51 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 52 | 53 | [format] 54 | # Like Black, use double quotes for strings. 55 | quote-style = "double" 56 | 57 | # Like Black, indent with spaces, rather than tabs. 58 | indent-style = "space" 59 | 60 | # Like Black, respect magic trailing commas. 61 | skip-magic-trailing-comma = false 62 | 63 | # Like Black, automatically detect the appropriate line ending. 64 | line-ending = "auto" 65 | 66 | # Enable auto-formatting of code examples in docstrings. Markdown, 67 | # reStructuredText code/literal blocks and doctests are all supported. 68 | # 69 | # This is currently disabled by default, but it is planned for this 70 | # to be opt-out in the future. 71 | docstring-code-format = false 72 | 73 | # Set the line length limit used when formatting code snippets in 74 | # docstrings. 75 | # 76 | # This only has an effect when the `docstring-code-format` setting is 77 | # enabled. 78 | docstring-code-line-length = "dynamic" 79 | 80 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/storage_variables/test_getters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from vyper.exceptions import OverflowException, TypeMismatch 4 | 5 | 6 | def test_state_accessor(get_contract): 7 | state_accessor = """ 8 | y: HashMap[int128, int128] 9 | 10 | @external 11 | def oo(): 12 | self.y[3] = 5 13 | 14 | @external 15 | def foo() -> int128: 16 | return self.y[3] 17 | 18 | """ 19 | 20 | c = get_contract(state_accessor) 21 | c.oo() 22 | assert c.foo() == 5 23 | 24 | 25 | def test_getter_code(get_contract): 26 | getter_code = """ 27 | interface V: 28 | def foo(): nonpayable 29 | 30 | struct W: 31 | a: uint256 32 | b: int128[7] 33 | c: Bytes[100] 34 | e: int128[3][3] 35 | f: uint256 36 | g: uint256 37 | x: public(uint256) 38 | y: public(int128[5]) 39 | z: public(Bytes[100]) 40 | w: public(HashMap[int128, W]) 41 | a: public(uint256[10][10]) 42 | b: public(HashMap[uint256, HashMap[address, uint256[4]]]) 43 | c: public(constant(uint256)) = 1 44 | d: public(immutable(uint256)) 45 | e: public(immutable(uint256[2])) 46 | f: public(constant(uint256[2])) = [3, 7] 47 | g: public(constant(V)) = V(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF) 48 | 49 | @deploy 50 | def __init__(): 51 | self.x = as_wei_value(7, "wei") 52 | self.y[1] = 9 53 | self.z = b"cow" 54 | self.w[1].a = 11 55 | self.w[1].b[2] = 13 56 | self.w[1].c = b"horse" 57 | self.w[2].e[1][2] = 17 58 | self.w[3].f = 750 59 | self.w[3].g = 751 60 | self.a[1][4] = 666 61 | self.b[42][self] = [5,6,7,8] 62 | d = 1729 63 | e = [2, 3] 64 | """ 65 | 66 | c = get_contract(getter_code) 67 | assert c.x() == 7 68 | assert c.y(1) == 9 69 | assert c.z() == b"cow" 70 | assert c.w(1)[0] == 11 # W.a 71 | assert c.w(1)[1][2] == 13 # W.b[2] 72 | assert c.w(1)[2] == b"horse" # W.c 73 | assert c.w(2)[3][1][2] == 17 # W.e[1][2] 74 | assert c.w(3)[4] == 750 # W.f 75 | assert c.w(3)[5] == 751 # W.g 76 | assert c.a(1, 4) == 666 77 | assert c.b(42, c.address, 2) == 7 78 | assert c.c() == 1 79 | assert c.d() == 1729 80 | assert c.e(0) == 2 81 | assert [c.f(i) for i in range(2)] == [3, 7] 82 | assert c.g() == "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF" 83 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/iteration/test_break.py: -------------------------------------------------------------------------------- 1 | from tests.ivy.utils import decimal_to_int 2 | 3 | 4 | def test_break_test(get_contract): 5 | break_test = """ 6 | @external 7 | def foo(n: decimal) -> int128: 8 | c: decimal = n * 1.0 9 | output: int128 = 0 10 | for i: int128 in range(400): 11 | c = c / 1.2589 12 | if c < 1.0: 13 | output = i 14 | break 15 | return output 16 | """ 17 | 18 | c = get_contract(break_test) 19 | 20 | assert c.foo(decimal_to_int("1")) == 0 21 | assert c.foo(decimal_to_int("2")) == 3 22 | assert c.foo(decimal_to_int("10")) == 10 23 | assert c.foo(decimal_to_int("200")) == 23 24 | 25 | print("Passed for-loop break test") 26 | 27 | 28 | def test_break_test_2(get_contract): 29 | break_test_2 = """ 30 | @external 31 | def foo(n: decimal) -> int128: 32 | c: decimal = n * 1.0 33 | output: int128 = 0 34 | for i: int128 in range(40): 35 | if c < 10.0: 36 | output = i * 10 37 | break 38 | c = c / 10.0 39 | for i: int128 in range(10): 40 | c = c / 1.2589 41 | if c < 1.0: 42 | output = output + i 43 | break 44 | return output 45 | """ 46 | 47 | c = get_contract(break_test_2) 48 | assert c.foo(decimal_to_int("1")) == 0 49 | assert c.foo(decimal_to_int("2")) == 3 50 | assert c.foo(decimal_to_int("10")) == 10 51 | assert c.foo(decimal_to_int("200")) == 23 52 | assert c.foo(decimal_to_int("4000000")) == 66 53 | print("Passed for-loop break test 2") 54 | 55 | 56 | def test_break_test_3(get_contract): 57 | break_test_3 = """ 58 | @external 59 | def foo(n: int128) -> int128: 60 | c: decimal = convert(n, decimal) 61 | output: int128 = 0 62 | for i: int128 in range(40): 63 | if c < 10.0: 64 | output = i * 10 65 | break 66 | c /= 10.0 67 | for i: int128 in range(10): 68 | c /= 1.2589 69 | if c < 1.0: 70 | output = output + i 71 | break 72 | return output 73 | """ 74 | 75 | c = get_contract(break_test_3) 76 | assert c.foo(1) == 0 77 | assert c.foo(2) == 3 78 | assert c.foo(10) == 10 79 | assert c.foo(200) == 23 80 | assert c.foo(4000000) == 66 81 | print("Passed aug-assignment break composite test") 82 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/builtins/codegen/test_unsafe_math.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import operator 3 | import random 4 | 5 | import pytest 6 | 7 | from vyper.semantics.types import IntegerT 8 | from vyper.utils import evm_div, unsigned_to_signed 9 | 10 | # TODO something less janky 11 | integer_types = sorted(list(IntegerT.all())) 12 | 13 | 14 | @pytest.mark.parametrize("typ", integer_types) 15 | @pytest.mark.parametrize("op", ["add", "sub", "mul", "div"]) 16 | @pytest.mark.fuzzing 17 | def test_unsafe_op_int(get_contract, typ, op): 18 | contract_1 = f""" 19 | @external 20 | def foo(x: {typ}, y: {typ}) -> {typ}: 21 | return unsafe_{op}(x, y) 22 | """ 23 | 24 | contract_2 = """ 25 | @external 26 | def foo(x: {typ}) -> {typ}: 27 | return unsafe_{op}(x, {literal}) 28 | """ 29 | 30 | fns = { 31 | "add": operator.add, 32 | "sub": operator.sub, 33 | "mul": operator.mul, 34 | "div": evm_div, 35 | } 36 | fn = fns[op] 37 | 38 | c1 = get_contract(contract_1) 39 | 40 | lo, hi = typ.ast_bounds 41 | # (roughly 8k cases total generated) 42 | # TODO refactor to use fixtures 43 | NUM_CASES = 15 44 | xs = [random.randrange(lo, hi) for _ in range(NUM_CASES)] 45 | ys = [random.randrange(lo, hi) for _ in range(NUM_CASES)] 46 | 47 | mod_bound = 2**typ.bits 48 | 49 | # poor man's fuzzing - hypothesis doesn't make it easy 50 | # with the parametrized strategy 51 | if typ.is_signed: 52 | xs += [lo, lo + 1, -1, 0, 1, hi - 1, hi] 53 | ys += [lo, lo + 1, -1, 0, 1, hi - 1, hi] 54 | for x, y in itertools.product(xs, ys): 55 | expected = unsigned_to_signed(fn(x, y) % mod_bound, typ.bits) 56 | 57 | assert c1.foo(x, y) == expected 58 | 59 | c2 = get_contract(contract_2.format(typ=typ, op=op, literal=y)) 60 | assert c2.foo(x) == expected 61 | 62 | else: 63 | # 0x80 has some weird properties, like 64 | # it's a fixed point of multiplication by 0xFF 65 | fixed_pt = 2 ** (typ.bits - 1) 66 | xs += [0, 1, hi - 1, hi, fixed_pt] 67 | ys += [0, 1, hi - 1, hi, fixed_pt] 68 | for x, y in itertools.product(xs, ys): 69 | expected = fn(x, y) % mod_bound 70 | assert c1.foo(x, y) == expected 71 | 72 | c2 = get_contract(contract_2.format(typ=typ, op=op, literal=y)) 73 | assert c2.foo(x) == expected 74 | -------------------------------------------------------------------------------- /src/fuzzer/corpus.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List, Optional, TypeVar 3 | 4 | from fuzzer.runner.scenario import Scenario 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | class FuzzCorpus: 10 | """ 11 | Two-tier corpus: immutable seeds + bounded evolved pool. 12 | 13 | Seeds are never removed. Evolved items are capped at max_evolved 14 | with replacement when full. 15 | """ 16 | 17 | def __init__( 18 | self, 19 | rng: random.Random, 20 | max_evolved: int = 10_000, 21 | seed_selection_prob: float = 0.3, 22 | ): 23 | self.rng = rng 24 | self.max_evolved = max_evolved 25 | self.seed_selection_prob = seed_selection_prob 26 | 27 | self._seeds: List = [] 28 | self._evolved: List = [] 29 | 30 | def add_seed(self, item) -> None: 31 | """Add an item to the immutable seed corpus.""" 32 | self._seeds.append(item) 33 | 34 | def add_evolved(self, item) -> None: 35 | """ 36 | Add an evolved item. If at capacity, randomly replace an existing one. 37 | """ 38 | if len(self._evolved) < self.max_evolved: 39 | self._evolved.append(item) 40 | else: 41 | idx = self.rng.randint(0, self.max_evolved - 1) 42 | self._evolved[idx] = item 43 | 44 | def pick(self) -> Optional[Scenario]: 45 | """ 46 | Pick an item from the corpus. 47 | 48 | With seed_selection_prob, picks from seeds. Otherwise from evolved 49 | (falls back to seeds if evolved is empty). 50 | """ 51 | if not self._seeds and not self._evolved: 52 | return None 53 | 54 | use_seed = self.rng.random() < self.seed_selection_prob or not self._evolved 55 | 56 | if use_seed and self._seeds: 57 | return self.rng.choice(self._seeds) 58 | elif self._evolved: 59 | return self.rng.choice(self._evolved) 60 | elif self._seeds: 61 | return self.rng.choice(self._seeds) 62 | 63 | return None 64 | 65 | @property 66 | def seeds(self) -> List: 67 | return self._seeds 68 | 69 | @property 70 | def seed_count(self) -> int: 71 | return len(self._seeds) 72 | 73 | @property 74 | def evolved_count(self) -> int: 75 | return len(self._evolved) 76 | 77 | @property 78 | def total_count(self) -> int: 79 | return len(self._seeds) + len(self._evolved) 80 | -------------------------------------------------------------------------------- /src/ivy/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import rlp 4 | from eth_utils import keccak 5 | 6 | from vyper.utils import method_id 7 | from vyper.semantics.types.subscriptable import TupleT 8 | 9 | 10 | def compute_call_abi_data(func_t, num_kwargs): 11 | sig_kwargs = func_t.keyword_args[:num_kwargs] 12 | sig_args = func_t.positional_args + sig_kwargs 13 | 14 | calldata_args_t = TupleT(list(arg.typ for arg in sig_args)) 15 | 16 | sig = func_t.name + calldata_args_t.abi_type.selector_name() 17 | 18 | selector = method_id(sig) 19 | 20 | return selector, calldata_args_t 21 | 22 | 23 | # lrudict from titanoboa: https://github.com/vyperlang/titanoboa/blob/bedd49e5a4c1e79a7d12c799e42a23a9dc449395/boa/util/lrudict.py 24 | class lrudict(dict): 25 | def __init__(self, n, *args, **kwargs): 26 | super().__init__(*args, **kwargs) 27 | self.n = n 28 | 29 | def __getitem__(self, k): 30 | val = super().__getitem__(k) 31 | del self[k] # move to the front of the queue 32 | super().__setitem__(k, val) 33 | return val 34 | 35 | def __setitem__(self, k, val): 36 | if len(self) == self.n and k not in self: 37 | del self[next(iter(self))] 38 | super().__setitem__(k, val) 39 | 40 | # set based on a lambda 41 | def setdefault_lambda(self, k, fn): 42 | try: 43 | return self[k] 44 | except KeyError: 45 | self[k] = (ret := fn(k)) 46 | return ret 47 | 48 | 49 | # version detection from titanoboa: https://github.com/vyperlang/titanoboa/blob/bedd49e5a4c1e79a7d12c799e42a23a9dc449395/boa/contracts/vvm/vvm_contract.py#L9 50 | VERSION_RE = re.compile(r"\s*#\s*(pragma\s+version|@version)\s+(\d+\.\d+\.\d+)") 51 | 52 | 53 | def _detect_version(source_code: str): 54 | res = VERSION_RE.findall(source_code) 55 | if len(res) < 1: 56 | return None 57 | # TODO: handle len(res) > 1 58 | return res[0][1] 59 | 60 | 61 | def compute_contract_address(address, nonce): 62 | computed_address = keccak(rlp.encode([address, nonce])) 63 | canonical_address = computed_address[-20:] 64 | return canonical_address 65 | 66 | 67 | def _trunc_div(n: int, d: int) -> int: 68 | """ 69 | Integer division rounded **toward 0** for all sign combinations. 70 | Caller must ensure d != 0. 71 | """ 72 | q = abs(n) // abs(d) # non‑negative → already truncated 73 | return -q if (n < 0) ^ (d < 0) else q 74 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/interface_registry.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import random 3 | from typing import List 4 | 5 | from vyper.ast import nodes as ast 6 | from vyper.semantics.types import InterfaceT 7 | from vyper.semantics.types.function import ContractFunctionT, StateMutability 8 | 9 | 10 | class InterfaceRegistry: 11 | """Registry for generating interfaces for external calls.""" 12 | 13 | def __init__(self, rng: random.Random): 14 | self.rng = rng 15 | self.counter = 0 16 | self._interfaces: List[ast.InterfaceDef] = [] 17 | 18 | def reset(self): 19 | self.counter = 0 20 | self._interfaces.clear() 21 | 22 | def _mutability_name(self, mutability: StateMutability) -> str: 23 | if mutability == StateMutability.PURE: 24 | return "pure" 25 | elif mutability == StateMutability.VIEW: 26 | return "view" 27 | elif mutability == StateMutability.PAYABLE: 28 | return "payable" 29 | else: 30 | return "nonpayable" 31 | 32 | def _build_interface_func_def(self, func: ContractFunctionT) -> ast.FunctionDef: 33 | """Build interface function signature by copying ast_def and modifying body.""" 34 | assert func.ast_def is not None, "External function must have ast_def" 35 | 36 | # Deep copy the function definition 37 | func_def: ast.FunctionDef = copy.deepcopy(func.ast_def) 38 | 39 | # Clear decorators (interface functions don't have them) 40 | func_def.decorator_list = [] 41 | 42 | # Replace body with just mutability indicator 43 | mut_name = self._mutability_name(func.mutability) 44 | func_def.body = [ast.Expr(value=ast.Name(id=mut_name))] 45 | 46 | return func_def 47 | 48 | def create_interface(self, func: ContractFunctionT) -> tuple[str, InterfaceT]: 49 | """Create a fresh interface for the given function.""" 50 | iface_name = f"IGen{self.counter}" 51 | self.counter += 1 52 | 53 | func_def = self._build_interface_func_def(func) 54 | iface_def = ast.InterfaceDef(name=iface_name, body=[func_def]) 55 | 56 | iface_type = InterfaceT( 57 | _id=iface_name, 58 | decl_node=iface_def, 59 | functions={func.name: func}, 60 | events={}, 61 | structs={}, 62 | flags={}, 63 | ) 64 | 65 | self._interfaces.append(iface_def) 66 | return iface_name, iface_type 67 | 68 | def get_interface_defs(self) -> List[ast.InterfaceDef]: 69 | return self._interfaces 70 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/test_bytes_literal.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import pytest 4 | 5 | 6 | def test_bytes_literal_code(get_contract): 7 | bytes_literal_code = """ 8 | @external 9 | def foo() -> Bytes[5]: 10 | return b"horse" 11 | 12 | @external 13 | def bar() -> Bytes[10]: 14 | return concat(b"b", b"a", b"d", b"m", b"i", b"", b"nton") 15 | 16 | @external 17 | def baz() -> Bytes[40]: 18 | return concat(b"0123456789012345678901234567890", b"12") 19 | 20 | @external 21 | def baz2() -> Bytes[40]: 22 | return concat(b"01234567890123456789012345678901", b"12") 23 | 24 | @external 25 | def baz3() -> Bytes[40]: 26 | return concat(b"0123456789012345678901234567890", b"1") 27 | 28 | @external 29 | def baz4() -> Bytes[100]: 30 | return concat(b"01234567890123456789012345678901234567890123456789", 31 | b"01234567890123456789012345678901234567890123456789") 32 | """ 33 | 34 | c = get_contract(bytes_literal_code) 35 | assert c.foo() == b"horse" 36 | assert c.bar() == b"badminton" 37 | assert c.baz() == b"012345678901234567890123456789012" 38 | assert c.baz2() == b"0123456789012345678901234567890112" 39 | assert c.baz3() == b"01234567890123456789012345678901" 40 | assert c.baz4() == b"0123456789" * 10 41 | 42 | print("Passed string literal test") 43 | 44 | 45 | @pytest.mark.parametrize( 46 | "i,e,_s", itertools.product([95, 96, 97], [63, 64, 65], [31, 32, 33]) 47 | ) 48 | def test_bytes_literal_splicing_fuzz(get_contract, i, e, _s): 49 | kode = f""" 50 | moo: Bytes[100] 51 | 52 | @external 53 | def foo(s: uint256, L: uint256) -> Bytes[100]: 54 | x: int128 = 27 55 | r: Bytes[100] = slice(b"{("c" * i)}", s, L) 56 | y: int128 = 37 57 | if x * y == 999: 58 | return r 59 | return b"3434346667777" 60 | 61 | @external 62 | def bar(s: uint256, L: uint256) -> Bytes[100]: 63 | self.moo = b"{("c" * i)}" 64 | x: int128 = 27 65 | r: Bytes[100] = slice(self.moo, s, L) 66 | y: int128 = 37 67 | if x * y == 999: 68 | return r 69 | return b"3434346667777" 70 | 71 | @external 72 | def baz(s: uint256, L: uint256) -> Bytes[100]: 73 | x: int128 = 27 74 | self.moo = slice(b"{("c" * i)}", s, L) 75 | y: int128 = 37 76 | if x * y == 999: 77 | return self.moo 78 | return b"3434346667777" 79 | """ 80 | 81 | c = get_contract(kode) 82 | o1 = c.foo(_s, e - _s) 83 | o2 = c.bar(_s, e - _s) 84 | o3 = c.baz(_s, e - _s) 85 | assert o1 == o2 == o3 == b"c" * (e - _s), (i, _s, e - _s, o1, o2, o3) 86 | 87 | print("Passed string literal splicing fuzz-test") 88 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/modules/test_module_constants.py: -------------------------------------------------------------------------------- 1 | def test_module_constant(make_input_bundle, get_contract): 2 | mod1 = """ 3 | X: constant(uint256) = 12345 4 | """ 5 | contract = """ 6 | import mod1 7 | 8 | @external 9 | def foo() -> uint256: 10 | return mod1.X 11 | """ 12 | 13 | input_bundle = make_input_bundle({"mod1.vy": mod1}) 14 | 15 | c = get_contract(contract, input_bundle=input_bundle) 16 | 17 | assert c.foo() == 12345 18 | 19 | 20 | def test_nested_module_constant(make_input_bundle, get_contract): 21 | # test nested module constants 22 | # test at least 3 modules deep to test the `path.reverse()` gizmo 23 | # in ConstantFolder.visit_Attribute() 24 | mod1 = """ 25 | X: constant(uint256) = 12345 26 | """ 27 | mod2 = """ 28 | import mod1 29 | X: constant(uint256) = 54321 30 | """ 31 | mod3 = """ 32 | import mod2 33 | X: constant(uint256) = 98765 34 | """ 35 | 36 | contract = """ 37 | import mod1 38 | import mod2 39 | import mod3 40 | 41 | @external 42 | def test_foo() -> bool: 43 | assert mod1.X == 12345 44 | assert mod2.X == 54321 45 | assert mod3.X == 98765 46 | assert mod2.mod1.X == mod1.X 47 | assert mod3.mod2.mod1.X == mod1.X 48 | assert mod3.mod2.X == mod2.X 49 | return True 50 | """ 51 | 52 | input_bundle = make_input_bundle( 53 | {"mod1.vy": mod1, "mod2.vy": mod2, "mod3.vy": mod3} 54 | ) 55 | 56 | c = get_contract(contract, input_bundle=input_bundle) 57 | assert c.test_foo() is True 58 | 59 | 60 | def test_import_constant_array(make_input_bundle, get_contract, tx_failed): 61 | mod1 = """ 62 | X: constant(uint256[3]) = [1,2,3] 63 | """ 64 | contract = """ 65 | import mod1 66 | 67 | @external 68 | def foo(ix: uint256) -> uint256: 69 | return mod1.X[ix] 70 | """ 71 | 72 | input_bundle = make_input_bundle({"mod1.vy": mod1}) 73 | 74 | c = get_contract(contract, input_bundle=input_bundle) 75 | 76 | assert c.foo(0) == 1 77 | assert c.foo(1) == 2 78 | assert c.foo(2) == 3 79 | with tx_failed(): 80 | c.foo(3) 81 | 82 | 83 | def test_module_constant_builtin(make_input_bundle, get_contract): 84 | # test empty builtin, which is not (currently) foldable 2024-02-06 85 | mod1 = """ 86 | X: constant(uint256) = empty(uint256) 87 | """ 88 | contract = """ 89 | import mod1 90 | 91 | @external 92 | def foo() -> uint256: 93 | return mod1.X 94 | """ 95 | 96 | input_bundle = make_input_bundle({"mod1.vy": mod1}) 97 | 98 | c = get_contract(contract, input_bundle=input_bundle) 99 | 100 | assert c.foo() == 0 101 | -------------------------------------------------------------------------------- /src/ivy/builtins/create_utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import Optional 3 | 4 | from vyper.compiler import CompilerData 5 | from vyper.semantics.types.module import ModuleT 6 | from vyper.ast import nodes as ast 7 | 8 | from ivy.evm.evm_core import EVMCore 9 | from ivy.evm.evm_state import StateAccess 10 | from ivy.evm.evm_structures import ContractData 11 | from ivy.types import Address 12 | 13 | 14 | def deepcopy_code(state: StateAccess, target: Address, reset_global_vars: bool = False): 15 | from ivy.variable import GlobalVariables 16 | 17 | # TODO what about the case when the target is empty? 18 | code = state.get_code(target) 19 | code_copy = copy.deepcopy(code) 20 | if reset_global_vars: 21 | # For blueprint creation, we need fresh global_vars since the new contract 22 | # will run its constructor and allocate variables 23 | code_copy.global_vars = GlobalVariables() 24 | return code_copy 25 | 26 | 27 | # TODO find a better name 28 | def create_builtin_shared( 29 | evm: EVMCore, 30 | code: ContractData, 31 | data: bytes = b"", 32 | value: int = 0, 33 | revert_on_failure: bool = True, 34 | salt: Optional[bytes] = None, 35 | is_runtime_copy: Optional[bool] = False, 36 | ): 37 | res, address = evm.do_create_message_call(value, data, code, salt, is_runtime_copy) 38 | 39 | if not res.is_error: 40 | return address 41 | 42 | # child evm resulted in error but we shouldn't revert so return zero address 43 | if not revert_on_failure: 44 | return Address(0) 45 | 46 | raise res.error 47 | 48 | 49 | class MinimalProxyFactory: 50 | # NOTE: the contract is not semantically eq. to the minimal proxy as per EIP1167 51 | # however, through an escape hatch in the interpreter we maintain clean passthrough of 52 | # data without touching abi-encoding 53 | _SOURCE = """ 54 | implementation: immutable(address) 55 | 56 | @deploy 57 | def __init__(_implementation: address): 58 | implementation = _implementation 59 | 60 | # use 2**32 which is sufficiently large not to cause runtime problems, 61 | # and small enough # not to cause allocator exception in the frontend 62 | @external 63 | @payable 64 | def __default__() -> Bytes[2**32]: 65 | return raw_call(implementation, msg.data, is_delegate_call=True, max_outsize=2**32) 66 | 67 | 68 | """ 69 | _ast: ast.Module = None 70 | 71 | @classmethod 72 | def get_proxy_contract_data(cls): 73 | if cls._ast is None: 74 | compiler_data = CompilerData(file_input=cls._SOURCE) 75 | cls._ast = compiler_data.annotated_vyper_module 76 | 77 | ast = cls._ast 78 | ast._metadata["is_minimal_proxy"] = True 79 | 80 | module_t = ast._metadata["type"] 81 | assert isinstance(module_t, ModuleT) 82 | 83 | return ContractData(module_t) 84 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/if_stmt.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | 5 | from ..context import ScopeType 6 | from ..strategy import Strategy, StrategyRegistry 7 | from .base import MutationCtx 8 | 9 | 10 | def register(registry: StrategyRegistry) -> None: 11 | registry.register( 12 | Strategy( 13 | name="if.negate_condition", 14 | type_classes=(ast.If,), 15 | tags=frozenset({"mutation", "if"}), 16 | is_applicable=lambda **_: True, 17 | weight=lambda **_: 1.0, 18 | run=_negate_condition, 19 | ) 20 | ) 21 | registry.register( 22 | Strategy( 23 | name="if.swap_branches", 24 | type_classes=(ast.If,), 25 | tags=frozenset({"mutation", "if"}), 26 | is_applicable=_has_both_branches, 27 | weight=lambda **_: 1.0, 28 | run=_swap_branches, 29 | ) 30 | ) 31 | registry.register( 32 | Strategy( 33 | name="if.inject_statement", 34 | type_classes=(ast.If,), 35 | tags=frozenset({"mutation", "if"}), 36 | is_applicable=lambda **_: True, 37 | weight=lambda **_: 1.0, 38 | run=_inject_statement, 39 | ) 40 | ) 41 | 42 | 43 | def _has_both_branches(*, ctx: MutationCtx, **_) -> bool: 44 | return bool(ctx.node.body and ctx.node.orelse) 45 | 46 | 47 | def _negate_expr(expr: ast.VyperNode) -> ast.VyperNode: 48 | """Return a logically negated version of the expression.""" 49 | if isinstance(expr, ast.Compare): 50 | op_map = { 51 | ast.Lt: ast.GtE(), 52 | ast.LtE: ast.Gt(), 53 | ast.Gt: ast.LtE(), 54 | ast.GtE: ast.Lt(), 55 | ast.Eq: ast.NotEq(), 56 | ast.NotEq: ast.Eq(), 57 | ast.In: ast.NotIn(), 58 | ast.NotIn: ast.In(), 59 | } 60 | if type(expr.op) in op_map: 61 | expr.op = op_map[type(expr.op)] 62 | return expr 63 | 64 | return ast.UnaryOp(op=ast.Not(), operand=expr) 65 | 66 | 67 | def _negate_condition(*, ctx: MutationCtx, **_) -> ast.If: 68 | ctx.node.test = _negate_expr(ctx.node.test) 69 | return ctx.node 70 | 71 | 72 | def _swap_branches(*, ctx: MutationCtx, **_) -> ast.If: 73 | ctx.node.body, ctx.node.orelse = ctx.node.orelse, ctx.node.body 74 | ctx.node.test = _negate_expr(ctx.node.test) 75 | return ctx.node 76 | 77 | 78 | def _inject_statement(*, ctx: MutationCtx, **_) -> ast.If: 79 | # Pick which branch to inject into 80 | if ctx.node.orelse and ctx.rng.random() < 0.5: 81 | target = ctx.node.orelse 82 | else: 83 | target = ctx.node.body 84 | 85 | with ctx.context.new_scope(ScopeType.IF): 86 | ctx.stmt_gen.inject_statements( 87 | target, ctx.context, ctx.node, depth=1, n_stmts=1 88 | ) 89 | return ctx.node 90 | -------------------------------------------------------------------------------- /src/fuzzer/runner/multi_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multi-runner that orchestrates running scenarios across Ivy and multiple Boa configurations. 3 | 4 | TODO: Long-term, CompilerConfig list should be loaded from a user config file 5 | rather than hardcoded, allowing users to specify which compiler settings to fuzz. 6 | """ 7 | 8 | from dataclasses import dataclass, field 9 | from typing import Any, Dict, List 10 | 11 | from vyper.compiler.settings import OptimizationLevel 12 | 13 | from .base_scenario_runner import ScenarioResult 14 | from .scenario import Scenario 15 | from .ivy_scenario_runner import IvyScenarioRunner 16 | from .boa_scenario_runner import BoaScenarioRunner 17 | 18 | 19 | @dataclass 20 | class CompilerConfig: 21 | """Configuration for a Boa runner with specific compiler settings.""" 22 | 23 | name: str 24 | compiler_args: Dict[str, Any] = field(default_factory=dict) 25 | 26 | 27 | @dataclass 28 | class MultiRunnerResults: 29 | """Results from running a scenario across all runners.""" 30 | 31 | ivy_result: ScenarioResult 32 | boa_results: Dict[str, tuple[CompilerConfig, ScenarioResult]] 33 | 34 | 35 | class MultiRunner: 36 | """Orchestrates running scenarios across Ivy and multiple Boa configurations.""" 37 | 38 | # Hardcoded configs for now - will be loaded from user config later 39 | BOA_CONFIGS = [ 40 | # CompilerConfig("default", {}), 41 | CompilerConfig("venom", {"experimental_codegen": True}), 42 | CompilerConfig("codesize", {"optimize": OptimizationLevel.CODESIZE}), 43 | CompilerConfig("gas", {"optimize": OptimizationLevel.GAS}), 44 | ] 45 | 46 | def __init__( 47 | self, 48 | collect_storage_dumps: bool = False, 49 | no_solc_json: bool = False, 50 | ): 51 | self.boa_configs = self.BOA_CONFIGS 52 | self.collect_storage_dumps = collect_storage_dumps 53 | self.no_solc_json = no_solc_json 54 | 55 | def run(self, scenario: Scenario) -> MultiRunnerResults: 56 | """Run scenario on all runners, creating fresh runner instances.""" 57 | # Create fresh Ivy runner 58 | ivy_runner = IvyScenarioRunner( 59 | collect_storage_dumps=self.collect_storage_dumps, 60 | no_solc_json=self.no_solc_json, 61 | ) 62 | ivy_result = ivy_runner.run(scenario) 63 | 64 | # Create fresh Boa runner for each config 65 | boa_results: Dict[str, tuple[CompilerConfig, ScenarioResult]] = {} 66 | for config in self.boa_configs: 67 | runner = BoaScenarioRunner( 68 | compiler_args=config.compiler_args, 69 | collect_storage_dumps=self.collect_storage_dumps, 70 | ) 71 | result = runner.run(scenario) 72 | boa_results[config.name] = (config, result) 73 | 74 | return MultiRunnerResults( 75 | ivy_result=ivy_result, 76 | boa_results=boa_results, 77 | ) 78 | 79 | def get_config_names(self) -> List[str]: 80 | """Return list of all Boa config names.""" 81 | return [c.name for c in self.boa_configs] 82 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/modules/test_interface_imports.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_import_interface_types(make_input_bundle, get_contract): 5 | ifaces = """ 6 | interface IFoo: 7 | def foo() -> uint256: nonpayable 8 | """ 9 | 10 | foo_impl = """ 11 | import ifaces 12 | 13 | implements: ifaces.IFoo 14 | 15 | @external 16 | def foo() -> uint256: 17 | return block.number 18 | """ 19 | 20 | contract = """ 21 | import ifaces 22 | 23 | @external 24 | def test_foo(s: ifaces.IFoo) -> bool: 25 | assert extcall s.foo() == block.number 26 | return True 27 | """ 28 | 29 | input_bundle = make_input_bundle({"ifaces.vy": ifaces}) 30 | 31 | foo = get_contract(foo_impl, input_bundle=input_bundle) 32 | c = get_contract(contract, input_bundle=input_bundle) 33 | 34 | assert c.test_foo(foo.address) is True 35 | 36 | 37 | def test_import_interface_types_stability(make_input_bundle, get_contract): 38 | lib1 = """ 39 | from ethereum.ercs import IERC20 40 | """ 41 | lib2 = """ 42 | from ethereum.ercs import IERC20 43 | """ 44 | 45 | main = """ 46 | import lib1 47 | import lib2 48 | 49 | from ethereum.ercs import IERC20 50 | 51 | @external 52 | def foo() -> bool: 53 | # check that this typechecks both directions 54 | a: lib1.IERC20 = IERC20(msg.sender) 55 | b: lib2.IERC20 = IERC20(msg.sender) 56 | c: IERC20 = lib1.IERC20(msg.sender) # allowed in call position 57 | 58 | # return the equality so we can sanity check it 59 | return a == b and b == c 60 | """ 61 | input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2}) 62 | c = get_contract(main, input_bundle=input_bundle) 63 | 64 | assert c.foo() is True 65 | 66 | 67 | @pytest.mark.parametrize("interface_syntax", ["__at__", "__interface__"]) 68 | def test_intrinsic_interface(get_contract, make_input_bundle, interface_syntax): 69 | lib = """ 70 | @external 71 | @view 72 | def foo() -> uint256: 73 | # detect self call 74 | if msg.sender == self: 75 | return 4 76 | else: 77 | return 5 78 | """ 79 | 80 | main = f""" 81 | import lib 82 | 83 | exports: lib.__interface__ 84 | 85 | @external 86 | @view 87 | def bar() -> uint256: 88 | return staticcall lib.{interface_syntax}(self).foo() 89 | """ 90 | input_bundle = make_input_bundle({"lib.vy": lib}) 91 | c = get_contract(main, input_bundle=input_bundle) 92 | 93 | assert c.foo() == 5 94 | assert c.bar() == 4 95 | 96 | 97 | def test_import_interface_flags(make_input_bundle, get_contract): 98 | ifaces = """ 99 | flag Foo: 100 | BOO 101 | MOO 102 | POO 103 | 104 | interface IFoo: 105 | def foo() -> Foo: nonpayable 106 | """ 107 | 108 | contract = """ 109 | import ifaces 110 | 111 | implements: ifaces 112 | 113 | @external 114 | def foo() -> ifaces.Foo: 115 | return ifaces.Foo.POO 116 | """ 117 | 118 | input_bundle = make_input_bundle({"ifaces.vyi": ifaces}) 119 | 120 | c = get_contract(contract, input_bundle=input_bundle) 121 | 122 | assert c.foo() == 4 123 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/mutations/int_literal.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from vyper.ast import nodes as ast 4 | from vyper.semantics.types import IntegerT 5 | 6 | from ..strategy import Strategy, StrategyRegistry 7 | from .base import MutationCtx 8 | from src.fuzzer.xfail import XFailExpectation 9 | 10 | 11 | def _check_bounds_and_xfail(ctx: MutationCtx, new_value: int) -> None: 12 | """Check if new_value is within type bounds, add xfail if not.""" 13 | if ctx.inferred_type is None or not isinstance(ctx.inferred_type, IntegerT): 14 | return 15 | lower, upper = ctx.inferred_type.ast_bounds 16 | if new_value < lower or new_value > upper: 17 | ctx.context.compilation_xfails.append( 18 | XFailExpectation( 19 | kind="compilation", 20 | reason=f"integer literal {new_value} out of bounds for {ctx.inferred_type}", 21 | ) 22 | ) 23 | 24 | 25 | def register(registry: StrategyRegistry) -> None: 26 | registry.register( 27 | Strategy( 28 | name="int.add_one", 29 | type_classes=(ast.Int,), 30 | tags=frozenset({"mutation", "int"}), 31 | is_applicable=lambda **_: True, 32 | weight=lambda **_: 1.0, 33 | run=_add_one, 34 | ) 35 | ) 36 | registry.register( 37 | Strategy( 38 | name="int.subtract_one", 39 | type_classes=(ast.Int,), 40 | tags=frozenset({"mutation", "int"}), 41 | is_applicable=lambda **_: True, 42 | weight=lambda **_: 1.0, 43 | run=_subtract_one, 44 | ) 45 | ) 46 | registry.register( 47 | Strategy( 48 | name="int.set_zero", 49 | type_classes=(ast.Int,), 50 | tags=frozenset({"mutation", "int"}), 51 | is_applicable=lambda **_: True, 52 | weight=lambda **_: 1.0, 53 | run=_set_zero, 54 | ) 55 | ) 56 | registry.register( 57 | Strategy( 58 | name="int.type_aware", 59 | type_classes=(ast.Int,), 60 | tags=frozenset({"mutation", "int"}), 61 | is_applicable=_has_integer_type, 62 | weight=lambda **_: 2.0, 63 | run=_type_aware_mutate, 64 | ) 65 | ) 66 | 67 | 68 | def _has_integer_type(*, ctx: MutationCtx, **_) -> bool: 69 | return ctx.inferred_type is not None and isinstance(ctx.inferred_type, IntegerT) 70 | 71 | 72 | def _add_one(*, ctx: MutationCtx, **_) -> ast.Int: 73 | new_value = ctx.node.value + 1 74 | _check_bounds_and_xfail(ctx, new_value) 75 | ctx.node.value = new_value 76 | return ctx.node 77 | 78 | 79 | def _subtract_one(*, ctx: MutationCtx, **_) -> ast.Int: 80 | new_value = ctx.node.value - 1 81 | _check_bounds_and_xfail(ctx, new_value) 82 | ctx.node.value = new_value 83 | return ctx.node 84 | 85 | 86 | def _set_zero(*, ctx: MutationCtx, **_) -> ast.Int: 87 | ctx.node.value = 0 88 | return ctx.node 89 | 90 | 91 | def _type_aware_mutate(*, ctx: MutationCtx, **_) -> ast.Int: 92 | ctx.node.value = ctx.value_mutator.mutate(ctx.node.value, ctx.inferred_type) 93 | return ctx.node 94 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/base_value_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base class for type-dispatched value generation. 3 | 4 | Provides the dispatch mechanism for generating values based on Vyper types. 5 | Subclasses implement specific generation strategies (boundary values for ABI 6 | fuzzing, small literals for codegen, etc.). 7 | """ 8 | 9 | import random 10 | from abc import ABC, abstractmethod 11 | from typing import Any, Optional 12 | 13 | from decimal import Decimal 14 | 15 | from vyper.semantics.types import ( 16 | AddressT, 17 | BoolT, 18 | BytesM_T, 19 | BytesT, 20 | DArrayT, 21 | DecimalT, 22 | IntegerT, 23 | SArrayT, 24 | StringT, 25 | StructT, 26 | TupleT, 27 | VyperType, 28 | ) 29 | 30 | 31 | class BaseValueGenerator(ABC): 32 | """Base class for type-dispatched value generation.""" 33 | 34 | def __init__(self, rng: Optional[random.Random] = None): 35 | self.rng = rng or random.Random() 36 | 37 | self._generators = { 38 | TupleT: self._generate_tuple, 39 | StructT: self._generate_struct, 40 | SArrayT: self._generate_sarray, 41 | DArrayT: self._generate_darray, 42 | StringT: self._generate_string, 43 | BytesT: self._generate_bytes, 44 | IntegerT: self._generate_integer, 45 | BytesM_T: self._generate_bytes_m, 46 | BoolT: self._generate_bool, 47 | AddressT: self._generate_address, 48 | DecimalT: self._generate_decimal, 49 | } 50 | 51 | def generate(self, vyper_type: VyperType) -> Any: 52 | """Generate a value for the given type.""" 53 | generator = self._generators.get(type(vyper_type)) 54 | assert generator is not None, f"No generator for {type(vyper_type).__name__}" 55 | return generator(vyper_type) 56 | 57 | # Composite types - shared implementation 58 | def _generate_tuple(self, vyper_type: TupleT) -> tuple: 59 | return tuple(self.generate(t) for t in vyper_type.member_types) 60 | 61 | def _generate_struct(self, vyper_type: StructT) -> dict: 62 | return { 63 | name: self.generate(field_type) 64 | for name, field_type in vyper_type.members.items() 65 | } 66 | 67 | def _generate_sarray(self, vyper_type: SArrayT) -> list: 68 | return [self.generate(vyper_type.value_type) for _ in range(vyper_type.length)] 69 | 70 | def _generate_darray(self, vyper_type: DArrayT) -> list: 71 | n = self.rng.randint(0, vyper_type.length) 72 | return [self.generate(vyper_type.value_type) for _ in range(n)] 73 | 74 | def _generate_bool(self, _vyper_type: BoolT) -> bool: 75 | return self.rng.choice([True, False]) 76 | 77 | # Abstract methods - subclasses define behavior 78 | @abstractmethod 79 | def _generate_integer(self, vyper_type: IntegerT) -> int: ... 80 | 81 | @abstractmethod 82 | def _generate_address(self, vyper_type: AddressT) -> str: ... 83 | 84 | @abstractmethod 85 | def _generate_bytes_m(self, vyper_type: BytesM_T) -> bytes: ... 86 | 87 | @abstractmethod 88 | def _generate_bytes(self, vyper_type: BytesT) -> bytes: ... 89 | 90 | @abstractmethod 91 | def _generate_string(self, vyper_type: StringT) -> str: ... 92 | 93 | @abstractmethod 94 | def _generate_decimal(self, vyper_type: DecimalT) -> Decimal: ... 95 | -------------------------------------------------------------------------------- /src/ivy/expr/default_values.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Type 2 | 3 | from vyper.semantics.types import ( 4 | IntegerT, 5 | DArrayT, 6 | SArrayT, 7 | BytesT, 8 | BytesM_T, 9 | StringT, 10 | StructT, 11 | HashMapT, 12 | BoolT, 13 | AddressT, 14 | InterfaceT, 15 | FlagT, 16 | TupleT, 17 | DecimalT, 18 | ) 19 | from ivy.types import ( 20 | Address, 21 | Struct, 22 | Flag, 23 | StaticArray, 24 | DynamicArray, 25 | Map, 26 | VyperDecimal, 27 | Tuple as IvyTuple, 28 | ) 29 | 30 | DEFAULT_FACTORY_REGISTRY: dict[Type[Any], Callable[[Any], Any]] = {} 31 | 32 | 33 | def register_default( 34 | type_cls: Type[Any], 35 | ) -> Callable[[Callable[[Any], Any]], Callable[[Any], Any]]: 36 | """ 37 | Decorator to register a default-value factory for a given Vyper semantic type. 38 | 39 | Usage: 40 | @register_default(IntegerT) 41 | def default_integer(typ: IntegerT) -> int: 42 | return 0 43 | """ 44 | 45 | def decorator(fn: Callable[[Any], Any]) -> Callable[[Any], Any]: 46 | DEFAULT_FACTORY_REGISTRY[type_cls] = fn 47 | return fn 48 | 49 | return decorator 50 | 51 | 52 | def get_default_value(typ: Any) -> Any: 53 | factory = DEFAULT_FACTORY_REGISTRY.get(type(typ)) 54 | if factory is None: 55 | raise KeyError(f"No default factory registered for type {type(typ)}") 56 | return factory(typ) 57 | 58 | 59 | @register_default(IntegerT) 60 | def default_integer(_: IntegerT) -> int: 61 | return 0 62 | 63 | 64 | @register_default(DecimalT) 65 | def default_decimal(_: DecimalT) -> VyperDecimal: 66 | return VyperDecimal(0) 67 | 68 | 69 | @register_default(BoolT) 70 | def default_bool(_: BoolT) -> bool: 71 | return False 72 | 73 | 74 | @register_default(AddressT) 75 | @register_default(InterfaceT) 76 | def default_address(_: Any) -> Address: 77 | return Address(0) 78 | 79 | 80 | @register_default(FlagT) 81 | def default_flag(typ: FlagT) -> Flag: 82 | return Flag(typ, 0) 83 | 84 | 85 | @register_default(DArrayT) 86 | def default_dynamic_array(typ: DArrayT) -> DynamicArray: 87 | return DynamicArray(typ) 88 | 89 | 90 | @register_default(SArrayT) 91 | def default_static_array(typ: SArrayT) -> StaticArray: 92 | return StaticArray(typ) 93 | 94 | 95 | @register_default(BytesT) 96 | def default_bytes(_: BytesT) -> bytes: 97 | # variable length byte array defaults to empty bytes 98 | return b"" 99 | 100 | 101 | @register_default(BytesM_T) 102 | def default_fixed_bytes(typ: BytesM_T) -> bytes: 103 | # fixed-length bytes default to zero-filled 104 | return b"\x00" * typ.length 105 | 106 | 107 | @register_default(StringT) 108 | def default_string(_: StringT) -> str: 109 | return "" 110 | 111 | 112 | @register_default(StructT) 113 | def default_struct(typ: StructT) -> Struct: 114 | # build defaults for each member 115 | defaults = { 116 | name: get_default_value(member_typ) for name, member_typ in typ.members.items() 117 | } 118 | return Struct(typ, defaults) 119 | 120 | 121 | @register_default(HashMapT) 122 | def default_map(typ: HashMapT) -> Map: 123 | return Map(typ) 124 | 125 | 126 | @register_default(TupleT) 127 | def default_tuple(typ: TupleT) -> IvyTuple: 128 | return IvyTuple(typ) 129 | -------------------------------------------------------------------------------- /src/fuzzer/runner/scenario.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enhanced scenario structure for unified execution. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from dataclasses import dataclass, field 8 | from typing import List, Optional 9 | from pathlib import Path 10 | 11 | from ..trace_types import Trace, TestItem, TestExport 12 | from ..export_utils import load_export 13 | 14 | 15 | @dataclass 16 | class Scenario: 17 | """ 18 | Scenario that can represent both fuzzer mutations and replay traces. 19 | 20 | This structure stores all traces (deployments, calls, etc.) in execution order 21 | and handles mutations uniformly for any trace type. 22 | """ 23 | 24 | traces: List[Trace] = field(default_factory=list) 25 | 26 | # Dependencies as fully-resolved Scenario objects (executed depth-first) 27 | dependencies: List[Scenario] = field(default_factory=list) 28 | 29 | # Unique identifier for deduplication during execution (e.g., "path::item_name") 30 | scenario_id: Optional[str] = None 31 | 32 | # Configuration 33 | use_python_args: bool = True # Default to python args for fuzzing 34 | 35 | 36 | def _fixup_dep_path(dep_path_str: str) -> Path: 37 | """Fix path prefix from 'tests/export/' to 'tests/vyper-exports/'.""" 38 | if dep_path_str.startswith("tests/export/"): 39 | dep_path_str = dep_path_str.replace("tests/export/", "tests/vyper-exports/", 1) 40 | return Path(dep_path_str) 41 | 42 | 43 | def _load_dependency_scenario( 44 | dep_path: Path, 45 | dep_item_name: str, 46 | use_python_args: bool, 47 | ) -> Scenario: 48 | """Load a dependency from disk and create a Scenario.""" 49 | dep_export = load_export(dep_path) 50 | 51 | if dep_item_name not in dep_export.items: 52 | raise ValueError(f"Dependency {dep_item_name} not found in {dep_path}") 53 | 54 | scenario_id = f"{dep_path}::{dep_item_name}" 55 | return create_scenario_from_item( 56 | dep_export.items[dep_item_name], 57 | use_python_args, 58 | scenario_id=scenario_id, 59 | ) 60 | 61 | 62 | def create_scenario_from_item( 63 | item: TestItem, 64 | use_python_args: bool = True, 65 | scenario_id: Optional[str] = None, 66 | ) -> Scenario: 67 | """Create a Scenario from a test item, recursively loading all dependencies.""" 68 | dependencies = [] 69 | for dep in item.deps: 70 | dep_path_str, dep_name = dep.rsplit("/", 1) 71 | dep_path = _fixup_dep_path(dep_path_str) 72 | dep_scenario = _load_dependency_scenario(dep_path, dep_name, use_python_args) 73 | dependencies.append(dep_scenario) 74 | 75 | return Scenario( 76 | traces=item.traces, 77 | dependencies=dependencies, 78 | scenario_id=scenario_id, 79 | use_python_args=use_python_args, 80 | ) 81 | 82 | 83 | def create_scenario_from_export( 84 | export: TestExport, 85 | item_name: str, 86 | use_python_args: bool = True, 87 | ) -> Scenario: 88 | """ 89 | Create a Scenario from a test export and item name. 90 | 91 | Convenience function that looks up the item and creates a scenario. 92 | """ 93 | if item_name not in export.items: 94 | raise ValueError(f"Test item '{item_name}' not found in export") 95 | 96 | item = export.items[item_name] 97 | scenario_id = f"{export.path}::{item_name}" 98 | return create_scenario_from_item(item, use_python_args, scenario_id=scenario_id) 99 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Generator 2 | 3 | import pytest 4 | from contextlib import contextmanager 5 | 6 | from vyper.compiler.input_bundle import FilesystemInputBundle 7 | from vyper.utils import keccak256 8 | 9 | from ivy.frontend.env import Env 10 | from ivy.frontend.loader import loads 11 | from ivy.frontend.vyper_contract import VyperContract 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def clear_env(): 16 | environment = Env().get_singleton() 17 | environment.clear_state() 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def env(clear_env): 22 | environment = Env().get_singleton() 23 | return environment 24 | 25 | 26 | @pytest.fixture(scope="module") 27 | def get_contract(clear_env): 28 | def fn(source_code, *args, **kwargs) -> VyperContract: 29 | return loads(source_code, *args, **kwargs) 30 | 31 | return fn 32 | 33 | 34 | # adapted from vyper: https://github.com/vyperlang/vyper/blob/6843e7915729f3a3ea0d8c765dffa52033f5818e/tests/conftest.py#L306 35 | @pytest.fixture(scope="module") 36 | def tx_failed(): 37 | @contextmanager 38 | # TODO make ivy-specific general exception 39 | def fn(exception=Exception, text=None): 40 | with pytest.raises(exception) as excinfo: 41 | yield 42 | 43 | if text: 44 | # TODO test equality 45 | assert text in str(excinfo.value), (text, excinfo.value) 46 | 47 | return fn 48 | 49 | 50 | @pytest.fixture 51 | def make_file(tmp_path): 52 | # writes file_contents to file_name, creating it in the 53 | # tmp_path directory. returns final path. 54 | def fn(file_name, file_contents): 55 | path = tmp_path / file_name 56 | path.parent.mkdir(parents=True, exist_ok=True) 57 | with path.open("w") as f: 58 | f.write(file_contents) 59 | 60 | return path 61 | 62 | return fn 63 | 64 | 65 | # adapted from vyper: https://github.com/vyperlang/vyper/blob/6843e7915729f3a3ea0d8c765dffa52033f5818e/tests/conftest.py#L160-L159 66 | # this can either be used for its side effects (to prepare a call 67 | # to get_contract), or the result can be provided directly to 68 | # compile_code / CompilerData. 69 | @pytest.fixture 70 | def make_input_bundle(tmp_path, make_file): 71 | def fn(sources_dict): 72 | for file_name, file_contents in sources_dict.items(): 73 | make_file(file_name, file_contents) 74 | return FilesystemInputBundle([tmp_path]) 75 | 76 | return fn 77 | 78 | 79 | @pytest.fixture 80 | def get_logs(): 81 | def fn( 82 | contract: VyperContract, 83 | event_name: Optional[str] = None, 84 | vyper_compat: bool = True, 85 | ): 86 | logs = contract.get_logs(include_id=True) 87 | 88 | if vyper_compat: 89 | for log in logs: 90 | log.args = log.args_obj 91 | 92 | if event_name: 93 | return [log for log in logs if log.event == event_name] 94 | 95 | return logs 96 | 97 | return fn 98 | 99 | 100 | @pytest.fixture 101 | def keccak(): 102 | return keccak256 103 | 104 | 105 | @pytest.hookimpl(hookwrapper=True) 106 | def pytest_runtest_call(item) -> Generator: 107 | """Isolate tests by reverting the state of the environment after each test.""" 108 | env = Env().get_singleton() 109 | with env.anchor(): 110 | yield 111 | -------------------------------------------------------------------------------- /src/fuzzer/runner/boa_scenario_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Boa implementation of the scenario runner. 3 | """ 4 | 5 | from typing import Any, Dict, List, Optional 6 | 7 | import boa 8 | 9 | from .base_scenario_runner import BaseScenarioRunner, ScenarioResult 10 | from .scenario import Scenario 11 | from ..trace_types import Env 12 | 13 | 14 | class BoaScenarioRunner(BaseScenarioRunner): 15 | """Runner for executing scenarios in Boa.""" 16 | 17 | def __init__( 18 | self, 19 | compiler_args: Optional[Dict[str, Any]] = None, 20 | collect_storage_dumps: bool = False, 21 | ): 22 | super().__init__(boa.env, collect_storage_dumps) 23 | self.compiler_args = compiler_args or {} 24 | 25 | def _deploy_from_source( 26 | self, 27 | source: str, 28 | solc_json: Optional[Dict[str, Any]], 29 | args: List[Any], 30 | kwargs: Dict[str, Any], 31 | sender: Optional[str] = None, 32 | compiler_settings: Optional[Dict[str, Any]] = None, 33 | ) -> Any: 34 | """Deploy a contract from source in Boa.""" 35 | sender = self._get_sender(sender) 36 | 37 | # Merge trace compiler_settings with runner's compiler_args. 38 | # Runner args (e.g. experimental_codegen) take precedence. 39 | merged_args = {**(compiler_settings or {}), **self.compiler_args} 40 | 41 | with self.env.prank(sender): 42 | self.env.set_balance( 43 | sender, self._get_balance(sender) + kwargs.get("value", 0) + 10**18 44 | ) 45 | contract = boa.loads(source, *args, compiler_args=merged_args, **kwargs) 46 | return contract 47 | 48 | def _call_method( 49 | self, 50 | contract: Any, 51 | method_name: str, 52 | args: List[Any], 53 | kwargs: Dict[str, Any], 54 | sender: Optional[str] = None, 55 | ) -> Any: 56 | """Call a contract method in Boa.""" 57 | sender = self._get_sender(sender) 58 | with self.env.prank(sender): 59 | self._set_balance( 60 | sender, self._get_balance(sender) + kwargs.get("value", 0) + 10**18 61 | ) 62 | method = getattr(contract, method_name) 63 | result = method(*args, **kwargs) 64 | 65 | return result 66 | 67 | def _set_balance(self, address: str, value: int) -> None: 68 | self.env.set_balance(address, value) 69 | 70 | def _get_balance(self, address: str) -> int: 71 | return self.env.get_balance(address) 72 | 73 | def _message_call( 74 | self, 75 | to_address: str, 76 | data: bytes, 77 | value: int = 0, 78 | sender: Optional[str] = None, 79 | ) -> bytes: 80 | raise NotImplementedError("TODO does boa support this") 81 | 82 | def _clear_transient_storage(self) -> None: 83 | raise NotImplementedError("TODO does boa support this") 84 | 85 | def _get_storage_dump(self, contract: Any) -> Optional[Dict[str, Any]]: 86 | return contract._storage.dump() 87 | 88 | def _set_block_env(self, trace_env: Optional[Env]) -> None: 89 | if trace_env is None: 90 | return 91 | self.env.vm.patch.block_number = trace_env.block.number 92 | self.env.vm.patch.timestamp = trace_env.block.timestamp 93 | 94 | def run(self, scenario: Scenario) -> ScenarioResult: 95 | with self.env.anchor(): 96 | return super().run(scenario) 97 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/numberz/test_isqrt.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import hypothesis 4 | import pytest 5 | 6 | from vyper.utils import SizeLimits 7 | 8 | 9 | @pytest.fixture(scope="module") 10 | def isqrt_contract(get_contract): 11 | code = """ 12 | @external 13 | def test(a: uint256) -> uint256: 14 | return isqrt(a) 15 | """ 16 | c = get_contract(code) 17 | return c 18 | 19 | 20 | def test_isqrt_literal(get_contract): 21 | val = 2 22 | code = f""" 23 | @external 24 | def test() -> uint256: 25 | return isqrt({val}) 26 | """ 27 | c = get_contract(code) 28 | assert c.test() == math.isqrt(val) 29 | 30 | 31 | def test_isqrt_variable(get_contract): 32 | code = """ 33 | @external 34 | def test(a: uint256) -> uint256: 35 | return isqrt(a) 36 | """ 37 | 38 | c = get_contract(code) 39 | 40 | val = 3333 41 | assert c.test(val) == math.isqrt(val) 42 | 43 | val = 10**17 44 | assert c.test(val) == math.isqrt(val) 45 | assert c.test(0) == 0 46 | 47 | 48 | def test_isqrt_internal_variable(get_contract): 49 | val = 44001 50 | code = f""" 51 | @external 52 | def test2() -> uint256: 53 | a: uint256 = {val} 54 | return isqrt(a) 55 | """ 56 | c = get_contract(code) 57 | assert c.test2() == math.isqrt(val) 58 | 59 | 60 | def test_isqrt_storage(get_contract): 61 | code = """ 62 | s_var: uint256 63 | 64 | @external 65 | def test(a: uint256) -> uint256: 66 | self.s_var = a + 1 67 | return isqrt(self.s_var) 68 | """ 69 | 70 | c = get_contract(code) 71 | val = 1221 72 | assert c.test(val) == math.isqrt(val + 1) 73 | val = 10001 74 | assert c.test(val) == math.isqrt(val + 1) 75 | 76 | 77 | def test_isqrt_storage_internal_variable(get_contract): 78 | val = 44444 79 | code = f""" 80 | s_var: uint256 81 | 82 | @external 83 | def test2() -> uint256: 84 | self.s_var = {val} 85 | return isqrt(self.s_var) 86 | """ 87 | c = get_contract(code) 88 | assert c.test2() == math.isqrt(val) 89 | 90 | 91 | def test_isqrt_inline_memory_correct(get_contract): 92 | code = """ 93 | @external 94 | def test(a: uint256) -> (uint256, uint256, uint256, uint256, uint256, String[100]): 95 | x: uint256 = 1 96 | y: uint256 = 2 97 | z: uint256 = 3 98 | e: uint256 = isqrt(a) 99 | f: String[100] = 'hello world' 100 | return a, x, y, z, e, f 101 | """ 102 | 103 | c = get_contract(code) 104 | 105 | val = 21 106 | assert c.test(val) == (val, 1, 2, 3, math.isqrt(val), "hello world") 107 | 108 | 109 | @pytest.mark.fuzzing 110 | @hypothesis.given( 111 | value=hypothesis.strategies.integers(min_value=0, max_value=SizeLimits.MAX_UINT256) 112 | ) 113 | @hypothesis.example(SizeLimits.MAX_UINT256) 114 | @hypothesis.example(0) 115 | @hypothesis.example(1) 116 | # the following examples demonstrate correct rounding mode 117 | # for an edge case in the babylonian method - the operand is 118 | # a perfect square - 1 119 | @hypothesis.example(2704) 120 | @hypothesis.example(110889) 121 | @hypothesis.example(32239684) 122 | def test_isqrt_valid_range(isqrt_contract, value): 123 | vyper_isqrt = isqrt_contract.test(value) 124 | actual_isqrt = math.isqrt(value) 125 | assert vyper_isqrt == actual_isqrt 126 | 127 | # check if sqrt limits are correct 128 | next = vyper_isqrt + 1 129 | assert vyper_isqrt * vyper_isqrt <= value 130 | assert next * next > value 131 | -------------------------------------------------------------------------------- /tests/unparser/test_unparser.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import vyper 4 | from vyper.compiler.phases import CompilerData 5 | 6 | from unparser.unparser import unparse 7 | from fuzzer.export_utils import load_all_exports, extract_test_cases, TestFilter 8 | 9 | # attributes that never affect semantics 10 | _IGNORE = { 11 | "lineno", 12 | "col_offset", 13 | "end_lineno", 14 | "end_col_offset", 15 | "src", 16 | "node_id", 17 | "path", 18 | "resolved_path", 19 | "source_id", 20 | "source_sha256sum", 21 | # analysis caches 22 | "variable_reads", 23 | "variable_writes", 24 | "type", 25 | } 26 | 27 | 28 | def _strip(obj): 29 | """Strip non-semantic attributes from AST nodes.""" 30 | if isinstance(obj, list): 31 | return [_strip(i) for i in obj] 32 | if isinstance(obj, dict): 33 | return {k: _strip(v) for k, v in obj.items() if k not in _IGNORE} 34 | return obj 35 | 36 | 37 | def _as_clean_dict(code: str) -> dict: 38 | """Convert source code to a normalized AST dict for comparison.""" 39 | ast = CompilerData(code).annotated_vyper_module 40 | return _strip(ast.to_dict()) 41 | 42 | 43 | def test_unparser(): 44 | """Test that unparsing Vyper test exports produces semantically equivalent code.""" 45 | # Load test exports 46 | exports_dir = Path("tests/vyper-exports") 47 | if not exports_dir.exists(): 48 | print(f"Warning: Test exports directory not found at {exports_dir}") 49 | return 50 | 51 | # Create filter to only test source deployments 52 | test_filter = TestFilter() 53 | # Skip tests with features that might not roundtrip perfectly 54 | test_filter.exclude_source(r"#\s*@version") # Skip version pragmas 55 | test_filter.exclude_source(r"@nonreentrant") # Skip nonreentrant for now 56 | 57 | # Load and extract test cases 58 | exports = load_all_exports(exports_dir) 59 | test_cases = extract_test_cases(exports) 60 | 61 | print(f"Testing unparser roundtrip on {len(test_cases)} test cases...") 62 | 63 | passed = 0 64 | failed = 0 65 | 66 | for i, (source_code, _) in enumerate(test_cases): 67 | try: 68 | # Parse the original source 69 | original_ast = vyper.ast.parse_to_ast(source_code) 70 | 71 | # Unparse it back to source 72 | roundtrip_source = unparse(original_ast) 73 | 74 | # Compare normalized ASTs 75 | original_dict = _as_clean_dict(source_code) 76 | roundtrip_dict = _as_clean_dict(roundtrip_source) 77 | 78 | if original_dict == roundtrip_dict: 79 | passed += 1 80 | else: 81 | failed += 1 82 | print(f"\nFailed roundtrip for test case {i + 1}:") 83 | print(f"Original source:\n{source_code[:200]}...") 84 | print(f"Roundtrip source:\n{roundtrip_source[:200]}...") 85 | 86 | except Exception as e: 87 | failed += 1 88 | print(f"\nError in test case {i + 1}: {e}") 89 | print(f"Source:\n{source_code[:200]}...") 90 | 91 | print(f"\n=== Unparser Test Results ===") 92 | print(f"Passed: {passed}") 93 | print(f"Failed: {failed}") 94 | print(f"Total: {passed + failed}") 95 | 96 | # Assert all tests passed 97 | assert failed == 0, f"{failed} tests failed" 98 | 99 | 100 | if __name__ == "__main__": 101 | test_unparser() 102 | -------------------------------------------------------------------------------- /src/fuzzer/runner/ivy_scenario_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ivy implementation of the scenario runner. 3 | """ 4 | 5 | from typing import Any, Dict, List, Optional 6 | 7 | import ivy 8 | from ivy.frontend.loader import loads_from_solc_json, loads 9 | from ivy.types import Address 10 | 11 | from .base_scenario_runner import BaseScenarioRunner, ScenarioResult 12 | from .scenario import Scenario 13 | from ..trace_types import Env 14 | 15 | 16 | class IvyScenarioRunner(BaseScenarioRunner): 17 | """Runner for executing scenarios in Ivy.""" 18 | 19 | def __init__(self, collect_storage_dumps: bool = False, no_solc_json: bool = False): 20 | super().__init__(ivy.env, collect_storage_dumps) 21 | self._original_eoa = None 22 | self.no_solc_json = no_solc_json 23 | 24 | def _deploy_from_source( 25 | self, 26 | source: str, 27 | solc_json: Optional[Dict[str, Any]], 28 | args: List[Any], 29 | kwargs: Dict[str, Any], 30 | sender: Optional[str] = None, 31 | compiler_settings: Optional[Dict[str, Any]] = None, 32 | ) -> Any: 33 | sender = self._get_sender(sender) 34 | 35 | with self.env.prank(sender): 36 | # Ensure deployer has enough balance 37 | self.env.set_balance( 38 | self.env.eoa, 39 | self.env.get_balance(self.env.eoa) + kwargs.get("value", 0) + 10**18, 40 | ) 41 | if (self.no_solc_json and source) or solc_json is None: 42 | contract = loads( 43 | source, *args, compiler_args=compiler_settings, **kwargs 44 | ) 45 | else: 46 | contract = loads_from_solc_json(solc_json, *args, **kwargs) 47 | 48 | return contract 49 | 50 | def _call_method( 51 | self, 52 | contract: Any, 53 | method_name: str, 54 | args: List[Any], 55 | kwargs: Dict[str, Any], 56 | sender: Optional[str] = None, 57 | ) -> Any: 58 | sender = self._get_sender(sender) 59 | with self.env.prank(sender): 60 | method = getattr(contract, method_name) 61 | result = method(*args, **kwargs) 62 | return result 63 | 64 | def _set_balance(self, address: str, value: int) -> None: 65 | addr = Address(address) 66 | self.env.set_balance(addr, value) 67 | 68 | def _get_balance(self, address: str) -> int: 69 | addr = Address(address) 70 | return self.env.get_balance(addr) 71 | 72 | def _message_call( 73 | self, 74 | to_address: str, 75 | data: bytes, 76 | value: int = 0, 77 | sender: Optional[str] = None, 78 | ) -> bytes: 79 | sender = self._get_sender(sender) 80 | with self.env.prank(sender): 81 | result = self.env.message_call( 82 | to_address=to_address, 83 | data=data, 84 | value=value, 85 | ) 86 | return result 87 | 88 | def _clear_transient_storage(self) -> None: 89 | self.env.clear_transient_storage() 90 | 91 | def _get_storage_dump(self, contract: Any) -> Dict[str, Any]: 92 | return contract.storage_dump() 93 | 94 | def _set_block_env(self, trace_env: Optional[Env]) -> None: 95 | if trace_env is None: 96 | return 97 | self.env.block_number = trace_env.block.number 98 | self.env.timestamp = trace_env.block.timestamp 99 | 100 | def run(self, scenario: Scenario) -> ScenarioResult: 101 | with self.env.anchor(): 102 | return super().run(scenario) 103 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/iteration/test_range_in.py: -------------------------------------------------------------------------------- 1 | from vyper.exceptions import TypeMismatch 2 | 3 | 4 | def test_basic_in_list(get_contract): 5 | code = """ 6 | @external 7 | def testin(x: int128) -> bool: 8 | y: int128 = 1 9 | s: int128[4] = [1, 2, 3, 4] 10 | if (x + 1) in s: 11 | return True 12 | return False 13 | """ 14 | 15 | c = get_contract(code) 16 | 17 | assert c.testin(0) is True 18 | assert c.testin(1) is True 19 | assert c.testin(2) is True 20 | assert c.testin(3) is True 21 | assert c.testin(4) is False 22 | assert c.testin(5) is False 23 | assert c.testin(-1) is False 24 | 25 | 26 | def test_in_storage_list(get_contract): 27 | code = """ 28 | allowed: int128[10] 29 | 30 | @external 31 | def in_test(x: int128) -> bool: 32 | self.allowed = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 33 | if x in self.allowed: 34 | return True 35 | return False 36 | """ 37 | 38 | c = get_contract(code) 39 | 40 | assert c.in_test(1) is True 41 | assert c.in_test(9) is True 42 | assert c.in_test(11) is False 43 | assert c.in_test(-1) is False 44 | assert c.in_test(32000) is False 45 | 46 | 47 | def test_in_calldata_list(get_contract): 48 | code = """ 49 | @external 50 | def in_test(x: int128, y: int128[10]) -> bool: 51 | if x in y: 52 | return True 53 | return False 54 | """ 55 | 56 | c = get_contract(code) 57 | 58 | assert c.in_test(1, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) is True 59 | assert c.in_test(9, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) is True 60 | assert c.in_test(11, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) is False 61 | assert c.in_test(-1, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) is False 62 | assert c.in_test(32000, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) is False 63 | 64 | 65 | def test_cmp_in_list(get_contract): 66 | code = """ 67 | @external 68 | def in_test(x: int128) -> bool: 69 | if x in [9, 7, 6, 5]: 70 | return True 71 | return False 72 | """ 73 | 74 | c = get_contract(code) 75 | 76 | assert c.in_test(1) is False 77 | assert c.in_test(-7) is False 78 | assert c.in_test(-9) is False 79 | assert c.in_test(5) is True 80 | assert c.in_test(7) is True 81 | 82 | 83 | def test_cmp_not_in_list(get_contract): 84 | code = """ 85 | @external 86 | def in_test(x: int128) -> bool: 87 | if x not in [9, 7, 6, 5]: 88 | return True 89 | return False 90 | """ 91 | 92 | c = get_contract(code) 93 | 94 | assert c.in_test(1) is True 95 | assert c.in_test(-7) is True 96 | assert c.in_test(-9) is True 97 | assert c.in_test(5) is False 98 | assert c.in_test(7) is False 99 | 100 | 101 | def test_ownership(env, tx_failed, get_contract): 102 | code = """ 103 | 104 | owners: address[2] 105 | 106 | @deploy 107 | def __init__(): 108 | self.owners[0] = msg.sender 109 | 110 | @external 111 | def set_owner(i: int128, new_owner: address): 112 | assert msg.sender in self.owners 113 | self.owners[i] = new_owner 114 | 115 | @external 116 | def is_owner() -> bool: 117 | return msg.sender in self.owners 118 | """ 119 | a1 = env.accounts[1] 120 | c = get_contract(code) 121 | 122 | assert c.is_owner() is True # contract creator is owner. 123 | assert c.is_owner(sender=a1) is False # no one else is. 124 | 125 | # only an owner may set another owner. 126 | with tx_failed(): 127 | c.set_owner(1, a1, sender=a1) 128 | 129 | c.set_owner(1, a1) 130 | assert c.is_owner(sender=a1) is True 131 | 132 | # Owner in place 0 can be replaced. 133 | c.set_owner(0, a1) 134 | assert c.is_owner() is False 135 | -------------------------------------------------------------------------------- /src/ivy/builtins/convert_utils.py: -------------------------------------------------------------------------------- 1 | # all the convert functionality is adapted from vyper's test suite 2 | # - https://github.com/vyperlang/vyper/blob/c32b9b4c6f0d8b8cdb103d3017ff540faf56a305/tests/functional/builtins/codegen/test_convert.py#L301 3 | 4 | import enum 5 | import math 6 | 7 | from vyper.semantics.types import ( 8 | BytesT, 9 | StringT, 10 | BytesM_T, 11 | DecimalT, 12 | IntegerT, 13 | BoolT, 14 | AddressT, 15 | ) 16 | from vyper.utils import unsigned_to_signed 17 | 18 | from ivy.abi import abi_encode, abi_decode, DecodeError 19 | from ivy.types import VyperDecimal 20 | 21 | 22 | class _PadDirection(enum.Enum): 23 | Left = enum.auto() 24 | Right = enum.auto() 25 | 26 | 27 | def _padding_direction(typ): 28 | if isinstance(typ, (BytesM_T, StringT, BytesT)): 29 | return _PadDirection.Right 30 | return _PadDirection.Left 31 | 32 | 33 | def _convert_decimal_to_int(val: VyperDecimal, o_typ: IntegerT): 34 | lo, hi = o_typ.int_bounds 35 | if not lo <= val.value <= hi: 36 | raise ConvertError() 37 | 38 | return val.truncate() 39 | 40 | 41 | def _convert_int_to_int(val, o_typ): 42 | lo, hi = o_typ.int_bounds 43 | if not lo <= val <= hi: 44 | raise ConvertError() 45 | return val 46 | 47 | 48 | def _convert_int_to_decimal(val, o_typ): 49 | assert isinstance(o_typ, DecimalT) 50 | try: 51 | ret = VyperDecimal(val) 52 | except ValueError: 53 | raise ConvertError() 54 | 55 | return ret 56 | 57 | 58 | def _to_bits(val, i_typ): 59 | # i_typ: the type to convert from 60 | if isinstance(i_typ, DecimalT): 61 | val = val * i_typ.divisor 62 | assert math.ceil(val) == math.floor(val) 63 | val = int(val) 64 | return abi_encode(i_typ, val) 65 | 66 | 67 | def _bits_of_type(typ): 68 | if isinstance(typ, (IntegerT, DecimalT)): 69 | return typ.bits 70 | if isinstance(typ, BoolT): 71 | return 8 72 | if isinstance(typ, AddressT): 73 | return 160 74 | if isinstance(typ, BytesM_T): 75 | return typ.m_bits 76 | if isinstance(typ, BytesT): 77 | return typ.length * 8 78 | 79 | raise Exception(f"Unknown type {typ}") 80 | 81 | 82 | def bytes_of_type(typ): 83 | ret = _bits_of_type(typ) 84 | assert ret % 8 == 0 85 | return ret // 8 86 | 87 | 88 | # TODO this could be a function in vyper.builtins._convert 89 | # which implements literal folding and also serves as a reference/spec 90 | def _padconvert(val_bits, direction, n, padding_byte=None): 91 | """ 92 | Takes the ABI representation of a value, and convert the padding if needed. 93 | If fill_zeroes is false, the two halves of the bytestring are just swapped 94 | and the dirty bytes remain dirty. If fill_zeroes is true, the padding 95 | bytes get set to 0 96 | """ 97 | assert len(val_bits) == 32 98 | 99 | # convert left-padded to right-padded 100 | if direction == _PadDirection.Right: 101 | tail = val_bits[:-n] 102 | if padding_byte is not None: 103 | tail = padding_byte * len(tail) 104 | return val_bits[-n:] + tail 105 | 106 | # right- to left- padded 107 | if direction == _PadDirection.Left: 108 | head = val_bits[n:] 109 | if padding_byte is not None: 110 | head = padding_byte * len(head) 111 | return head + val_bits[:n] 112 | 113 | 114 | def _signextend(val_bytes, bits): 115 | as_uint = int.from_bytes(val_bytes, byteorder="big") 116 | 117 | as_sint = unsigned_to_signed(as_uint, bits) 118 | 119 | return (as_sint % 2**256).to_bytes(32, byteorder="big") 120 | 121 | 122 | def _from_bits(val_bits, o_typ): 123 | # o_typ: the type to convert to 124 | try: 125 | ret = abi_decode(o_typ, val_bits) 126 | return ret 127 | except DecodeError: 128 | raise ConvertError() 129 | 130 | 131 | class ConvertError(Exception): 132 | pass 133 | -------------------------------------------------------------------------------- /src/ivy/allocator.py: -------------------------------------------------------------------------------- 1 | import vyper.ast.nodes as ast 2 | from vyper.semantics.analysis.base import ModuleInfo 3 | from vyper.semantics.types.module import ModuleT 4 | from vyper.semantics.data_locations import DataLocation 5 | 6 | IGNORED_LOCATIONS = (DataLocation.CALLDATA,) 7 | 8 | 9 | class Allocator: 10 | def __init__(self): 11 | # NOTE: maybe from the delegatecall persepective it would be better to make 12 | # constants and immutables affect the same counter 13 | self.counters = { 14 | location: 0 15 | for location in DataLocation 16 | if location not in IGNORED_LOCATIONS 17 | } 18 | self.visited = {} 19 | 20 | def _get_allocatable(self, vyper_module: ast.Module) -> list[ast.VyperNode]: 21 | allocable = (ast.InitializesDecl, ast.VariableDecl) 22 | return [node for node in vyper_module.body if isinstance(node, allocable)] 23 | 24 | def _increment_counter(self, location: DataLocation) -> int: 25 | current = self.counters[location] 26 | self.counters[location] += 1 27 | return current 28 | 29 | def allocate_nonreentrant_key(self): 30 | return self._increment_counter(DataLocation.TRANSIENT) 31 | 32 | def _allocate_var(self, node, allocate_constants=False): 33 | assert isinstance(node, ast.VariableDecl) 34 | varinfo = node.target._metadata["varinfo"] 35 | if varinfo.is_constant and not allocate_constants: 36 | return 37 | if not varinfo.is_constant and allocate_constants: 38 | return 39 | 40 | # sanity check 41 | assert varinfo not in self.visited 42 | assert varinfo.is_state_variable 43 | 44 | varinfo.position = self._increment_counter(varinfo.location) 45 | self.visited[varinfo] = True 46 | 47 | def _allocate_r(self, mod: ast.Module): 48 | nodes = self._get_allocatable(mod) 49 | for node in nodes: 50 | if isinstance(node, ast.InitializesDecl): 51 | module_info = node._metadata["initializes_info"].module_info 52 | self._allocate_r(module_info.module_node) 53 | continue 54 | 55 | assert isinstance(node, ast.VariableDecl) 56 | self._allocate_var(node, allocate_constants=False) 57 | 58 | def _allocate_constants_r(self, vyper_module: ast.Module, visited: set): 59 | """ 60 | Constants must be allocated even in modules which aren't initialized. 61 | Thus, for cleanliness, we do it in a separate function. 62 | 63 | Consider: 64 | # mod1.vy 65 | X: constant(uint256) = empty(uint256) 66 | 67 | # main.vy 68 | import mod1 69 | 70 | @external 71 | def foo() -> uint256: 72 | return mod1.X 73 | """ 74 | if vyper_module in visited: 75 | return 76 | visited.add(vyper_module) 77 | 78 | decls = [ 79 | node for node in vyper_module.body if isinstance(node, ast.VariableDecl) 80 | ] 81 | 82 | for d in decls: 83 | assert isinstance(d, ast.VariableDecl) 84 | self._allocate_var(d, allocate_constants=True) 85 | 86 | import_nodes = (ast.Import, ast.ImportFrom) 87 | imports = [node for node in vyper_module.body if isinstance(node, import_nodes)] 88 | 89 | for node in imports: 90 | import_info = node._metadata["import_info"] 91 | # could be an interface 92 | if isinstance(import_info.typ, ModuleInfo): 93 | typ = import_info.typ.module_t 94 | self._allocate_constants_r(typ.decl_node, visited) 95 | 96 | def allocate_addresses(self, module_t: ModuleT): 97 | nonreentrant = self.allocate_nonreentrant_key() 98 | self._allocate_r(module_t.decl_node) 99 | self._allocate_constants_r(module_t.decl_node, set()) 100 | return nonreentrant, self.visited 101 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/builtins/codegen/test_keccak256.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from vyper.utils import hex_to_int 4 | from vyper.exceptions import StateAccessViolation 5 | 6 | 7 | def test_hash_code(get_contract, keccak): 8 | hash_code = """ 9 | @external 10 | def foo(inp: Bytes[100]) -> bytes32: 11 | return keccak256(inp) 12 | 13 | @external 14 | def foob() -> bytes32: 15 | return keccak256(b"inp") 16 | 17 | @external 18 | def bar() -> bytes32: 19 | return keccak256("inp") 20 | """ 21 | 22 | c = get_contract(hash_code) 23 | for inp in (b"", b"cow", b"s" * 31, b"\xff" * 32, b"\n" * 33, b"g" * 64, b"h" * 65): 24 | assert c.foo(inp) == keccak(inp) 25 | 26 | assert c.bar() == keccak(b"inp") 27 | assert c.foob() == keccak(b"inp") 28 | 29 | 30 | def test_hash_code2(get_contract): 31 | hash_code2 = """ 32 | @external 33 | def foo(inp: Bytes[100]) -> bool: 34 | return keccak256(inp) == keccak256("badminton") 35 | """ 36 | c = get_contract(hash_code2) 37 | assert c.foo(b"badminto") is False 38 | assert c.foo(b"badminton") is True 39 | 40 | 41 | def test_hash_code3(get_contract): 42 | hash_code3 = """ 43 | test: Bytes[100] 44 | 45 | @external 46 | def set_test(inp: Bytes[100]): 47 | self.test = inp 48 | 49 | @external 50 | def tryy(inp: Bytes[100]) -> bool: 51 | return keccak256(inp) == keccak256(self.test) 52 | 53 | @external 54 | def tryy_str(inp: String[100]) -> bool: 55 | return keccak256(inp) == keccak256(self.test) 56 | 57 | @external 58 | def trymem(inp: Bytes[100]) -> bool: 59 | x: Bytes[100] = self.test 60 | return keccak256(inp) == keccak256(x) 61 | 62 | @external 63 | def try32(inp: bytes32) -> bool: 64 | return keccak256(inp) == keccak256(self.test) 65 | 66 | """ 67 | c = get_contract(hash_code3) 68 | c.set_test(b"") 69 | assert c.tryy(b"") is True 70 | assert c.tryy_str("") is True 71 | assert c.trymem(b"") is True 72 | assert c.tryy(b"cow") is False 73 | c.set_test(b"cow") 74 | assert c.tryy(b"") is False 75 | assert c.tryy(b"cow") is True 76 | assert c.tryy_str("cow") is True 77 | c.set_test(b"\x35" * 32) 78 | assert c.tryy(b"\x35" * 32) is True 79 | assert c.trymem(b"\x35" * 32) is True 80 | assert c.try32(b"\x35" * 32) is True 81 | assert c.tryy(b"\x35" * 33) is False 82 | c.set_test(b"\x35" * 33) 83 | assert c.tryy(b"\x35" * 32) is False 84 | assert c.trymem(b"\x35" * 32) is False 85 | assert c.try32(b"\x35" * 32) is False 86 | assert c.tryy(b"\x35" * 33) is True 87 | 88 | print("Passed KECCAK256 hash test") 89 | 90 | 91 | def test_hash_constant_bytes32(get_contract, keccak): 92 | hex_val = "0x1234567890123456789012345678901234567890123456789012345678901234" 93 | code = f""" 94 | FOO: constant(bytes32) = {hex_val} 95 | BAR: constant(bytes32) = keccak256(FOO) 96 | @external 97 | def foo() -> bytes32: 98 | x: bytes32 = BAR 99 | return x 100 | """ 101 | c = get_contract(code) 102 | assert c.foo() == keccak(hex_to_int(hex_val).to_bytes(32, "big")) 103 | 104 | 105 | def test_hash_constant_string(get_contract, keccak): 106 | str_val = "0x1234567890123456789012345678901234567890123456789012345678901234" 107 | code = f""" 108 | FOO: constant(String[66]) = "{str_val}" 109 | BAR: constant(bytes32) = keccak256(FOO) 110 | @external 111 | def foo() -> bytes32: 112 | x: bytes32 = BAR 113 | return x 114 | """ 115 | c = get_contract(code) 116 | assert c.foo() == keccak(str_val.encode()) 117 | 118 | 119 | @pytest.mark.xfail( 120 | raises=StateAccessViolation, 121 | reason="haven't updated ivy to latest vyper which supports this", 122 | ) 123 | def test_hash_constant_hexbytes(get_contract, keccak): 124 | hexbytes_val = "67363d3d37363d34f03d5260086018f3" 125 | code = f""" 126 | FOO: constant(Bytes[16]) = x"{hexbytes_val}" 127 | BAR: constant(bytes32) = keccak256(FOO) 128 | @external 129 | def foo() -> bytes32: 130 | x: bytes32 = BAR 131 | return x 132 | """ 133 | c = get_contract(code) 134 | assert c.foo() == keccak(bytes.fromhex(hexbytes_val)) 135 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/literal_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Literal generator for contract codegen. 3 | 4 | Generates small, readable literal values for use in generated contracts. 5 | Occasionally delegates to ValueMutator for boundary values. 6 | """ 7 | 8 | import random 9 | from typing import Any, Dict, Optional, Type 10 | 11 | from decimal import Decimal 12 | 13 | from vyper.semantics.types import ( 14 | AddressT, 15 | BoolT, 16 | BytesM_T, 17 | BytesT, 18 | DArrayT, 19 | DecimalT, 20 | IntegerT, 21 | StringT, 22 | VyperType, 23 | ) 24 | 25 | from .base_value_generator import BaseValueGenerator 26 | from .value_mutator import ValueMutator 27 | 28 | 29 | class LiteralGenerator(BaseValueGenerator): 30 | """Generates small, readable literals for codegen.""" 31 | 32 | # BytesM_T and AddressT always delegate - they have fixed length so 33 | # there's no benefit to special "small" handling. 34 | DEFAULT_BOUNDARY_PROBS: Dict[Type, float] = { 35 | IntegerT: 0.0001, 36 | StringT: 0.0001, 37 | BytesT: 0.0001, 38 | BytesM_T: 1.0, 39 | AddressT: 1.0, 40 | # binary outcame, handled in the super class 41 | BoolT: 0.0, 42 | } 43 | 44 | def __init__( 45 | self, 46 | rng: Optional[random.Random] = None, 47 | boundary_probs: Optional[Dict[Type, float]] = None, 48 | ): 49 | super().__init__(rng) 50 | self._boundary_probs = boundary_probs or self.DEFAULT_BOUNDARY_PROBS 51 | self._boundary_generator = ValueMutator(self.rng) 52 | 53 | def generate(self, vyper_type: VyperType) -> Any: 54 | # Check if we should delegate to boundary generator 55 | prob = self._boundary_probs.get(type(vyper_type), 0.0) 56 | if self.rng.random() < prob: 57 | return self._boundary_generator.generate(vyper_type) 58 | 59 | return super().generate(vyper_type) 60 | 61 | def _generate_integer(self, vyper_type: IntegerT) -> int: 62 | lo, hi = vyper_type.ast_bounds 63 | smol = [v for v in [-2, -1, 0, 1, 2] if lo <= v <= hi] 64 | if self.rng.random() < 0.99: 65 | return self.rng.choice(smol) 66 | return self.rng.choice([-10, 10, 100, 255, 256]) 67 | 68 | def _generate_address(self, vyper_type: AddressT) -> str: 69 | # Always delegate to boundary generator 70 | return self._boundary_generator._generate_address(vyper_type) 71 | 72 | def _generate_string(self, vyper_type: StringT) -> str: 73 | max_length = vyper_type.length 74 | # Short readable strings 75 | choices = ["", "a", "ab", "abc", "test", "hello", "foo", "bar"] 76 | valid = [s for s in choices if len(s) <= max_length] 77 | return self.rng.choice(valid) 78 | 79 | def _generate_bytes(self, vyper_type: BytesT) -> bytes: 80 | max_length = vyper_type.length 81 | # Short byte sequences 82 | choices = [b"", b"\x00", b"\xff", b"\x01\x02", b"test"] 83 | valid = [b for b in choices if len(b) <= max_length] 84 | return self.rng.choice(valid) 85 | 86 | def _generate_bytes_m(self, vyper_type: BytesM_T) -> bytes: 87 | # Always delegate to boundary generator 88 | return self._boundary_generator._generate_bytes_m(vyper_type) 89 | 90 | def _generate_darray(self, vyper_type: DArrayT) -> list: 91 | # Bias toward small lengths (1-10), very rarely use full random length 92 | max_len = vyper_type.length 93 | if self.rng.random() < 0.01: 94 | n = self.rng.randint(0, max_len) 95 | else: 96 | n = self.rng.randint(0, min(10, max_len)) 97 | return [self.generate(vyper_type.value_type) for _ in range(n)] 98 | 99 | def _generate_decimal(self, _vyper_type: DecimalT) -> Decimal: 100 | return self.rng.choice( 101 | [ 102 | Decimal("0.0"), 103 | Decimal("1.0"), 104 | Decimal("-1.0"), 105 | Decimal("2.0"), 106 | Decimal("-2.0"), 107 | Decimal("0.5"), 108 | Decimal("-0.5"), 109 | Decimal("1.5"), 110 | ] 111 | ) 112 | -------------------------------------------------------------------------------- /src/ivy/variable.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from vyper.semantics.analysis.base import VarInfo 4 | from vyper.semantics.data_locations import DataLocation 5 | from vyper.semantics.types import VyperType, BoolT 6 | 7 | from ivy.expr.default_values import get_default_value 8 | from ivy.journal import Journal, JournalEntryType 9 | 10 | 11 | class GlobalVariable: 12 | address: int 13 | typ: VyperType 14 | get_location: callable # This will store the lambda 15 | varinfo: Optional[VarInfo] 16 | 17 | def __init__( 18 | self, 19 | address: int, 20 | typ: VyperType, 21 | get_location: callable, # Now receives a function instead of direct location 22 | varinfo: Optional[VarInfo] = None, 23 | initial_value: Optional[bool] = None, 24 | ): 25 | self.typ = typ 26 | self.get_location = get_location 27 | self.address = address 28 | if initial_value is None: 29 | initial_value = get_default_value(typ) 30 | self.get_location()[self.address] = initial_value 31 | self.varinfo = varinfo 32 | 33 | @property 34 | def value(self): 35 | location = self.get_location() 36 | if self.address not in location: 37 | location[self.address] = get_default_value(self.typ) 38 | return location[self.address] 39 | 40 | @value.setter 41 | def value(self, new_value): 42 | location = self.get_location() 43 | if self.varinfo and Journal.journalable_loc(self.varinfo.location): 44 | old_value = location.get(self.address, None) 45 | entry_type = ( 46 | JournalEntryType.TRANSIENT_STORAGE 47 | if self.varinfo.location == DataLocation.TRANSIENT 48 | else JournalEntryType.STORAGE 49 | ) 50 | Journal().record(entry_type, location, self.address, old_value) 51 | 52 | location[self.address] = new_value 53 | 54 | 55 | class GlobalVariables: 56 | def __init__(self): 57 | self.variables = {} 58 | self.reentrant_key_address = None 59 | self.adrr_to_name = {} 60 | 61 | def _get_address(self, var: VarInfo): 62 | return (var.position, var.location) 63 | 64 | def new_variable( 65 | self, 66 | var: VarInfo, 67 | get_location: callable, 68 | initial_value=None, 69 | name=Optional[str], 70 | ): 71 | address = self._get_address(var) 72 | assert address not in self.variables 73 | variable = GlobalVariable( 74 | var.position, var.typ, get_location, var, initial_value 75 | ) 76 | self.variables[address] = variable 77 | if name: 78 | self.adrr_to_name[address] = name 79 | 80 | def __setitem__(self, key: VarInfo, value): 81 | address = self._get_address(key) 82 | self.variables[address] = value 83 | 84 | def __getitem__(self, key: VarInfo): 85 | address = self._get_address(key) 86 | res = self.variables[address] 87 | assert res is not None 88 | return res 89 | 90 | def allocate_reentrant_key(self, position: int, get_location): 91 | assert self.reentrant_key_address is None 92 | address = (position, DataLocation.TRANSIENT) 93 | self.reentrant_key_address = address 94 | assert address not in self.variables 95 | # Create a VarInfo for the reentrant key so it gets journaled properly 96 | from vyper.semantics.analysis.base import Modifiability 97 | 98 | varinfo = VarInfo( 99 | typ=BoolT(), 100 | location=DataLocation.TRANSIENT, 101 | modifiability=Modifiability.MODIFIABLE, 102 | is_public=False, 103 | decl_node=None, 104 | ) 105 | varinfo.position = position 106 | self.variables[address] = GlobalVariable( 107 | position, BoolT(), get_location, varinfo 108 | ) 109 | 110 | def set_reentrant_key(self, value: bool): 111 | address = self.reentrant_key_address 112 | self.variables[address].value = value 113 | 114 | def get_reentrant_key(self): 115 | address = self.reentrant_key_address 116 | return self.variables[address].value 117 | -------------------------------------------------------------------------------- /src/fuzzer/trace_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Data types for representing Vyper test traces (export format). 3 | 4 | Traces match the JSON export format and contain expected outputs for validation. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from dataclasses import dataclass, field 10 | from pathlib import Path 11 | from typing import Any, Dict, List, Optional, TypeAlias, Union 12 | 13 | from .xfail import XFailExpectation 14 | 15 | 16 | @dataclass 17 | class Tx: 18 | origin: str 19 | gas: int 20 | gas_price: int 21 | blob_hashes: List[str] 22 | 23 | 24 | @dataclass 25 | class Block: 26 | number: int 27 | timestamp: int 28 | gas_limit: int 29 | excess_blob_gas: Optional[int] 30 | 31 | 32 | @dataclass 33 | class Env: 34 | """Represents environment data for a trace.""" 35 | 36 | tx: Tx 37 | block: Block 38 | 39 | 40 | @dataclass 41 | class DeploymentTrace: 42 | """Represents a contract deployment trace.""" 43 | 44 | deployment_type: str # "source", "ir", "blueprint", "raw_bytecode" 45 | contract_abi: List[Dict[str, Any]] 46 | initcode: str 47 | calldata: Optional[str] 48 | value: int 49 | source_code: Optional[str] 50 | annotated_ast: Optional[Dict[str, Any]] 51 | solc_json: Optional[Dict[str, Any]] 52 | raw_ir: Optional[str] 53 | blueprint_initcode_prefix: Optional[str] 54 | deployed_address: str 55 | runtime_bytecode: str 56 | deployment_succeeded: bool 57 | env: Env 58 | python_args: Optional[Dict[str, Any]] = None # {"args": [], "kwargs": {}} 59 | compiler_settings: Optional[Dict[str, Any]] = None 60 | compilation_xfails: List[XFailExpectation] = field(default_factory=list) 61 | runtime_xfails: List[XFailExpectation] = field(default_factory=list) 62 | 63 | def to_trace_info(self, index: int) -> Dict[str, Any]: 64 | info: Dict[str, Any] = {"type": self.__class__.__name__, "index": index} 65 | if self.source_code: 66 | info["source_code"] = self.source_code 67 | if self.python_args: 68 | info["args"] = self.python_args 69 | return info 70 | 71 | 72 | @dataclass 73 | class CallTrace: 74 | """Represents a function call trace.""" 75 | 76 | output: Optional[str] 77 | call_args: Dict[str, Any] 78 | call_succeeded: Optional[bool] = None 79 | env: Optional[Env] = None 80 | python_args: Optional[Dict[str, Any]] = None # {"args": [], "kwargs": {}} 81 | function_name: Optional[str] = None 82 | runtime_xfails: List[XFailExpectation] = field(default_factory=list) 83 | 84 | def to_trace_info(self, index: int) -> Dict[str, Any]: 85 | info: Dict[str, Any] = {"type": self.__class__.__name__, "index": index} 86 | if self.function_name: 87 | info["function"] = self.function_name 88 | elif self.python_args and "method" in self.python_args: 89 | info["function"] = self.python_args["method"] 90 | if self.python_args: 91 | info["args"] = self.python_args 92 | return info 93 | 94 | 95 | @dataclass 96 | class SetBalanceTrace: 97 | """Represents a set_balance trace.""" 98 | 99 | address: str 100 | value: int 101 | 102 | def to_trace_info(self, index: int) -> Dict[str, Any]: 103 | return {"type": self.__class__.__name__, "index": index} 104 | 105 | 106 | @dataclass 107 | class ClearTransientStorageTrace: 108 | """Represents a clear_transient_storage trace.""" 109 | 110 | # No fields needed for this trace type 111 | 112 | def to_trace_info(self, index: int) -> Dict[str, Any]: 113 | return {"type": self.__class__.__name__, "index": index} 114 | 115 | 116 | # Type alias for any trace type 117 | Trace: TypeAlias = Union[ 118 | DeploymentTrace, CallTrace, SetBalanceTrace, ClearTransientStorageTrace 119 | ] 120 | 121 | 122 | @dataclass 123 | class TestItem: 124 | """Represents a test or fixture with its traces.""" 125 | 126 | name: str 127 | item_type: str # "test" or "fixture" 128 | deps: List[str] 129 | traces: List[Trace] 130 | 131 | 132 | @dataclass 133 | class TestExport: 134 | """Container for all test exports from a file.""" 135 | 136 | path: Path 137 | items: Dict[str, TestItem] = field(default_factory=dict) 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Ivy 3 | 4 | An AST interpreter for Vyper with a custom EVM backend. 5 | 6 | ## Overview 7 | 8 | Ivy is an experimental interpreter that executes Vyper smart contracts by interpreting their Abstract Syntax Tree (AST) directly, rather than compiling to bytecode. This approach enables powerful debugging, testing, and analysis capabilities. 9 | 10 | ### Key Features 11 | 12 | - Direct AST interpretation without bytecode compilation 13 | - Custom EVM implementation 14 | - Integration with Vyper test exports for differential testing 15 | - AST mutation capabilities for fuzzing 16 | - Compatible with most Vyper language features 17 | 18 | ### How it Works 19 | 20 | - Uses the Vyper compiler as a library to parse and analyze contracts 21 | - Obtains the annotated AST from the compiler 22 | - Stores the contract's annotated AST (instead of bytecode) in EVM accounts 23 | - Interprets the AST when transactions or calls are made to contracts 24 | - Maintains global state (storage, balances, etc.) like a regular EVM 25 | 26 | ## Setup and Installation 27 | 28 | ### Installation 29 | 30 | 1. **Clone the repository:** 31 | ```bash 32 | git clone https://github.com/cyberthirst/ivy.git 33 | cd ivy 34 | ``` 35 | 36 | 2. **Create and activate a virtual environment (recommended):** 37 | ```bash 38 | python -m venv venv 39 | source venv/bin/activate 40 | ``` 41 | 42 | 3. **Install the package and dependencies:** 43 | ```bash 44 | pip install -e . 45 | ``` 46 | 47 | Or if you're using `uv`: 48 | ```bash 49 | uv pip install -e . 50 | ``` 51 | 52 | ### Development Setup 53 | 54 | For development, you'll need additional dependencies: 55 | 56 | 1. **Install development dependencies:** 57 | ```bash 58 | pip install -e ".[dev]" 59 | ``` 60 | 61 | 2. **Configure your environment:** 62 | - The project uses `PYTHONPATH=src` for imports 63 | - This is already configured in `pytest.ini` for tests 64 | - For running scripts, you may need to set: `export PYTHONPATH=src` 65 | 66 | ### Testing 67 | 68 | Run the test suite to ensure everything is working: 69 | 70 | ```bash 71 | # Run all tests 72 | pytest 73 | 74 | # Run specific test modules 75 | pytest tests/ivy/test_e2e.py 76 | ``` 77 | 78 | ### Using Vyper Test Exports 79 | 80 | Ivy supports loading and executing Vyper test exports for differential testing: 81 | 82 | 1. **Obtain Vyper test exports:** 83 | ```bash 84 | # From your Vyper repository, export tests: 85 | pytest -n 1 --export=/path/to/exports 86 | ``` 87 | 88 | 2. **Copy exports to Ivy:** 89 | ```bash 90 | cp -r /path/to/exports tests/vyper-exports 91 | ``` 92 | 93 | 3. **Run differential testing:** 94 | ```bash 95 | python examples/differential_testing.py 96 | ``` 97 | 98 | ## Quick Start 99 | 100 | ### Basic Usage 101 | 102 | ```python 103 | from ivy.frontend.loader import loads 104 | 105 | # Deploy a simple contract 106 | src = """ 107 | @external 108 | def foo(a: uint256=42) -> uint256: 109 | return a 110 | """ 111 | 112 | c = loads(src) 113 | assert c.foo() == 42 114 | ``` 115 | 116 | ### Command Line Usage 117 | 118 | ```bash 119 | # Run a Vyper contract directly 120 | python -m ivy tests/example_contracts/example.vy 121 | 122 | # With uv 123 | uv run python -m ivy tests/example_contracts/example.vy 124 | ``` 125 | 126 | ### Limitations 127 | 128 | - **Gas metering**: Gas costs are not supported as they can't be accurately mapped to AST interpretation 129 | - **Delegatecall**: Basic support only - proper variable mapping between caller and callee requires compatible storage layouts 130 | - **Bytecode operations**: Some low-level operations that depend on bytecode are not supported 131 | 132 | ### Design Decisions 133 | 134 | - The interpreter frontend is heavily inspired by [Titanoboa](https://github.com/vyperlang/titanoboa) 135 | - State changes are journaled for proper rollback support 136 | - The EVM implementation focuses on correctness over performance 137 | 138 | ### Development Tips 139 | 140 | - The `PYTHONPATH=src` environment variable may be needed for some scripts 141 | - Check `examples/` directory for usage patterns 142 | - Run `ruff` for linting: `ruff check src tests` 143 | 144 | ## Acknowledgments 145 | 146 | - The Vyper team for the compiler and Titanoboa 147 | -------------------------------------------------------------------------------- /src/ivy/context.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from vyper.semantics.types import VyperType 4 | from vyper.semantics.types.function import ContractFunctionT 5 | 6 | from ivy.expr.default_values import get_default_value 7 | from ivy.evm.evm_structures import Account, Message, EntryPointInfo 8 | from ivy.types import Address 9 | from ivy.variable import GlobalVariables 10 | 11 | 12 | class FunctionContext: 13 | scopes: list[dict[str, Any]] 14 | function: ContractFunctionT 15 | 16 | def __init__(self, function: ContractFunctionT): 17 | self.scopes = [] 18 | self.function = function 19 | 20 | def push(self): 21 | self.scopes.append({}) 22 | 23 | def pop(self): 24 | self.scopes.pop() 25 | 26 | def new_variable(self, key, typ: VyperType): 27 | value = get_default_value(typ) 28 | assert key not in self.scopes[-1] 29 | self.scopes[-1][key] = value 30 | 31 | # TODO should we optimize this? we have guarantee of unique names 32 | # so we could use one global dict (however, when popping a scope 33 | # we would have to iterate over its keys to delete them from 34 | # the dict 35 | def __contains__(self, item): 36 | for scope in reversed(self.scopes): 37 | if item in scope: 38 | return True 39 | return False 40 | 41 | def __setitem__(self, key, value): 42 | for scope in reversed(self.scopes): 43 | if key in scope: 44 | scope[key] = value 45 | return 46 | self.scopes[-1][key] = value 47 | 48 | def __getitem__(self, key): 49 | for scope in reversed(self.scopes): 50 | if key in scope: 51 | return scope[key] 52 | raise KeyError(key) 53 | 54 | def __repr__(self): 55 | return f"Ctx({self.scopes})" 56 | 57 | 58 | class ExecutionContext: 59 | global_vars: Optional[GlobalVariables] 60 | immutables: Optional[dict[str, Any]] 61 | constants: Optional[dict[str, Any]] 62 | entry_points: Optional[dict[bytes, EntryPointInfo]] 63 | 64 | def __init__(self, acc: Account, msg: Message): 65 | self.msg = msg 66 | self.contract = msg.code 67 | self.function = None 68 | self.function_contexts = [] 69 | # set storage-related attributes based on the target account 70 | self.storage = acc.storage 71 | self.transient = acc.transient 72 | # set code-related attributes based on whether message.code is None 73 | code_attrs = ["global_vars", "immutables", "constants", "entry_points"] 74 | for attr in code_attrs: 75 | setattr(self, attr, getattr(msg.code, attr) if msg.code else None) 76 | self.execution_output = ExecutionOutput() 77 | # return_data is not part of the ExecutionOutput, it's filled from the output 78 | # of child evm calls 79 | self.returndata: bytes = b"" 80 | # TODO should we keep it or only use execution_output.output? 81 | # - is it used for internal functions? 82 | self.func_return: Optional[bytes] = None 83 | 84 | def push_fun_context(self, func_t: ContractFunctionT): 85 | self.function_contexts.append(FunctionContext(func_t)) 86 | 87 | def pop_fun_context(self): 88 | self.function_contexts.pop() 89 | 90 | def current_fun_context(self): 91 | return self.function_contexts[-1] 92 | 93 | def push_scope(self): 94 | self.current_fun_context().push() 95 | 96 | def pop_scope(self): 97 | self.current_fun_context().pop() 98 | 99 | 100 | class ExecutionOutput: 101 | def __init__(self): 102 | self._output: Optional[bytes] = None 103 | self.error: Optional[Exception] = None 104 | self.accessed_addresses: set[Address] = set() 105 | self.accounts_to_delete: set[Address] = set() 106 | self.logs: list = [] 107 | self.refund_counter = 0 108 | self.touched_accounts: set[Address] = set() 109 | 110 | @property 111 | def output(self): 112 | if self._output is None: 113 | return b"" 114 | return self._output 115 | 116 | @output.setter 117 | def output(self, value): 118 | self._output = value 119 | 120 | @property 121 | def is_error(self): 122 | return self.error is not None 123 | -------------------------------------------------------------------------------- /src/ivy/expr/clamper.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, Type 2 | 3 | from vyper.semantics.types import ( 4 | IntegerT, 5 | BoolT, 6 | BytesT, 7 | StringT, 8 | SArrayT, 9 | DArrayT, 10 | FlagT, 11 | DecimalT, 12 | StructT, 13 | HashMapT, 14 | InterfaceT, 15 | ) 16 | from ivy.types import ( 17 | StaticArray, 18 | DynamicArray, 19 | Flag, 20 | VyperDecimal, 21 | ) 22 | 23 | VALIDATOR_REGISTRY: Dict[Type[Any], Callable[[Any, Any], None]] = {} 24 | 25 | 26 | def register_validator( 27 | type_cls: Type[Any], 28 | ) -> Callable[[Callable[[Any, Any], None]], Callable[[Any, Any], None]]: 29 | """ 30 | Decorator to register a validation function for a Vyper semantic type. 31 | 32 | Usage: 33 | @register_validator(IntegerT) 34 | def validate_integer(value, typ: IntegerT): 35 | ... 36 | """ 37 | 38 | def decorator(fn: Callable[[Any, Any], None]) -> Callable[[Any, Any], None]: 39 | VALIDATOR_REGISTRY[type_cls] = fn 40 | return fn 41 | 42 | return decorator 43 | 44 | 45 | def validate_value(node: Any, value: Any) -> None: 46 | typ = node._metadata["type"] 47 | validator = VALIDATOR_REGISTRY.get(type(typ)) 48 | if validator is None: 49 | raise NotImplementedError(f"Missing validator for type {typ}") 50 | validator(value, typ) 51 | 52 | 53 | @register_validator(IntegerT) 54 | def validate_integer(value: int, typ: IntegerT) -> None: 55 | lo, hi = typ.ast_bounds 56 | if not (lo <= value <= hi): 57 | raise ValueError(f"Value {value} out of bounds for {typ}") 58 | 59 | 60 | @register_validator(BoolT) 61 | def validate_bool(value: bool, typ: BoolT) -> None: 62 | if not isinstance(value, bool): 63 | raise TypeError(f"Expected a bool for {typ}, got {type(value).__name__}") 64 | 65 | 66 | @register_validator(DecimalT) 67 | def validate_decimal(value: VyperDecimal, typ: DecimalT) -> None: 68 | if not (VyperDecimal.min() <= value <= VyperDecimal.max()): 69 | raise ValueError(f"Value {value} out of bounds for {typ}") 70 | 71 | 72 | def _validate_sequence_len(value: Any, length: int) -> None: 73 | actual = len(value) 74 | if actual != length: 75 | raise ValueError(f"Invalid length: expected {length}, got {actual}") 76 | 77 | 78 | @register_validator(SArrayT) 79 | def validate_static_array(value: StaticArray, typ: SArrayT) -> None: 80 | if value.length != typ.length: 81 | raise ValueError( 82 | f"Invalid length for {typ}: expected {typ.length}, got {value.length}" 83 | ) 84 | for elem in value: 85 | validate_value(elem, elem) 86 | 87 | 88 | @register_validator(DArrayT) 89 | def validate_dynamic_array(value: DynamicArray, typ: DArrayT) -> None: 90 | if len(value) > typ.count: 91 | raise ValueError( 92 | f"Invalid length for {typ}: expected at most {typ.count}, got {len(value)}" 93 | ) 94 | for elem in value: 95 | validate_value(elem, elem) 96 | 97 | 98 | @register_validator(BytesT) 99 | def validate_bytes(value: bytes, typ: BytesT) -> None: 100 | if len(value) > typ.length: 101 | raise ValueError( 102 | f"Invalid bytes length for {typ}: expected at most {typ.length}, got {len(value)}" 103 | ) 104 | 105 | 106 | @register_validator(StringT) 107 | def validate_string(value: str, typ: StringT) -> None: 108 | if len(value) > typ.length: 109 | raise ValueError( 110 | f"Invalid string length for {typ}: expected at most {typ.length}, got {len(value)}" 111 | ) 112 | 113 | 114 | @register_validator(FlagT) 115 | def validate_flag(value: Flag, typ: FlagT) -> None: 116 | if (value.value >> len(typ._flag_members)) != 0: 117 | raise ValueError(f"Invalid flag value {value} for {typ}") 118 | 119 | 120 | @register_validator(StructT) 121 | def validate_struct(value: Any, typ: StructT) -> None: 122 | for name, member_typ in typ.members.items(): 123 | member_value = getattr(value, name) 124 | validate_value(member_value, member_value) 125 | 126 | 127 | @register_validator(HashMapT) 128 | def validate_map(value: Any, typ: HashMapT) -> None: 129 | for k, v in value.items(): 130 | continue 131 | 132 | 133 | @register_validator(InterfaceT) 134 | def validate_interface(value: Any, typ: InterfaceT) -> None: 135 | return None 136 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/features/test_short_circuiting.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import pytest 4 | 5 | 6 | def test_short_circuit_and_left_is_false(get_contract): 7 | code = """ 8 | 9 | called_left: public(bool) 10 | called_right: public(bool) 11 | 12 | @internal 13 | def left() -> bool: 14 | self.called_left = True 15 | return False 16 | 17 | @internal 18 | def right() -> bool: 19 | self.called_right = True 20 | return False 21 | 22 | @external 23 | def foo() -> bool: 24 | return self.left() and self.right() 25 | """ 26 | c = get_contract(code) 27 | assert not c.foo() 28 | 29 | c.foo() 30 | assert c.called_left() 31 | assert not c.called_right() 32 | 33 | 34 | def test_short_circuit_and_left_is_true(get_contract): 35 | code = """ 36 | 37 | called_left: public(bool) 38 | called_right: public(bool) 39 | 40 | @internal 41 | def left() -> bool: 42 | self.called_left = True 43 | return True 44 | 45 | @internal 46 | def right() -> bool: 47 | self.called_right = True 48 | return True 49 | 50 | @external 51 | def foo() -> bool: 52 | return self.left() and self.right() 53 | """ 54 | c = get_contract(code) 55 | assert c.foo() 56 | 57 | c.foo() 58 | assert c.called_left() 59 | assert c.called_right() 60 | 61 | 62 | def test_short_circuit_or_left_is_true(get_contract): 63 | code = """ 64 | 65 | called_left: public(bool) 66 | called_right: public(bool) 67 | 68 | @internal 69 | def left() -> bool: 70 | self.called_left = True 71 | return True 72 | 73 | @internal 74 | def right() -> bool: 75 | self.called_right = True 76 | return True 77 | 78 | @external 79 | def foo() -> bool: 80 | return self.left() or self.right() 81 | """ 82 | c = get_contract(code) 83 | assert c.foo() 84 | 85 | c.foo() 86 | assert c.called_left() 87 | res = not c.called_right() 88 | assert not c.called_right() 89 | 90 | 91 | def test_short_circuit_or_left_is_false(get_contract): 92 | code = """ 93 | 94 | called_left: public(bool) 95 | called_right: public(bool) 96 | 97 | @internal 98 | def left() -> bool: 99 | self.called_left = True 100 | return False 101 | 102 | @internal 103 | def right() -> bool: 104 | self.called_right = True 105 | return False 106 | 107 | @external 108 | def foo() -> bool: 109 | return self.left() or self.right() 110 | """ 111 | c = get_contract(code) 112 | assert not c.foo() 113 | 114 | c.foo() 115 | assert c.called_left() 116 | assert c.called_right() 117 | 118 | 119 | @pytest.mark.parametrize("op", ["and", "or"]) 120 | @pytest.mark.parametrize("a, b", itertools.product([True, False], repeat=2)) 121 | def test_from_memory(get_contract, a, b, op): 122 | code = f""" 123 | @external 124 | def foo(a: bool, b: bool) -> bool: 125 | c: bool = a 126 | d: bool = b 127 | return c {op} d 128 | """ 129 | c = get_contract(code) 130 | assert c.foo(a, b) == eval(f"{a} {op} {b}") 131 | 132 | 133 | @pytest.mark.parametrize("op", ["and", "or"]) 134 | @pytest.mark.parametrize("a, b", itertools.product([True, False], repeat=2)) 135 | def test_from_storage(get_contract, a, b, op): 136 | code = f""" 137 | c: bool 138 | d: bool 139 | 140 | @external 141 | def foo(a: bool, b: bool) -> bool: 142 | self.c = a 143 | self.d = b 144 | return self.c {op} self.d 145 | """ 146 | c = get_contract(code) 147 | assert c.foo(a, b) == eval(f"{a} {op} {b}") 148 | 149 | 150 | @pytest.mark.parametrize("op", ["and", "or"]) 151 | @pytest.mark.parametrize("a, b", itertools.product([True, False], repeat=2)) 152 | def test_from_calldata(get_contract, a, b, op): 153 | code = f""" 154 | @external 155 | def foo(a: bool, b: bool) -> bool: 156 | return a {op} b 157 | """ 158 | c = get_contract(code) 159 | assert c.foo(a, b) == eval(f"{a} {op} {b}") 160 | 161 | 162 | @pytest.mark.parametrize("a, b, c, d", itertools.product([True, False], repeat=4)) 163 | @pytest.mark.parametrize("ops", itertools.product(["and", "or"], repeat=3)) 164 | def test_complex_combination(get_contract, a, b, c, d, ops): 165 | boolop = f"a {ops[0]} b {ops[1]} c {ops[2]} d" 166 | 167 | code = f""" 168 | @external 169 | def foo(a: bool, b: bool, c: bool, d: bool) -> bool: 170 | return {boolop} 171 | """ 172 | contract = get_contract(code) 173 | if eval(boolop): 174 | assert contract.foo(a, b, c, d) 175 | else: 176 | assert not contract.foo(a, b, c, d) 177 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/numberz/test_exponents.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from hypothesis import example, given, settings 3 | from hypothesis import strategies as st 4 | 5 | from vyper.codegen.arithmetic import calculate_largest_base, calculate_largest_power 6 | 7 | 8 | def test_fold_nonliteral(get_contract): 9 | # test we can fold non-literal constants 10 | code = """ 11 | N: constant(uint256) = 3 12 | N2: public(constant(uint256)) = N**N 13 | """ 14 | c = get_contract(code) 15 | assert c.N2() == 3**3 16 | 17 | 18 | @pytest.mark.fuzzing 19 | @pytest.mark.parametrize("power", range(2, 255)) 20 | def test_exp_uint256(get_contract, tx_failed, power): 21 | code = f""" 22 | @external 23 | def foo(a: uint256) -> uint256: 24 | return a ** {power} 25 | """ 26 | _min_base, max_base = calculate_largest_base(power, 256, False) 27 | assert max_base**power < 2**256 28 | assert (max_base + 1) ** power >= 2**256 29 | 30 | c = get_contract(code) 31 | 32 | c.foo(max_base) 33 | with tx_failed(): 34 | c.foo(max_base + 1) 35 | 36 | 37 | @pytest.mark.fuzzing 38 | @pytest.mark.parametrize("power", range(2, 127)) 39 | def test_exp_int128(get_contract, tx_failed, power): 40 | code = f""" 41 | @external 42 | def foo(a: int128) -> int128: 43 | return a ** {power} 44 | """ 45 | min_base, max_base = calculate_largest_base(power, 128, True) 46 | 47 | assert -(2**127) <= max_base**power < 2**127 48 | assert -(2**127) <= min_base**power < 2**127 49 | 50 | assert not -(2**127) <= (max_base + 1) ** power < 2**127 51 | assert not -(2**127) <= (min_base - 1) ** power < 2**127 52 | 53 | c = get_contract(code) 54 | 55 | c.foo(max_base) 56 | c.foo(min_base) 57 | 58 | with tx_failed(): 59 | c.foo(max_base + 1) 60 | with tx_failed(): 61 | c.foo(min_base - 1) 62 | 63 | 64 | @pytest.mark.fuzzing 65 | @pytest.mark.parametrize("power", range(2, 15)) 66 | def test_exp_int16(get_contract, tx_failed, power): 67 | code = f""" 68 | @external 69 | def foo(a: int16) -> int16: 70 | return a ** {power} 71 | """ 72 | min_base, max_base = calculate_largest_base(power, 16, True) 73 | 74 | assert -(2**15) <= max_base**power < 2**15 75 | assert -(2**15) <= min_base**power < 2**15 76 | 77 | assert not -(2**15) <= (max_base + 1) ** power < 2**15 78 | assert not -(2**15) <= (min_base - 1) ** power < 2**15 79 | 80 | c = get_contract(code) 81 | 82 | c.foo(max_base) 83 | c.foo(min_base) 84 | 85 | with tx_failed(): 86 | c.foo(max_base + 1) 87 | with tx_failed(): 88 | c.foo(min_base - 1) 89 | 90 | 91 | @pytest.mark.fuzzing 92 | @given(a=st.integers(min_value=2, max_value=2**256 - 1)) 93 | # 8 bits 94 | @example(a=2**7) 95 | @example(a=2**7 - 1) 96 | # 16 bits 97 | @example(a=2**15) 98 | @example(a=2**15 - 1) 99 | # 32 bits 100 | @example(a=2**31) 101 | @example(a=2**31 - 1) 102 | # 64 bits 103 | @example(a=2**63) 104 | @example(a=2**63 - 1) 105 | # 128 bits 106 | @example(a=2**127) 107 | @example(a=2**127 - 1) 108 | # 256 bits 109 | @example(a=2**256 - 1) 110 | @settings(max_examples=200) 111 | def test_max_exp(get_contract, tx_failed, a): 112 | code = f""" 113 | @external 114 | def foo(b: uint256) -> uint256: 115 | return {a} ** b 116 | """ 117 | 118 | c = get_contract(code) 119 | 120 | max_power = calculate_largest_power(a, 256, False) 121 | 122 | assert a**max_power < 2**256 123 | assert a ** (max_power + 1) >= 2**256 124 | 125 | c.foo(max_power) 126 | with tx_failed(): 127 | c.foo(max_power + 1) 128 | 129 | 130 | @pytest.mark.fuzzing 131 | @given(a=st.integers(min_value=2, max_value=2**127 - 1)) 132 | # 8 bits 133 | @example(a=2**7) 134 | @example(a=2**7 - 1) 135 | # 16 bits 136 | @example(a=2**15) 137 | @example(a=2**15 - 1) 138 | # 32 bits 139 | @example(a=2**31) 140 | @example(a=2**31 - 1) 141 | # 64 bits 142 | @example(a=2**63) 143 | @example(a=2**63 - 1) 144 | # 128 bits 145 | @example(a=2**127 - 1) 146 | @settings(max_examples=200) 147 | def test_max_exp_int128(get_contract, tx_failed, a): 148 | code = f""" 149 | @external 150 | def foo(b: int128) -> int128: 151 | return {a} ** b 152 | """ 153 | 154 | c = get_contract(code) 155 | 156 | max_power = calculate_largest_power(a, 128, True) 157 | 158 | assert -(2**127) <= a**max_power < 2**127 159 | assert not -(2**127) <= a ** (max_power + 1) < 2**127 160 | 161 | c.foo(max_power) 162 | with tx_failed(): 163 | c.foo(max_power + 1) 164 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/types/test_array_indexing.py: -------------------------------------------------------------------------------- 1 | # TODO: rewrite the tests in type-centric way, parametrize array and indices types 2 | 3 | 4 | def test_negative_ix_access(get_contract, tx_failed): 5 | # Arrays can't be accessed with negative indices 6 | code = """ 7 | arr: uint256[3] 8 | 9 | @external 10 | def foo(i: int128): 11 | self.arr[i] = 1 12 | """ 13 | 14 | c = get_contract(code) 15 | 16 | with tx_failed(): 17 | c.foo(-1) 18 | with tx_failed(): 19 | c.foo(-3) 20 | with tx_failed(): 21 | c.foo(-(2**127) + 1) 22 | 23 | 24 | def test_negative_ix_access_to_large_arr(get_contract, tx_failed): 25 | # Arrays can't be accessed with negative indices 26 | code = """ 27 | arr: public(uint256[max_value(uint256)-1]) 28 | 29 | @external 30 | def set(idx: int256): 31 | self.arr[idx] = 1 32 | """ 33 | 34 | c = get_contract(code) 35 | with tx_failed(): 36 | c.set(-(2**255)) 37 | with tx_failed(): 38 | c.set(-(2**255) + 5) 39 | with tx_failed(): 40 | c.set(-(2**128)) 41 | with tx_failed(): 42 | c.set(-1) 43 | 44 | 45 | def test_oob_access_to_large_arr(get_contract, tx_failed): 46 | # Test OOB access to large array 47 | code = """ 48 | arr: public(uint256[max_value(uint256)-1]) 49 | 50 | @external 51 | def set(idx: int256): 52 | self.arr[idx] = 3 53 | 54 | @external 55 | def set2(idx: uint256): 56 | self.arr[idx] = 3 57 | """ 58 | c = get_contract(code) 59 | 60 | with tx_failed(): 61 | c.set2(2**256 - 1) 62 | with tx_failed(): 63 | c.set2(2**256 - 2) 64 | 65 | 66 | def test_boundary_access_to_arr(get_contract): 67 | # Test access to the boundary of the array 68 | code = """ 69 | arr1: public(int256[max_value(int256)]) 70 | 71 | @external 72 | def set1(idx: int256): 73 | self.arr1[idx] = 3 74 | 75 | """ 76 | code2 = """ 77 | arr2: public(uint256[max_value(uint256)-1]) 78 | 79 | @external 80 | def set2(idx: uint256): 81 | self.arr2[idx] = 3 82 | """ 83 | c1 = get_contract(code) 84 | 85 | c1.set1(2**255 - 2) 86 | assert c1.arr1(2**255 - 2) == 3 87 | c1.set1(0) 88 | assert c1.arr1(0) == 3 89 | 90 | c2 = get_contract(code2) 91 | 92 | c2.set2(2**256 - 3) 93 | assert c2.arr2(2**256 - 3) == 3 94 | 95 | 96 | def test_valid_ix_access(get_contract): 97 | code = """ 98 | arr: public(uint256[3]) 99 | arr2: public(int256[3]) 100 | 101 | @external 102 | def foo(i: int128): 103 | self.arr[i] = 1 104 | 105 | @external 106 | def bar(i: uint256): 107 | self.arr[i] = 2 108 | """ 109 | 110 | c = get_contract(code) 111 | for i in range(3): 112 | c.foo(i) 113 | assert c.arr(i) == 1 114 | c.bar(i) 115 | assert c.arr(i) == 2 116 | 117 | 118 | def test_for_loop_ix_access(get_contract): 119 | # Arrays can be accessed with for loop iterators of type int 120 | code = """ 121 | arr: public(int256[10]) 122 | 123 | @external 124 | def foo(): 125 | for i: int256 in range(10): 126 | self.arr[i] = i 127 | """ 128 | 129 | c = get_contract(code) 130 | c.foo() 131 | for i in range(10): 132 | assert c.arr(i) == i 133 | 134 | 135 | def test_array_index_overlap(get_contract, tx_failed): 136 | code = """ 137 | a: public(DynArray[DynArray[Bytes[96], 5], 5]) 138 | 139 | @external 140 | def foo() -> Bytes[96]: 141 | self.a.append([b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx']) 142 | return self.a[0][self.bar()] 143 | 144 | 145 | @internal 146 | def bar() -> uint256: 147 | self.a[0] = [b'yyy'] 148 | self.a.pop() 149 | return 0 150 | """ 151 | c = get_contract(code) 152 | # tricky to get this right, for now we just panic instead of generating code 153 | with tx_failed(): 154 | c.foo() 155 | 156 | 157 | def test_array_index_overlap_extcall(get_contract, tx_failed): 158 | code = """ 159 | 160 | interface Bar: 161 | def bar() -> uint256: payable 162 | 163 | a: public(DynArray[DynArray[Bytes[96], 5], 5]) 164 | 165 | @external 166 | def foo() -> Bytes[96]: 167 | self.a.append([b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx']) 168 | return self.a[0][extcall Bar(self).bar()] 169 | 170 | 171 | @external 172 | def bar() -> uint256: 173 | self.a[0] = [b'yyy'] 174 | self.a.pop() 175 | return 0 176 | """ 177 | c = get_contract(code) 178 | with tx_failed(): 179 | c.foo() 180 | 181 | 182 | def test_array_index_overlap_extcall2(get_contract): 183 | code = """ 184 | interface B: 185 | def calculate_index() -> uint256: nonpayable 186 | 187 | a: HashMap[uint256, DynArray[uint256, 5]] 188 | 189 | @external 190 | def bar() -> uint256: 191 | self.a[0] = [2] 192 | return self.a[0][extcall B(self).calculate_index()] 193 | 194 | @external 195 | def calculate_index() -> uint256: 196 | self.a[0] = [1] 197 | return 0 198 | """ 199 | c = get_contract(code) 200 | 201 | assert c.bar() == 1 202 | -------------------------------------------------------------------------------- /src/ivy/evm/evm_structures.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from dataclasses import dataclass 3 | 4 | from vyper.semantics.types.function import ContractFunctionT 5 | from vyper.semantics.types.module import ModuleT 6 | from vyper.semantics.types.subscriptable import TupleT 7 | 8 | from ivy.utils import compute_call_abi_data 9 | from ivy.variable import GlobalVariables 10 | from ivy.types import Address 11 | from ivy import journal 12 | 13 | 14 | @dataclass 15 | class EntryPointInfo: 16 | function: ContractFunctionT 17 | calldata_args_t: TupleT 18 | calldata_min_size: int 19 | 20 | 21 | # dict for constant variables 22 | # TODO: should we use it also for immutables? 23 | class AssignOnceDict(dict): 24 | def __setitem__(self, key, value): 25 | if key in self: 26 | raise ValueError( 27 | f"Cannot reassign key '{key}': already has value '{self[key]}'" 28 | ) 29 | super().__setitem__(key, value) 30 | 31 | 32 | class ContractData: 33 | module_t: ModuleT 34 | ext_funs: dict[str, ContractFunctionT] 35 | internal_funs: dict[str, ContractFunctionT] 36 | immutables: dict[str, Any] 37 | constants: dict[str, Any] 38 | entry_points: dict[bytes, EntryPointInfo] 39 | global_vars: GlobalVariables 40 | fallback: Optional[ContractFunctionT] 41 | 42 | def __init__(self, module: ModuleT): 43 | self.module_t = module 44 | 45 | self.ext_funs: dict[str, ContractFunctionT] = { 46 | f.name: f for f in module.exposed_functions 47 | } 48 | self.internal_funs: dict[str, ContractFunctionT] = { 49 | f: f for f in module.functions if f not in self.ext_funs.values() 50 | } 51 | self.immutables = {} 52 | self.constants = AssignOnceDict() 53 | self.entry_points = {} 54 | self._generate_entry_points() 55 | self.global_vars = GlobalVariables() 56 | self.fallback = next( 57 | (f for f in module.exposed_functions if f.is_fallback), None 58 | ) 59 | 60 | def _generate_entry_points(self): 61 | def process(func_t, calldata_kwargs): 62 | selector, calldata_args_t = compute_call_abi_data( 63 | func_t, len(calldata_kwargs) 64 | ) 65 | 66 | args_abi_t = calldata_args_t.abi_type 67 | calldata_min_size = args_abi_t.static_size() + 4 68 | 69 | return selector, calldata_min_size, calldata_args_t 70 | 71 | for f in self.module_t.exposed_functions: 72 | if f.name == "__default__": 73 | continue 74 | 75 | keyword_args = f.keyword_args 76 | 77 | for i, _ in enumerate(keyword_args): 78 | calldata_kwargs = keyword_args[:i] 79 | 80 | selector, calldata_min_size, calldata_args_t = process( 81 | f, calldata_kwargs 82 | ) 83 | 84 | assert selector not in self.entry_points 85 | self.entry_points[selector] = EntryPointInfo( 86 | f, calldata_args_t, calldata_min_size 87 | ) 88 | 89 | selector, calldata_min_size, calldata_args_t = process(f, keyword_args) 90 | assert selector not in self.entry_points 91 | self.entry_points[selector] = EntryPointInfo( 92 | f, calldata_args_t, calldata_min_size 93 | ) 94 | 95 | 96 | @dataclass 97 | class Account: 98 | nonce: int 99 | _balance: int 100 | storage: dict[int, Any] 101 | transient: dict[int, Any] 102 | contract_data: Optional[ContractData] 103 | 104 | # Define balance as a property 105 | @property 106 | def balance(self) -> int: 107 | return self._balance 108 | 109 | @balance.setter 110 | def balance(self, new_value: int): 111 | journal.Journal().record( 112 | entry_type=journal.JournalEntryType.BALANCE, 113 | obj=self, 114 | key="balance", 115 | old_value=self._balance, 116 | ) 117 | self._balance = new_value 118 | 119 | def __hash__(self): 120 | return hash(id(self)) 121 | 122 | def __eq__(self, other): 123 | return self is other 124 | 125 | 126 | @dataclass 127 | class Environment: 128 | caller: Any # Address 129 | block_hashes: Any # List[Hash32] 130 | origin: Any # Address 131 | coinbase: Any # Address 132 | block_number: Any # Uint 133 | time: Any # U256 134 | prev_randao: Any # Bytes32 135 | chain_id: Any # U64 136 | 137 | 138 | @dataclass 139 | class Message: # msg from execution specs 140 | caller: Any # Address 141 | to: Any # Union[Bytes0, Address] 142 | create_address: Any # Address 143 | value: Any # U256 144 | data: Any # Bytes 145 | code_address: Any # Optional[Address] 146 | code: Optional[ContractData] 147 | depth: Any # Uint 148 | is_static: bool 149 | 150 | 151 | @dataclass 152 | class Log: 153 | address: Address 154 | topics: list[bytes] 155 | data: bytes 156 | -------------------------------------------------------------------------------- /src/ivy/builtins/builtin_registry.py: -------------------------------------------------------------------------------- 1 | from ivy.builtins.builtins import ( 2 | builtin_len, 3 | builtin_slice, 4 | builtin_concat, 5 | builtin_max, 6 | builtin_min, 7 | builtin_uint2str, 8 | builtin_empty, 9 | builtin_max_value, 10 | builtin_min_value, 11 | builtin_range, 12 | builtin_convert, 13 | builtin_as_wei_value, 14 | builtin_abi_decode, 15 | builtin_abi_encode, 16 | builtin_method_id, 17 | builtin_print, 18 | builtin_raw_call, 19 | builtin_send, 20 | builtin_create_copy_of, 21 | builtin_create_from_blueprint, 22 | builtin_create_minimal_proxy_to, 23 | builtin_raw_revert, 24 | builtin_unsafe_add, 25 | builtin_unsafe_sub, 26 | builtin_unsafe_mul, 27 | builtin_unsafe_div, 28 | builtin_floor, 29 | builtin_ceil, 30 | builtin_epsilon, 31 | builtin_abs, 32 | builtin_sqrt, 33 | builtin_isqrt, 34 | builtin_keccak256, 35 | builtin_extract32, 36 | builtin_uint256_addmod, 37 | builtin_uint256_mulmod, 38 | builtin_shift, 39 | ) 40 | 41 | 42 | class BuiltinWrapper: 43 | def __init__(self, fn, context=None, needs_types=False): 44 | self.fn = fn 45 | self.context = context 46 | # we don't tag runtime values with a corresponding vyper type, thus we have to 47 | # collect the types separately and for certain builtins inject them 48 | self.needs_types = needs_types 49 | 50 | def execute(self, *args, typs=None, **kwargs): 51 | if self.needs_types: 52 | if typs is None: 53 | raise ValueError("Type information is required for this built-in") 54 | if self.context: 55 | return self.fn(self.context, typs, *args, **kwargs) 56 | return self.fn(typs, *args, **kwargs) 57 | if self.context: 58 | return self.fn(self.context, *args, **kwargs) 59 | return self.fn(*args, **kwargs) 60 | 61 | 62 | class BuiltinRegistry: 63 | def __init__(self, evm_core, state): 64 | self.evm = evm_core 65 | self.state = state 66 | self.builtins = self._register_builtins() 67 | 68 | def _register_builtins(self): 69 | return { 70 | # Pure built-ins (no context, no types) 71 | "len": BuiltinWrapper(builtin_len), 72 | "slice": BuiltinWrapper(builtin_slice), 73 | "concat": BuiltinWrapper(builtin_concat), 74 | "max": BuiltinWrapper(builtin_max), 75 | "min": BuiltinWrapper(builtin_min), 76 | "uint2str": BuiltinWrapper(builtin_uint2str), 77 | "empty": BuiltinWrapper(builtin_empty), 78 | "max_value": BuiltinWrapper(builtin_max_value), 79 | "min_value": BuiltinWrapper(builtin_min_value), 80 | "range": BuiltinWrapper(builtin_range), 81 | "as_wei_value": BuiltinWrapper(builtin_as_wei_value), 82 | "_abi_decode": BuiltinWrapper(builtin_abi_decode), 83 | "abi_decode": BuiltinWrapper(builtin_abi_decode), 84 | "method_id": BuiltinWrapper(builtin_method_id), 85 | "print": BuiltinWrapper(builtin_print), 86 | "raw_revert": BuiltinWrapper(builtin_raw_revert), 87 | # Pure built-ins (no context, with types) 88 | "convert": BuiltinWrapper(builtin_convert, needs_types=True), 89 | "abi_encode": BuiltinWrapper(builtin_abi_encode, needs_types=True), 90 | "_abi_encode": BuiltinWrapper(builtin_abi_encode, needs_types=True), 91 | # EVM built-ins (with evm context, no types) 92 | "raw_call": BuiltinWrapper(builtin_raw_call, context=self.evm), 93 | "send": BuiltinWrapper(builtin_send, context=self.evm), 94 | "create_copy_of": BuiltinWrapper(builtin_create_copy_of, context=self.evm), 95 | "create_from_blueprint": BuiltinWrapper( 96 | builtin_create_from_blueprint, context=self.evm, needs_types=True 97 | ), 98 | "create_minimal_proxy_to": BuiltinWrapper( 99 | builtin_create_minimal_proxy_to, context=self.evm 100 | ), 101 | "unsafe_add": BuiltinWrapper(builtin_unsafe_add, needs_types=True), 102 | "unsafe_sub": BuiltinWrapper(builtin_unsafe_sub, needs_types=True), 103 | "unsafe_mul": BuiltinWrapper(builtin_unsafe_mul, needs_types=True), 104 | "unsafe_div": BuiltinWrapper(builtin_unsafe_div, needs_types=True), 105 | "floor": BuiltinWrapper(builtin_floor), 106 | "ceil": BuiltinWrapper(builtin_ceil), 107 | "epsilon": BuiltinWrapper(builtin_epsilon), 108 | "abs": BuiltinWrapper(builtin_abs), 109 | "sqrt": BuiltinWrapper(builtin_sqrt), 110 | "isqrt": BuiltinWrapper(builtin_isqrt), 111 | "keccak256": BuiltinWrapper(builtin_keccak256), 112 | "extract32": BuiltinWrapper(builtin_extract32), 113 | "uint256_addmod": BuiltinWrapper(builtin_uint256_addmod), 114 | "uint256_mulmod": BuiltinWrapper(builtin_uint256_mulmod), 115 | "shift": BuiltinWrapper(builtin_shift), 116 | } 117 | 118 | def get(self, name): 119 | if name not in self.builtins: 120 | raise ValueError(f"Unknown builtin: {name}") 121 | return self.builtins[name] 122 | -------------------------------------------------------------------------------- /src/fuzzer/mutator/argument_mutator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Argument mutation utilities for function calls. 3 | 4 | This module provides type-aware mutation of function arguments for both 5 | deployment (constructor) and regular function calls. 6 | """ 7 | 8 | import random 9 | from typing import List, Any, Optional, Tuple 10 | 11 | from .value_mutator import ValueMutator 12 | from vyper.semantics.types import BytesT, BytesM_T, VyperType 13 | from vyper.semantics.types.function import ContractFunctionT 14 | 15 | 16 | class ArgumentMutator: 17 | """Mutates function arguments based on Vyper type information.""" 18 | 19 | def __init__( 20 | self, rng: random.Random, value_mutator: Optional[ValueMutator] = None 21 | ): 22 | """Initialize the argument mutator. 23 | 24 | Args: 25 | rng: Random number generator for consistent randomness 26 | value_mutator: Value mutator instance (creates one if not provided) 27 | """ 28 | self.rng = rng 29 | self.value_mutator = value_mutator or ValueMutator(rng) 30 | 31 | def normalize_arguments_with_types( 32 | self, 33 | arg_types: List[VyperType], 34 | args: List[Any], 35 | ) -> List[Any]: 36 | """ 37 | Normalize args to ensure boa type compatibility (e.g., converting hex strings to bytes) 38 | """ 39 | normalized_args = args.copy() 40 | 41 | for i, (arg_type, arg_value) in enumerate(zip(arg_types, args)): 42 | # abi encoder in boa requires the bytes to be of bytes type 43 | if isinstance(arg_value, str) and isinstance(arg_type, (BytesT, BytesM_T)): 44 | assert arg_value.startswith("0x") 45 | normalized_args[i] = bytes.fromhex(arg_value.removeprefix("0x")) 46 | 47 | return normalized_args 48 | 49 | def mutate_arguments_with_types( 50 | self, 51 | arg_types: List[Any], # VyperType objects 52 | args: List[Any], 53 | mutation_prob: float = 0.3, 54 | ) -> List[Any]: 55 | """Mutate arguments based on Vyper types. 56 | 57 | Args: 58 | arg_types: List of Vyper type objects 59 | args: List of argument values 60 | mutation_prob: Probability of mutating each argument 61 | 62 | Returns: 63 | List of potentially mutated arguments 64 | """ 65 | mutated_args = args.copy() 66 | 67 | for i, (arg_type, arg_value) in enumerate(zip(arg_types, args)): 68 | if i < len(mutated_args) and self.rng.random() < mutation_prob: 69 | if self.rng.random() < 0.2: 70 | mutated_args[i] = self.value_mutator.mutate(arg_value, arg_type) 71 | 72 | return mutated_args 73 | 74 | def mutate_function_call( 75 | self, 76 | function: Optional[ContractFunctionT], 77 | args: List[Any], 78 | value: int, 79 | mutation_prob: float = 0.3, 80 | ) -> Tuple[List[Any], int]: 81 | """Unified mutation logic for any function call (deployment or regular). 82 | 83 | This method provides consistent mutation behavior for both deployment 84 | and regular function calls, ensuring code reuse and maintainability. 85 | 86 | Args: 87 | function: Vyper function type (None for functions without type info) 88 | args: Function arguments 89 | value: ETH value sent with the call 90 | mutation_prob: Probability of mutating arguments 91 | 92 | Returns: 93 | Tuple of (mutated_args, mutated_value) 94 | """ 95 | mutated_args = args.copy() 96 | 97 | # Mutate arguments if we have type information 98 | if function and args: 99 | arg_types = function.argument_types 100 | mutated_args = self.mutate_arguments_with_types( 101 | arg_types, args, mutation_prob 102 | ) 103 | 104 | # Mutate ETH value based on payability 105 | mutated_value = self.value_mutator.mutate_eth_value( 106 | value, 107 | is_payable=function.is_payable if function else False, 108 | ) 109 | 110 | return mutated_args, mutated_value 111 | 112 | def mutate_deployment_args( 113 | self, 114 | init_function: Optional[ContractFunctionT], 115 | deploy_args: List[Any], 116 | deploy_value: int, 117 | ) -> Tuple[List[Any], int]: 118 | """Mutate deployment arguments using init function type info. 119 | 120 | Args: 121 | init_function: Contract's init function type (None if no constructor) 122 | deploy_args: Constructor arguments 123 | deploy_value: ETH value sent with deployment 124 | 125 | Returns: 126 | Tuple of (mutated_args, mutated_value) 127 | """ 128 | return self.mutate_function_call(init_function, deploy_args, deploy_value) 129 | 130 | def mutate_call_args( 131 | self, function: ContractFunctionT, call_args: List[Any], call_value: int = 0 132 | ) -> Tuple[List[Any], int]: 133 | """Mutate call arguments using function type info. 134 | 135 | Args: 136 | function: Function type information 137 | call_args: Function arguments 138 | call_value: ETH value sent with call 139 | 140 | Returns: 141 | Tuple of (mutated_args, mutated_value) 142 | """ 143 | return self.mutate_function_call(function, call_args, call_value) 144 | -------------------------------------------------------------------------------- /tests/ivy/compiler/functional/codegen/builtins/test_minmax.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.ivy.utils import decimal_to_int 4 | from vyper.semantics.types import IntegerT 5 | 6 | 7 | def test_minmax(get_contract): 8 | minmax_test = """ 9 | @external 10 | def foo() -> decimal: 11 | return min(3.0, 5.0) + max(10.0, 20.0) + min(200.1, 400.0) + max(3000.0, 8000.02) + min(50000.003, 70000.004) # noqa: E501 12 | 13 | @external 14 | def goo() -> uint256: 15 | return min(3, 5) + max(40, 80) 16 | """ 17 | 18 | c = get_contract(minmax_test) 19 | assert c.foo() == decimal_to_int("58223.123") 20 | assert c.goo() == 83 21 | 22 | print("Passed min/max test") 23 | 24 | 25 | @pytest.mark.parametrize("return_type", sorted(IntegerT.all())) 26 | def test_minmax_var_and_literal_and_bultin(get_contract, return_type): 27 | """ 28 | Tests to verify that min and max work as expected when a variable/literal 29 | and a literal are passed for all integer types. 30 | """ 31 | lo, hi = return_type.ast_bounds 32 | 33 | code = f""" 34 | @external 35 | def foo() -> {return_type}: 36 | a: {return_type} = {hi} 37 | b: {return_type} = 5 38 | return max(a, 5) 39 | 40 | @external 41 | def bar() -> {return_type}: 42 | a: {return_type} = {lo} 43 | b: {return_type} = 5 44 | return min(a, 5) 45 | 46 | @external 47 | def both_literals_max() -> {return_type}: 48 | return max({hi}, 2) 49 | 50 | @external 51 | def both_literals_min() -> {return_type}: 52 | return min({lo}, 2) 53 | 54 | @external 55 | def both_builtins_max() -> {return_type}: 56 | return max(min_value({return_type}), max_value({return_type})) 57 | 58 | @external 59 | def both_builtins_min() -> {return_type}: 60 | return min(min_value({return_type}), max_value({return_type})) 61 | """ 62 | c = get_contract(code) 63 | assert c.foo() == hi 64 | assert c.bar() == lo 65 | assert c.both_literals_max() == hi 66 | assert c.both_literals_min() == lo 67 | assert c.both_builtins_max() == hi 68 | assert c.both_builtins_min() == lo 69 | 70 | 71 | def test_max_var_uint256_literal_int128(get_contract): 72 | """ 73 | Tests to verify that max works as expected when a variable/literal uint256 74 | and a literal int128 are passed. 75 | """ 76 | code = """ 77 | @external 78 | def foo() -> uint256: 79 | a: uint256 = 2 ** 200 80 | b: uint256 = 5 81 | return max(a, 5) + max(b, 5) 82 | 83 | @external 84 | def goo() -> uint256: 85 | a: uint256 = 2 ** 200 86 | b: uint256 = 5 87 | return max(5, a) + max(5, b) 88 | 89 | @external 90 | def bar() -> uint256: 91 | a: uint256 = 2 92 | b: uint256 = 5 93 | return max(a, 5) + max(b, 5) 94 | 95 | @external 96 | def baz() -> uint256: 97 | a: uint256 = 2 98 | b: uint256 = 5 99 | return max(5, a) + max(5, b) 100 | 101 | @external 102 | def both_literals() -> uint256: 103 | return max(2 ** 200, 2) 104 | """ 105 | c = get_contract(code) 106 | assert c.foo() == 2**200 + 5 107 | assert c.goo() == 2**200 + 5 108 | assert c.bar() == 5 + 5 109 | assert c.baz() == 5 + 5 110 | assert c.both_literals() == 2**200 111 | 112 | 113 | def test_min_var_uint256_literal_int128(get_contract): 114 | """ 115 | Tests to verify that max works as expected when a variable/literal uint256 116 | and a literal int128 are passed. 117 | """ 118 | code = """ 119 | @external 120 | def foo() -> uint256: 121 | a: uint256 = 2 ** 200 122 | b: uint256 = 5 123 | return min(a, 5) + min(b, 5) 124 | 125 | @external 126 | def goo() -> uint256: 127 | a: uint256 = 2 ** 200 128 | b: uint256 = 5 129 | return min(5, a) + min(5, b) 130 | 131 | @external 132 | def bar() -> uint256: 133 | a: uint256 = 2 134 | b: uint256 = 5 135 | return min(a, 5) + min(b, 5) 136 | 137 | @external 138 | def baz() -> uint256: 139 | a: uint256 = 2 140 | b: uint256 = 5 141 | return min(5, a) + min(5, b) 142 | 143 | @external 144 | def both_literals() -> uint256: 145 | return min(2 ** 200, 2) 146 | """ 147 | c = get_contract(code) 148 | assert c.foo() == 5 + 5 149 | assert c.goo() == 5 + 5 150 | assert c.bar() == 2 + 5 151 | assert c.baz() == 2 + 5 152 | assert c.both_literals() == 2 153 | 154 | 155 | def test_unsigned(get_contract): 156 | code = """ 157 | @external 158 | def foo1() -> uint256: 159 | return min(0, 2**255) 160 | 161 | @external 162 | def foo2() -> uint256: 163 | return min(2**255, 0) 164 | 165 | @external 166 | def foo3() -> uint256: 167 | return max(0, 2**255) 168 | 169 | @external 170 | def foo4() -> uint256: 171 | return max(2**255, 0) 172 | """ 173 | 174 | c = get_contract(code) 175 | assert c.foo1() == 0 176 | assert c.foo2() == 0 177 | assert c.foo3() == 2**255 178 | assert c.foo4() == 2**255 179 | 180 | 181 | def test_signed(get_contract): 182 | code = """ 183 | @external 184 | def foo1() -> int128: 185 | return min(min_value(int128), max_value(int128)) 186 | 187 | @external 188 | def foo2() -> int128: 189 | return min(max_value(int128), min_value(int128)) 190 | 191 | @external 192 | def foo3() -> int128: 193 | return max(min_value(int128), max_value(int128)) 194 | 195 | @external 196 | def foo4() -> int128: 197 | return max(max_value(int128), min_value(int128)) 198 | """ 199 | 200 | c = get_contract(code) 201 | assert c.foo1() == -(2**127) 202 | assert c.foo2() == -(2**127) 203 | assert c.foo3() == 2**127 - 1 204 | assert c.foo4() == 2**127 - 1 205 | -------------------------------------------------------------------------------- /src/ivy/stmt.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from vyper.ast.nodes import VyperNode 4 | from vyper.ast import nodes as ast 5 | from vyper.codegen.core import calculate_type_for_external_return 6 | from vyper.semantics.types import VyperType 7 | from vyper.utils import method_id 8 | 9 | from ivy.abi import abi_encode 10 | from ivy.exceptions import Assert, Raise, Invalid 11 | from ivy.visitor import BaseVisitor 12 | from ivy.expr.operators import get_operator_handler 13 | from ivy.expr.clamper import validate_value 14 | 15 | 16 | class ReturnException(Exception): 17 | def __init__(self, value): 18 | self.value = value 19 | 20 | 21 | class ContinueException(Exception): 22 | pass 23 | 24 | 25 | class BreakException(Exception): 26 | pass 27 | 28 | 29 | class StmtVisitor(BaseVisitor): 30 | def visit_Expr(self, node: ast.Expr): 31 | return self.visit(node.value) 32 | 33 | def visit_Pass(self, node: ast.Pass): 34 | return None 35 | 36 | def visit_AnnAssign(self, node: ast.AnnAssign): 37 | value = self.deep_copy_visit(node.value) 38 | typ = node.target._expr_info.typ 39 | self._new_local(node.target.id, typ) 40 | self.set_variable(node.target.id, value) 41 | return None 42 | 43 | def visit_Assign(self, node: ast.Assign): 44 | value = self.deep_copy_visit(node.value) 45 | self._assign_target(node.target, value) 46 | 47 | return None 48 | 49 | def visit_If(self, node: ast.If): 50 | condition = self.visit(node.test) 51 | if condition: 52 | return self.visit_body(node.body) 53 | elif node.orelse: 54 | return self.visit_body(node.orelse) 55 | return None 56 | 57 | def _revert_with_msg(self, msg_node, is_assert=False): 58 | msg_string = self.visit(msg_node) 59 | 60 | assert isinstance(msg_string, str) 61 | if msg_string == "UNREACHABLE": 62 | raise Invalid() 63 | 64 | # encode the msg and raise Revert 65 | error_method_id = method_id("Error(string)") 66 | typ = msg_node._metadata["type"] 67 | wrapped_typ = calculate_type_for_external_return(typ) 68 | wrapped_msg = (msg_string,) 69 | 70 | encoded = abi_encode(wrapped_typ, wrapped_msg) 71 | 72 | to_raise = Assert if is_assert else Raise 73 | 74 | raise to_raise(message=msg_string, data=error_method_id + encoded) 75 | 76 | def visit_Assert(self, node: ast.Assert): 77 | condition = self.visit(node.test) 78 | if not condition: 79 | if node.msg: 80 | self._revert_with_msg(node.msg, is_assert=True) 81 | else: 82 | raise Assert() 83 | return None 84 | 85 | def visit_Raise(self, node: ast.Raise): 86 | if node.exc: 87 | self._revert_with_msg(node.exc) 88 | else: 89 | raise Raise() 90 | 91 | def visit_For(self, node: ast.For): 92 | iterable = self.visit(node.iter) 93 | # Scope for the iterator variable (not stricly neccessary) 94 | self._push_scope() 95 | target_name = node.target.target 96 | target_typ = target_name._expr_info.typ 97 | self._new_local(target_name.id, target_typ) 98 | 99 | for item in iterable: 100 | # New scope for each iteration 101 | self._push_scope() 102 | self._assign_target(target_name, item) 103 | 104 | try: 105 | for stmt in node.body: 106 | self.visit(stmt) 107 | except ContinueException: 108 | continue 109 | except BreakException: 110 | break 111 | finally: 112 | self._pop_scope() # Pop iteration scope 113 | 114 | self._pop_scope() # Pop iterator variable scope 115 | return None 116 | 117 | def visit_AugAssign(self, node: ast.AugAssign): 118 | target_val = self.visit(node.target) 119 | rhs_val = self.visit(node.value) 120 | 121 | handler = get_operator_handler(node.op) 122 | new_val = handler(target_val, rhs_val) 123 | validate_value(node.target, new_val) 124 | self._assign_target(node.target, new_val) 125 | return None 126 | 127 | def visit_Continue(self, node: ast.Continue): 128 | raise ContinueException 129 | 130 | def visit_Break(self, node: ast.Break): 131 | raise BreakException 132 | 133 | def visit_Log(self, node: ast.Log): 134 | assert isinstance(node.value, ast.Call) 135 | self.visit(node.value) 136 | 137 | def visit_Return(self, node: ast.Return): 138 | if node.value: 139 | value = self.deep_copy_visit(node.value) 140 | raise ReturnException(value) 141 | raise ReturnException(None) 142 | 143 | def visit_body(self, body: list[VyperNode]): 144 | for stmt in body: 145 | result = self.visit(stmt) 146 | if ( 147 | result == "continue" 148 | or result == "break" 149 | or isinstance(result, Exception) 150 | ): 151 | return result 152 | return None 153 | 154 | @abstractmethod 155 | def _push_scope(self): 156 | pass 157 | 158 | @abstractmethod 159 | def _pop_scope(self): 160 | pass 161 | 162 | @abstractmethod 163 | def _new_local(self, name: str, typ: VyperType): 164 | pass 165 | 166 | @property 167 | @abstractmethod 168 | def memory(self): 169 | pass 170 | -------------------------------------------------------------------------------- /src/fuzzer/replay_divergence.py: -------------------------------------------------------------------------------- 1 | """ 2 | Replay a saved divergence by reconstructing the exact mutated scenario. 3 | 4 | Usage: 5 | PYTHONPATH=src python -m fuzzer.replay_divergence path/to/divergence.json 6 | """ 7 | 8 | import json 9 | import sys 10 | from pathlib import Path 11 | from typing import Any, Dict, List 12 | 13 | from vyper.compiler.settings import OptimizationLevel 14 | 15 | from .trace_types import DeploymentTrace, CallTrace, Env, Tx, Block 16 | from .xfail import XFailExpectation 17 | from .runner.scenario import Scenario 18 | from .runner.multi_runner import MultiRunner 19 | from .divergence_detector import DivergenceDetector 20 | 21 | 22 | def _load_divergence(path: Path) -> dict: 23 | with open(path, "r") as f: 24 | return json.load(f) 25 | 26 | 27 | def _deserialize_env(data: Dict[str, Any] | None) -> Env | None: 28 | """Deserialize Env from dict.""" 29 | if data is None: 30 | return None 31 | return Env( 32 | tx=Tx(**data["tx"]), 33 | block=Block(**data["block"]), 34 | ) 35 | 36 | 37 | def _deserialize_xfails(data: List[Dict[str, Any]] | None) -> List[XFailExpectation]: 38 | """Deserialize list of XFailExpectation from dicts.""" 39 | if not data: 40 | return [] 41 | return [XFailExpectation(**x) for x in data] 42 | 43 | 44 | def _deserialize_compiler_settings( 45 | data: Dict[str, Any] | None, 46 | ) -> Dict[str, Any] | None: 47 | """Deserialize compiler settings, converting string enums back to proper types.""" 48 | if not data: 49 | return None 50 | result = dict(data) 51 | # Convert 'optimize' string back to OptimizationLevel enum 52 | if "optimize" in result and isinstance(result["optimize"], str): 53 | opt_str = result["optimize"].upper() 54 | result["optimize"] = OptimizationLevel[opt_str] 55 | return result 56 | 57 | 58 | def _deserialize_deployment_trace(data: Dict[str, Any]) -> DeploymentTrace: 59 | """Deserialize DeploymentTrace from dict.""" 60 | return DeploymentTrace( 61 | deployment_type=data["deployment_type"], 62 | contract_abi=data["contract_abi"], 63 | initcode=data["initcode"], 64 | calldata=data.get("calldata"), 65 | value=data["value"], 66 | source_code=data.get("source_code"), 67 | annotated_ast=data.get("annotated_ast"), 68 | solc_json=data.get("solc_json"), 69 | raw_ir=data.get("raw_ir"), 70 | blueprint_initcode_prefix=data.get("blueprint_initcode_prefix"), 71 | deployed_address=data["deployed_address"], 72 | runtime_bytecode=data["runtime_bytecode"], 73 | deployment_succeeded=data["deployment_succeeded"], 74 | env=_deserialize_env(data.get("env")), 75 | python_args=data.get("python_args"), 76 | compiler_settings=_deserialize_compiler_settings(data.get("compiler_settings")), 77 | compilation_xfails=_deserialize_xfails(data.get("compilation_xfails")), 78 | runtime_xfails=_deserialize_xfails(data.get("runtime_xfails")), 79 | ) 80 | 81 | 82 | def _deserialize_call_trace(data: Dict[str, Any]) -> CallTrace: 83 | """Deserialize CallTrace from dict.""" 84 | return CallTrace( 85 | output=data.get("output"), 86 | call_args=data["call_args"], 87 | call_succeeded=data.get("call_succeeded"), 88 | env=_deserialize_env(data.get("env")), 89 | python_args=data.get("python_args"), 90 | function_name=data.get("function_name"), 91 | runtime_xfails=_deserialize_xfails(data.get("runtime_xfails")), 92 | ) 93 | 94 | 95 | def _build_scenario_from_traces(data: dict) -> Scenario: 96 | """Build a Scenario directly from divergence trace data.""" 97 | traces_data = data.get("traces", []) 98 | 99 | traces = [] 100 | for trace_data in traces_data: 101 | # Check for deployment_type to identify DeploymentTrace 102 | if "deployment_type" in trace_data: 103 | traces.append(_deserialize_deployment_trace(trace_data)) 104 | # Check for call_args to identify CallTrace 105 | elif "call_args" in trace_data: 106 | traces.append(_deserialize_call_trace(trace_data)) 107 | 108 | return Scenario(traces=traces, use_python_args=True) 109 | 110 | 111 | def replay_divergence(divergence_path: Path) -> bool: 112 | data = _load_divergence(divergence_path) 113 | 114 | if not data.get("traces"): 115 | raise ValueError("Cannot replay: no traces in divergence file") 116 | 117 | scenario = _build_scenario_from_traces(data) 118 | return _run_and_check(scenario) 119 | 120 | 121 | def _run_and_check(scenario: Scenario) -> bool: 122 | """Run scenario and check for divergences.""" 123 | multi_runner = MultiRunner(collect_storage_dumps=True, no_solc_json=True) 124 | detector = DivergenceDetector() 125 | 126 | results = multi_runner.run(scenario) 127 | 128 | divergences = detector.compare_all_results( 129 | results.ivy_result, results.boa_results, scenario 130 | ) 131 | return len(divergences) > 0 132 | 133 | 134 | def main(argv: list[str]) -> None: 135 | if len(argv) != 2: 136 | print("Usage: python -m fuzzer.replay_divergence path/to/divergence.json") 137 | raise SystemExit(2) 138 | 139 | path = Path(argv[1]) 140 | ok = replay_divergence(path) 141 | if ok: 142 | print("Reproduced divergence") 143 | raise SystemExit(0) 144 | else: 145 | print("No divergence reproduced; Ivy and Boa matched") 146 | raise SystemExit(1) 147 | 148 | 149 | if __name__ == "__main__": 150 | main(sys.argv) 151 | --------------------------------------------------------------------------------