-
Notifications
You must be signed in to change notification settings - Fork 94
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
feat: automated ejector #146
Changes from 11 commits
befc8b3
dd9911d
b2d6f7c
ee49669
f991bf9
7d2f287
7194bce
b21373e
2e92c1e
bbea695
fab636e
1d06dd5
91b0d36
d965f9d
3b48a39
a32bbc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity ^0.8.9; | ||
|
||
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"; | ||
import {BitmapUtils} from "./libraries/BitmapUtils.sol"; | ||
|
||
/** | ||
* @title Used for automated ejection of operators from the RegistryCoordinator | ||
* @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; | ||
|
||
IRegistryCoordinator public immutable registryCoordinator; | ||
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 within a time delta | ||
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(); | ||
} | ||
|
||
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 | ||
* @dev This function will eject as many operators as possible without reverting | ||
* @param _operatorIds The ids of the operators to eject for each quorum | ||
*/ | ||
function ejectOperators(bytes32[][] memory _operatorIds) external { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i feel like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
||
_cleanOldEjections(quorumNumber); | ||
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( | ||
msg.sender == ejector && | ||
quorumEjectionParams[quorumNumber].rateLimitWindow > 0 && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why are we pushing and not continuing here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 registryCoordinator.ejectOperator( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why would we not revert if this failed? why would this fail? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would fail if the operator leaves the set before ejection is called There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} | ||
|
||
if(!broke){ | ||
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 bitmaps for each respective operator | ||
*/ | ||
function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external onlyOwner() { | ||
_setQuorumEjectionParams(_quorumNumber, _quorumEjectionParams); | ||
} | ||
|
||
/** | ||
* @notice Sets the address permissioned to eject operators | ||
* @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; | ||
} | ||
|
||
/** | ||
* @dev Removes stale ejections for a quorums history | ||
* @param _quorumNumber The quorum number to clean ejections for | ||
*/ | ||
function _cleanOldEjections(uint8 _quorumNumber) internal { | ||
uint256 cutoffTime = block.timestamp - quorumEjectionParams[_quorumNumber].rateLimitWindow; | ||
uint256 index = 0; | ||
StakeEjection[] storage stakeEjections = stakeEjectedForQuorum[_quorumNumber]; | ||
while (index < stakeEjections.length && stakeEjections[index].timestamp < cutoffTime) { | ||
index++; | ||
} | ||
if (index > 0) { | ||
for (uint256 i = index; i < stakeEjections.length; ++i) { | ||
stakeEjections[i - index] = stakeEjections[i]; | ||
} | ||
for (uint256 i = 0; i < index; ++i) { | ||
stakeEjections.pop(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @notice Returns the amount of stake that can be ejected for a quorum | ||
* @dev This function only returns a valid amount after _cleanOldEjections has been called | ||
* @param _quorumNumber The quorum number to view ejectable stake for | ||
*/ | ||
function _amountEjectableForQuorum(uint8 _quorumNumber) internal view returns (uint256) { | ||
uint256 totalEjected = 0; | ||
for (uint256 i = 0; i < stakeEjectedForQuorum[_quorumNumber].length; i++) { | ||
totalEjected += stakeEjectedForQuorum[_quorumNumber][i].stakeEjected; | ||
} | ||
uint256 totalEjectable = quorumEjectionParams[_quorumNumber].ejectableStakePercent * stakeRegistry.getCurrentTotalStake(_quorumNumber) / BIPS_DENOMINATOR; | ||
return totalEjectable - totalEjected; | ||
0x0aa0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.9; | ||
|
||
import {IRegistryCoordinator} from "./IRegistryCoordinator.sol"; | ||
import {IStakeRegistry} from "./IStakeRegistry.sol"; | ||
|
||
/** | ||
* @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 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 Emitted when the ratelimit parameters for a quorum are set | ||
event QuorumEjectionParamsSet(uint8 quorumNumber, uint32 rateLimitWindow, uint16 ejectableStakePercent); | ||
|
||
/** | ||
* @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 bitmaps for each respective operator | ||
*/ | ||
function setQuorumEjectionParams(uint8 _quorumNumber, QuorumEjectionParams memory _quorumEjectionParams) external; | ||
|
||
/** | ||
* @notice Sets the address permissioned to eject operators | ||
* @param _ejector The address to permission | ||
*/ | ||
function setEjector(address _ejector) external; | ||
|
||
|
||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's assumed in this parameter by this function?
From the impl below it looks: 1) the caller provides all operators eligible for ejection, but delegate to this function for rate limiting; 2) the operators in each quorum are ordered by priority to eject (in case not all can be ejected, it'll just eject a leading prefix of the array)
Might be good to document the assumptions so the caller knows how to pass the params.