titanoboa icon indicating copy to clipboard operation
titanoboa copied to clipboard

Something is off with the version selector, causing weird issues across imports

Open PatrickAlphaC opened this issue 8 months ago • 4 comments

I get the following errors:

vyper.exceptions.VersionException: Version specification "==0.4.0" is not compatible with compiler version "0.4.1"

  contract "src/interfaces/i_decentralized_stable_coin.vyi:1", line 1:0 
  ---> 1 # pragma version 0.4.0
  -------^
       2 """
vyper.exceptions.ModuleNotFound: interfaces.IERC20Permit

  contract "lib/pypi/snekmate/tokens/erc20.vy:69", line 69:0 
       68 # syntax.
  ---> 69 import interfaces.IERC20Permit as IERC20Permit
  --------^
       70 implements: IERC20Permit

After running the following:

git clone https://github.com/cyfrin/mox-stablecoin-cu
cd mox-stablecoin-cu
mox install
mox build

I'm thinking maybe it's getting confused with using snekmate? I'm not sure where it's breaking down...

PatrickAlphaC avatar Mar 25 '25 15:03 PatrickAlphaC

I might have an idea. It will be an addition to Titanoboa and also Moccasin to make it works with prior versions of the latest vyper. And let project with vyper==0.4.0 to run without crashing with the VVM.

The idea is to have access to the base_path in interpret.load_partial() to add it to _load_partial_vvm:

def _loads_partial_vvm(
    source_code: str,
    version: Version,
    name: Optional[str],
    filename: str,
    base_path=None,
):
    global _disk_cache

    if base_path is None:
        base_path = Path(".") # --> here if I a put manually the path `./contracts` or `./src` it works

The goal is to send this from the moccasin compile command so that VVM does not break.

- /home/s3bc40/gh-projects/moccasin/tests/data/complex_project
+ /home/s3bc40/gh-projects/moccasin/tests/data/complex_project/contracts
+ /home/s3bc40/gh-projects/moccasin/tests/data/complex_project/src

We avoid the following import error, which works if we are not going through the VVM:

- import contracts.interfaces.IERC20Permit as IERC20Permit
+ import interfaces.IERC20Permit as IERC20Permit

Two steps

  • [ ] 1. Modifying mox compile_ function to add the base_path to load_partial and handle vvm.VyperError to display the right path (not tmp to be able to ctrl+click)

  • [ ] 2. Adding the base_path as an optionnal arg in load_partial and loads_partial to reach _loads_partial_vvm

@PatrickAlphaC @charles-cooper does the process sound logical to you?

s3bc40 avatar Mar 27 '25 09:03 s3bc40

My proposal here: https://github.com/vyperlang/titanoboa/pull/391

Tell me if there is any breakpoint or if it is not relevant, I don't mind!

Thanks for your time!

s3bc40 avatar Mar 27 '25 10:03 s3bc40

I tried the get_search_paths(_search_path) but the main problem does not seem to be this. I struggled between my personal mistakes and the errors, but I always end up in the same error when we try to import from Moccasin lib:

from snekmate.auth import ownable as ow # does not work in VVM
from lib.pypi.snekmate.auth import ownable as ow # it works

I can not redo what seems to make the vyper compiler to take into account all the paths available for VVM like this:

# interpret.compiler_data()

global _disk_cache, _search_path

    path = Path(filename)
    resolved_path = Path(filename).resolve(strict=False)

    file_input = FileInput(
        contents=source_code, source_id=-1, path=path, resolved_path=resolved_path
    )

    search_paths = get_search_paths(_search_path)
    input_bundle = FilesystemInputBundle(search_paths)

    settings = Settings(**kwargs)
    ret = CompilerData(file_input, input_bundle, settings)
    if _disk_cache is None:
        return ret

    with anchor_settings(ret.settings):
        # note that this actually parses and analyzes all dependencies,
        # even if they haven't changed. an optimization would be to
        # somehow convince vyper (in ModuleAnalyzer) to get the module_t
        # from the cache.
        module_t = ret.annotated_vyper_module._metadata["type"]
    fingerprint = get_module_fingerprint(module_t)

It's the only missing part, my previous assumptions were not correct, thinking it was only the base_path value. I do not know if it is possible to make the vyper installed from _load_partial_vvm aware of the search_path availables:

# interpret._loads_partial_vvm

# install the requested version if not already installed
    vvm.install_vyper(version=version)

    def _compile():
        return vvm.compile_source(
            source_code, vyper_version=version, base_path=base_path
        )

s3bc40 avatar Mar 27 '25 18:03 s3bc40

New update with something that "works" but I do not think it is stable. At least it leads to something.


I essentially investigated inside the VVM code from the lib in my venv, and tried to get something out of it.

I ended up in the .venv/lib64/python3.12/site-packages/vvm/wrapper.py and added something that compile from the VVM and does not break on mox imports for snekmate or interfaces (last one is kind of random):

# [...]
from vyper.cli.vyper_compile import get_search_paths
# [...]
def vyper_wrapper(
    vyper_binary: Union[Path, str] = None,
    stdin: str = None,
    source_files: Union[List, Path, str] = None,
    success_return_code: int = 0,
    **kwargs: Any,
) -> Tuple[str, str, List, subprocess.Popen]:
# [...]
if source_files is not None:
        if isinstance(source_files, (str, Path)):
            command.append(_to_string("source_files", source_files))
        else:
            command.extend([_to_string("source_files", i) for i in source_files])

    for key, value in kwargs.items():
        if value is None or value is False:
            continue

        if len(key) == 1:
            key = f"-{key}"
        else:
            key = f"--{key.replace('_', '-')}"
        if value is True:
            command.append(key)
        else:
            command.extend([key, _to_string(key, value)])

    # ------------------ HERE ------------------------
    # Using search_paths and removing `.zip` path
    search_paths = [path for path in get_search_paths() if not str(path).endswith(".zip")]

    for path in search_paths:
        command.extend(["-p", path.as_posix()])
    # ------------------ HERE ------------------------

    if stdin is not None:
        stdin = str(stdin)
    # [...]

Why removing .zip? Because of the following error while compiling with the vyper binary:

FileNotFoundError: [Errno 2] No such file or directory: '/usr/lib/python312.zip'

I also had this error with a standalone python added in uv (3.13), that I removed afterward.

It is not a stable solution, but for the sake of the investigation I had to understand and find a lead.

I have added some print and disabled the _disk_cache to tests and see how it looks like (I couldn't see the prints after one successful compilation):

source_file.name: /tmp/vyper-b8ki6row.vy
base_path: .
command: [PosixPath('/home/s3bc40/.vvm/vyper-0.4.0'), '/tmp/vyper-g_nd5nr0.vy', '-f', 'combined_json', '-p', 'src', '-p', '/home/s3bc40/mox-stablecoin-cu/.venv/lib/python3.12/site-packages', '-p', '/usr/lib/python3.12/lib-dynload', '-p', '/usr/lib/python3.12', '-p', '/home/s3bc40/mox-stablecoin-cu/.venv/bin', '-p', '/home/s3bc40/mox-stablecoin-cu/lib', '-p', '/home/s3bc40/mox-stablecoin-cu/lib/pypi', '-p', '/home/s3bc40/mox-stablecoin-cu/lib/github', '-p', '/home/s3bc40/mox-stablecoin-cu/src', '-p', '/home/s3bc40/mox-stablecoin-cu', '-p', '.']

What I can't understand is why I need to do this? Even if the vyper cli function vyper_compile already has this system:

# vyper/cli/vyper_compile.py
@_apply_warnings_filter
def compile_files(
    input_files: list[str],
    output_formats: OutputFormats,
    paths: list[str] = None,
    include_sys_path: bool = True,
    show_gas_estimates: bool = False,
    settings: Optional[Settings] = None,
    storage_layout_paths: list[str] = None,
    no_bytecode_metadata: bool = False,
    warnings_control: Optional[str] = None,
) -> dict:
    search_paths = get_search_paths(paths, include_sys_path)
    input_bundle = FilesystemInputBundle(search_paths)
    # [...]

    output = vyper.compile_from_file_input(
            file,
            input_bundle=input_bundle,
            output_formats=final_formats,
            exc_handler=exc_handler,
            settings=settings,
            storage_layout_override=storage_layout_override,
            show_gas_estimates=show_gas_estimates,
            no_bytecode_metadata=no_bytecode_metadata,
        )

I think I won't search any deeper since I'm lacking of experience and knowledge. I might make more mistakes along the way. Hope this helps @PatrickAlphaC @charles-cooper 🙏

s3bc40 avatar Mar 28 '25 12:03 s3bc40