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
6 changes: 3 additions & 3 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ jobs:
- name: Run ${{ matrix.suite }} tests
run: |
case "${{ matrix.suite }}" in
Unit) forge test --no-match-contract Integration ;;
Integration) forge test --match-contract Integration ;;
Fork) forge test --match-contract Integration ;;
Unit) forge test --no-match-contract Integration -vvv ;;
Integration) forge test --match-contract Integration -vvv ;;
Fork) forge test --match-contract Integration -vvv ;;
esac
env:
FOUNDRY_PROFILE: ${{ matrix.suite == 'Fork' && 'forktest' || 'medium' }}
Expand Down
5 changes: 3 additions & 2 deletions src/contracts/core/StrategyManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ contract StrategyManager is

for (uint256 i = 0; i < keys.length; ++i) {
strategies[i] = IStrategy(keys[i]);
shares[i] = burnOrRedistributableShares.get(keys[i]);
(, shares[i]) = burnOrRedistributableShares.tryGet(keys[i]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a small bug present with the getBurnOrRedistributableShares getters. EnumerableMap.get() reverts if a key does not exist.

}

return (strategies, shares);
Expand All @@ -478,7 +478,8 @@ contract StrategyManager is
uint256 slashId,
IStrategy strategy
) external view returns (uint256) {
return _burnOrRedistributableShares[operatorSet.key()][slashId].get(address(strategy));
(, uint256 shares) = _burnOrRedistributableShares[operatorSet.key()][slashId].tryGet(address(strategy));
return shares;
}

/// @inheritdoc IStrategyManager
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/interfaces/ISlashEscrowFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ interface ISlashEscrowFactory is ISlashEscrowFactoryErrors, ISlashEscrowFactoryE
) external pure returns (bytes32);

/**
* @notice Returns whether a slash escrow is deployed or still counterfactual.
* @notice Returns whether a slash escrow is deployed or not.
* @param operatorSet The operator set whose slash escrow is being queried.
* @param slashId The slash ID of the slash escrow that is being queried.
* @return Whether the slash escrow is deployed.
Expand Down
110 changes: 97 additions & 13 deletions src/test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,16 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
view
{
for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
assert_HasUnderlyingTokenBalance(user, strategies[i], expectedBalances[i], err);
}
}

uint expectedBalance = expectedBalances[i];
uint tokenBalance;
if (strat == BEACONCHAIN_ETH_STRAT) tokenBalance = address(user).balance;
else tokenBalance = strat.underlyingToken().balanceOf(address(user));
function assert_HasUnderlyingTokenBalance(User user, IStrategy strategy, uint expectedBalance, string memory err) internal view {
uint tokenBalance;
if (strategy == BEACONCHAIN_ETH_STRAT) tokenBalance = address(user).balance;
else tokenBalance = strategy.underlyingToken().balanceOf(address(user));

assertApproxEqAbs(expectedBalance, tokenBalance, 1, err);
}
assertApproxEqAbs(expectedBalance, tokenBalance, 1, err);
}

function assert_HasNoUnderlyingTokenBalance(User user, IStrategy[] memory strategies, string memory err) internal view {
Expand Down Expand Up @@ -1408,17 +1409,38 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
OperatorSet memory operatorSet,
User operator,
SlashingParams memory params,
uint slashId,
string memory err
) internal {
uint[] memory curShares = _getOperatorShares(operator, params.strategies);
uint[] memory prevShares = _getPrevOperatorShares(operator, params.strategies);

for (uint i = 0; i < params.strategies.length; i++) {
uint curBurnable = _getBurnableShares(params.strategies[i]);
uint prevBurnable = _getPrevBurnableShares(params.strategies[i]);
uint curBurnable = _getBurnOrRedistributableShares(operatorSet, slashId, params.strategies[i]);
uint prevBurnable = _getPrevBurnOrRedistributableShares(operatorSet, slashId, params.strategies[i]);
uint slashedAtLeast = prevShares[i] - curShares[i];
// Not factoring in slashable shares in queue here, because that gets more complex (TODO)
assertTrue(curBurnable >= (prevBurnable + slashedAtLeast), err);

// TODO: Improve this check in the future, it's not very optimized.
// In the future, we can simply use a flag to communicate whether the operator set is redistributable.
if (curShares[i] == prevShares[i]) continue;
bool flag = false;
for (uint j = 0; j < params.strategies.length; j++) {
if (params.strategies[j] == BEACONCHAIN_ETH_STRAT) flag = true;
}
if (flag) continue;

assertTrue(_getIsPendingOperatorSet(operatorSet), "operator set should be pending");

assertTrue(_getIsPendingSlashId(operatorSet, slashId), "slash id should be pending");
assertFalse(_getPrevIsPendingSlashId(operatorSet, slashId), "slash id should not be pending");

assertEq(_getEscrowStartBlock(operatorSet, slashId), block.number, "escrow start block should be current block");
assertEq(_getPrevEscrowStartBlock(operatorSet, slashId), 0, "escrow start block should be 0");

assertTrue(_getIsDeployedSlashEscrow(operatorSet, slashId), "escrow should be deployed");
assertFalse(_getPrevIsDeployedSlashEscrow(operatorSet, slashId), "escrow should not be deployed");
}
}

Expand Down Expand Up @@ -2215,6 +2237,22 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
}
}

function _genSlashing_SingleStrategy(User operator, OperatorSet memory operatorSet, IStrategy strategy)
internal
returns (SlashingParams memory params)
{
params.operator = address(operator);
params.operatorSetId = operatorSet.id;
params.description = "_genSlashing_SingleStrategy";
params.strategies = strategy.toArray();
params.wadsToSlash = new uint[](params.strategies.length);

// slash 100%
for (uint i = 0; i < params.wadsToSlash.length; i++) {
params.wadsToSlash[i] = 1e18;
}
}

function _genSlashing_Custom(User operator, OperatorSet memory operatorSet, uint wadsToSlash)
internal
returns (SlashingParams memory params)
Expand Down Expand Up @@ -2533,6 +2571,10 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
cheats.roll(latest + 1);
}

function _rollBlocksForCompleteSlashEscrow() internal {
cheats.roll(block.number + INITIAL_GLOBAL_DELAY_BLOCKS + 1);
}

/// @dev Uses timewarp modifier to get the operator set strategy allocations at the last snapshot.
function _getPrevAllocations(User operator, OperatorSet memory operatorSet, IStrategy[] memory strategies)
internal
Expand Down Expand Up @@ -2709,12 +2751,54 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter {
return allocationManager.isMemberOfOperatorSet(address(operator), operatorSet);
}

function _getPrevBurnableShares(IStrategy strategy) internal timewarp returns (uint) {
return _getBurnableShares(strategy);
function _getPrevBurnOrRedistributableShares(OperatorSet memory operatorSet, uint slashId, IStrategy strategy)
internal
timewarp
returns (uint)
{
return _getBurnOrRedistributableShares(operatorSet, slashId, strategy);
}

function _getBurnOrRedistributableShares(OperatorSet memory operatorSet, uint slashId, IStrategy strategy)
internal
view
returns (uint)
{
return strategy == beaconChainETHStrategy
? eigenPodManager.burnableETHShares()
: strategyManager.getBurnOrRedistributableShares(operatorSet, slashId, strategy);
}

function _getPrevIsPendingOperatorSet(OperatorSet memory operatorSet) internal timewarp returns (bool) {
return _getIsPendingOperatorSet(operatorSet);
}

function _getIsPendingOperatorSet(OperatorSet memory operatorSet) internal view returns (bool) {
return slashEscrowFactory.isPendingOperatorSet(operatorSet);
}

function _getPrevIsPendingSlashId(OperatorSet memory operatorSet, uint slashId) internal timewarp returns (bool) {
return _getIsPendingSlashId(operatorSet, slashId);
}

function _getIsPendingSlashId(OperatorSet memory operatorSet, uint slashId) internal view returns (bool) {
return slashEscrowFactory.isPendingSlashId(operatorSet, slashId);
}

function _getPrevEscrowStartBlock(OperatorSet memory operatorSet, uint slashId) internal timewarp returns (uint) {
return _getEscrowStartBlock(operatorSet, slashId);
}

function _getEscrowStartBlock(OperatorSet memory operatorSet, uint slashId) internal view returns (uint) {
return slashEscrowFactory.getEscrowStartBlock(operatorSet, slashId);
}

function _getPrevIsDeployedSlashEscrow(OperatorSet memory operatorSet, uint slashId) internal timewarp returns (bool) {
return _getIsDeployedSlashEscrow(operatorSet, slashId);
}

function _getBurnableShares(IStrategy strategy) internal view returns (uint) {
return strategy == beaconChainETHStrategy ? eigenPodManager.burnableETHShares() : strategyManager.getBurnableShares(strategy);
function _getIsDeployedSlashEscrow(OperatorSet memory operatorSet, uint slashId) internal view returns (bool) {
return slashEscrowFactory.isDeployedSlashEscrow(operatorSet, slashId);
}

function _getPrevSlashableSharesInQueue(User operator, IStrategy[] memory strategies) internal timewarp returns (uint[] memory) {
Expand Down
47 changes: 41 additions & 6 deletions src/test/integration/IntegrationChecks.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,9 @@ contract IntegrationCheckUtils is IntegrationBase {
* ALM - SLASHING
*
*/
function check_Base_Slashing_State(User operator, AllocateParams memory allocateParams, SlashingParams memory slashParams) internal {
function check_Base_Slashing_State(User operator, AllocateParams memory allocateParams, SlashingParams memory slashParams, uint slashId)
internal
{
OperatorSet memory operatorSet = allocateParams.operatorSet;

check_MaxMag_Invariants(operator);
Expand All @@ -1065,7 +1067,7 @@ contract IntegrationCheckUtils is IntegrationBase {
assert_Snap_Slashed_SlashableStake(operator, operatorSet, slashParams, "slash should lower slashable stake");
assert_Snap_Slashed_OperatorShares(operator, slashParams, "slash should remove operator shares");
assert_Snap_Slashed_Allocation(operator, operatorSet, slashParams, "slash should reduce current magnitude");
// assert_Snap_Increased_BurnableShares(operatorSet, operator, slashParams, "slash should increase burnable shares");
assert_Snap_Increased_BurnableShares(operatorSet, operator, slashParams, slashId, "slash should increase burnable shares");

// Slashing SHOULD NOT change allocatable magnitude, registration, and slashability status
assert_Snap_Unchanged_AllocatableMagnitude(operator, allStrats, "slashing should not change allocatable magnitude");
Expand All @@ -1079,22 +1081,55 @@ contract IntegrationCheckUtils is IntegrationBase {
User operator,
AllocateParams memory allocateParams,
SlashingParams memory slashParams,
Withdrawal[] memory withdrawals
Withdrawal[] memory withdrawals,
uint slashId
) internal {
check_Base_Slashing_State(operator, allocateParams, slashParams);
check_Base_Slashing_State(operator, allocateParams, slashParams, slashId);
assert_Snap_Decreased_SlashableSharesInQueue(operator, slashParams, withdrawals, "slash should decrease slashable shares in queue");
}

/// Slashing invariants when the operator has been fully slashed for every strategy in the operator set
function check_FullySlashed_State(User operator, AllocateParams memory allocateParams, SlashingParams memory slashParams) internal {
check_Base_Slashing_State(operator, allocateParams, slashParams);
function check_FullySlashed_State(User operator, AllocateParams memory allocateParams, SlashingParams memory slashParams, uint slashId)
internal
{
check_Base_Slashing_State(operator, allocateParams, slashParams, slashId);

assert_Snap_Removed_AllocatedSet(operator, allocateParams.operatorSet, "should not have updated allocated sets");
assert_Snap_Removed_AllocatedStrats(
operator, allocateParams.operatorSet, slashParams.strategies, "should not have updated allocated strategies"
);
}

function check_releaseSlashEscrow_State(
OperatorSet memory operatorSet,
uint slashId,
IStrategy[] memory strategies,
uint[] memory initTokenBalances,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: naming, not init token balances

address redistributionRecipient
) internal {
assert_HasUnderlyingTokenBalances(
User(payable(redistributionRecipient)),
strategies,
initTokenBalances,
"redistribution recipient should have underlying token balances"
);

assertFalse(_getIsPendingSlashId(operatorSet, slashId), "slash id should not be pending");
assertEq(_getEscrowStartBlock(operatorSet, slashId), 0, "escrow start block should be deleted after");
assertTrue(_getIsDeployedSlashEscrow(operatorSet, slashId), "escrow should be deployed after");
}

function check_releaseSlashEscrow_State_NoneRemaining(
OperatorSet memory operatorSet,
uint slashId,
IStrategy[] memory strategies,
uint[] memory initTokenBalances,
address redistributionRecipient
) internal {
check_releaseSlashEscrow_State(operatorSet, slashId, strategies, initTokenBalances, redistributionRecipient);
assertFalse(_getIsPendingOperatorSet(operatorSet), "operator set should not be pending");
}

/**
*
* DUAL SLASHING CHECKS
Expand Down
26 changes: 8 additions & 18 deletions src/test/integration/IntegrationDeployer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {

uint8 constant NUM_LST_STRATS = 32;

uint32 INITIAL_GLOBAL_DELAY_BLOCKS = 28_800; // 4 days in blocks
uint32 INITIAL_GLOBAL_DELAY_BLOCKS = 4 days / 12 seconds; // 4 days in blocks

// Lists of strategies used in the system
//
Expand Down Expand Up @@ -443,6 +443,12 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
allocationManager.initialize({initialPausedStatus: 0});

strategyFactory.initialize({_initialOwner: executorMultisig, _initialPausedStatus: 0, _strategyBeacon: strategyBeacon});

slashEscrowFactory.initialize({
initialOwner: communityMultisig,
initialPausedStatus: 0,
initialGlobalDelayBlocks: INITIAL_GLOBAL_DELAY_BLOCKS
});
}

/// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist
Expand Down Expand Up @@ -720,29 +726,13 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
return userType;
}

function _shuffle(IStrategy[] memory strats) internal returns (IStrategy[] memory) {
uint[] memory casted;

assembly {
casted := strats
}

casted = vm.shuffle(casted);

assembly {
strats := casted
}

return strats;
}

function _randomStrategies() internal returns (IStrategy[][] memory strategies) {
uint numOpSets = _randUint({min: 1, max: 5});

strategies = new IStrategy[][](numOpSets);

for (uint i; i < numOpSets; ++i) {
IStrategy[] memory randomStrategies = _shuffle(allStrats);
IStrategy[] memory randomStrategies = allStrats.shuffle();
uint numStrategies = _randUint({min: 1, max: maxUniqueAssetsHeld});

// Modify the length of the array in memory (thus ignoring remaining elements).
Expand Down
Loading