Skip to content

feat(draft): AllocationManager redistribution support #1346

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

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
154 changes: 117 additions & 37 deletions src/contracts/core/AllocationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,25 @@ contract AllocationManager is
function slashOperator(
address avs,
SlashingParams calldata params
) external onlyWhenNotPaused(PAUSED_OPERATOR_SLASHING) checkCanCall(avs) {
)
external
onlyWhenNotPaused(PAUSED_OPERATOR_SLASHING)
checkCanCall(avs)
returns (uint256 slashId, uint256[] memory shares)
{
// Check that the operator set exists and the operator is registered to it
OperatorSet memory operatorSet = OperatorSet(avs, params.operatorSetId);
require(params.strategies.length == params.wadsToSlash.length, InputArrayLengthMismatch());
require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet());
_checkArrayLengthsMatch(params.strategies.length, params.wadsToSlash.length);
_checkIsOperatorSet(operatorSet);
require(isOperatorSlashable(params.operator, operatorSet), OperatorNotSlashable());

uint256[] memory wadSlashed = new uint256[](params.strategies.length);

// Increment the slash count for the operator set, first `slashId` equals 1.
slashId = ++_slashCount[operatorSet.key()];
// Initialize the shares array.
shares = new uint256[](params.strategies.length);

// For each strategy in the operator set, slash any existing allocation
for (uint256 i = 0; i < params.strategies.length; i++) {
// Check that `strategies` is in ascending order.
Expand Down Expand Up @@ -155,7 +165,7 @@ contract AllocationManager is
) external onlyWhenNotPaused(PAUSED_MODIFY_ALLOCATIONS) {
// Check that the caller is allowed to modify allocations on behalf of the operator
// We do not use a modifier to avoid `stack too deep` errors
require(_checkCanCall(operator), InvalidCaller());
_checkCanCall(operator);

// Check that the operator exists and has configured an allocation delay
uint32 operatorAllocationDelay;
Expand All @@ -166,15 +176,15 @@ contract AllocationManager is
}

for (uint256 i = 0; i < params.length; i++) {
require(params[i].strategies.length == params[i].newMagnitudes.length, InputArrayLengthMismatch());
_checkArrayLengthsMatch(params[i].strategies.length, params[i].newMagnitudes.length);

// Check that the operator set exists and get the operator's registration status
// Operators do not need to be registered for an operator set in order to allocate
// slashable magnitude to the set. In fact, it is expected that operators will
// allocate magnitude before registering, as AVS's will likely only accept
// registrations from operators that are already slashable.
OperatorSet memory operatorSet = params[i].operatorSet;
require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet());
_checkIsOperatorSet(operatorSet);

bool _isOperatorSlashable = isOperatorSlashable(operator, operatorSet);

Expand Down Expand Up @@ -246,8 +256,7 @@ contract AllocationManager is
IStrategy[] calldata strategies,
uint16[] calldata numToClear
) external onlyWhenNotPaused(PAUSED_MODIFY_ALLOCATIONS) {
require(strategies.length == numToClear.length, InputArrayLengthMismatch());

_checkArrayLengthsMatch(strategies.length, numToClear.length);
for (uint256 i = 0; i < strategies.length; ++i) {
_clearDeallocationQueue({operator: operator, strategy: strategies[i], numToClear: numToClear[i]});
}
Expand All @@ -258,13 +267,13 @@ contract AllocationManager is
address operator,
RegisterParams calldata params
) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION) checkCanCall(operator) {
// Check that the operator exists
require(delegation.isOperator(operator), InvalidOperator());
// Check if the operator has registered.
_checkIsOperator(operator);

for (uint256 i = 0; i < params.operatorSetIds.length; i++) {
// Check the operator set exists and the operator is not currently registered to it
OperatorSet memory operatorSet = OperatorSet(params.avs, params.operatorSetIds[i]);
require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet());
_checkIsOperatorSet(operatorSet);
require(!isOperatorSlashable(operator, operatorSet), AlreadyMemberOfSet());

// Add operator to operator set
Expand All @@ -285,12 +294,12 @@ contract AllocationManager is
DeregisterParams calldata params
) external onlyWhenNotPaused(PAUSED_OPERATOR_SET_REGISTRATION_AND_DEREGISTRATION) {
// Check that the caller is either authorized on behalf of the operator or AVS
require(_checkCanCall(params.operator) || _checkCanCall(params.avs), InvalidCaller());
require(_canCall(params.operator) || _canCall(params.avs), InvalidPermissions());

for (uint256 i = 0; i < params.operatorSetIds.length; i++) {
// Check the operator set exists and the operator is registered to it
OperatorSet memory operatorSet = OperatorSet(params.avs, params.operatorSetIds[i]);
require(_operatorSets[params.avs].contains(operatorSet.id), InvalidOperatorSet());
_checkIsOperatorSet(operatorSet);
require(registrationStatus[params.operator][operatorSet.key()].registered, NotMemberOfSet());

// Remove operator from operator set
Expand All @@ -313,8 +322,8 @@ contract AllocationManager is
/// @inheritdoc IAllocationManager
function setAllocationDelay(address operator, uint32 delay) external {
if (msg.sender != address(delegation)) {
require(_checkCanCall(operator), InvalidCaller());
require(delegation.isOperator(operator), InvalidOperator());
_checkCanCall(operator);
_checkIsOperator(operator);
}
_setAllocationDelay(operator, delay);
}
Expand All @@ -330,31 +339,28 @@ contract AllocationManager is

/// @inheritdoc IAllocationManager
function updateAVSMetadataURI(address avs, string calldata metadataURI) external checkCanCall(avs) {
if (!_avsRegisteredMetadata[avs]) {
_avsRegisteredMetadata[avs] = true;
}

if (!_avsRegisteredMetadata[avs]) _avsRegisteredMetadata[avs] = true;
emit AVSMetadataURIUpdated(avs, metadataURI);
}

/// @inheritdoc IAllocationManager
function createOperatorSets(address avs, CreateSetParams[] calldata params) external checkCanCall(avs) {
// Check that the AVS exists and has registered metadata
require(_avsRegisteredMetadata[avs], NonexistentAVSMetadata());

for (uint256 i = 0; i < params.length; i++) {
OperatorSet memory operatorSet = OperatorSet(avs, params[i].operatorSetId);

// Create the operator set, ensuring it does not already exist
require(_operatorSets[avs].add(operatorSet.id), InvalidOperatorSet());
emit OperatorSetCreated(OperatorSet(avs, operatorSet.id));
_createOperatorSet(avs, params[i], DEFAULT_BURN_ADDRESS);
}
}

// Add strategies to the operator set
bytes32 operatorSetKey = operatorSet.key();
for (uint256 j = 0; j < params[i].strategies.length; j++) {
_operatorSetStrategies[operatorSetKey].add(address(params[i].strategies[j]));
emit StrategyAddedToOperatorSet(operatorSet, params[i].strategies[j]);
}
/// @inheritdoc IAllocationManager
function createRedistributingOperatorSets(
address avs,
CreateSetParams[] calldata params,
address[] calldata redistributionRecipients
) external checkCanCall(avs) {
_checkArrayLengthsMatch(params.length, redistributionRecipients.length);
require(_avsRegisteredMetadata[avs], NonexistentAVSMetadata());
for (uint256 i = 0; i < params.length; i++) {
_createOperatorSet(avs, params[i], redistributionRecipients[i]);
}
}

Expand All @@ -365,10 +371,8 @@ contract AllocationManager is
IStrategy[] calldata strategies
) external checkCanCall(avs) {
OperatorSet memory operatorSet = OperatorSet(avs, operatorSetId);
_checkIsOperatorSet(operatorSet);
bytes32 operatorSetKey = operatorSet.key();

require(_operatorSets[avs].contains(operatorSet.id), InvalidOperatorSet());

for (uint256 i = 0; i < strategies.length; i++) {
require(_operatorSetStrategies[operatorSetKey].add(address(strategies[i])), StrategyAlreadyInOperatorSet());
emit StrategyAddedToOperatorSet(operatorSet, strategies[i]);
Expand All @@ -382,8 +386,7 @@ contract AllocationManager is
IStrategy[] calldata strategies
) external checkCanCall(avs) {
OperatorSet memory operatorSet = OperatorSet(avs, operatorSetId);
require(_operatorSets[avs].contains(operatorSet.id), InvalidOperatorSet());

_checkIsOperatorSet(operatorSet);
bytes32 operatorSetKey = operatorSet.key();
for (uint256 i = 0; i < strategies.length; i++) {
require(_operatorSetStrategies[operatorSetKey].remove(address(strategies[i])), StrategyNotInOperatorSet());
Expand All @@ -397,6 +400,41 @@ contract AllocationManager is
*
*/

/**
* @notice Creates a new operator set for an AVS.
* @param avs The AVS address that owns the operator set.
* @param params The parameters for creating the operator set.
* @param redistributionRecipient Address to receive redistributed funds when operators are slashed.
* @dev If `redistributionRecipient` is address(0), the operator set is considered non-redistributing
* and slashed funds are sent to the `DEFAULT_BURN_ADDRESS`.
* @dev Providing `BEACONCHAIN_ETH_STRAT` as a strategy will revert since it's not currently supported.
*/
function _createOperatorSet(
address avs,
CreateSetParams calldata params,
address redistributionRecipient
) internal {
OperatorSet memory operatorSet = OperatorSet(avs, params.operatorSetId);

// Create the operator set, ensuring it does not already exist.
require(_operatorSets[avs].add(operatorSet.id), InvalidOperatorSet());
emit OperatorSetCreated(operatorSet);

bool isRedistributing = redistributionRecipient != DEFAULT_BURN_ADDRESS;

if (isRedistributing) {
_redistributionRecipients[operatorSet.key()] = redistributionRecipient;
emit RedistributionAddressSet(operatorSet, redistributionRecipient);
}

// Add strategies to the operator set
for (uint256 j = 0; j < params.strategies.length; j++) {
if (isRedistributing && params.strategies[j] == BEACONCHAIN_ETH_STRAT) revert InvalidStrategy();
_operatorSetStrategies[operatorSet.key()].add(address(params.strategies[j]));
emit StrategyAddedToOperatorSet(operatorSet, params.strategies[j]);
}
}

/**
* @dev Clear one or more pending deallocations to a strategy's allocated magnitude
* @param operator the operator whose pending deallocations will be cleared
Expand Down Expand Up @@ -605,6 +643,25 @@ contract AllocationManager is
return uint256(int256(int128(uint128(a)) + b)).toUint64();
}

/// @dev Reverts if the operator set does not exist.
function _checkIsOperatorSet(
OperatorSet memory operatorSet
) internal view {
require(_operatorSets[operatorSet.avs].contains(operatorSet.id), InvalidOperatorSet());
}

/// @dev Reverts if the provided arrays have different lengths.
function _checkArrayLengthsMatch(uint256 left, uint256 right) internal pure {
require(left == right, InputArrayLengthMismatch());
}

/// @dev Reverts if the operator is not registered.
function _checkIsOperator(
address operator
) internal view {
require(delegation.isOperator(operator), InvalidOperator());
}

/**
*
* VIEW FUNCTIONS
Expand Down Expand Up @@ -905,4 +962,27 @@ contract AllocationManager is
// less than or equal to
return status.registered || block.number <= status.slashableUntil;
}

/// @inheritdoc IAllocationManager
function getRedistributionRecipient(
OperatorSet memory operatorSet
) external view returns (address) {
// Load the redistribution recipient and return it if set, otherwise return the default burn address.
address redistributionRecipient = _redistributionRecipients[operatorSet.key()];
return redistributionRecipient == address(0) ? DEFAULT_BURN_ADDRESS : redistributionRecipient;
}

/// @inheritdoc IAllocationManager
function isRedistributingOperatorSet(
OperatorSet memory operatorSet
) external view returns (bool) {
return _redistributionRecipients[operatorSet.key()] != address(0);
}

/// @inheritdoc IAllocationManager
function getSlashCount(
OperatorSet memory operatorSet
) external view returns (uint256) {
return _slashCount[operatorSet.key()];
}
}
18 changes: 17 additions & 1 deletion src/contracts/core/AllocationManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ abstract contract AllocationManagerStorage is IAllocationManager {

// Constants

/// @dev The default burn address for slashed funds.
address internal constant DEFAULT_BURN_ADDRESS = 0x00000000000000000000000000000000000E16E4;

/// @dev The beacon chain ETH strategy.
IStrategy internal constant BEACONCHAIN_ETH_STRAT = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);

/// @dev Index for flag that pauses operator allocations/deallocations when set.
uint8 internal constant PAUSED_MODIFY_ALLOCATIONS = 0;

Expand Down Expand Up @@ -93,6 +99,16 @@ abstract contract AllocationManagerStorage is IAllocationManager {
/// @notice bool is not used and is always true if the avs has registered metadata
mapping(address avs => bool) internal _avsRegisteredMetadata;

/// @notice Returns the number of slashes for a given operator set.
/// @dev This is also used as a unique slash identifier.
/// @dev This tracks the number of slashes after the redistribution release.
mapping(bytes32 operatorSetKey => uint256 slashId) public _slashCount;

/// @notice Returns the address where slashed funds will be sent for a given operator set.
/// @dev For redistributing Operator Sets, returns the configured redistribution address set during Operator Set creation.
/// For non-redistributing or non-existing operator sets, returns `address(0)`.
mapping(bytes32 operatorSetKey => address redistributionAddr) internal _redistributionRecipients;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We could combine the above two mappings into an OperatorSetInfo struct? Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

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

Scott had an issue with storage packing

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's with regards to using uint96 instead of uint256. Using a struct should be fine IMO, see:

/// @notice Returns the operator details for a given `operator`.
/// Note: two of the `OperatorDetails` fields are deprecated. The only relevant field
/// is `OperatorDetails.delegationApprover`.
mapping(address operator => OperatorDetails) internal _operatorDetails;

Copy link
Member Author

Choose a reason for hiding this comment

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

Why combine them if not packing?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Less total storage slots used

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, if we added slashers, way easier to just have operatorSetDetails. Not blocking review rn but we should consider


// Construction

constructor(IDelegationManager _delegation, uint32 _DEALLOCATION_DELAY, uint32 _ALLOCATION_CONFIGURATION_DELAY) {
Expand All @@ -106,5 +122,5 @@ abstract contract AllocationManagerStorage is IAllocationManager {
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[36] private __gap;
uint256[34] private __gap;
}
2 changes: 1 addition & 1 deletion src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ contract DelegationManager is
if (msg.sender != staker) {
address operator = delegatedTo[staker];

require(_checkCanCall(operator) || msg.sender == delegationApprover(operator), CallerCannotUndelegate());
require(_canCall(operator) || msg.sender == delegationApprover(operator), CallerCannotUndelegate());
emit StakerForceUndelegated(staker, operator);
}

Expand Down
Loading