vyperlang / titanoboa

a vyper interpreter
https://titanoboa.readthedocs.io
Other
247 stars 49 forks source link

Can't pass tuple representations of structs to @internal Vyper methods #263

Open scherrey opened 1 month ago

scherrey commented 1 month ago

Vyper 0.3.10 and titanoboa @ git+https://github.com/vyperlang/titanoboa@a06e134b25c8206cb4d6d76521e6705111e92c68

In the unit test, test_allocate_balance_adapter_tx, passing the tuple representation of the Vyper struct to the @external method works fine but passing it to the @internal method fails. See execution result at bottom.

test_yield_bearing_asset_funds_allocator.py

import pytest
import boa
from decimal import Decimal
from dataclasses import dataclass, field

# ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
# MAX_ADAPTERS = 5 # Must match the value from AdapterVault.vy

@pytest.fixture
def deployer():
    acc = boa.env.generate_address(alias="deployer")
    boa.env.set_balance(acc, 1000*10**18)
    return acc

# @pytest.fixture
# def trader():
#     acc = boa.env.generate_address(alias="trader")
#     boa.env.set_balance(acc, 1000*10**18)
#     return acc

# @pytest.fixture
# def dai(deployer, trader):
#     with boa.env.prank(deployer):
#         erc = boa.load("contracts/test_helpers/ERC20.vy", "DAI Token", "DAI", 18, 1000*10**18, deployer)
#         erc.mint(deployer, 100000)
#         erc.mint(trader, 100000)
#     return erc    

# @pytest.fixture
# def erc20(deployer, trader):
#     with boa.env.prank(deployer):
#         erc = boa.load("contracts/test_helpers/ERC20.vy", "ERC20", "Coin", 18, 1000*10**18, deployer)
#         erc.mint(deployer, 100000)
#         erc.mint(trader, 100000)
#     return erc     

@pytest.fixture
def funds_alloc(deployer):
    with boa.env.prank(deployer):
        f = boa.load("contracts/YieldBearingAssetFundsAllocator.vy")
    return f

def test_is_full_rebalance(funds_alloc):
    assert funds_alloc.internal._is_full_rebalance() == False

max_uint256 = 2**256 - 1
max_int256 = 2**255 - 1
min_uint256 = 0
min_int256 = -2**255
neutral_max_deposit = max_int256 - 42

@dataclass
class BalanceAdapter:
    adapter: str
    current: int = field(default=0)
    last_value: int = field(default=0)
    max_deposit: int = field(default=max_int256)
    max_withdraw: int = field(default=min_int256)
    ratio: int = field(default=0)
    target: int = field(default=0)
    delta: int = field(default=0)

    @classmethod
    def from_dict(cls, data: dict):
        return cls(**data)

    def to_tuple(self):
        return (
            self.adapter,
            self.current,
            self.last_value,
            self.max_deposit,
            self.max_withdraw,
            self.ratio,
            self.target,
            self.delta
        )        

balance_adapters_data = [
    {
        'adapter': '0x0000000000000000000000000000000000000001',
        'current': 1000,
        'last_value': 900,
        'ratio': 10
    },
    {
        'adapter': '0x0000000000000000000000000000000000000002',
        'current': 2000,
        'last_value': 1800,
        'max_deposit': 1000,
        'max_withdraw': -600,
        'ratio': 20,
        'target' : 2000,
        'delta' : 0
    },
]

def test_allocate_balance_adapter_tx(funds_alloc):
    adapter = BalanceAdapter.from_dict(balance_adapters_data[0])
    adapter_tuple = adapter.to_tuple()

    result = funds_alloc.allocate_balance_adapter_tx(100, adapter_tuple)
    result = funds_alloc.internal._allocate_balance_adapter_tx(100, adapter_tuple) # This fails with boa now.    

    adapter.target = 100 * adapter.ratio
    adapter.delta = adapter.target - adapter.current
    assert result == (adapter.to_tuple(), 0, False, False)

YieldBearingAssetFundsAllocator.vy

#pragma evm-version cancun
"""
@title Adapter Fund Allocation Logic
@license Copyright 2023, 2024 Biggest Lab Co Ltd, Benjamin Scherrey, Sajal Kayan, and Eike Caldeweyher
@author BiggestLab (https://biggestlab.io) Benjamin Scherrey
"""

#interface AdapterVault:
#    pass

##
## Must match AdapterVault.vy
##

MAX_ADAPTERS : constant(uint256) = 5 

ADAPTER_BREAKS_LOSS_POINT : constant(decimal) = 0.05

# This structure must match definition in AdapterVault.vy
struct BalanceTX:
    qty: int256
    adapter: address

# This structure must match definition in AdapterVault.vy
struct BalanceAdapter:
    adapter: address
    current: uint256
    last_value: uint256
    max_deposit: int256
    max_withdraw: int256 # represented as a negative number
    ratio: uint256
    target: uint256 
    delta: int256

@external
@view
def getBalanceTxs(_vault_balance: uint256, _target_asset_balance: uint256, _min_proposer_payout: uint256, _total_assets: uint256, _total_ratios: uint256, _adapter_states: BalanceAdapter[MAX_ADAPTERS], _withdraw_only : bool = False) -> (BalanceTX[MAX_ADAPTERS], address[MAX_ADAPTERS]):  
    return self._getBalanceTxs(_vault_balance, _target_asset_balance, _min_proposer_payout, _total_assets, _total_ratios, _adapter_states, _withdraw_only )

@internal
@pure
def _getBalanceTxs(_vault_balance: uint256, _target_asset_balance: uint256, _min_proposer_payout: uint256, _total_assets: uint256, _total_ratios: uint256, _adapter_states: BalanceAdapter[MAX_ADAPTERS], _withdraw_only : bool = False) -> (BalanceTX[MAX_ADAPTERS], address[MAX_ADAPTERS]): 
    # _BDM TODO : max_txs is ignored for now.    
    adapter_txs : BalanceTX[MAX_ADAPTERS] = empty(BalanceTX[MAX_ADAPTERS])
    blocked_adapters : address[MAX_ADAPTERS] = empty(address[MAX_ADAPTERS])
    adapter_states: BalanceAdapter[MAX_ADAPTERS] = empty(BalanceAdapter[MAX_ADAPTERS])
    d4626_delta : int256 = 0
    tx_count : uint256 = 0

    #d4626_delta, tx_count, adapter_states, blocked_adapters = self._getTargetBalances(_vault_balance, _target_asset_balance, _total_assets, _total_ratios, _adapter_states, _min_proposer_payout, _withdraw_only)

    pos : uint256 = 0
    for tx_bal in adapter_states:
        adapter_txs[pos] = BalanceTX({qty: tx_bal.delta, adapter: tx_bal.adapter})
        pos += 1

    return adapter_txs, blocked_adapters

@internal
@view
def _is_full_rebalance() -> bool:
    return False

NEUTRAL_ADAPTER_MAX_DEPOSIT : constant(int256) = max_value(int256) - 42

@internal
@pure
def _allocate_balance_adapter_tx(_ratio_value : uint256, _balance_adapter : BalanceAdapter) -> (BalanceAdapter, int256, bool, bool):
    """
    Given a value per strategy ratio and an un-allocated BalanceAdapter, return the newly allocated BalanceAdapter
    constrained by min & max limits and also identify if this adapter should be blocked due to unexpected losses,
    plus identify whether or not this is our "neutral adapter".
    """
    is_neutral_adapter : bool = _balance_adapter.max_deposit == NEUTRAL_ADAPTER_MAX_DEPOSIT

    # Have funds been lost?
    should_we_block_adapter : bool = False
    if _balance_adapter.current < _balance_adapter.last_value:
        _balance_adapter.ratio = 0
        should_we_block_adapter = True

    target : uint256 = _ratio_value * _balance_adapter.ratio
    delta : int256 = convert(_balance_adapter.current, int256) - convert(target, int256)

    leftovers : int256 = 0
    # Limit deposits to max_deposit
    if delta > _balance_adapter.max_deposit:
        leftovers = _balance_adapter.max_deposit - delta
        delta = _balance_adapter.max_deposit

    # Limit withdraws to max_withdraw    
    elif delta < _balance_adapter.max_withdraw:
        leftovers = delta - _balance_adapter.max_withdraw
        delta = _balance_adapter.max_withdraw

    _balance_adapter.delta = delta
    _balance_adapter.target = target  # We are not adjusting the optimium target for now.

    return _balance_adapter, leftovers, should_we_block_adapter, is_neutral_adapter

@external
@pure
def allocate_balance_adapter_tx(_ratio_value : uint256, _balance_adapter : BalanceAdapter) -> (BalanceAdapter, int256, bool, bool):
    return self._allocate_balance_adapter_tx(_ratio_value, _balance_adapter)

script execution:

pytest tests_boa/ --ignore tests_boa/test_transient.py tests_boa/test_yield_bearing_asset_funds_allocator.py
============================================================================== test session starts ===============================================================================
platform linux -- Python 3.11.2, pytest-8.2.1, pluggy-1.5.0
rootdir: /home/scherrey/projects/adapter-fi/AdapterVault
plugins: hypothesis-6.103.0, cov-5.0.0, titanoboa-0.1.10b1, web3-6.11.0
collected 2 items                                                                                                                                                                

tests_boa/test_yield_bearing_asset_funds_allocator.py .F                                                                                                                   [100%]

==================================================================================== FAILURES ====================================================================================
________________________________________________________________________ test_allocate_balance_adapter_tx ________________________________________________________________________

source_code = '\n@external\n@payable\ndef __boa_private__allocate_balance_adapter_tx__(_ratio_value: uint256, _balance_adapter: Bala...claration object, int256, bool, bool):\n    return self._allocate_balance_adapter_tx(_ratio_value, _balance_adapter)\n'
source_id = {}, contract_name = None, add_fn_node = None

    def parse_to_ast_with_settings(
        source_code: str,
        source_id: int = 0,
        contract_name: Optional[str] = None,
        add_fn_node: Optional[str] = None,
    ) -> tuple[Settings, vy_ast.Module]:
        """
        Parses a Vyper source string and generates basic Vyper AST nodes.

        Parameters
        ----------
        source_code : str
            The Vyper source code to parse.
        source_id : int, optional
            Source id to use in the `src` member of each node.
        contract_name: str, optional
            Name of contract.
        add_fn_node: str, optional
            If not None, adds a dummy Python AST FunctionDef wrapper node.

        Returns
        -------
        list
            Untyped, unoptimized Vyper AST nodes.
        """
        if "\x00" in source_code:
            raise ParserException("No null bytes (\\x00) allowed in the source code.")
        settings, class_types, reformatted_code = pre_parse(source_code)
        try:
>           py_ast = python_ast.parse(reformatted_code)

../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/vyper/ast/utils.py:44: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

source = '\n@external\n@payable\ndef __boa_private__allocate_balance_adapter_tx__(_ratio_value: uint256, _balance_adapter: Bala...claration object, int256, bool, bool):\n    return self._allocate_balance_adapter_tx(_ratio_value, _balance_adapter)\n'
filename = '<unknown>', mode = 'exec'

    def parse(source, filename='<unknown>', mode='exec', *,
              type_comments=False, feature_version=None):
        """
        Parse the source into an AST node.
        Equivalent to compile(source, filename, mode, PyCF_ONLY_AST).
        Pass type_comments=True to get back type comments where the syntax allows.
        """
        flags = PyCF_ONLY_AST
        if type_comments:
            flags |= PyCF_TYPE_COMMENTS
        if isinstance(feature_version, tuple):
            major, minor = feature_version  # Should be a 2-tuple.
            assert major == 3
            feature_version = minor
        elif feature_version is None:
            feature_version = -1
        # Else it should be an int giving the minor version for 3.x.
>       return compile(source, filename, mode, flags,
                       _feature_version=feature_version)
E         File "<unknown>", line 4
E           def __boa_private__allocate_balance_adapter_tx__(_ratio_value: uint256, _balance_adapter: BalanceAdapter declaration object) -> (BalanceAdapter declaration object, int256, bool, bool):
E                                                                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^
E       SyntaxError: invalid syntax. Perhaps you forgot a comma?

/usr/lib/python3.11/ast.py:50: SyntaxError

The above exception was the direct cause of the following exception:

funds_alloc = <contracts/YieldBearingAssetFundsAllocator.vy at 0x8A369A3a3a60866B01ABB5b30D3Cce00F06b98F2, compiled with vyper-0.3.10+9136169>

    def test_allocate_balance_adapter_tx(funds_alloc):
        adapter = BalanceAdapter.from_dict(balance_adapters_data[0])
        adapter_tuple = adapter.to_tuple()

        result = funds_alloc.allocate_balance_adapter_tx(100, adapter_tuple)
>       result = funds_alloc.internal._allocate_balance_adapter_tx(100, adapter_tuple) # This fails with boa now.

tests_boa/test_yield_bearing_asset_funds_allocator.py:106: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/boa/contracts/vyper/vyper_contract.py:1022: in __call__
    if hasattr(self, "_ir_executor"):
/usr/lib/python3.11/functools.py:1001: in __get__
    val = self.func(instance)
../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/boa/contracts/vyper/vyper_contract.py:1065: in _ir_executor
    _, ir_executor, _, _, _ = self._compiled
/usr/lib/python3.11/functools.py:1001: in __get__
    val = self.func(instance)
../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/boa/contracts/vyper/vyper_contract.py:1055: in _compiled
    return generate_bytecode_for_internal_fn(self)
../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/boa/contracts/vyper/compiler_utils.py:115: in generate_bytecode_for_internal_fn
    return compile_vyper_function(wrapper_code, contract)
../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/boa/contracts/vyper/compiler_utils.py:41: in compile_vyper_function
    ast = parse_to_ast(vyper_function, ifaces)
../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/vyper/ast/utils.py:12: in parse_to_ast
    return parse_to_ast_with_settings(*args, **kwargs)[1]
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

source_code = '\n@external\n@payable\ndef __boa_private__allocate_balance_adapter_tx__(_ratio_value: uint256, _balance_adapter: Bala...claration object, int256, bool, bool):\n    return self._allocate_balance_adapter_tx(_ratio_value, _balance_adapter)\n'
source_id = {}, contract_name = None, add_fn_node = None

    def parse_to_ast_with_settings(
        source_code: str,
        source_id: int = 0,
        contract_name: Optional[str] = None,
        add_fn_node: Optional[str] = None,
    ) -> tuple[Settings, vy_ast.Module]:
        """
        Parses a Vyper source string and generates basic Vyper AST nodes.

        Parameters
        ----------
        source_code : str
            The Vyper source code to parse.
        source_id : int, optional
            Source id to use in the `src` member of each node.
        contract_name: str, optional
            Name of contract.
        add_fn_node: str, optional
            If not None, adds a dummy Python AST FunctionDef wrapper node.

        Returns
        -------
        list
            Untyped, unoptimized Vyper AST nodes.
        """
        if "\x00" in source_code:
            raise ParserException("No null bytes (\\x00) allowed in the source code.")
        settings, class_types, reformatted_code = pre_parse(source_code)
        try:
            py_ast = python_ast.parse(reformatted_code)
        except SyntaxError as e:
            # TODO: Ensure 1-to-1 match of source_code:reformatted_code SyntaxErrors
>           raise SyntaxException(str(e), source_code, e.lineno, e.offset) from e
E           vyper.exceptions.SyntaxException: invalid syntax. Perhaps you forgot a comma? (<unknown>, line 4)
E             line 4:91 
E                  3 @payable
E             ---> 4 def __boa_private__allocate_balance_adapter_tx__(_ratio_value: uint256, _balance_adapter: BalanceAdapter declaration object) -> (BalanceAdapter declaration object, int256, bool, bool):
E             --------------------------------------------------------------------------------------------------^
E                  5     return self._allocate_balance_adapter_tx(_ratio_value, _balance_adapter)

../../../.virtualenvs/AdapterBoa/lib/python3.11/site-packages/vyper/ast/utils.py:47: SyntaxException
============================================================================ short test summary info =============================================================================
FAILED tests_boa/test_yield_bearing_asset_funds_allocator.py::test_allocate_balance_adapter_tx - vyper.exceptions.SyntaxException: invalid syntax. Perhaps you forgot a comma? (<unknown>, line 4)
========================================================================== 1 failed, 1 passed in 0.35s ===========================================================================
make: *** [Makefile:41: test] Error 1