Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
scherrey opened this issue Jul 28, 2024 · 0 comments
Open

Comments

@scherrey
Copy link

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


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant