erc_4626.vault_protocol.goat.vault

Documentation for eth_defi.erc_4626.vault_protocol.goat.vault Python module.

Goat vault support.

Classes

GoatVault

Goat protocol vaults.

class GoatVault

Bases: eth_defi.erc_4626.vault.ERC4626Vault

Goat protocol vaults.

  • An example vault

  • Each goat can have multiple strategies with each strategy with different unlocked and locked profit and loss

  • Multistrategy contract on Github

  • Fees are internalised into the share price of the strategy (similar as Yearn, Harvest).

Withdraw logic (from _withdraw function):

/// @notice Handles withdrawals from the contract.
///
/// This function performs the following actions:
/// - If the caller is not the owner, it checks and spends the allowance for the withdrawal.
/// - Ensures that the amount to be withdrawn is greater than zero.
/// - If the requested withdrawal amount exceeds the available balance, it withdraws the necessary amount from the strategies in the withdrawal order.
///   - Iterates through the withdrawal queue, withdrawing from each strategy until the balance requirement is met or the queue is exhausted.
///   - Updates the total debt of both the strategy and the contract as assets are withdrawn.
///   - Requests the strategy to report, accounting for potential gains or losses.
/// - Reverts if the withdrawal process does not result in sufficient balance.
/// - Burns the corresponding shares and transfers the requested assets to the receiver.
/// - Emits a `Withdraw` event with the caller, receiver, owner, amount of assets withdrawn, and shares burned.
///
/// @param _caller The address of the entity initiating the withdrawal.
/// @param _receiver The address of the recipient to receive the withdrawn assets.
/// @param _owner The address of the owner of the shares being withdrawn.
/// @param _assets The amount of assets to withdraw.
/// @param _shares The amount of shares to burn.
/// @param _consumeAllShares True if all `_shares` should be used to withdraw. False if it should withdraw just `_assets`.
/// @return The number of assets withdrawn and the shares burned as a result of the withdrawal.
function _withdraw(
    address _caller,
    address _receiver,
    address _owner,
    uint256 _assets,
    uint256 _shares,
    bool _consumeAllShares
) internal returns (uint256, uint256) {
    require(_shares > 0, Errors.ZeroAmount(_shares));

    if (_caller != _owner) {
        _spendAllowance(_owner, _caller, _shares);
    }

    uint256 assets = _consumeAllShares ? _convertToAssets(_shares, Math.Rounding.Floor) : _assets;

    if(assets > _balance()) {
        for(uint8 i = 0; i <= withdrawOrder.length; ++i){
            address strategy = withdrawOrder[i];

            // We reached the end of the withdraw queue and assets are still higher than the balance
            require(strategy != address(0), Errors.InsufficientBalance(assets, _balance()));

            // We can't withdraw from a strategy more than what it has asked as credit.
            uint256 assetsToWithdraw = Math.min(assets - _balance(), strategies[strategy].totalDebt);
            if(assetsToWithdraw == 0) continue;

            uint256 withdrawn = IStrategyAdapter(strategy).withdraw(assetsToWithdraw);
            strategies[strategy].totalDebt -= withdrawn;
            totalDebt -= withdrawn;

            IStrategyAdapter(strategy).askReport();

            // Update assets, as a loss could have been reported and user should get less assets for
            // the same amount of shares.
            if(_consumeAllShares) assets = _convertToAssets(_shares, Math.Rounding.Floor);
            if(assets <= _balance()) break;
        }
    }

    uint256 shares = _consumeAllShares ? _shares : _convertToShares(assets, Math.Rounding.Ceil);
    _burn(_owner, shares);
    IERC20(asset()).safeTransfer(_receiver, assets);

    emit Withdraw(_caller, _receiver, _owner, assets, shares);

    return (assets, shares);
}

Fee calculation (from _report function):

/// @notice Reports the performance of a strategy.
///
/// This function performs the following actions:
/// - Validates that the reporting strategy does not claim both a gain and a loss simultaneously.
/// - Checks that the strategy has sufficient tokens to cover the debt repayment and the gain.
/// - If there is a loss, it realizes the loss.
/// - Calculates and deducts the performance fee from the gain.
/// - Determines the excess debt of the strategy.
/// - Adjusts the strategy's and contract's total debt accordingly.
/// - Calculates and updates the new locked profit after accounting for any losses.
/// - Updates the reporting timestamps for the strategy and the contract.
/// - Transfers the debt repayment and the gains to this contract.
///
/// Emits a `StrategyReported` event.
///
/// @param _debtRepayment The amount of debt being repaid by the strategy.
/// @param _gain The amount of profit reported by the strategy.
/// @param _loss The amount of loss reported by the strategy.
function _report(uint256 _debtRepayment, uint256 _gain, uint256 _loss) internal {
    uint256 strategyBalance = IERC20(asset()).balanceOf(msg.sender);
    require(!(_gain > 0 && _loss > 0), Errors.GainLossMismatch());
    require(strategyBalance >= _debtRepayment + _gain, Errors.InsufficientBalance(strategyBalance, _debtRepayment + _gain));

    uint256 profit = 0;
    uint256 feesCollected = 0;
    if(_loss > 0) _reportLoss(msg.sender, _loss);
    if(_gain > 0) {
        strategies[msg.sender].totalGain += _gain;
        feesCollected = _gain.mulDiv(performanceFee, MAX_BPS);
        profit = _gain - feesCollected;
    }

    uint256 debtToRepay = Math.min(_debtRepayment, _debtExcess(msg.sender));
    if(debtToRepay > 0) {
        strategies[msg.sender].totalDebt -= debtToRepay;
        totalDebt -= debtToRepay;
    }

    uint256 newLockedProfit = _calculateLockedProfit() + profit;
    if(newLockedProfit > _loss) {
        lockedProfit = newLockedProfit - _loss;
    } else {
        lockedProfit = 0;
    }

    strategies[msg.sender].lastReport = block.timestamp;
    lastReport = block.timestamp;

    if(debtToRepay + _gain > 0) IERC20(asset()).safeTransferFrom(msg.sender, address(this), debtToRepay + _gain);
    if(feesCollected > 0) IERC20(asset()).safeTransfer(protocolFeeRecipient, feesCollected);

    emit StrategyReported(msg.sender, debtToRepay, profit, _loss);
}
Parameters
  • web3 – Connection we bind this instance to

  • spec – Chain, address tuple

  • token_cache

    Cache used with fetch_erc20_details() to avoid multiple calls to the same token.

    Reduces the number of RPC calls when scanning multiple vaults.

  • features – Pass vault feature flags along, externally detected.

  • default_block_identifier

    Override block identifier for on-chain metadata reads.

    When None, use get_safe_cached_latest_block_number() (the default, safe for broken RPCs). Set to "latest" for freshly deployed vaults whose contracts do not exist at the safe-cached block.

property vault_contract: web3.contract.contract.Contract

Get vault deployment.

fetch_pnl()

Fetch profit and loss from the vault.

* This function performs the following actions:
* - Iterates through the `withdrawOrder` array, which defines the order in which strategies are withdrawn from.
* - For each strategy in the `withdrawOrder`:
*   - If the strategy address is zero, it breaks the loop, indicating the end of the list.
*   - If the strategy has no debt, it skips to the next strategy.
*   - Otherwise, it retrieves the current profit and loss (PnL) from the strategy by calling `currentPnL`.
*   - Adds the strategy's profit to the total profit, after deducting the performance fee.
*   - Adds the strategy's loss to the total loss.
* - Returns the total profit and total loss across all active strategies.
*
* @return totalProfit The total profit across all active strategies, after deducting the performance fee.
* @return totalLoss The total loss across all active strategies.
Returns

(locked, unlocked) PnL amounts

Return type

tuple[decimal.Decimal, decimal.Decimal]

has_custom_fees()

Deposit/withdrawal fees.

Return type

bool

get_management_fee(block_identifier)

Internalised to the share price

Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, int]) –

Return type

float

get_performance_fee(block_identifier)

Internalised to the share price

Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, int]) –

Return type

float | None

get_estimated_lock_up()

ERC-4626 vaults do not have a lock up by fault.

Note

Because of so many protocol specific lockups, this must be explicitly set to zero.

Return type

datetime.timedelta

__init__(web3, spec, token_cache=None, features=None, default_block_identifier=None)
Parameters
  • web3 (web3.main.Web3) – Connection we bind this instance to

  • spec (eth_defi.vault.base.VaultSpec) – Chain, address tuple

  • token_cache (dict | None) –

    Cache used with fetch_erc20_details() to avoid multiple calls to the same token.

    Reduces the number of RPC calls when scanning multiple vaults.

  • features (set[eth_defi.erc_4626.core.ERC4626Feature] | None) – Pass vault feature flags along, externally detected.

  • default_block_identifier (Optional[Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, hexbytes.main.HexBytes, int]]) –

    Override block identifier for on-chain metadata reads.

    When None, use get_safe_cached_latest_block_number() (the default, safe for broken RPCs). Set to "latest" for freshly deployed vaults whose contracts do not exist at the safe-cached block.

property address: eth_typing.evm.HexAddress

Get the vault smart contract address.

property chain_id: int

Chain this vault is on

property denomination_token: eth_defi.token.TokenDetails | None

Get the token which denominates the vault valuation

  • Used in deposits and redemptions

  • Used in NAV calculation

  • Used in profit benchmarks

  • Usually USDC

Returns

Token wrapper instance.

Maybe None for broken vaults like https://arbiscan.io/address/0x9d0fbc852deccb7dcdd6cb224fa7561efda74411#code

property deposit_manager: eth_defi.vault.deposit_redeem.VaultDepositManager

Deposit manager assocaited with this vault

property erc_7540: bool

Is this ERC-7540 vault with asynchronous deposits.

  • For example previewDeposit() function and other functions will revert

fetch_denomination_token()

Read denomination token from onchain.

Use denomination_token() for cached access.

Return type

eth_defi.token.TokenDetails | None

fetch_denomination_token_address()

Get the address for the denomination token.

Triggers RCP call

Return type

Optional[eth_typing.evm.HexAddress]

fetch_deposit_closed_reason()

Check ERC-4626 maxDeposit to determine if deposits are closed.

Return type

str | None

fetch_deposit_next_open()

Generic ERC-4626 - no timing information available.

Return type

datetime.datetime | None

fetch_info()

Use info() property for cached access.

Returns

See LagoonVaultInfo

Return type

eth_defi.erc_4626.vault.ERC4626VaultInfo

fetch_nav(block_identifier=None)

Fetch the most recent onchain NAV value.

  • In the case of Lagoon, this is the last value written in the contract with updateNewTotalAssets() and ` settleDeposit()`

  • TODO: updateNewTotalAssets() there is no way to read pending asset update on chain

Returns

Vault NAV, denominated in denomination_token()

Return type

decimal.Decimal

fetch_portfolio(universe, block_identifier=None, allow_fallback=True)

Read the current token balances of a vault.

  • SHould be supported by all implementations

Parameters
Return type

eth_defi.vault.base.VaultPortfolio

fetch_redemption_closed_reason()

Check ERC-4626 maxRedeem to determine if redemptions are closed.

Return type

str | None

fetch_redemption_next_open()

Generic ERC-4626 - no timing information available.

Return type

datetime.datetime | None

fetch_share_price(block_identifier)

Get the current share price.

Returns

The share price in underlying token.

If supply is zero return zero.

Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, hexbytes.main.HexBytes, int]) –

Return type

decimal.Decimal

fetch_share_token()

Read share token details onchain.

Use share_token() for cached access.

Return type

eth_defi.token.TokenDetails

fetch_share_token_address(block_identifier='latest')

Get share token of this vault.

  • Vault itself (ERC-4626)

  • share() accessor (ERc-7575)

Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, hexbytes.main.HexBytes, int]) –

Return type

eth_typing.evm.HexAddress

fetch_total_assets(block_identifier)

What is the total NAV of the vault.

Example:

assert vault.denomination_token.symbol == "USDC"
assert vault.share_token.symbol == "ipUSDCfusion"
assert vault.fetch_total_assets(block_identifier=test_block_number) == Decimal("1437072.77357")
assert vault.fetch_total_supply(block_identifier=test_block_number) == Decimal("1390401.22652875")
Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, hexbytes.main.HexBytes, int]) –

Block number to read.

Use web3.eth.block_number for the last block.

Returns

The vault value in underlyinh token

Return type

decimal.Decimal | None

fetch_total_supply(block_identifier)

What is the current outstanding shares.

Example:

Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, hexbytes.main.HexBytes, int]) –

Block number to read.

Use web3.eth.block_number for the last block.

Returns

The vault value in underlyinh token

Return type

decimal.Decimal

fetch_vault_info()

Get all information we can extract from the vault smart contracts.

Return type

eth_defi.erc_4626.vault.ERC4626VaultInfo

property flow_manager: eth_defi.vault.base.VaultFlowManager

Flow manager associated with this vault

get_deposit_fee(block_identifier)

Deposit fee is set to zero by default as vaults usually do not have deposit fees.

Internal: Use get_fee_data().

Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, int]) –

Return type

float | None

get_deposit_manager()

Get deposit manager to deposit/redeem from the vault.

Return type

eth_defi.erc_4626.deposit_redeem.ERC4626DepositManager

get_fee_data()

Get fee data structure for this vault.

Raises

ValueError – In the case of broken or unimplemented fee reading methods in the smart contract

Return type

eth_defi.vault.fee.FeeData

get_fee_mode()

Get how this vault accounts its fees.

Return type

eth_defi.vault.fee.VaultFeeMode | None

get_flags()

Get various vault state flags from the smart contract.

  • Override to add status flags

  • Also add flags from our manual flag list in eth_defi.vault.flag

Returns

Flag set.

Do not modify in place.

Return type

set[eth_defi.vault.flag.VaultFlag]

get_flow_manager()

Get flow manager to read indiviaul settle events.

Return type

eth_defi.vault.base.VaultFlowManager

get_historical_reader(stateful)

Get share price reader to fetch historical returns.

Parameters

stateful – If True, use a stateful reading strategy.

Returns

None if unsupported

Return type

eth_defi.vault.base.VaultHistoricalReader

Get a link to the vault dashboard on its native site.

  • By default, give RouteScan link

Parameters

referral (str | None) – Optional referral code to append to the URL.

Returns

URL string

Return type

str

get_notes()

Get a human readable message if we know somethign special is going on with this vault.

Return type

str | None

get_protocol_name()

Return the name of the vault protocol.

Return type

str

get_risk()

Get risk profile of this vault.

Return type

eth_defi.vault.risk.VaultTechnicalRisk | None

get_withdraw_fee(block_identifier)

Withdraw fee is set to zero by default as vaults usually do not have withdraw fees.

Internal: Use get_fee_data().

Parameters

block_identifier (Union[Literal['latest', 'earliest', 'pending', 'safe', 'finalized'], eth_typing.evm.BlockNumber, eth_typing.evm.Hash32, eth_typing.encoding.HexStr, int]) –

Return type

float

has_block_range_event_support()

Does this vault support block range-based event queries for deposits and redemptions.

  • If not we use chain balance polling-based approach

has_deposit_distribution_to_all_positions()

Deposits go automatically to all open positions.

  • Deposits do not land into the vault as cash

  • Instead, smart contracts automatically increase all open positions

  • The behaviour of Velvet Capital

property info: eth_defi.vault.base.VaultInfo

Get info dictionary related to this vault deployment.

  • Get cached data on the various vault parameters

Returns

Vault protocol specific information dictionary

is_valid()

Check if this vault is valid.

  • Call a known smart contract function to verify the function exists

Return type

bool

property name: str

Vault name.

property share_token: eth_defi.token.TokenDetails

ERC-20 that presents vault shares.

  • User gets shares on deposit and burns them on redemption

property symbol: str

Vault share token symbol

property underlying_token: eth_defi.token.TokenDetails

Alias for denomination_token()

first_seen_at_block: int | None

Block number hint when this vault was deployed.

Must be set externally, as because of shitty Ethereum RPC we cannot query this. Allows us to avoid unnecessary work when scanning historical price data.