Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: delegate shares #1045

Open
wants to merge 45 commits into
base: slashing-magnitudes-fixes
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f893aeb
fix: changing accounting math when delegating beacon chain ETH
Jan 22, 2025
9c22d31
fix: changing to special-case approach
Jan 23, 2025
eb011b6
docs: removing logs
Jan 23, 2025
aaa92c8
docs: removing more logs
Jan 23, 2025
e543dd1
refactor: remove getDelegatableShares
Jan 23, 2025
d2f5fe0
docs: shares upon delegation
8sunyuan Jan 24, 2025
a4b8281
chore: fix md view
8sunyuan Jan 24, 2025
3bac447
Merge branch 'slashing-magnitudes-fixes' into delegate-shares-fix
eigenmikem Jan 28, 2025
7f7bd79
refactor: move logic into increaseDelegation and slashingLib
Jan 28, 2025
769f6d7
style: undoing formatting changes
Jan 28, 2025
811c240
chore: lint
Jan 28, 2025
b584ee4
chore: lint
Jan 28, 2025
71e498b
Merge branch 'beacon-delegation-accounting' into delegate-shares-fix
Jan 29, 2025
ef50a8b
test: base scenario integration test
Jan 29, 2025
2b46b93
test: integration tests
Jan 30, 2025
5c6f931
docs: natspec comment for dsf.updateNewDelegation
Jan 30, 2025
9eb35aa
Merge branch 'slashing-magnitudes-fixes' into delegate-shares-fix
eigenmikem Jan 30, 2025
2e5d79d
refactor: use view function to decrease contract size
Jan 30, 2025
0b556de
chore: update allocation refs
Jan 30, 2025
5b8703f
Merge branch 'slashing-magnitudes-fixes' into delegate-shares-fix
eigenmikem Jan 30, 2025
d467531
Merge branch 'slashing-magnitudes-fixes' into delegate-shares-fix
eigenmikem Jan 31, 2025
832153e
chore: cleanup new delegation branching
wadealexc Jan 31, 2025
d6837d5
refactor: explicitly reset dsf for simplicity
Jan 31, 2025
57883c1
chore: lint
Jan 31, 2025
73df934
refactor: removing unnecessary logic after explicit dsf reset
Feb 3, 2025
6b87670
chore: lint
Feb 3, 2025
19952dc
refactor: move reset dsf block
Feb 3, 2025
a465514
feat: clarify edge casing in delegate
wadealexc Feb 3, 2025
8ec0162
test: remove all shares integration test
Feb 3, 2025
4f6ba21
docs: comment explaining dsf rest
Feb 3, 2025
13b5170
style: dsf reset condensed to one line
Feb 3, 2025
55abe6a
Merge branch 'slashing-magnitudes-fixes' into delegate-shares-fix
eigenmikem Feb 3, 2025
1ce151a
Update SharesAccounting.md
eigenmikem Feb 3, 2025
9757137
docs: added explanation of dsf rest
eigenmikem Feb 3, 2025
4292393
chore: remove extra reset in undelegate
Feb 3, 2025
b24ad11
refactor: reset dsf to 0 for gas rebate
Feb 3, 2025
6eb8ab3
refactor: change removeDepositShares to return updated shares and use…
Feb 5, 2025
bfa0c7f
chore: integration prefix
Feb 5, 2025
c5d71cf
Merge branch 'slashing-magnitudes-fixes' into delegate-shares-fix
eigenmikem Feb 5, 2025
2fc9a4c
refactor: fixing tests and rounding issues
Feb 11, 2025
578b9f0
chore: remove logs
Feb 11, 2025
31e04bd
chore: updating queuewithdrawalsm2
Feb 12, 2025
768597f
chore: Revert "chore: updating queuewithdrawalsm2"
Feb 12, 2025
9a6b3de
fix: m2 user creation logic
ypatil12 Feb 12, 2025
6948470
test: randomize beacon chain deposit amount
Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/core/accounting/SharesAccounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,67 @@ See implementation in:
* [`StrategyManager.depositIntoStrategy`](../../../src/contracts/core/StrategyManager.sol)
* [`EigenPodManager.recordBeaconChainETHBalanceUpdate`](../../../src/contracts/pods/EigenPodManager.sol)


---

### Delegation

Suppose we have an undelegated staker who decides to delegate to an operator.
We have the following properties that should be preserved.

#### Operator Level

Operator shares should be increased by the amount of delegatable shares the staker has, this is synonymous to their withdrawable shares $a_n$. Therefore,

$$
op_{n+1} = op_{n} + a_n
$$

$$
= op_{n} + s_n k_n l_n m_n
$$


#### Staker Level

withdrawable shares should remain unchanged

$$
a_{n+1} = a_n
$$

deposit shares should remain unchanged

$$
s_{n+1} = s_n
$$

beaconChainSlashingFactor and maxMagnitude should also remain unchanged. In this case, since the staker is not delegated, then their maxMagnitude should by default be equal to 1.

$$
l_{n+1} = l_n
$$

Now the question is what is the new depositScalingFactor equal to?

$$
a_{n+1} = a_n
$$

$$
=> s_{n+1} k_{n+1} l_{n+1} m_{n+1} = s_n k_n l_n m_n
$$

$$
=> s_{n} k_{n+1} l_{n} m_{n+1} = s_n k_n l_n m_n
$$

$$
=> k_{n+1} = \frac {k_n m_n} { m_{n+1} }
$$

Notice how the staker variables that update $k_{n+1}$ and $m_{n+1}$ do not affect previously queued withdrawals and shares received upon withdrawal completion. This is because the maxMagnitude that is looked up is dependent on the operator at the time of the queued withdrawal and the $k_n$ is effectively stored in the scaled shares field.

---

### Slashing
Expand Down
47 changes: 35 additions & 12 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ contract DelegationManager is
strategy: strategy,
prevDepositShares: prevDepositShares,
addedShares: addedShares,
slashingFactor: slashingFactor
slashingFactor: slashingFactor,
newDelegation: false
});
}

Expand Down Expand Up @@ -340,24 +341,38 @@ contract DelegationManager is
* 1) new delegations are not paused (PAUSED_NEW_DELEGATION)
*/
function _delegate(address staker, address operator) internal onlyWhenNotPaused(PAUSED_NEW_DELEGATION) {
// read staker's withdrawable shares and strategies to add to operator's shares
// also update the staker depositScalingFactor for each strategy
(IStrategy[] memory strategies,) = strategyManager.getDeposits(staker);
uint256 podOwnerShares = eigenPodManager.stakerDepositShares(staker, beaconChainETHStrategy);
if (podOwnerShares != 0) {
// Allocate extra space for beaconChainETHStrategy and shares
IStrategy[] memory newStrategies = new IStrategy[](strategies.length + 1);
newStrategies[strategies.length] = beaconChainETHStrategy;

// Copy any strategy manager shares to complete array
for (uint256 i = 0; i < strategies.length; i++) {
newStrategies[i] = strategies[i];
}
strategies = newStrategies;
}
eigenmikem marked this conversation as resolved.
Show resolved Hide resolved
(uint256[] memory withdrawableShares,) = getWithdrawableShares(staker, strategies);
eigenmikem marked this conversation as resolved.
Show resolved Hide resolved
//Ignoring beaconChainSlashingFactor here because Beacon Chain ETH DSF already reflects BCSF
uint64[] memory slashingFactors = allocationManager.getMaxMagnitudes(operator, strategies);
// record the delegation relation between the staker and operator, and emit an event
delegatedTo[staker] = operator;
emit StakerDelegated(staker, operator);

// read staker's deposited shares and strategies to add to operator's shares
// and also update the staker depositScalingFactor for each strategy
(IStrategy[] memory strategies, uint256[] memory depositedShares) = getDepositedShares(staker);
uint256[] memory slashingFactors = _getSlashingFactors(staker, operator, strategies);

for (uint256 i = 0; i < strategies.length; ++i) {
// forgefmt: disable-next-item
_increaseDelegation({
operator: operator,
staker: staker,
strategy: strategies[i],
prevDepositShares: uint256(0),
addedShares: depositedShares[i],
slashingFactor: slashingFactors[i]
addedShares: withdrawableShares[i],
slashingFactor: slashingFactors[i],
newDelegation: true
});
}
}
Expand Down Expand Up @@ -592,7 +607,8 @@ contract DelegationManager is
strategy: withdrawal.strategies[i],
prevDepositShares: prevDepositShares,
addedShares: addedShares,
slashingFactor: newSlashingFactors[i]
slashingFactor: newSlashingFactors[i],
newDelegation: false
});
}
}
Expand All @@ -607,24 +623,31 @@ contract DelegationManager is
* @param prevDepositShares The number of delegated deposit shares the staker had in the strategy prior to the increase
* @param addedShares The shares added to the staker in the StrategyManager/EigenPodManager
* @param slashingFactor The current slashing factor for the staker/operator/strategy
* @param newDelegation Boolean flag which signifies whether this is a new delegation
*/
function _increaseDelegation(
address operator,
address staker,
IStrategy strategy,
uint256 prevDepositShares,
uint256 addedShares,
uint256 slashingFactor
uint256 slashingFactor,
bool newDelegation
) internal {
// Ensure that the operator has not been fully slashed for a strategy
// and that the staker has not been fully slashed if it is the beaconChainStrategy
// This is to prevent a divWad by 0 when updating the depositScalingFactor
require(slashingFactor != 0, FullySlashed());

// Update the staker's depositScalingFactor. This only results in an update
// if the slashing factor has changed for this strategy.
// if the slashing factor has changed for this strategy. Dsf update formula
// is different in the on delegation case.
DepositScalingFactor storage dsf = _depositScalingFactor[staker][strategy];
dsf.update(prevDepositShares, addedShares, slashingFactor);
if (newDelegation) {
dsf.updateNewDelegation(slashingFactor);
} else {
dsf.update(prevDepositShares, addedShares, slashingFactor);
}
emit DepositScalingFactorUpdated(staker, strategy, dsf.scalingFactor());

// If the staker is delegated to an operator, update the operator's shares
Expand Down
9 changes: 9 additions & 0 deletions src/contracts/libraries/SlashingLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ library SlashingLib {
dsf._scalingFactor = newDepositScalingFactor;
}

/**
* @notice In the case of a new delegation, slashingFactor is just maxMagnitude, because the
* BCSF is accounted for in _delegate by adjusting the shares added to the operator
*/
function updateNewDelegation(DepositScalingFactor storage dsf, uint256 slashingFactor) internal {
//On delegation, we use the old deposit scaling factor in case that it is non-WAD
dsf._scalingFactor = dsf.scalingFactor().divWad(slashingFactor);
eigenmikem marked this conversation as resolved.
Show resolved Hide resolved
}

// CONVERSION

function calcWithdrawable(
Expand Down
18 changes: 17 additions & 1 deletion src/test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ abstract contract IntegrationBase is IntegrationDeployer {

return result;
}

/// @dev Choose a random subset of validators (selects AT LEAST ONE but NOT ALL)
function _chooseSubset(uint40[] memory validators) internal returns (uint40[] memory) {
require(validators.length >= 2, "Need at least 2 validators to choose subset");

uint40[] memory result = new uint40[](validators.length);
uint newLen;

uint rand = _randUint({ min: 1, max: validators.length ** 2 });
for (uint i = 0; i < validators.length; i++) {
if (rand >> i & 1 == 1) {
result[newLen] = validators[i];
newLen++;
}
}
}

function _getTokenName(IERC20 token) internal view returns (string memory) {
if (token == NATIVE_ETH) {
Expand Down Expand Up @@ -697,7 +713,7 @@ abstract contract IntegrationBase is IntegrationDeployer {
}
}

/// @dev Check that the staker's withdrawable shares have decreased by `removedShares`
/// @dev Check that the staker's withdrawable shares have increased by `addedShares`
function assert_Snap_Added_Staker_WithdrawableShares(
User staker,
IStrategy[] memory strategies,
Expand Down
53 changes: 52 additions & 1 deletion src/test/integration/IntegrationChecks.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ contract IntegrationCheckUtils is IntegrationBase {
) internal {
/// Undelegate from an operator
//
// ... check that the staker is undelegated, all strategies from which the staker is deposited are unqeuued,
// ... check that the staker is undelegated, all strategies from which the staker is deposited are unqueued,
// that the returned root matches the hashes for each strategy and share amounts, and that the staker
// and operator have reduced shares
assertFalse(delegationManager.isDelegated(address(staker)),
Expand All @@ -249,6 +249,35 @@ contract IntegrationCheckUtils is IntegrationBase {
"check_QueuedWithdrawal_State: failed to remove staker withdrawable shares");
}

function check_Redelegate_State(
User staker,
User operator,
IDelegationManagerTypes.Withdrawal[] memory withdrawals,
bytes32[] memory withdrawalRoots,
IStrategy[] memory strategies,
uint[] memory shares
) internal {
/// Redelegate to a new operator
//
// ... check that the staker is delegated to new operator, all strategies from which the staker is deposited are unqueued,
// that the returned root matches the hashes for each strategy and share amounts, and that the staker
// and operator have reduced shares
assertTrue(delegationManager.isDelegated(address(staker)),
"check_Undelegate_State: staker should not be delegated");
assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots,
"check_Undelegate_State: calculated withdrawl should match returned root");
assert_AllWithdrawalsPending(withdrawalRoots,
"check_Undelegate_State: stakers withdrawal should now be pending");
assert_Snap_Added_QueuedWithdrawals(staker, withdrawals,
"check_Undelegate_State: staker should have increased nonce by withdrawals.length");
assert_Snap_Removed_OperatorShares(operator, strategies, shares,
"check_Undelegate_State: failed to remove operator shares");
assert_Snap_Removed_Staker_DepositShares(staker, strategies, shares,
"check_Undelegate_State: failed to remove staker shares");
assert_Snap_Removed_Staker_WithdrawableShares(staker, strategies, shares,
"check_QueuedWithdrawal_State: failed to remove staker withdrawable shares");
}

/**
* @notice Overloaded function to check the state after a withdrawal as tokens, accepting a non-user type for the operator.
* @param staker The staker who completed the withdrawal.
Expand Down Expand Up @@ -328,6 +357,28 @@ contract IntegrationCheckUtils is IntegrationBase {
assert_Snap_Unchanged_StrategyShares(strategies, "strategies should have total shares unchanged");
}

function check_Withdrawal_AsShares_Redelegated_State(
User staker,
User operator,
User newOperator,
IDelegationManagerTypes.Withdrawal memory withdrawal,
IStrategy[] memory strategies,
uint[] memory shares
) internal {
/// Complete withdrawal(s):
// The staker will complete the withdrawal as shares
//
// ... check that the withdrawal is not pending, that the token balances of the staker and operator are unchanged,
// that the withdrawer received the expected shares, and that that the total shares of each o
// strategy withdrawn remains unchanged
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances");
assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances");
assert_Snap_Added_Staker_DepositShares(staker, strategies, shares, "staker should have received expected shares");
assert_Snap_Unchanged_OperatorShares(operator, "operator should have shares unchanged");
assert_Snap_Unchanged_StrategyShares(strategies, "strategies should have total shares unchanged");
}

/*******************************************************************************
ALLOCATION MANAGER CHECKS
*******************************************************************************/
Expand Down
Loading
Loading