diff --git a/docs/core/accounting/SharesAccounting.md b/docs/core/accounting/SharesAccounting.md index a5ae17cab..940dfab0a 100644 --- a/docs/core/accounting/SharesAccounting.md +++ b/docs/core/accounting/SharesAccounting.md @@ -1,120 +1,113 @@ -[magnitude-doc]: https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md [elip-002]: https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md # Shares Accounting -## Prerequisite Documents +This document outlines the changes to the staker and operator Shares accounting resulting from the Slashing Upgrade. There are several introduced variables such as the _deposit scaling factor_ ($k_n$), _max magnitude_ ($m_n$), and _beacon chain slashing factor_ ($l_n$). How these interact with the operator and staker events like deposits, slashing, withdrawals will all be described below. -## Terminology +## Prior Reading -The word "shares" in EigenLayer has historically referred to the amount of shares a Staker receives upon depositing assets either through the `StrategyManager` or `EigenPodManager`. Outside of some conversion ratios in the `StrategyManager` to account for rebasing tokens, shares roughly correspond 1:1 with deposit amounts (i.e. 1e18 shares in the `beaconChainETHStrategy` corresponds to 1 ETH of assets). When delegating to an operator or queueing a withdrawal, the `DelegationManager` reads deposit shares from the `StrategyManager` or `EigenPodManager` to determine how many shares to delegate (or undelegate). +* [ELIP-002: Slashing via Unique Stake and Operator Sets](https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md) -With the slashing release, there is a need to differentiate "classes" of shares. - -**Deposit shares**: - -Formerly known as "shares," these are the same shares used before the slashing release. They continue to be managed by the `StrategyManager` and `EigenPodManager`, and roughly correspond 1:1 with deposited assets. - -**Withdrawable shares**: - -When an operator is slashed, the slash is applied to their stakers _asynchronously_ (otherwise, slashing would require iterating over each of an operator's stakers; this is prohibitively expensive). - -The `DelegationManager` must find a common representation for the deposit shares of many stakers, each of which may have experienced different amounts of slashing depending on which operator they are delegated to, and when they delegated. This common representation is achieved in part through a value called the `depositScalingFactor`: a per-staker, per-strategy value that scales a staker's deposit shares as they deposit assets over time. - -When a staker does just about anything (changing their delegated operator, queueing/completing a withdrawal, depositing new assets), the `DelegationManager` converts their _deposit shares_ to _withdrawable shares_ by applying the staker's `depositScalingFactor` and the current _slashing factor_ (a per-strategy scalar primarily derived from the amount of slashing an operator has received in the `AllocationManager`). +## Pre-Slashing Upgrade -These _withdrawable shares_ are used to determine how many of a staker's deposit shares are actually able to be withdrawn from the protocol, as well as how many shares can be delegated to an operator. A staker's withdrawable shares are not reflected anywhere in storage; they are calculated on-demand. +We'll look at the "shares" model as historically defined prior to the Slashing upgrade. Pre-slashing, stakers could receive shares for deposited assets, delegate those shares to operators, and withdraw those shares from the protocol. We can write this a bit more formally: -**Operator shares**: - -_Operator shares_ are derivative of _withdrawable shares_. When a staker delegates to an operator, they are delegating their _withdrawable shares_. Thus, an operator's _operator shares_ represent the sum of all of their stakers' _withdrawable shares_. Note that when a staker first delegates to an operator, this is a special case where _deposit shares_ == _withdrawable shares_. If the staker deposits additional assets later, this case will not hold if slashing was experienced in the interim. - -We can write this a bit more formally with the following: - -### Staker Level +#### Staker Level $s_n$ - The amount of shares in the storage of the `StrategyManager`/`EigenPodManager` at time n. -### Operator Level +#### Operator Level $op_n$ - The operator shares in the storage of the `DelegationManager` at time n which can also be rewritten as \ $op_n = \sum_{i=1}^{k} s_{n,i}$ where the operator has $k$ number of stakers delegated to them. -### Staker Deposits +#### Staker Deposits -Upon each Staker deposit of amount $d_n$ at time $n$, the Staker's shares and delegated Operator's shares are updated as follows: +Upon each staker deposit of amount $d_n$ at time $n$, the staker's shares and delegated operator's shares are updated as follows: $$ - s_{n+1} = s_{n} + d_{n} + s_{n+1} = s_{n} + d_{n} $$ $$ - op_{n+1} = op_{n} + d_{n} + op_{n+1} = op_{n} + d_{n} $$ -### Staker Withdrawals +#### Staker Withdrawals -Similarly for Staker withdrawals, given an amount $w_n$ to withdraw at time $n$, the Staker and Operator's shares are decremented at the point of the withdrawal being queued: +Similarly for staker withdrawals, given an amount $w_n$ to withdraw at time $n$, the staker and operator's shares are decremented at the point of the withdrawal being queued: $$ - s_{n+1} = s_{n} - w_{n} + s_{n+1} = s_{n} - w_{n} $$ $$ - op_{n+1} = op_{n} - w_{n} + op_{n+1} = op_{n} - w_{n} $$ -Later after the withdrawal delay has passed, the Staker can complete their withdrawal to withdraw the full amount $w_n$ of shares. +Later after the withdrawal delay has passed, the staker can complete their withdrawal to withdraw the full amount $w_n$ of shares. +--- ## Slashing Upgrade Changes -As of release `v1.0.0` and the introduction of Unique Stake and Operator Sets, programmatic slashing will be enabled in the core EigenLayer protocol where Staker deposits can be subject to slashing. -The remaining portions of this document will assume understanding of Allocations/Deallocations, and Max Magnitudes, and OperatorSets. -For more information on this, there is the [ELIP-002][elip-002] which has a high-level but detailed overview of the Slashing upgrade as well as the separate [Magnitude document][magnitude-doc] here. +The remaining portions of this document will assume understanding of Allocations/Deallocations, Max Magnitudes, and Operator Sets as described in [ELIP-002][elip-002]. + +### Terminology + +The word "shares" in EigenLayer has historically referred to the amount of shares a staker receives upon depositing assets through the `StrategyManager` or `EigenPodManager`. Outside of some conversion ratios in the `StrategyManager` to account for rebasing tokens, shares roughly correspond 1:1 with deposit amounts (i.e. 1e18 shares in the `beaconChainETHStrategy` corresponds to 1 ETH of assets). When delegating to an operator or queueing a withdrawal, the `DelegationManager` reads deposit shares from the `StrategyManager` or `EigenPodManager` to determine how many shares to delegate (or undelegate). + +With the slashing release, there is a need to differentiate "classes" of shares. + +**Deposit shares**: -We now introduce a few new types of Shares concepts: +Formerly known as "shares," these are the same shares used before the slashing release. They continue to be managed by the `StrategyManager` and `EigenPodManager`, and roughly correspond 1:1 with deposited assets. -1. **deposit shares**: \ - For a Staker, this is the amount of Strategy shares deposited -2. **withdrawable shares**: \ - For a Staker, this is the actual amount of Strategy shares they are eligible to withdraw. \ - This does not live in storage but is read through the view function `DelegationManager.getWithdrawableShares`. Note that this amount is <= deposit shares as the Staker may have had their shares slashed. -3. **operator/delegated shares**: \ - This still remains the same definition as before, the amount of delegated shares of an operator from all their delegated stakers. - However, this is now equal to the summation of all their staker's withdrawable shares. +**Withdrawable shares**: -Notice that these definitions also apply to the shares model prior to the Slashing upgrade but with the caveat that for all Stakers, withdrawable shares equals the deposit shares. After the Slashing upgrade this is not neccesarily the case if a Staker's delegated Operator were slashed resulting in less withdrawable shares for the Staker. \ -Now lets look at these updated definitions in detail and how the accounting math works with deposits, withdrawals, and slashing. +When an operator is slashed, the slash is applied to their stakers _asynchronously_ (otherwise, slashing would require iterating over each of an operator's stakers; this is prohibitively expensive). + +The `DelegationManager` must find a common representation for the deposit shares of many stakers, each of which may have experienced different amounts of slashing depending on which operator they are delegated to, and when they delegated. This common representation is achieved in part through a value called the `depositScalingFactor`: a per-staker, per-strategy value that scales a staker's deposit shares as they deposit assets over time. + +When a staker does just about anything (changing their delegated operator, queueing/completing a withdrawal, depositing new assets), the `DelegationManager` converts their _deposit shares_ to _withdrawable shares_ by applying the staker's `depositScalingFactor` and the current _slashing factor_ (a per-strategy scalar primarily derived from the amount of slashing an operator has received in the `AllocationManager`). + +These _withdrawable shares_ are used to determine how many of a staker's deposit shares are actually able to be withdrawn from the protocol, as well as how many shares can be delegated to an operator. An individual staker's withdrawable shares are not reflected anywhere in storage; they are calculated on-demand. + +**Operator shares**: + +_Operator shares_ are derivative of _withdrawable shares_. When a staker delegates to an operator, they are delegating their _withdrawable shares_. Thus, an operator's _operator shares_ represent the sum of all of their stakers' _withdrawable shares_. Note that when a staker first delegates to an operator, this is a special case where _deposit shares_ == _withdrawable shares_. If the staker deposits additional assets later, this case will not hold if slashing was experienced in the interim. --- +Each of these definitions can also be applied to the pre-slashing share model, but with the caveat that for all stakers, _withdrawable shares equal deposit shares_. After the slashing upgrade this is not necessarily the case - a staker may not be able to withdraw the amount they deposited if their operator got slashed. + +Now let's look at these updated definitions in detail and how the accounting math works with deposits, withdrawals, and slashing. + ### Stored Variables -Note that these variables are all defined within the context of a single Strategy. +Note that these variables are all defined within the context of a single Strategy. Also note that the concept of "1" used within these equations is represented in the code by the constant `1 WAD`, or `1e18`. + #### Staker Level -$s_n$ - The amount of deposit shares in the storage of the `StrategyManager`/`EigenPodManager` at time $n$. \ - Exists in storage: `StrategyManager.stakerDepositShares`, `EigenPodManager.stakerDepositShares` for beaconChainETHStrategy\ -$k_n$ - The Staker “deposit scaling factor” at time $n$. This is initialized to 1. \ - Exists in storage: `DelegationManager.depositScalingFactor` \ -$l_n$ - The Staker's "beacon chain slashing factor" at time $n$. This is initialized to 1 and for any non-native ETH Strategies always is always fixed to 1 rather than calculating withdrawable shares completely different depending on native versus non-native ETH. - Exists in storage: `EigenPodManager.beaconChainSlashingFactor` +$s_n$ - The amount of deposit shares in the storage of the `StrategyManager`/`EigenPodManager` at time $n$. In storage: `StrategyManager.stakerDepositShares` and `EigenPodManager.podOwnerDepositShares` + +$k_n$ - The staker's “deposit scaling factor” at time $n$. This is initialized to 1. In storage: `DelegationManager.depositScalingFactor` + +$l_n$ - The staker's "beacon chain slashing factor" at time $n$. This is initialized to 1. For any equations concerning non-native ETH strategies, this can be assumed to be 1. In storage: `EigenPodManager.beaconChainSlashingFactor` #### Operator Level -$m_n$ - The operator magnitude at time n. This is initialized to 1. \ -$op_n$ - The operator shares in the storage of the `DelegationManager` at time n which can also be rewritten as $op_n = \sum_{i=1}^{k} a_{n,i}$ \ - Exists in storage: `DelegationManager.operatorShares` +$m_n$ - The operator magnitude at time n. This is initialized to 1. -### Conceptual Variables +$op_n$ - The operator shares in the storage of the `DelegationManager` at time n. In storage: `DelegationManager.operatorShares` -$a_n = s_n k_n l_n m_n$ - The withdrawable shares that the staker owns at time $n$. - Read from view function `DelegationManager.getWithdrawableShares` +### Conceptual Variables +$a_n = s_n k_n l_n m_n$ - The withdrawable shares that the staker owns at time $n$. Read from view function `DelegationManager.getWithdrawableShares` +Note that $op_n = \sum_{i=1}^{k} a_{n,i}$. --- @@ -122,23 +115,9 @@ $a_n = s_n k_n l_n m_n$ - The withdrawable shares that the staker owns at time $ For an amount of newly deposited shares $d_n$, -#### Operator Level - -The operator magnitude doesn’t change. - -$$ -m_{n+1} = m_n -$$ - -For the operator, - -$$ -op_{n+1} = op_n+d_n -$$ - #### Staker Level -From the conceptual level, +Conceptually, the staker's deposit shares and withdrawable shares both increase by the deposited amount $d_n$. Let's work out how this math impacts the deposit scaling factor $k_n$. $$ a_{n+1} = a_n + d_n @@ -152,18 +131,38 @@ $$ l_{n+1} = l_n $$ +$$ +m_{n+1} = m_n +$$ + Expanding the $a_{n+1}$ calculation $$ s_{n+1} k_{n+1} l_{n+1} m_{n+1} = s_n k_n l_n m_n + d_n $$ -Which yields +Simplifying yields: $$ -k_{n+1} = \frac{s_n k_n m_n + d_n}{s_{n+1} l_{n+1} m_{n+1}}=\frac{s_n k_n l_n m_n + d_n}{(s_n+d_n)m_n} +k_{n+1} = \frac{s_n k_n l_n m_n + d_n}{s_{n+1} l_{n+1} m_{n+1}}=\frac{s_n k_n l_n m_n + d_n}{(s_n+d_n)l_nm_n} $$ +Updating the slashing factor is implemented in `SlashingLib.update`. + +#### Operator Level + +For the operator (if the staker is delegated), the delegated operator shares should increase by the exact amount +the staker just deposited. Therefore $op_n$ is updated as follows: + +$$ +op_{n+1} = op_n+d_n +$$ + +See implementation in: +* [`StrategyManager.depositIntoStrategy`](../../../src/contracts/core/StrategyManager.sol) +* [`EigenPodManager.recordBeaconChainETHBalanceUpdate`](../../../src/contracts/pods/EigenPodManager.sol) + +--- ### Slashing @@ -174,56 +173,42 @@ Given a proportion to slash $p_n = \frac {m_{n+1}}{m_n}$ , From a conceptual level, operator shares should be decreased by the proportion according to the following: $$ - op_{n+1} = op_n p_n + op_{n+1} = op_n p_n $$ $$ - => op_{n+1} = op_n \frac {m_{n+1}} {m_n} + => op_{n+1} = op_n \frac {m_{n+1}} {m_n} $$ -However, since we don't overwrite `operatorShares` directly in storage and perform increments/decrements we will calculate the amount of $sharesToDecrement$. +Calculating the amount of $sharesToDecrement$: $$ - sharesToDecrement = op_n - op_{n+1} + sharesToDecrement = op_n - op_{n+1} $$ $$ - = op_n - op_n \frac {m_{n+1}} {m_n} + = op_n - op_n \frac {m_{n+1}} {m_n} $$ -which is exactly how we calculate sharesToDecrement in our library `SlashingLib.sol` - -```solidity -function calcSlashedAmount( - uint256 operatorShares, - uint256 prevMaxMagnitude, - uint256 newMaxMagnitude -) internal pure returns (uint256) { - // round up mulDiv so we don't overslash - return operatorShares - operatorShares.mulDiv(newMaxMagnitude, prevMaxMagnitude, Math.Rounding.Up); -} -``` +This calculation is performed in `SlashingLib.calcSlashedAmount`. #### Staker Level -From the conceptual level, a Staker's withdrawable shares should also be proportionally slashed so the following must be true: +From the conceptual level, a staker's withdrawable shares should also be proportionally slashed so the following must be true: $$ a_{n+1} = a_n p_n $$ -We don't want to update storage at the Staker level during slashing as this would be computationally too expensive given an operator has a 1-many relationship with its delegated stakers. +We don't want to update storage at the staker level during slashing as this would be computationally too expensive given an operator has a 1-many relationship with its delegated stakers. Therefore we want to prove $a_{n+1} = a_n p_n$ since withdrawable shares are slashed by $p_n$. -Therefore we want to prove $a_{n+1} = a_n p_n$ since withdrawable shares are slashed by $p_n$ given the following: +Given the following: $l_{n+1} = l_n$ \ $k_{n+1} = k_n$ \ $s_{n+1} = s_n$ - -because we don’t want to update EigenPodManager,StrategyManager storage. - -Expanding the $a_{n+1}$ equation +Expanding the $a_{n+1}$ equation: $$ a_{n+1} = s_{n+1} k_{n+1} l_{n+1} m_{n+1} @@ -243,59 +228,250 @@ $$ => a_n p_n $$ -Which is exactly as wanted so a Staker's withdrawable shares are immediately affected upon their operator's maxMagnitude being slashed(decreased). +This means that a staker's withdrawable shares are immediately affected upon their operator's maxMagnitude being decreased via slashing. +--- ### Queue Withdrawal -Withdrawals are queued by inputting a `depositShares` amount $x_n$ which corresponds to the amount stored in $s_n$. -The actual withdrawable amount $w_n$ corresponding to $x_n$ is given by the following: +Withdrawals are queued by inputting a `depositShares` amount $x_n <= s_n$. The actual withdrawable amount $w_n$ corresponding to $x_n$ is given by the following: $$ - w_n = x_n k_n l_n m_n + w_n = x_n k_n l_n m_n $$ -This conceptually makes sense as the amount being withdrawn $w_n$ is some amount <= $a_n$ which is the total withdrawable shares amount for the Staker. +This conceptually makes sense as the amount being withdrawn $w_n$ is some amount <= $a_n$ which is the total withdrawable shares amount for the staker. #### Operator Level -The operator shares are reduced accordingly by the reduced delegated shares of the Staker. +When a staker queues a withdrawal, their operator's shares are reduced accordingly: $$ - op_{n+1} = op_n - w_n + op_{n+1} = op_n - w_n $$ #### Staker Level $$ - a_{n+1} = a_n - w_n + a_{n+1} = a_n - w_n $$ $$ - s_{n+1} = s_n - x_n + s_{n+1} = s_n - x_n $$ -The DelegationManager will tell the EigenPodManager/StrategyManager to decrement the depositShares the staker is withdrawing. -We want to show that the withdrawable shares for the Staker are decreased accordingly where $a_{n+1} = a_n - w_n$. +This means that when queuing a withdrawal, the staker inputs a `depositShares` amount $x_n$. The `DelegationManager` calls the the `EigenPodManager`/`StrategyManager` to decrement their `depositShares` by this amount. Additionally, the `depositShares` are converted to a withdrawable amount $w_n$, which are decremented from the operator's shares. + +We want to show that the total withdrawable shares for the staker are decreased accordingly such that $a_{n+1} = a_n - w_n$. + +Given the following: + +$l_{n+1} = l_n$ \ +$k_{n+1} = k_n$ \ +$s_{n+1} = s_n$ + +Expanding the $a_{n+1}$ equation: + $$ - a_{n+1} = s_{n+1} k_{n+1} l_{n+1} m_{n+1} + a_{n+1} = s_{n+1} k_{n+1} l_{n+1} m_{n+1} $$ $$ - => (s_{n} - x_n) k_{n+1} l_{n+1} m_{n+1} + => (s_{n} - x_n) k_{n+1} l_{n+1} m_{n+1} $$ $$ - = (s_{n} - x_n) k_n l_n m_n + = (s_{n} - x_n) k_n l_n m_n $$ $$ - = s_n k_n l_n m_n - x_n k_n l_n m_n + = s_n k_n l_n m_n - x_n k_n l_n m_n $$ $$ - = a_n - w_n -$$ \ No newline at end of file + = a_n - w_n +$$ + +Note that when a withdrawal is queued, a `Withdrawal` struct is created with _scaled shares_ defined as $q_t = x_t k_t$ where $t$ is the time of the queuing. The reason we define and store scaled shares like this will be clearer in [Complete Withdrawal](#complete-withdrawal) below. + +See implementation in: +* `DelegationManager.queueWithdrawals` +* `SlashingLib.scaleForQueueWithdrawal` + +
+ +--- + +### Complete Withdrawal + +Now the staker completes a withdrawal $(q_t, t)$ which was queued at time $t$. + +#### Operator Level + +If the staker completes the withdrawal _as tokens_, any operator shares remain unchanged. The original operator's shares were decremented when the withdrawal was queued, and a new operator does not receive shares if the staker is withdrawing assets ("as tokens"). + +However, if the staker completes the withdrawal _as shares_, the shares are added to the staker's current operator according to the formulae in [Deposits](#deposits). + +#### Staker Level + + + +Recall from [Queue Withdrawal](#queue-withdrawal) that, when a withdrawal is queued, the `Withdrawal` struct stores _scaled shares_, defined as $q_t = x_t k_t$ where $x_t$ is the deposit share amount requested for withdrawal and $t$ is the time of the queuing. + +And, given the formula for calculating withdrawable shares, the withdrawable shares given to the staker are $w_t$: + +$$ +w_t = q_t m_t l_t = x_t k_t l_t m_t +$$ + +However, the staker's shares in their withdrawal may have been slashed while the withdrawal was in the queue. Their operator may have been slashed by an AVS, or, if the strategy is the `beaconChainETHStrategy`, the staker's validators may have been slashed/penalized. + +The amount of shares they actually receive is proportionally the following: + +$$ + \frac{m_{t+delay} l_{now} }{m_t l_t} +$$ + +So the actual amount of shares withdrawn on completion is calculated to be: + +$$ +sharesWithdrawn = w_t (\frac{m_{t+delay} l_{now}}{m_t l_t} ) +$$ + +$$ += x_t k_t l_t m_t (\frac{m_{t+delay} l_{now}}{m_t l_t} ) +$$ + +$$ += x_t k_t m_{t+delay} l_{now} +$$ + +Now we know that $q_t = x_t k_t$ so we can substitute this value in here. + +$$ += q_t m_{t+delay} l_{now} +$$ + +From the above equations the known values we have during the time of queue withdrawal is $x_t k_t$ and we only know $m_{t+delay} l_{now}$ when the queued withdrawal is completable. This is why we store scaled shares as $q_t = x_t k_t$. The other term ($m_{t+delay} l_{now}$) is read during the completing transaction of the withdrawal. + +Note: Reading $m_{t+delay}$ is performed by a historical Snapshot lookup of the max magnitude in the `AllocationManager` while $l_{now}$, the current beacon chain slashing factor, is done through the `EigenPodManager`. Recall that if the strategy in question is not the `beaconChainETHStrategy`, $l_{now}$ will default to "1". + +The definition of scaled shares is used solely for handling withdrawals and accounting for slashing that may have occurred (both on EigenLayer and on the beacon chain) during the queue period. + +See implementation in: +* `DelegationManager.completeQueuedWithdrawal` +* `SlashingLib.scaleForCompleteWithdrawal` + +--- + +### Handling Beacon Chain Balance Decreases in EigenPods + +Beacon chain balance decreases are handled differently after the slashing upgrade with the introduction of $l_n$ the beacon chain slashing factor. + +Prior to the upgrade, any decreases in an `EigenPod` balance for a staker as a result of completing a checkpoint immediately decrements from the staker's shares in the `EigenPodManager`. As an edge case, this meant that a staker's shares could go negative if, for example, they queued a withdrawal for all their shares and then completed a checkpoint on their `EigenPod` showing a balance decrease. + +With the introduction of the beacon chain slashing factor, beacon chain balance decreases no longer result in a decrease in deposit shares. Instead, the staker's beacon chain slashing factor is decreased, allowing the system to realize that slash in any existing shares, as well as in any existing queued withdrawals. Effectively, this means that beacon chain slashing is accounted for similarly to EigenLayer-native slashing; _deposit shares remain the same, while withdrawable shares are reduced:_ + +![.](../../images/slashing-model.png) + +Now let's consider how beacon chain balance decreases are handled when they represent a negative share delta for a staker's EigenPod. + +#### Added Definitions + +$welw$ is `withdrawableExecutionLayerGwei`. This is purely native ETH in the `EigenPod`, attributed via checkpoint and considered withdrawable by the pod (but without factoring in any EigenLayer-native slashing). `DelegationManager.getWithdrawableShares` can be called to account for both EigenLayer and beacon chain slashing. + +$before\text{ }start$ is time just before a checkpoint is started + + + +$after\text{ }complete$ is the time just after a checkpoint is completed + +As a checkpoint is completed, the total assets represented by the pod's native ETH and beacon chain balances _before_ and _after_ are given by: + +$g_n = welw_{before\text{ }start}+\sum_i validator_i.balance_{before\text{ }start}$ \ +$h_n = welw_{after\text{ }complete}+\sum_i validator_i.balance_{after\text{ }complete}$ + +#### Staker Level + +Conceptually, the above logic specifies that we decrease the staker's withdrawable shares proportionally to the balance decrease: + +$$ +a_{n+1} = \frac{h_n}{g_n}a_n +$$ + +We implement this by setting + +$$ +l_{n+1}=\frac{h_n}{g_n}l_n +$$ + +Given: + +$m_{n+1}=m_n$ (staker beacon chain slashing does not affect its operator's magnitude) +$s_{n+1} = s_n$ (no subtraction of deposit shares) +$k_{n+1}=k_n$ + +Then, plugging into the formula for withdrawable shares: + +$$ +a_{n+1} = s_{n+1}k_{n+1}l_{n+1}m_{n+1} +$$ + +$$ +=s_nk_n\frac{h_n}{g_n}l_nm_n +$$ + +$$ += \frac{h_n}{g_n}a_n +$$ + +#### Operator Level + +Now we want to update the operator's shares accordingly. At a conceptual level $op_{n+1}$ should be the following: + +$$ + op_{n+1} = op_n - a_n + a_{n+1} +$$ + +We can simplify this further + + +$$ + =op_{n}-s_nk_nl_nm_n + s_nk_nl_{n+1}m_n +$$ + + +$$ + = op_{n}+s_nk_nm_n(l_{n+1}-l_n) +$$ + +See implementation in: +* `EigenPodManager.recordBeaconChainETHBalanceUpdate` +* `DelegationManager.decreaseDelegatedShares` + +--- + +## Implementation Details + +In practice, we can’t actually have floating values so we will substitute all $k_n, l_n, m_n$ terms with $m_n$/1e18 $\frac{k_n}{1e18},\frac{l_n}{1e18} ,\frac{m_n}{1e18}$ respectively where $k_n, l_n, m_n$ are the values in storage, all initialized to 1e18. This allows us to conceptually have values in the range $[0,1]$. + +We make use of OpenZeppelin's Math library and `mulDiv` for calculating $floor(\frac{x \cdot y}{denominator})$ with full precision. Sometimes for specific rounding edge cases, $ceiling(\frac{x \cdot y}{denominator})$ is explicitly used. + +#### Multiplication and Division Operations +For all the equations in the above document, we substitute any product operations of $k_n, l_n, m_n$ with the `mulWad` pure function. +```solidity +function mulWad(uint256 x, uint256 y) internal pure returns (uint256) { + return x.mulDiv(y, WAD); +} +``` + +Conversely, for any divisions of $k_n, l_n, m_n$ we use the `divWad` pure function. + +```solidity +function divWad(uint256 x, uint256 y) internal pure returns (uint256) { + return x.mulDiv(WAD, y); +} +``` diff --git a/docs/core/accounting/SharesAccountingEdgeCases.md b/docs/core/accounting/SharesAccountingEdgeCases.md new file mode 100644 index 000000000..918196157 --- /dev/null +++ b/docs/core/accounting/SharesAccountingEdgeCases.md @@ -0,0 +1,339 @@ +[elip-002]: https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md + +# Shares Accounting Edge Cases + +This document is meant to explore and analyze the different mathematical operations we are performing in the slashing release. Primarily we want to ensure safety on rounding and overflow situations. Prior reading of the [Shares Accounting](./SharesAccounting.md) is required to make sense of this document. + +## Prior Reading + +* [ELIP-002: Slashing via Unique Stake and Operator Sets](https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md) +* [Shares Accounting](./SharesAccounting.md) + + +## Fully Slashed for a Strategy + +Within the context of a single Strategy, recall that updates to the deposit scaling factor $k_n$ are defined as the following: + +$$ +k_{n+1} = \frac{s_n k_n m_n + d_n}{s_{n+1} l_{n+1} m_{n+1}}=\frac{s_n k_n l_n m_n + d_n}{(s_n+d_n)l_nm_n} +$$ + +We can see here that calculating $k_{n+1}$ can give us a divide by 0 error if any of $(s_n + d_n)$, $l_n$, or $m_n$ are equal to 0. The $(s_n + d_n) = 0$ case should not arise because the `EigenPodManager` and `StrategyManager` will not report share increases in this case. However, the other two terms may reach 0: +* When an operator is 100% slashed for a given strategy and their max magnitude $m_n = 0$ +* When a staker's `EigenPod` native ETH balance is 0 _and_ their validators have all been slashed such that $l_n = 0$ + +In these cases, updates to a staker's deposit scaling factor will encounter a division by 0 error. In either case, we know that since either the operator was fully slashed or the staker was fully slashed for the `beaconChainETHStrategy` then their withdrawable shares $a_n = 0$. + +In practice, if $m_n = 0$ for a given operator, then: +1. Any staker who is already delegated to this operator _will be unable to deposit additional assets into the corresponding strategy_ +2. Any staker that currently holds deposit shares in this strategy and is NOT delegated to the operator _will be unable to delegate to the operator_ + +Note that in the first case, it _is_ possible for the staker to undelegate, queue, and complete withdrawals - though as $a_n = 0$, they will not receive any withdrawable shares as a result. + +Additionally, if $l_n = 0$ for a given staker in the beacon chain ETH strategy, then **any further deposits of ETH or restaking of validators will not yield shares in EigenLayer.** This should only occur in extraordinary circumstances, as a beacon chain slashing factor of 0 means that a staker both has ~0 assets in their `EigenPod`, and ALL of their validators have been ~100% slashed on the beacon chain - something that happens only when coordinated groups of validators are slashed. If this case occurs, an `EigenPod` is essentially bricked - the pod owner should NOT send ETH to the pod, and should NOT point additional validators at the pod. + +These are all expected edge cases and their occurances and side effects are within acceptable tolerances. + +## Upper Bound on Deposit Scaling Factor $k_n$ + +Let's examine potential overflow situations with respect to calculating a staker's withdrawable shares. +Below is the function in `SlashingLib.sol` which calculates $a_n = s_nk_nl_nm_n$. \ +Note: `slashingFactor` = $l_nm_n$ + +```solidity +function calcWithdrawable( + DepositScalingFactor memory dsf, + uint256 depositShares, + uint256 slashingFactor +) internal pure returns (uint256) { + /// forgefmt: disable-next-item + return depositShares + .mulWad(dsf.scalingFactor()) + .mulWad(slashingFactor); +} +``` + +`depositShares` are the staker’s shares $s_n$ in storage. We know this can at max be 1e38 - 1 as this is the max total shares we allow in a strategy. $l_n ≤ 1e18$ and $m_n ≤ 1e18$ as they are montonically decreasing values. So a `mulWad` of the `slashingFactor` operation should never result in a overflow, it will always result in a smaller or equal number. + +The question now comes to `depositShares.mulWad(dsf.scalingFactor())` and whether this term will overflow a `uint256`. Let's examine the math behind this. The function `SlashingLib.update` performs the following calculation: + +$$ +k_{n+1} =\frac{s_n k_n l_n m_n + d_n}{(s_n+d_n)l_nm_n} +$$ + +Assuming: +- $k_0 = 1$ +- 0 < $l_0$ ≤ 1 and is monotonically decreasing but doesn’t reach 0 +- 0 < $m_0$ ≤ 1 and is monotonically decreasing but doesn’t reach 0 +- 0 ≤ $s_n, {s_{n+1}}$ ≤ 1e38 - 1 (`MAX_TOTAL_SHARES = 1e38 - 1` in StrategyBase.sol) +- 0 < $d_n$ ≤ 1e38 - 1 +- ${s_{n+1}}={s_n} + {d_n}$ + +Rewriting above we can get the following by factoring out the k and cancelling out some terms. + +$$ +k_{n+1} = k_n\frac{s_n}{s_n + d_n} + \frac{d_n}{(s_n+d_n)l_nm_n} +$$ + +The first term $\frac{s_n}{{{s_n} + {d_n}}}$ < 1 so when multiplied with $k_n$ will not contribute to the growth of ${k_{n+1}}$ if only considering this term. + +The second term $\frac{d_n}{({{s_n} + {d_n}}){l_n}{m_n}}$ however can make $k_n$ grow over time depending on how small ${l_n}{m_n}$ becomes and also how large $d_n$ is proportionally compared to $s_n$. We only care about the worst case scenario here so let’s assume the upper bound on the existing shares and new deposit amount by rounding the value up to 1. + +Now in practice, the smallest values ${l_n}$ and ${m_n}$ could equal to is 1/1e18. Substituting this in the above second term gives the following: + +$$ +\frac{d_n}{(s_n+d_n)l_nm_n} = \frac{d_n}{s_n+d_n}*1e18^2 +$$ + +So lets round up the first term $\frac{s_n}{{{s_n} + {d_n}}}$ to 1 and also $\frac{d_n}{{{s_n} + {d_n}}}$ in the second term to 1. We can simplify the recursive definition of k in this worst case scenario as the following. + +$$ +k_{n+1} = k_n\frac{s_n}{s_n + d_n} + \frac{d_n}{(s_n+d_n)l_nm_n} +$$ + +$$ +=> k_{n+1} = k_n+ \frac{d_n}{(s_n+d_n)l_nm_n} +$$ + +$$ +=> k_{n+1} = k_n + 1e36 +$$ + +Because of the max shares in storage for a strategy is 1e38 - 1 and deposits must be non-zero we can actually come up with an upper bound on ${k_n}$ by having 1e38-1 deposits of amount 1, updating ${k_n}$ each time. + +$$ +k_{1e38-1} \approx (1e38-1)\cdot 1e36 < 1e74 +$$ + +After 1e38-1 iterations/deposits, the upper bound on k we calculate is 1e74 in the _worst_ case scenario. This is technically possible if as a staker, you are delegated to an operator for the beaconChainStrategy where your operator has been slashed 99.9999999…% for native ETH but also as a staker you have had proportional EigenPod balance decreases up to 99.9999999…..%. + +The max shares of 1e38-1 also accommodates the entire supply of ETH as well (only needs 27 bits). For normal StrategyManager strategies, ${l_n} = 1$ and ${k_n}$ would not grow nearly to the same extent. + +Clearly this value of 1e74 for ${k_n}$ fits within a uint256 storage slot. + +Bringing this all back to the `calcWithdrawable` method used to calculate your actual withdrawable shares for a staker as well as the actual next ${k_{n+1}}$ value. We can see here that the shares is not expected to overflow given the constraints on all our variables and the use of the depositScalingFactor is safe. + + +The staker depositScalingFactor is unbounded on how it can increase over time but because of the lower bounds we have ${l_n}$ and ${m_n}$ as well as the upper bound on number of shares a strategy has (or amount of ETH in existence w.r.t beaconChainStrategy) we can see that it is infeasble for the deposit scaling factor $k_n$ to overflow in our contracts. + + + +## Rounding Behavior Considerations + +The `SlashingLib.sol` introduces some small rounding precision errors due to the usage of `mulWad`/`divWad` operations in the contracts where we are doing a `x * y / denominator` operation. In Solidity, we round down to the nearest integer introducing an absolute error of up to 1 wei. Taking this into consideration, in certain portions of code, we will explicitly use either take the floor or ceiling value of `x * y / denominator`. + +### Rounding up on Slashing + +When an operator is slashed by an operatorSet in the `AllocationManager`, we actually want to round up on slashing. Rather than calculating `floor(x * y / denominator)` from mulDiv, we want `ceiling(x * y / denominator)`. This is because we don’t want any kind of DOS scenario where an operatorSet attempting to slash an operator is rounded to 0; potentially possible if an operator registered for their own fake AVS and slashed themselves repeatedly to bring their maxMagnitude to a small enough value. This will ensure an operator is always slashed for some amount from their maxMagnitude which eventually, if they are slashed enough, can reach 0. + +`AllocationManager.slashOperator` +```solidity +// 3. Calculate the amount of magnitude being slashed, and subtract from +// the operator's currently-allocated magnitude, as well as the strategy's +// max and encumbered magnitudes +uint64 slashedMagnitude = uint64(uint256(allocation.currentMagnitude).mulWadRoundUp(params.wadsToSlash[i])); +``` + +### Deposits actually _reducing_ withdrawableShares + +There are some very particular edge cases where, due to rounding error, deposits can actually decrease withdrawble shares for a staker which is conceptually wrong. +The unit test `DelegationUnit.t.sol:test_increaseDelegatedShares_depositRepeatedly` exemplifies this where there is an increasing difference over the course of multiple deposits between a staker's withdrawable shares and the staker's delegated operator shares. +Essentially, what’s happening in this test case is that after the very first deposit of a large amount of shares, subsequent deposits of amount 1000 are causing the getWithdrawable shares to actually decrease for the staker. + +Since the operatorShares are simply incrementing by the exact depositShares, the operatorShares mapping is increasing as expected. This ends up creating a very big discrepancy/drift between the two values after performing 1000 deposits. The difference between the operatorShares and the staker’s withdrawableShares ends up being `4.418e13`. + +Granted the initial deposit amount was `4.418e28` which is magnitudes larger than the discrepancy here but this its important to note the side effects of the redesigned accounting model. +Instead of purely incremented/decremented amounts, we have introduced magnitudes and scaling factor variables which now result in small amounts of rounding error from division in several places. We deem this rounding behavior to be tolerable given the costs associated for the number of transactions to emulate this and the proportional error is very small. + +## Upper bound on Residual Operator Shares + +Related to the above rounding error on deposits, we want to calculate what is the worst case rounding error for a staker depositing shares into EigenLayer. +That is, what is the largest difference between the depositShares deposited and the resulting withdrawableShares? For a staker who initially deposits without getting slashed, these two values should conceptually be equal. Let's examine below. + +Below is a code snippet of `SlashingLib.sol` +```solidity +function update( + DepositScalingFactor storage dsf, + uint256 prevDepositShares, + uint256 addedShares, + uint256 slashingFactor +) internal { + // If this is the staker's first deposit, set the scaling factor to + // the inverse of slashingFactor + if (prevDepositShares == 0) { + dsf._scalingFactor = uint256(WAD).divWad(slashingFactor); + return; + } + +... + +function calcWithdrawable( + DepositScalingFactor memory dsf, + uint256 depositShares, + uint256 slashingFactor +) internal pure returns (uint256) { + /// forgefmt: disable-next-item + return depositShares + .mulWad(dsf.scalingFactor()) + .mulWad(slashingFactor); +} +``` + +Mathematically, withdrawable shares can be represented as below + +$$ +withdrawableShares = d\space\cdot\space \frac{k}{WAD} \space\cdot\space \frac{slashingFactor}{WAD} +$$ + +Substituting $k$ with `WAD.divWad(slashingFactor)` (see update function above) if the staker only has done one single deposit of amount $d$. Also expanding out slashingFactor which is `maxMagnitude.mulWad(beaconChainScalingFactor)` + +$$ += d\space\cdot\space \frac{\frac{WAD\space\cdot \space WAD}{m_{deposit} \cdot l_{deposit}}}{WAD} \space\cdot\space \frac{\frac{m \space\cdot\space l}{WAD}}{WAD} +$$ + +Above is the real true value of the amount of withdrawable shares a staker has but in practice, there are rounding implications at each division operation. It becomes the following + +$$ +withdrawableShares (rounded) = +\lfloor +\lfloor +d \space\cdot\space +\frac{\lfloor\frac{WAD\space\cdot \space WAD +}{m_{deposit} \space\cdot\space l_{deposit}} +\rfloor }{WAD} +\rfloor +\space\cdot\space \frac{\lfloor \frac{m \space\cdot\space l}{WAD}\rfloor}{WAD} +\rfloor +$$ + +Each floor operation can introduce a rounding error of at most 1 wei. Because there are nested divisions however, this error can result in a total error thats larger than just off by 1 wei. +We can rewrite parts of above with epsilon $e$ which is in the range of [0,1]. + +1. First inner rounded term + +$$ +\frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit}} = \lfloor \frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit}} \rfloor + \epsilon_1 +$$ + +$$ +\frac{\lfloor \frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit}} \rfloor}{WAD} = \frac{\frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit}} - \epsilon_1}{WAD} +$$ + +2. Second rounded term + +$$ +\lfloor d \cdot \frac{\lfloor \frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit}} \rfloor}{WAD} \rfloor +$$ + +$$ += \lfloor d \cdot \frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} \rfloor +$$ + +$$ += d \cdot \frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} - \epsilon_2 +$$ + +3. Third rounded term + +$$ +\lfloor \frac{m \cdot l}{WAD} \rfloor = \frac{m \cdot l}{WAD} - \epsilon_3 +$$ + +$$ +=> +\frac{\lfloor \frac{m \cdot l}{WAD} \rfloor}{WAD} = \frac{\frac{m \cdot l}{WAD} - \epsilon_3}{WAD} +$$ + +$$ +=> +\frac{\lfloor \frac{m \cdot l}{WAD} \rfloor}{WAD} = \frac{m \cdot l}{WAD^2} - \frac{\epsilon_3}{WAD} +$$ + +4. Now bringing it all back to the original equation + +$$ +withdrawableShares (rounded) = +\lfloor +\lfloor +d \space\cdot\space +\frac{\lfloor\frac{WAD\space\cdot \space WAD +}{m_{deposit} \space\cdot\space l_{deposit}} +\rfloor }{WAD} +\rfloor +\space\cdot\space \frac{\lfloor \frac{m \space\cdot\space l}{WAD}\rfloor}{WAD} +\rfloor +$$ + +$$ += \lfloor\left(d \cdot \frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} - \epsilon_2\right)\cdot\left(\frac{m \cdot l}{WAD^2} - \frac{\epsilon_3}{WAD}\right)\rfloor +$$ + +$$ += \left( +d \cdot \frac{WAD \cdot WAD}{m_{deposit} \cdot l_{deposit} \cdot WAD} - d \cdot \frac{\epsilon_1}{WAD} - \epsilon_2 +\right) +\cdot +\left( +\frac{m \cdot l}{WAD^2} - \frac{\epsilon_3}{WAD} +\right) - \epsilon_4 +$$ + +After expansion and some simplification + +$$ +withdrawableShares (rounded) = +d \cdot \frac{m\cdot l}{m_{deposit} \cdot l_{deposit}\cdot WAD} - d \cdot \frac{\epsilon_1 \cdot m \cdot l}{WAD^3} - \frac{\epsilon_2 \cdot m \cdot l}{WAD^2} - d \cdot \frac{\epsilon_3}{m_{deposit} \cdot l_{deposit} } + \text{(higher-order terms)} +$$ + +Note that (higher-order terms) are the terms with multiple epsilon terms where the amounts become negligible, because each term $e$ is < 1. + +The true value term is the following: + +$$ +withdrawableShares = d\space\cdot\space \frac{\frac{WAD \space\cdot\space WAD}{m_{deposit} \cdot l_{deposit}}}{WAD} \space\cdot\space \frac{\frac{m \space\cdot\space l}{WAD}}{WAD} +$$ + +$$ += d\space\cdot\space \frac{WAD }{m_{deposit} \cdot l_{deposit}}\space\cdot\space \frac{m \space\cdot\space l}{WAD^2} +$$ + +$$ +d \cdot \frac{m\cdot l}{m_{deposit } \cdot l_{deposit}\cdot WAD} +$$ + +But we can see this term show in the withdrawableShares(rounded) above in the first term! Then we can see that we can represent the equations as the following. + +$$ +withdrawableShares (rounded) = +withdrawableShares - d \cdot \frac{\epsilon_1 \cdot m \cdot l}{WAD^3} - \frac{\epsilon_2 \cdot m \cdot l}{WAD^2} - d \cdot \frac{\epsilon_3 }{m_{deposit} \cdot l_{deposit} } + \text{(higher-order terms)} +$$ + +This intuitively makes sense as all the rounding error comes from the epsilon terms and how they propagate out from being nested. Therefore the introduced error from rounding are all the rounding terms added up ignoring the higher-order terms. + +$$ +roundedError =d \cdot \frac{\epsilon_1 \cdot m \cdot l}{WAD^3} + \frac{\epsilon_2 \cdot m \cdot l}{WAD^2} + d \cdot \frac{\epsilon_3 }{m_{\text{deposit}} \cdot l_{deposit} } +$$ + +Now lets assume the worst case scenario of maximizing this sum above, if each epsilon $e$ is replaced with the value of 1 due to a full wei being rounded off we can get the following. + +$$ +d \cdot \frac{m \cdot l}{WAD^3} + \frac{ m \cdot l}{WAD^2} + \frac{ d}{m_{\text{deposit}} \cdot l_{deposit}} +$$ + +Assuming close to max values that results in rounding behaviour, we can maximize this total sum by having $d = 1e38$ , $m, m_{deposit}, l, l_{deposit}$ equal to WAD(1e18) then we get the following: + +$$ +\frac{1e38\cdot WAD^2}{WAD^3} + \frac{ WAD^2}{WAD^2} + \frac{1e38}{1e36} +$$ + +$$ +=> \frac{1e38}{1e18} + 1 + 100 +$$ + +$$ +\approx 1e20 +$$ + +Framed in another way, the amount of loss a staker can have is $\frac{1}{1e18}$ th of the deposit amount. This makes sense as a result of having nested flooring operations that are then multiplied against outer terms. +Over time, as stakers deposit and withdraw, they may not receive as many shares as their “real” withdrawable amount as this is rounded down and there could be residual/dust shares amount in the delegated operatorShares mapping AND in the original Strategy contract. +This is known and we specifically round down to avoid underflow of operatorShares if all their delegated stakers were to withdraw. \ No newline at end of file diff --git a/docs/images/slashing-model.png b/docs/images/slashing-model.png new file mode 100644 index 000000000..b5c4b89ee Binary files /dev/null and b/docs/images/slashing-model.png differ