-
Notifications
You must be signed in to change notification settings - Fork 114
feat: automated ejector #146
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
Changes from all 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,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 { | ||
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 && | ||
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-catch used to prevent race condition of operator deregistering before ejection | ||
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); | ||
} | ||
} | ||
|
||
//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; | ||
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: 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); | ||
} |
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.
i feel like
ejectOperators(uint8 quorumNumber, bytes32[] calldata _operatorIds)
is more in line with our styleThere 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.
#146 (comment)
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.
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 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