Skip to content
172 changes: 172 additions & 0 deletions src/EjectionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import {IEjectionManager} from "./interfaces/IEjectionManager.sol";
import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol";
import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol";

/**
* @title Used for automated ejection of operators from the RegistryCoordinator under a ratelimit
* @author Layr Labs, Inc.
*/
contract EjectionManager is IEjectionManager, OwnableUpgradeable{

/// @notice The basis point denominator for the ejectable stake percent
uint16 internal constant BIPS_DENOMINATOR = 10000;

/// @notice the RegistryCoordinator contract that is the entry point for ejection
IRegistryCoordinator public immutable registryCoordinator;
/// @notice the StakeRegistry contract that keeps track of quorum stake
IStakeRegistry public immutable stakeRegistry;

/// @notice Address permissioned to eject operators under a ratelimit
address public ejector;

/// @notice Keeps track of the total stake ejected for a quorum
mapping(uint8 => StakeEjection[]) public stakeEjectedForQuorum;
/// @notice Ratelimit parameters for each quorum
mapping(uint8 => QuorumEjectionParams) public quorumEjectionParams;

constructor(
IRegistryCoordinator _registryCoordinator,
IStakeRegistry _stakeRegistry
) {
registryCoordinator = _registryCoordinator;
stakeRegistry = _stakeRegistry;

_disableInitializers();
}

/**
* @param _owner will hold the owner role
* @param _ejector will hold the ejector role
* @param _quorumEjectionParams are the ratelimit parameters for the quorum at each index
*/
function initialize(
address _owner,
address _ejector,
QuorumEjectionParams[] memory _quorumEjectionParams
) external initializer {
_transferOwnership(_owner);
_setEjector(_ejector);

for(uint8 i = 0; i < _quorumEjectionParams.length; i++) {
_setQuorumEjectionParams(i, _quorumEjectionParams[i]);
}
}

/**
* @notice Ejects operators from the AVSs RegistryCoordinator under a ratelimit
* @param _operatorIds The ids of the operators 'j' to eject for each quorum 'i'
* @dev This function will eject as many operators as possible without reverting prioritizing operators at the lower index
* @dev The owner can eject operators without recording of stake ejection
*/
function ejectOperators(bytes32[][] memory _operatorIds) external {
Copy link
Contributor

Choose a reason for hiding this comment

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

i feel like ejectOperators(uint8 quorumNumber, bytes32[] calldata _operatorIds) is more in line with our style

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

you caught me red handed. now i realize why i wanted that is we want to eject from multiple quorums at the same time

Copy link
Contributor

Choose a reason for hiding this comment

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

we could do that outside of the contract too, but ok

require(msg.sender == ejector || msg.sender == owner(), "Ejector: Only owner or ejector can eject");

for(uint i = 0; i < _operatorIds.length; ++i) {
uint8 quorumNumber = uint8(i);

uint256 amountEjectable = amountEjectableForQuorum(quorumNumber);
uint256 stakeForEjection;

bool broke;
for(uint8 j = 0; j < _operatorIds[i].length; ++j) {
uint256 operatorStake = stakeRegistry.getCurrentStake(_operatorIds[i][j], quorumNumber);

//if caller is ejector enforce ratelimit
if(
msg.sender == ejector &&
quorumEjectionParams[quorumNumber].rateLimitWindow > 0 &&
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like this means that no limit will be enforced for new quorums until a limit is explicitly set on this contract for the quorum?
possibly a bit of a "footgun"?
please correct me if I'm reading this wrong though.

Copy link
Contributor Author

@0x0aa0 0x0aa0 Apr 11, 2024

Choose a reason for hiding this comment

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

That is correct. My thinking here is that when adding a new quorum, while it is finding its footing we will not want to run automated ejection but reserve the ability to eject without ratelimit in that period. Once the set is more stable the quorum params can be set and offchain can be turned on to run regularly.

stakeForEjection + operatorStake > amountEjectable
){
stakeEjectedForQuorum[quorumNumber].push(StakeEjection({
Copy link
Contributor

Choose a reason for hiding this comment

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

why are we pushing and not continuing here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we do not want to revert if the stake of the ops in the input is greater than the ratelimit. Here we checkpoint the stake ejected inside the ratelimit and then end execution

Copy link
Contributor

Choose a reason for hiding this comment

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

oh my bad, i completely misread this

timestamp: block.timestamp,
stakeEjected: stakeForEjection
}));
broke = true;
break;
}

//try-catch used to prevent race condition of operator deregistering before ejection
try registryCoordinator.ejectOperator(
Copy link
Contributor

Choose a reason for hiding this comment

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

why would we not revert if this failed? why would this fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would fail if the operator leaves the set before ejection is called

Copy link
Contributor

Choose a reason for hiding this comment

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

ah gpgp

registryCoordinator.getOperatorFromId(_operatorIds[i][j]),
abi.encodePacked(quorumNumber)
) {
stakeForEjection += operatorStake;
emit OperatorEjected(_operatorIds[i][j], quorumNumber);
} catch (bytes memory err) {
emit FailedOperatorEjection(_operatorIds[i][j], quorumNumber, err);
}
}

//record the stake ejected if ejector and ratelimit enforced
if(!broke && msg.sender == ejector){
stakeEjectedForQuorum[quorumNumber].push(StakeEjection({
timestamp: block.timestamp,
stakeEjected: stakeForEjection
}));
}

}
}

/**
* @notice Sets the ratelimit parameters for a quorum
* @param _quorumNumber The quorum number to set the ratelimit parameters for
* @param _quorumEjectionParams The quorum ratelimit parameters to set for the given quorum
*/
function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() {
_setQuorumEjectionParams(_quorumNumber, _quorumEjectionParams);
}

/**
* @notice Sets the address permissioned to eject operators under a ratelimit
* @param _ejector The address to permission
*/
function setEjector(address _ejector) external onlyOwner() {
_setEjector(_ejector);
}

///@dev internal function to set the quorum ejection params
function _setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) internal {
quorumEjectionParams[_quorumNumber] = _quorumEjectionParams;
emit QuorumEjectionParamsSet(_quorumNumber, _quorumEjectionParams.rateLimitWindow, _quorumEjectionParams.ejectableStakePercent);
}

///@dev internal function to set the ejector
function _setEjector(address _ejector) internal {
emit EjectorUpdated(ejector, _ejector);
ejector = _ejector;
}

/**
* @notice Returns the amount of stake that can be ejected for a quorum at the current block.timestamp
* @param _quorumNumber The quorum number to view ejectable stake for
*/
function amountEjectableForQuorum(uint8 _quorumNumber) public view returns (uint256) {
uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow;
uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR;
uint256 totalEjected;
uint256 i;
if (stakeEjectedForQuorum[_quorumNumber].length == 0) {
return totalEjectable;
}
i = stakeEjectedForQuorum[_quorumNumber].length - 1;

while(stakeEjectedForQuorum[_quorumNumber][i].timestamp > cutoffTime) {
totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected;
if(i == 0){
break;
} else {
--i;
}
}

if(totalEjected >= totalEjectable){
return 0;
}
return totalEjectable - totalEjected;
}
}
55 changes: 55 additions & 0 deletions src/interfaces/IEjectionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

/**
* @title Interface for a contract that ejects operators from an AVSs RegistryCoordinator
* @author Layr Labs, Inc.
*/
interface IEjectionManager {

/// @notice A quorum's ratelimit parameters
struct QuorumEjectionParams {
uint32 rateLimitWindow; // Time delta to track ejection over
uint16 ejectableStakePercent; // Max stake to be ejectable per time delta
}

/// @notice A stake ejection event
struct StakeEjection {
uint256 timestamp; // Timestamp of the ejection
uint256 stakeEjected; // Amount of stake ejected at the timestamp
}

///@notice Emitted when the ejector address is set
event EjectorUpdated(address previousAddress, address newAddress);
///@notice Emitted when the ratelimit parameters for a quorum are set
event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 rateLimitWindow, uint16 ejectableStakePercent);
///@notice Emitted when an operator is ejected
event OperatorEjected(bytes32 operatorId, uint8 quorumNumber);
///@notice Emitted when an operator ejection fails
event FailedOperatorEjection(bytes32 operatorId, uint8 quorumNumber, bytes err);

/**
* @notice Ejects operators from the AVSs registryCoordinator under a ratelimit
* @param _operatorIds The ids of the operators to eject for each quorum
*/
function ejectOperators(bytes32[][] memory _operatorIds) external;

/**
* @notice Sets the ratelimit parameters for a quorum
* @param _quorumNumber The quorum number to set the ratelimit parameters for
* @param _quorumEjectionParams The quorum ratelimit parameters to set for the given quorum
*/
function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external;

/**
* @notice Sets the address permissioned to eject operators under a ratelimit
* @param _ejector The address to permission
*/
function setEjector(address _ejector) external;

/**
* @notice Returns the amount of stake that can be ejected for a quorum at the current block.timestamp
* @param _quorumNumber The quorum number to view ejectable stake for
*/
function amountEjectableForQuorum(uint8 _quorumNumber) external view returns (uint256);
}
Loading