vyperlang / titanoboa

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

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

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, 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
$ 
charles-cooper commented 1 month ago

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

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