Titanoboa Vyper Interpreter

repository·master·Indexed 43 days ago

https://github.com/vyperlang/titanoboa

An advanced Vyper interpreter (v0.2.8) that compiles code to bytecode and executes it via py-evm. It provides a modern development experience with features such as pretty tracebacks, easy forking, deep introspection, and the ability to interact with contracts via ABI using ABIContract.

Tokens
44.5K
Snippets
190
Records
221
Agent score
52%

What's inside Titanoboa

  1. Overview of Titanoboa (boa)

    master

    Titanoboa (also referred to as boa) is a Vyper interpreter designed to provide an advanced development experience within Python environments. It bridges the gap between Python and Vyper by allowing developers to run Vyper code directly in Python scripts, Jupyter notebooks, or testing frameworks with minimal context-switching.

    Key features include:

    • Pretty tracebacks for easier error debugging.
    • Forking capabilities for testing.
    • Debugging features and opcode patching.
    • Native Python import syntax for Vyper modules.
    • Integration with pytest, Jupyter, and iPython.
  2. What is the `boa.env` object?

    master
    The boa.env object is a wrapper class around py-evm that provides a "contract-centric" API. It manages the execution environment, including state, gas metering, and account context. It is typically accessed via a singleton pattern to ensure a consistent environment across your testing or development session.
  3. What is ABIContract and when to use it

    master

    An ABIContract is a specialized contract class used to interact with Ethereum contracts using only their Application Binary Interface (ABI) rather than their source code.

    Use ABIContract when:

    • You have the ABI but not the source code (e.g., interacting with external or third-party contracts).
    • You want to interact with a contract already deployed on the blockchain.

    Limitations:

    • It cannot deploy new contracts (use boa.load() with Vyper source code for that).
    • It lacks storage introspection (_storage) because it has no source code.
    • It can only access external or public functions defined in the ABI.
    • Gas estimation may be less accurate than with a full VyperContract.
  4. Manage state with the Open Context Manager pattern

    master

    Titanoboa supports an "open" pattern where certain functions can act as both regular calls and context managers to manage state changes.

    Permanent Changes

    Calling a function directly (e.g., boa.set_env(new_env)) applies changes permanently to the current environment.

    Temporary Changes (Reverting)

    Using a function as a context manager (e.g., with boa.set_env(env):) ensures that any changes made within the block are reverted when the block exits.

    Nested Anchoring

    You can use boa.env.anchor() to create nested state checkpoints. When an inner with boa.env.anchor(): block exits, all state changes made within that block are reverted, but the outer anchor remains active.

    import boa
    
    # As a context manager - changes revert
    original_env = boa.env
    with boa.set_env(boa.Env()):
        # Temporary environment
        assert boa.env != original_env
    # Environment reverts to original
    assert boa.env == original_env
    
    # Nested state management
    contract = boa.loads("""
    value: uint256
    checkpoint: uint256
    """)
    
    with boa.env.anchor():
        contract.eval("self.value = 100")
    
        with boa.env.anchor():
            contract.eval("self.value = 200")
            contract.eval("self.checkpoint = self.value")
            assert contract.eval("self.checkpoint") == 200
    
        # Inner anchor reverted
        assert contract.eval("self.value") == 100
        assert contract.eval("self.checkpoint") == 0
    
    # Outer anchor reverted
    assert contract.eval("self.value") == 0
  5. Understand Titanoboa Environments

    master

    Titanoboa uses environments to manage state and execution context. When you call functions like boa.load or boa.loads, Titanoboa uses a global (singleton) environment by default. There are three primary types of environments:

    1. Env (Default): Uses a py-evm backend. It starts empty unless forked. Ideal for most testing purposes.
    2. NetworkEnv: Connects to a real network via an RPC URL. Used for deploying contracts to live networks.
    3. BrowserEnv: A specialized NetworkEnv for Jupyter notebooks or Google Colab. It uses the browser's wallet for signing, meaning no RPC URL or manual account configuration is required.
  6. Support for Legacy Vyper Contracts via VVM

    master

    Titanoboa can execute legacy Vyper contracts (contracts written in versions older than the latest Vyper release) using the Vyper Version Manager (vvm).

    When Titanoboa detects a contract with a Vyper version lower than the current latest version, it automatically invokes vvm to install the required compiler version on the fly and use it for compilation.

    Important Considerations:

    • Performance: The first run may be slow because the correct compiler version must be downloaded.
    • Feature Limitations: Some advanced VyperContract features, such as enhanced/pretty error traces, may not be available for legacy contracts.
    • Security Warning: Using legacy Vyper contracts is not recommended because older compiler versions may contain known bugs and security vulnerabilities.
  7. Understand how `handle_error` manages contract execution failures

    master

    The handle_error method is the base mechanism for error handling in EVM contracts within Titanoboa. When a contract execution fails, this method is responsible for transforming the failed computation into a user-friendly error.

    It performs the following steps:

    1. Creates a BoaError based on the failed ComputationAPI object.
    2. Strips internal framework frames from the traceback to ensure the stack trace only shows code relevant to the user's contract.
    3. Re-raises the error with a clean stack trace.

    Note: This method is called automatically by the framework when an error occurs. It is not intended to be called directly by users.

    def handle_error(self, computation: ComputationAPI) -> None
  8. Use anchoring for multi-step process debugging

    master

    When debugging complex, multi-step transactions, use with boa.env.anchor(): to create checkpoints. If an inner anchor reverts, the state reverts to the point of the last successful anchor, allowing you to inspect the state at specific stages of a process.

    # Debug with checkpoints
    with boa.env.anchor():
        contract.process_stage_1(10)
        print("After stage 1:", contract.eval("self.data"))
    
        with boa.env.anchor():
            contract.process_stage_2(20)
        # Inner anchor reverts
    
        print("Back to stage 1:", contract.eval("self.data"))
  9. How the Deployer pattern works in Titanoboa

    master

    The Deployer pattern provides a flexible way to deploy contracts with custom settings, initialization parameters, and deployment strategies. Instead of deploying a contract immediately upon loading, you create a Deployer instance using contract loading functions (like boa.load_partial or boa.loads_abi). This allows you to configure deployment parameters—such as constructor arguments, ETH value, or gas limits—before actually executing the deployment on the network.

    import boa
    
    # 1. Create a deployer (loads source but does not deploy)
    deployer = boa.load_partial("MyToken.vy")
    
    # 2. Deploy the contract with specific arguments
    token = deployer.deploy("My Token", "MTK", 18, 1000000)
    
    # 3. Interact with the deployed instance
    print(token.address)
  10. Use VyperContract to interact with deployed contracts

    master

    A VyperContract instance represents a deployed contract. It provides a high-level Pythonic interface to interact with the contract's state and functions.

    Function Access

    • External functions: Available directly as methods on the VyperContract instance. They are wrapped as VyperFunction objects.
    • Internal functions: Accessible via the .internal attribute. They are wrapped as VyperInternalFunction objects.

    State Access

    You can access contract state through specialized properties that handle automatic decoding:

    • _storage: Access storage variables.
    • _immutables: Access immutable values.
    • _constants: Access contract constants.

    Metadata

    • address: The deployed address of the contract.
    • created_from: The address that deployed the contract.
    import boa
    src = """
    @external
    def main():
        pass
    
    @internal
    def foo() -> uint256:
        return 123
    """
    contract = boa.loads_partial(src, name="Foo").deploy()
    
    # Accessing external functions
    print(type(contract.main))  # <class 'boa.vyper.contract.VyperFunction'>
    
    # Accessing internal functions
    print(type(contract.internal.foo))  # <class 'boa.vyper.contract.VyperInternalFunction'>
    print(contract.internal.foo())  # 123
  11. Fork mainnet or use a specific RPC network

    master

    Titanoboa can interact with existing networks by forking a state from an RPC URL or setting a network environment.

    • Forking: Use boa.env.fork(url="...") to create a local fork of a live network.
    • Network Mode: Use boa.set_network_env("<rpc_url>") to point Titanoboa to a specific RPC server. When in network mode, you must add accounts using boa.env.add_account() to sign transactions.
    import boa
    from eth_account import Account
    
    # Forking
    boa.env.fork(url="<rpc_server_address>")
    
    # Network Mode
    boa.set_network_env("<rpc_server_address>")
    boa.env.add_account(Account.from_key("<private_key>"))
  12. How at() works with Proxies and Factories

    master

    The at() method is versatile for common smart contract patterns:

    Proxy Contracts

    To interact with a proxy, load the Implementation ABI using load_partial() and then call .at(proxy_address). Calls made through the resulting instance will be routed through the proxy to the implementation.

    Factory Pattern

    When a factory contract deploys a child contract, capture the returned address from the transaction and use at() with a load_partial() of the child's ABI to connect to the new instance.

    # Factory Pattern Example
    # 1. Deploy child through factory
    tx = factory.deploy_child()
    child_address = tx.return_value
    
    # 2. Connect to deployed child
    child_deployer = boa.load_partial("Child.vy")
    child = child_deployer.at(child_address)