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

Titanoboa seems to be returning fields in tuples as tuples. Types get all screwed up. #262

Open
scherrey opened this issue Jul 28, 2024 · 1 comment

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, the types that get returned back shift around unnaturally.

Also mypy complains about titanoboa.

$ mypy tests_boa/test_yield_bearing_asset_funds_allocator.py
tests_boa/test_yield_bearing_asset_funds_allocator.py:3: error: Skipping analyzing "boa": module is installed, but missing library stubs or py.typed marker  [import-untyped]
tests_boa/test_yield_bearing_asset_funds_allocator.py:4: error: Skipping analyzing "boa.util.abi": module is installed, but missing library stubs or py.typed marker  [import-untyped]
tests_boa/test_yield_bearing_asset_funds_allocator.py:4: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
Found 2 errors in 1 file (checked 1 source file)
(AdapterBoa) scherrey@squire:~/projects/adapter-fi/AdapterVault
$ 

test_yield_bearing_asset_funds_allocator.py

import pytest
import boa
from boa.util.abi import Address as Address
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: Address
    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
        )        

    def from_tuple(self, t):
        self.adapter = Address(t[0]),
        self.current = t[1],
        self.last_value = t[2],
        self.max_deposit = t[3],
        self.max_withdraw = t[4],
        self.ratio = t[5],
        self.target = t[6],
        self.delta = t[7]
                       

balance_adapters_data = [
    ({  'adapter': Address('0x0000000000000000000000000000000000000001'), 'current': 1000, 'last_value': 900, 'ratio': 10 },
        {'exception': None, 'ratio_value': 100, 'target':1000, 'delta':0, 'leftovers':0, 'block': False, 'neutral': False}),
    #({  'adapter': Address('0x0000000000000000000000000000000000000002'), 'current': 1500, 'last_value': 1500, 'max_deposit': 1000, 'ratio': 20 },
    #    {'exception': None, 'ratio_value': 100, 'target':2000, 'delta':500, 'leftovers':0, 'block': False, 'neutral': False}),
]

def test_allocate_balance_adapter_tx(funds_alloc):
    for adapter_data in balance_adapters_data:
        adapter = BalanceAdapter.from_dict(adapter_data[0])
        print("adapter = %s" % adapter)
        target_result = adapter_data[1]
        adapter_tuple = adapter.to_tuple()
        # result = funds_alloc.internal._allocate_balance_adapter_tx(100, adapter_tuple) # This fails with boa now.
        allocated_adapter, leftovers, block_adapter, neutral_adapter = funds_alloc.allocate_balance_adapter_tx(target_result['ratio_value'], adapter_tuple)
        print("allocated_adapter[0] = %s" % allocated_adapter[0])
        print("allocated_adapter[1] = %s" % allocated_adapter[1])
        print("type(allocated_adapter[0]) = %s" % type(allocated_adapter[0]))
        print("before type(adapter.adapter) = %s" % type(adapter.adapter))
        adapter.from_tuple(allocated_adapter)
        print("after type(adapter.adapter) = %s" % type(adapter.adapter))
        print("adapter.adapter = %s" % Address(adapter.adapter[0]))
        print("adapter_data[0]['adapter'] = %s" % adapter_data[0]['adapter'])
        print("type(adapter.adapter) = %s" % type(adapter.adapter))
        print("type(adapter_data[0]['adapter']) = %s" % type(adapter_data[0]['adapter']))        
        print("adapter.adapter == adapter_data[0]['adapter'] = %s" % adapter.adapter == adapter_data[0]['adapter'])
        assert Address(adapter.adapter[0]) == adapter_data[0]['adapter'] # BDM WTF?!?!? Why is adapter.adapter becoming a tuple????
        assert adapter.current == adapter_data[0]['current']
        adapter.target = target_result['target'] # 100 * adapter.ratio
        adapter.delta = target_result['delta'] # adapter.target - adapter.current
        #assert result == (adapter.to_tuple(), target_result['leftovers'], target_result['block'], target_result['neutral'])

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
"""


##
## 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:
        # There's an unexpected loss of value. Let's try to empty this adapter and stop
        # further allocations to it by setting the ratio to 0 going forward.
        # This will not necessarily result in any "leftovers" unless withdrawing the full
        # balance of the adapter is limited by max_withdraw limits below.
        _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)

Execution of test run:

$ make test tests_boa/test_yield_bearing_asset_funds_allocator.py
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 ________________________________________________________________________

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

    def test_allocate_balance_adapter_tx(funds_alloc):
        for adapter_data in balance_adapters_data:
            adapter = BalanceAdapter.from_dict(adapter_data[0])
            print("adapter = %s" % adapter)
            target_result = adapter_data[1]
            adapter_tuple = adapter.to_tuple()
            # result = funds_alloc.internal._allocate_balance_adapter_tx(100, adapter_tuple) # This fails with boa now.
            allocated_adapter, leftovers, block_adapter, neutral_adapter = funds_alloc.allocate_balance_adapter_tx(target_result['ratio_value'], adapter_tuple)
            print("allocated_adapter[0] = %s" % allocated_adapter[0])
            print("allocated_adapter[1] = %s" % allocated_adapter[1])
            print("type(allocated_adapter[0]) = %s" % type(allocated_adapter[0]))
            print("before type(adapter.adapter) = %s" % type(adapter.adapter))
            adapter.from_tuple(allocated_adapter)
            print("after type(adapter.adapter) = %s" % type(adapter.adapter))
            print("adapter.adapter = %s" % Address(adapter.adapter[0]))
            print("adapter_data[0]['adapter'] = %s" % adapter_data[0]['adapter'])
            print("type(adapter.adapter) = %s" % type(adapter.adapter))
            print("type(adapter_data[0]['adapter']) = %s" % type(adapter_data[0]['adapter']))
            print("adapter.adapter == adapter_data[0]['adapter'] = %s" % adapter.adapter == adapter_data[0]['adapter'])
            assert Address(adapter.adapter[0]) == adapter_data[0]['adapter'] # BDM WTF?!?!? Why is adapter.adapter becoming a tuple????
>           assert adapter.current == adapter_data[0]['current']
E           AssertionError: assert (1000,) == 1000
E            +  where (1000,) = BalanceAdapter(adapter=(Address('0x0000000000000000000000000000000000000001'),), current=(1000,), last_value=(900,), m...-57896044618658097711785492504343953926634992332820282019728792003956564819968,), ratio=(10,), target=(1000,), delta=0).current

tests_boa/test_yield_bearing_asset_funds_allocator.py:121: AssertionError
------------------------------------------------------------------------------ Captured stdout call ------------------------------------------------------------------------------
adapter = BalanceAdapter(adapter=Address('0x0000000000000000000000000000000000000001'), current=1000, last_value=900, max_deposit=57896044618658097711785492504343953926634992332820282019728792003956564819967, max_withdraw=-57896044618658097711785492504343953926634992332820282019728792003956564819968, ratio=10, target=0, delta=0)
allocated_adapter[0] = 0x0000000000000000000000000000000000000001
allocated_adapter[1] = 1000
type(allocated_adapter[0]) = <class 'boa.util.abi.Address'>
before type(adapter.adapter) = <class 'boa.util.abi.Address'>
after type(adapter.adapter) = <class 'tuple'>
adapter.adapter = 0x0000000000000000000000000000000000000001
adapter_data[0]['adapter'] = 0x0000000000000000000000000000000000000001
type(adapter.adapter) = <class 'tuple'>
type(adapter_data[0]['adapter']) = <class 'boa.util.abi.Address'>
False
============================================================================ short test summary info =============================================================================
FAILED tests_boa/test_yield_bearing_asset_funds_allocator.py::test_allocate_balance_adapter_tx - AssertionError: assert (1000,) == 1000
========================================================================== 1 failed, 1 passed in 0.31s ===========================================================================
make: *** [Makefile:41: test] Error 1
(AdapterBoa) scherrey@squire:~/projects/adapter-fi/AdapterVault
$ 

scherrey added a commit to adapter-fi/AdapterVault that referenced this issue Jul 28, 2024
@charles-cooper
Copy link
Member

i think maybe the bug is in your implementation of BalanceAdapter.from_tuple()? adapter-fi/AdapterVault@d7fbf6b#diff-a9b84898eba76eb6d9fb13497ebefe93ee7ab2d201f54741dba470f64a257fa5R84 looks like it has a trailing comma which might be unintentional

        self.adapter = Address(t[0]),  # <- trailing comma

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

2 participants