Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/certora-prover.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable
- name: Install forge dependencies
run: forge install
- name: Install python
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable
- name: Run coverage
run: forge coverage --report lcov
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable
- name: Run forge install
run: forge install
- name: Start anvil and deploy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable
- name: Run forge fmt
run: |
forge fmt --check src/contracts
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/storage-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable

- name: "Generate and prepare the storage reports for current branch"
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/testinparallel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable

- name: Run Forge build
run: |
Expand Down
10 changes: 8 additions & 2 deletions src/contracts/pods/EigenPodManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,14 @@ contract EigenPodManager is
if (updatedDepositShares <= 0) {
return (0, 0);
}

return (prevDepositShares < 0 ? 0 : uint256(prevDepositShares), shares);
// If we have gone from negative to positive shares, return (0, positive delta)
else if (prevDepositShares < 0) {
return (0, uint256(updatedDepositShares));
}
// Else, return true previous shares and added shares
else {
return (uint256(prevDepositShares), shares);
}
}

/// @dev Calculates the proportion a pod owner's restaked balance has decreased, and
Expand Down
8 changes: 7 additions & 1 deletion src/test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2017,7 +2017,13 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
}

function _calcNativeETHOperatorShareDelta(User staker, int shareDelta) internal view returns (int) {
int curPodOwnerShares = eigenPodManager.podOwnerDepositShares(address(staker));
// TODO: Maybe we update parent method to have an M2 and Slashing version?
int curPodOwnerShares;
if (!isUpgraded) {
curPodOwnerShares = IEigenPodManager_DeprecatedM2(address(eigenPodManager)).podOwnerShares(address(staker));
} else {
curPodOwnerShares = eigenPodManager.podOwnerDepositShares(address(staker));
}
int newPodOwnerShares = curPodOwnerShares + shareDelta;

if (curPodOwnerShares <= 0) {
Expand Down
133 changes: 133 additions & 0 deletions src/test/integration/tests/upgrade/EigenPod_Negative_Shares.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;

import "src/test/integration/UpgradeTest.t.sol";

contract Integration_Upgrade_EigenPod_Negative_Shares is UpgradeTest {
function _init() internal override {
_configAssetTypes(HOLDS_ETH);
_configUserTypes(DEFAULT);
}

function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsShares(
uint24 _rand
) public rand(_rand) {
/// 0. Create an operator and staker with some underlying assets
(User staker, IStrategy[] memory strategies, uint256[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint256[] memory shares = _calculateExpectedShares(strategies, tokenBalances);

/// 1. Deposit into strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);

/// 2. Delegate to operator
staker.delegateTo(operator);

/// 3. Queue a withdrawal for all shares
IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
IDelegationManagerTypes.Withdrawal memory withdrawal = withdrawals[0];

/// 4. Update balance randomly (can be positive or negative)
(int256[] memory tokenDeltas, int256[] memory balanceUpdateShareDelta,) = _randBalanceUpdate(staker, strategies);
staker.updateBalances(strategies, tokenDeltas);

/// 5. Upgrade contracts
_upgradeEigenLayerContracts();

/// 6. Complete the withdrawal as shares
_rollBlocksForCompleteWithdrawals(withdrawals);
staker.completeWithdrawalAsShares(withdrawal);

// Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check
(uint256[] memory expectedOperatorShareDelta, int256[] memory expectedStakerShareDelta) =
_getPostWithdrawalExpectedShareDeltas(balanceUpdateShareDelta[0], withdrawal);
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_Added_OperatorShares(operator, withdrawal.strategies, expectedOperatorShareDelta, "operator should have received shares");
assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares");
}

function testFuzz_deposit_delegate_updateBalance_upgrade_completeAsTokens(
uint24 _rand
) public rand(_rand) {
/// 0. Create an operator and staker with some underlying assets
(User staker, IStrategy[] memory strategies, uint256[] memory tokenBalances) = _newRandomStaker();
(User operator,,) = _newRandomOperator();
uint256[] memory shares = _calculateExpectedShares(strategies, tokenBalances);

/// 1. Deposit into strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);

/// 2. Delegate to operator
staker.delegateTo(operator);

/// 3. Queue a withdrawal for all shares
IDelegationManagerTypes.Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
IDelegationManagerTypes.Withdrawal memory withdrawal = withdrawals[0];

/// 4. Update balance randomly (can be positive or negative)
(int256[] memory tokenDeltas, int256[] memory balanceUpdateShareDelta,) = _randBalanceUpdate(staker, strategies);
staker.updateBalances(strategies, tokenDeltas);

/// 5. Upgrade contracts
_upgradeEigenLayerContracts();

/// 6. Complete the withdrawal as shares
_rollBlocksForCompleteWithdrawals(withdrawals);
IERC20[] memory tokens = staker.completeWithdrawalAsTokens(withdrawal);
uint256[] memory expectedTokens = _getPostWithdrawalExpectedTokenDeltas(balanceUpdateShareDelta[0], withdrawal);

// Manually complete checks since we could still negative shares prior to the upgrade, causing a revert in the share check
assert_WithdrawalNotPending(delegationManager.calculateWithdrawalRoot(withdrawal), "staker withdrawal should no longer be pending");
assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens");
assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed");

// If we had a positive balance update, then the staker shares should not have changed
if (balanceUpdateShareDelta[0] > 0) {
assert_Snap_Unchanged_Staker_DepositShares(staker, "staker shares should not have changed");
}
// Else, the staker shares should have increased by the magnitude of the negative share delta
else {
int256[] memory expectedStakerShareDelta = new int256[](1);
expectedStakerShareDelta[0] = -balanceUpdateShareDelta[0];
assert_Snap_Delta_StakerShares(staker, strategies, expectedStakerShareDelta, "staker should have received expected shares");
}
}



function _getPostWithdrawalExpectedShareDeltas(
int256 balanceUpdateShareDelta,
IDelegationManagerTypes.Withdrawal memory withdrawal
) internal pure returns (uint256[] memory, int256[] memory) {
uint256[] memory operatorShareDelta = new uint256[](1);
int256[] memory stakerShareDelta = new int256[](1);
// The staker share delta is the withdrawal scaled shares since it can go from negative to positive
stakerShareDelta[0] = int256(withdrawal.scaledShares[0]);

if (balanceUpdateShareDelta > 0) {
// If balanceUpdateShareDelta is positive, then the operator delta is the withdrawal scaled shares
operatorShareDelta[0] = withdrawal.scaledShares[0];
} else {
// Operator shares never went negative, so we can just add the withdrawal scaled shares and the negative share delta
operatorShareDelta[0] = uint256(int256(withdrawal.scaledShares[0]) + balanceUpdateShareDelta);
}

return (operatorShareDelta, stakerShareDelta);
}

function _getPostWithdrawalExpectedTokenDeltas(
int256 balanceUpdateShareDelta,
IDelegationManagerTypes.Withdrawal memory withdrawal
) internal pure returns (uint256[] memory) {
uint256[] memory expectedTokenDeltas = new uint256[](1);
if (balanceUpdateShareDelta > 0) {
// If we had a positive balance update, then the expected token delta is the withdrawal scaled shares
expectedTokenDeltas[0] = withdrawal.scaledShares[0];
} else {
// If we had a negative balance update, then the expected token delta is the withdrawal scaled shares plus the negative share delta
expectedTokenDeltas[0] = uint256(int256(withdrawal.scaledShares[0]) + balanceUpdateShareDelta);
}
return expectedTokenDeltas;
}
}
6 changes: 3 additions & 3 deletions src/test/integration/users/User.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,9 @@ contract User is Logger, IDelegationManagerTypes, IAllocationManagerTypes {
if (strat == BEACONCHAIN_ETH_STRAT) {
tokens[i] = NATIVE_ETH;

// If we're withdrawing native ETH as tokens, stop ALL validators
// and complete a checkpoint
if (receiveAsTokens) {
// If we're withdrawing native ETH as tokens && do not have negative shares
// stop ALL validators and complete a checkpoint
if (receiveAsTokens && eigenPodManager.podOwnerDepositShares(address(this)) >= 0) {
console.log("- exiting all validators and completing checkpoint");
_exitValidators(getActiveValidators());

Expand Down
2 changes: 1 addition & 1 deletion src/test/integration/users/User_M2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ contract User_M2 is User {
// If any balance update has occured, a checkpoint will pick it up
_startCheckpoint();
if (pod.activeValidatorCount() != 0) {
_completeCheckpoint();
_completeCheckpoint_M2();
}
} else {
uint256 tokens = uint256(delta);
Expand Down
64 changes: 64 additions & 0 deletions src/test/unit/EigenPodManagerUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,22 @@ contract EigenPodManagerUnitTests_StakeTests is EigenPodManagerUnitTests {

contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests {

// Wrapper contract that exposes the internal `_calculateChangeInDelegatableShares` function
EigenPodManagerWrapper public eigenPodManagerWrapper;

function setUp() virtual override public {
super.setUp();

// Upgrade eigenPodManager to wrapper
eigenPodManagerWrapper = new EigenPodManagerWrapper(
ethPOSMock,
eigenPodBeacon,
IDelegationManager(address(delegationManagerMock)),
pauserRegistry
);
eigenLayerProxyAdmin.upgrade(ITransparentUpgradeableProxy(payable(address(eigenPodManager))), address(eigenPodManagerWrapper));
}

/*******************************************************************************
Add Shares Tests
*******************************************************************************/
Expand Down Expand Up @@ -223,6 +239,54 @@ contract EigenPodManagerUnitTests_ShareUpdateTests is EigenPodManagerUnitTests {
assertEq(eigenPodManager.podOwnerDepositShares(defaultStaker), int256(shares), "Incorrect number of shares added");
}

function test_addShares_negativeInitial() public {
_initializePodWithShares(defaultStaker, -1);

cheats.prank(address(delegationManagerMock));

(uint256 prevDepositShares, uint256 addedShares) = eigenPodManager.addShares(
defaultStaker,
beaconChainETHStrategy,
5
);

assertEq(prevDepositShares, 0);
assertEq(addedShares, 4);
}

function testFuzz_addShares_negativeSharesInitial(int256 sharesToStart, int256 sharesToAdd) public {
cheats.assume(sharesToStart < 0);
cheats.assume(sharesToAdd >= 0);

_initializePodWithShares(defaultStaker, sharesToStart);
int256 expectedDepositShares = sharesToStart + sharesToAdd;

cheats.prank(address(delegationManagerMock));

cheats.expectEmit(true, true, true, true, address(eigenPodManager));
emit PodSharesUpdated(defaultStaker, sharesToAdd);
cheats.expectEmit(true, true, true, true, address(eigenPodManager));
emit NewTotalShares(defaultStaker, expectedDepositShares);

(uint256 prevDepositShares, uint256 addedShares) = eigenPodManager.addShares(
defaultStaker,
beaconChainETHStrategy,
uint256(sharesToAdd)
);

// validate that prev shares return 0 since we started from a negative balance
assertEq(prevDepositShares, 0);

// If we now have positive shares, expect return
if (expectedDepositShares > 0) {
assertEq(addedShares, uint256(expectedDepositShares));
}
// We still have negative shares, return 0
else {
assertEq(addedShares, 0);
}
}

/*******************************************************************************
Remove Shares Tests
******************************************************************************/
Expand Down
Loading