Skip to content

Commit

Permalink
Vault share price calculation makes sense
Browse files Browse the repository at this point in the history
  • Loading branch information
miohtama committed Jan 5, 2025
1 parent 64ab453 commit c8af635
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 7 deletions.
83 changes: 80 additions & 3 deletions eth_defi/lagoon/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ class LagoonSettlementEvent:
"""Capture Lagoon vault flow when it is settled.
- Use to adjust vault treasury balances for internal accounting
- Shows the Lagoon vault status after the settlement at a certain block height
- We do not capture individual users
The cycle is
- Value vault
- Settle deposits (USD in) and redeemds (USDC out, shares in)
- Because valuation is done before the settle, you need to be careful what the values reflect here
- We pull some values from receipt, some values at the end of the block
"""

#: Chain we checked
Expand All @@ -39,6 +46,12 @@ class LagoonSettlementEvent:
#: Vault address
vault: LagoonVault

#: Number of deposit event processes (0..1)
deposit_events: int

#: Number of deposit event processes (0..1)
redeem_events: int

#: How much new underlying was added to the vault
deposited: Decimal

Expand All @@ -51,6 +64,33 @@ class LagoonSettlementEvent:
#: Shares burned for redemptions
shares_burned: Decimal

#: Vault latest settled valuation.
#:
#: This does not include the newly settled deposits,
#: as they were not part of the previous share valuation cycle.
#:
total_assets: Decimal

#: Outstanding shares.
#:
#: Vault latest issued share count
total_supply: Decimal

#: Share price in the underlying token, after the settlement
share_price: Decimal

#: Amount of redemptions we could not settle (USD),
#: because the lack of cash in the previous cycle.
#:
#: This much of cash needs to be made available for the next settlement cycle.
#:
pending_redemptions_underlying: Decimal

#: Amount of redemptions we could not settle (share count),
#: because the lack of cash in the previous cycle.
#:
pending_redemptions_shares: Decimal

@property
def underlying(self) -> TokenDetails:
"""Get USDC."""
Expand All @@ -68,29 +108,46 @@ def get_serialiable_diagnostics_data(self) -> dict:
"block_number": self.block_number,
"timestamp": self.timestamp,
"tx_hash": self.tx_hash.hex(),
"deposit_events": self.deposit_events,
"redeem_events": self.redeem_events,
"vault": self.vault.vault_address,
"underlying": self.underlying.address,
"share_token": self.share_token.address,
"deposited": self.deposited,
"redeemed": self.redeemed,
"shares_minted": self.shares_minted,
"shares_burned": self.shares_minted,
"shares_burned": self.shares_burned,
"total_assets": self.total_assets,
"total_supply": self.total_supply,
"share_price": self.share_price,
"pending_redemptions_underlying": self.pending_redemptions_underlying,
"pending_redemptions_shares": self.pending_redemptions_shares,
}

def analyse_vault_flow_in_settlement(
vault: LagoonVault,
tx_hash: HexBytes,
) -> LagoonSettlementEvent:
"""Extract deposit and redeem events from a settlement transaction"""
"""Extract deposit and redeem events from a settlement transaction.
- Analyse vault asset flow based on the settlement tx logs in the receipt
- May need to call vault contract if no deposist or redeem events were prevent.
This needs an archive node for historical lookback.
"""
web3 = vault.web3
receipt = web3.eth.get_transaction_receipt(tx_hash)
assert receipt is not None, f"Cannot find tx: {tx_hash}"
assert isinstance(tx_hash, HexBytes), f"Got {tx_hash}"

assert receipt["status"] == 1, f"Lagoon vault settlement transaction did not succeed: {tx_hash.hex()}"

deposits = vault.vault_contract.events.SettleDeposit().process_receipt(receipt, errors=EventLogErrorFlags.Discard)
redeems = vault.vault_contract.events.SettleRedeem().process_receipt(receipt, errors=EventLogErrorFlags.Discard)

assert len(deposits) == 1, f"Does not look like settleDeposit() tx: {tx_hash.hex()}"
total_asset_updates = vault.vault_contract.events.TotalAssetsUpdated().process_receipt(receipt, errors=EventLogErrorFlags.Discard)
assert len(total_asset_updates) == 1, f"Does not look like Lagoon settlement tx, lacking event TotalAssetsUpdated: {tx_hash.hex()}"
assert len(deposits) in (0, 1), "Only zer or one events per settlement TX"
assert len(redeems) in (0, 1), "Only zer or one events per settlement TX"

new_deposited_raw = sum(log["args"]["assetsDeposited"] for log in deposits)
new_minted_raw = sum(log["args"]["sharesMinted"] for log in deposits)
Expand All @@ -101,14 +158,34 @@ def analyse_vault_flow_in_settlement(
block_number = receipt["blockNumber"]
timestamp = get_block_timestamp(web3, block_number)

# The amount of shares that could not be redeemed due to lack of cash,
# at the end of the block
pending_shares = vault.get_flow_manager().fetch_pending_redemption(block_number)

# Always pull these numbers at the end of the block
total_supply = vault.fetch_total_supply(block_number)
total_assets = vault.fetch_total_assets(block_number)

if total_assets:
share_price = total_supply / total_assets
else:
share_price = Decimal(0)

return LagoonSettlementEvent(
chain_id=vault.chain_id,
tx_hash=tx_hash,
block_number=block_number,
timestamp=timestamp,
deposit_events=len(deposits),
redeem_events=len(redeems),
vault=vault,
deposited=vault.underlying_token.convert_to_decimals(new_deposited_raw),
redeemed=vault.underlying_token.convert_to_decimals(new_redeem_raw),
shares_minted=vault.share_token.convert_to_decimals(new_minted_raw),
shares_burned=vault.share_token.convert_to_decimals(new_burned_raw),
total_assets=total_assets,
total_supply=total_supply,
pending_redemptions_shares=pending_shares,
pending_redemptions_underlying=pending_shares * share_price,
share_price=share_price,
)
11 changes: 8 additions & 3 deletions eth_defi/lagoon/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,20 @@ def valuation_manager(self) -> HexAddress:
"""Valuation manager role on the vault."""
return self.info["valuationManager"]

@cached_property
def silo_address(self) -> HexAddress:
"""Pending Silo contract address"""
vault_contract = self.vault_contract
silo_address = vault_contract.functions.pendingSilo().call()
return silo_address

@cached_property
def silo_contract(self) -> Contract:
"""Pending Silo contract.
- This contract does not have any functionality, but stores deposits (pending USDC) and redemptions (pending share token)
"""
vault_contract = self.vault_contract
silo_address = vault_contract.functions.pendingSilo().call()
return get_deployed_contract(self.web3, "lagoon/Silo.json", silo_address)
return get_deployed_contract(self.web3, "lagoon/Silo.json", self.silo_address)

@property
def underlying_token(self) -> TokenDetails:
Expand Down
69 changes: 68 additions & 1 deletion tests/lagoon/test_lagoon_flow_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def test_lagoon_deposit_redeem(
- Redeem from user 1, deposit from user 2
- Analyse
When we mess around
- Share price should never change, because we do not trade and incur PnL
To run with Tenderly tx inspector:
.. code-block:: shell
Expand All @@ -43,6 +47,7 @@ def test_lagoon_deposit_redeem(
usdc = base_usdc

assert usdc.fetch_balance_of(new_depositor) == 500
assert usdc.fetch_balance_of(vault.address) == pytest.approx(Decimal(0))

# Deposit 9.00 USDC into the vault from the first user
usdc_amount = Decimal(9.00)
Expand All @@ -52,6 +57,7 @@ def test_lagoon_deposit_redeem(
deposit_func = vault.request_deposit(depositor, raw_usdc_amount)
tx_hash = deposit_func.transact({"from": depositor})
assert_transaction_success_with_explanation(web3, tx_hash)
assert usdc.fetch_balance_of(vault.silo_address) == pytest.approx(Decimal(9))

# Settle the first deposit
tx_hash = vault.post_valuation_and_settle(Decimal(0), asset_manager)
Expand All @@ -63,6 +69,16 @@ def test_lagoon_deposit_redeem(
assert analysis.redeemed == 0
assert analysis.shares_minted == 9
assert analysis.shares_burned == 0
assert analysis.pending_redemptions_shares == 0
assert analysis.pending_redemptions_underlying == 0
assert analysis.total_assets == 9
assert analysis.total_supply == 9
assert analysis.pending_redemptions_underlying == 0
assert analysis.share_price == Decimal(1) # No share price yet, because valuation as done for the empty vault
assert analysis.deposit_events == 1
assert analysis.redeem_events == 0
assert usdc.fetch_balance_of(vault.silo_address) == pytest.approx(Decimal(0))
assert usdc.fetch_balance_of(vault.safe_address) == pytest.approx(Decimal(9))

# Second round:
# - Partial redeem
Expand Down Expand Up @@ -98,11 +114,62 @@ def test_lagoon_deposit_redeem(
tx_hash = vault.post_valuation_and_settle(Decimal(9), asset_manager)
analysis = analyse_vault_flow_in_settlement(vault, tx_hash)

# Check how the balance look like
assert vault.share_token.contract.functions.totalSupply().call() == pytest.approx(11*10**18)
assert vault.share_token.contract.functions.balanceOf(vault.address).call() == pytest.approx(5 * 10 ** 18) # Shares are held on the vault contract until redeem() called by yhe user
assert vault.share_token.contract.functions.balanceOf(vault.silo_address).call() == pytest.approx(0)

assert analysis.deposited == 5
assert analysis.redeemed == pytest.approx(Decimal(3))
assert analysis.shares_minted == pytest.approx(Decimal(5))
assert analysis.shares_burned == pytest.approx(Decimal(3))
assert analysis.pending_redemptions_shares == 0
assert analysis.pending_redemptions_underlying == 0
assert analysis.share_price == pytest.approx(Decimal(1))
assert analysis.total_assets == pytest.approx(Decimal(11)) # Redeem not processed yet
assert analysis.total_supply == pytest.approx(Decimal(11))
assert analysis.deposit_events == 1
assert analysis.redeem_events == 1

# No events, 11 USDC still hold in the vault as the user has not claimed redemption
assert usdc.fetch_balance_of(vault.address) == pytest.approx(Decimal(3))
tx_hash = vault.post_valuation_and_settle(Decimal(11), asset_manager)
analysis = analyse_vault_flow_in_settlement(vault, tx_hash)
assert analysis.deposited == 0
assert analysis.redeemed == pytest.approx(Decimal(0))
assert analysis.shares_minted == pytest.approx(Decimal(0))
assert analysis.shares_burned == pytest.approx(Decimal(0))
assert analysis.pending_redemptions_shares == 0
assert analysis.pending_redemptions_underlying == 0
assert analysis.total_assets == pytest.approx(Decimal(11)) # Redeem not processed yet
assert analysis.total_supply == pytest.approx(Decimal(11))
assert analysis.share_price == pytest.approx(Decimal(1))
assert analysis.deposit_events == 0
assert analysis.redeem_events == 0

# Finally claim the redemption
bound_func = vault.finalise_redeem(depositor, shares_to_redeem_raw)
tx_hash = bound_func.transact({"from": depositor, "gas": 1_000_000})
assert_transaction_success_with_explanation(web3, tx_hash)


# 3 USDC was moved away from the vault, 5 USDC added, making total 11 USDC
assert usdc.fetch_balance_of(vault.safe_address) == pytest.approx(Decimal(11))
tx_hash = vault.post_valuation_and_settle(Decimal(11), asset_manager)
analysis = analyse_vault_flow_in_settlement(vault, tx_hash)
assert analysis.deposited == 0
assert analysis.redeemed == pytest.approx(Decimal(0))
assert analysis.shares_minted == pytest.approx(Decimal(0))
assert analysis.shares_burned == pytest.approx(Decimal(0))
assert analysis.pending_redemptions_shares == 0
assert analysis.pending_redemptions_underlying == 0
assert analysis.total_assets == pytest.approx(Decimal(11)) # Redeem not processed yet
assert analysis.total_supply == pytest.approx(Decimal(11))
assert analysis.share_price == pytest.approx(Decimal(1))
assert analysis.deposit_events == 0
assert analysis.redeem_events == 0

# Check data exporter
data = analysis.get_serialiable_diagnostics_data()
assert isinstance(data, dict)


0 comments on commit c8af635

Please sign in to comment.