Skip to content

Conversation

@kyzooghost
Copy link
Contributor

Revised semantics of userFunds and lstLiabilityPrincipal

Definition of userFunds

We define userFunds precisely as “the total funds owed to Linea bridge users.”

In the current audit branch implementation, userFunds conflates two distinct concepts:

  1. total deposits - total withdrawals in the StakingVault

  2. Reported yield

While reported yield represents circulating L2 ETH (and therefore a legitimate component of “funds owed to Linea bridge users”), the first term — total deposits - total withdrawals — does not always accurately track "funds owed to Linea users". The discrepancy emerges when users withdraw Liquid Staking Tokens (LSTs), as shown below.

Example: Divergence Scenario

T=0 
Action: 200 ETH is deposited into the StakingVault
State:
  `total deposits - total withdrawals` = 200 ETH
  `funds owed to Linea bridge users` = 200 ETH

T=1
Action: A user withdraws LST worth 100 ETH  - State:
  `total deposits - total withdrawals` = 200 ETH
  `funds owed to Linea bridge users` = 100 ETH

Here, funds owed to Linea bridge users no longer equals total deposits - total withdrawals. According to our chosen definition of userFunds, we must decouple it from the StakingVault deposit/withdrawal deltas.

This choice has broad accounting and design implications that cascade through related components, as detailed below.

Relationship Between lstLiabilityPrincipal and userFunds

Whenever LST is minted, lstLiabilityPrincipal increases by the ETH-equivalent value of the LST issued.

In the current audit branch, lstLiabilityPrincipal is treated as an “advance” to the Linea user against userFunds. However, once we adopt the stricter definition of userFunds as “funds owed to Linea users,” this interpretation becomes invalid.

Instead, during LST withdrawal, userFunds must decrease in proportion to the increment in lstLiabilityPrincipal.

T=0
Action: 200 ETH deposited into StakingVault
State:
  StakingVault.balance = 200 ETH
  userFunds = 200 ETH
  lstLiabilityPrincipal = 0

T=1
Action: LST withdrawal worth 100 ETH
State:
  StakingVault.balance = 200 ETH
  userFunds = 100 ETH
  lstLiabilityPrincipal = 100 ETH

Here, lstLiabilityPrincipal represents funds deposited to the StakingVault but owed to the Lido protocol. Although the vault balance remains unchanged, the system’s liabilities have shifted — now owing 100 ETH to Linea users and 100 ETH to Lido.

Functions That Mutate lstLiabilityPrincipal

Four other functions can change the value of lstLiabilityPrincipal. We analyze each in turn.

  1. YieldManager.fundYieldProvider

  2. YieldManager._withdrawWithTargetDeficitPriorityAndLSTLiabilityPrincipalReduction

  3. LidoStVaultYieldProvider.reportYield

  4. LidoStVaultYieldProvider._initiateOssification


1. YieldManager.fundYieldProvider

This function stakes funds from the YieldManager into the StakingVault, and proactively settles any outstanding lstLiabilityPrincipal.

T=0
Starting State:
  StakingVault.balance = 200 ETH
  userFunds = 100 ETH
  lstLiabilityPrincipal = 100 ETH

T=1
Action: fundYieldProvider(200 ETH)
  - 100 ETH used to pay down lstLiabilityPrincipal
  - Remaining 100 ETH deposited into the vault

Resulting State:
  StakingVault.balance = 300 ETH (+100)
  userFunds = 300 ETH (+200)
  lstLiabilityPrincipal = 0 ETH (-100)

Note that once the LST principal is settled, the discrepancy between vault balance and userFunds disappears.


2. YieldManager._withdrawWithTargetDeficitPriorityAndLSTLiabilityPrincipalReduction

This function withdraws funds from the StakingVault to the YieldManager. Under our refined semantics, withdrawals must not exceed userFunds.

Before executing the withdrawal, the function prioritizes reducing outstanding LST liabilities. The LST liability amount is locked (plus an added margin determined by collateralization ratio) by the Lido protocol, and thus unavailable for withdrawal to the L1MessageService.

Because the position is overcollateralized, repaying as much LST liability principal as possible first maximizes the amount of ETH that becomes “unlocked” and withdrawable from the StakingVault.

Revised withdrawal logic:

  • Begin by making the maximum possible repayment of lstLiabilityPrincipal
  • Permit withdrawals only up to userFunds

Example Scenario

T=0
Starting State:
  StakingVault.balance = 200 ETH
  userFunds = 100 ETH
  lstLiabilityPrincipal = 100 ETH

T=1
Action: withdrawFromYieldProvider(100 ETH)
  - Maximum possible payment of LST liability principal first

Intermediate State (after LST liability repayment):
  StakingVault.balance = 100 ETH
  userFunds = 100 ETH
  lstLiabilityPrincipal = 0 ETH

Final State (after withdrawal):
  StakingVault.balance = 0 ETH
  userFunds = 0 ETH
  lstLiabilityPrincipal = 0 ETH

3. LidoStVaultYieldProvider._initiateOssification

This function pays all outstanding Lido protocol fees, node operator fees, and LST liabilities.

These amounts are strictly separate from userFunds, so no adjustment to userFunds is required.


4. LidoStVaultYieldProvider.reportYield

Yield is defined as:

yield = dashboard.totalValue()
       - outstandingLSTLiabilities
       - outstandingLidoProtocolFees
       - outstandingNodeOperatorFees
       - userFunds

Positive Yield Handling

  • When yield > 0: userFunds is incremented by the calculated yield amount
  • Positive yield is immediately applied and does not accumulate across reporting periods

Negative Yield Handling

Recording & State Management:

  • When yield < 0: The negative amount becomes outstandingNegativeYield while userFunds remains unmodified
  • Critical invariant: Once stored, the negative yield snapshot is immutable—no other functions may alter this value

Accumulation Behavior:

  • Negative yield persists across reporting periods until the underlying issues are resolved
  • Creates asymmetric handling: positive yield resets each report, negative yield accumulates until cleared

Protocol Safeguards:

  • The immutable negative yield snapshot can serve as a reference point for risk management decisions

LST Liability Management

  • outstandingLSTLiabilities encompasses both principal and interest, eliminating the need for separate lstLiabilityPrincipal handling
  • During yield reporting, liability payments may reduce lstLiabilityPrincipal as a side effect, sourced from outstandingLSTLiabilities (not userFunds)
  • Critical: Lido protocol fees, operator fees, and LST liabilities must remain isolated from userFunds to maintain proper accounting separation

Function Behaviour

function reportYield(...) returns (uint256 newReportedYield, uint256 outstandingNegativeYield)
  • yield >0: Returns (yield, 0)
  • yield <0: Returns (0, |yield|)

Summary

  • userFunds represents only funds owed to Linea bridge users.
  • userFunds must not be intermingled with pending LST liability, pending node operator fees, or pending Lido protocol fees
  • userFunds must not be intermingled with the Vault deposit/withdrawal delta

@kyzooghost kyzooghost changed the base branch from main to yield-management-only November 7, 2025 09:24
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

Successfully merging this pull request may close these issues.

2 participants