-
Notifications
You must be signed in to change notification settings - Fork 327
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: slashing release #679
base: custom-errors
Are you sure you want to change the base?
Conversation
f2a7515
to
f0873d1
Compare
9f91f03
to
b9fe3e5
Compare
This comment was marked as spam.
This comment was marked as spam.
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.
Review on alloc/dealloc
src/contracts/core/AVSDirectory.sol
Outdated
); | ||
} | ||
// 2. allocate magnitude which will take effect in the future 21 days from now | ||
_magnitudeUpdate[operator][allocation.strategy][opSets[i].avs][opSets[i].operatorSetId].push({ |
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.
Is the ordering of _magnitudeUpdate
guaranteed if an operator updates their allocation delay?
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.
TODO: need to set allocation delay before you call initialize allocation delay
src/contracts/core/AVSDirectory.sol
Outdated
// 1. ensure only pending queued deallocation per operator, operatorSet, strategy | ||
_checkQueuedDeallocations(operator, allocation.strategy, opSets[i]); | ||
|
||
// 2. update and decrement current and future queued amounts in case any pending allocations exist |
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.
The behavior here is that deallocations supersede allocations (ie. if a user allocates, then the deallocation overrides the allocation). Assume this is by design, but don't remember it being documented anywhere?
Would it be bad / hard / undesirable to enforce this ordering on the checkpoints history level? I would suggest that if an attempt is made to push an entry with a timestamp that is earlier than the timestamp of the previous entry, either (a) it's simply not allowed or (b) the new entry has its timestamp modified to match (or be 1 higher? not sure if the ascending ordering you have is strict or non-strict). Perhaps some option (c) could work where you keep the original timestamp but overwrite the intentions of the other entry? IDK if that would be incompatible with other aspects of the storage model though. I was initially thinking option (b) was the most logical but modifying entries before pushing them can be messy, especially if the entry or its contents is used elsewhere (e.g. if the timestamp is emitted in events, its hard to make sure the event emits the correct timestamp when you have conditional logic for modifying it). |
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.
Minor slashing comments
src/contracts/core/AVSDirectory.sol
Outdated
// 3. update totalMagnitude, get total magnitude and subtract slashedMagnitude | ||
_totalMagnitudeUpdate[operator][strategies[i]].push({ | ||
key: uint32(block.timestamp), | ||
value: _getLatestTotalMagnitude(operator, strategies[i]) - slashedMagnitude |
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.
is the value of slashedMagnitude
what you want it to be here? It's set in the first scope and then incremented again in the second scope, and returned here
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.
Yup, since a deallocation immediately decrements current amount and places the decrement in a pendingFreeMagnitude.
When an operatorSet tries to slash, you need to slash from current and pendingFreeMagnitude amount to slash the same amount.
src/contracts/core/AVSDirectory.sol
Outdated
{ | ||
uint256 queuedDeallocationIndicesLen = | ||
_queuedDeallocationIndices[operator][strategies[i]][msg.sender][operatorSetId].length; | ||
for (uint256 j = queuedDeallocationIndicesLen; j > 0; --j) { |
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.
Since MAX_PENDING_UPDATES
is 1, we don't need a loop here?
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.
@gpsanant mentioned we want to check without assuming max 1 and be able to update in the future to change MAX_PENDING_UPDATES
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.
eh
@@ -113,5 +136,5 @@ abstract contract DelegationManagerStorage is IDelegationManager { | |||
* variables without shifting down storage in the inheritance chain. | |||
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps | |||
*/ | |||
uint256[39] private __gap; | |||
uint256[38] private __gap; |
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.
Needs update here too
678f212
to
e70e85d
Compare
0e122f9
to
b871886
Compare
* fix: remove sort check * fix: remove sort check
* fix: remove numToComplete and hardcode with max * fix: make it cleaner
* abstract with ShareManager * start on totalMagnitude cleanup * fix magnitude sourcing * conversion logic and definitions * abstract completability * fix getDelegatableShares * rename deposit scaling factors and bring depositScalingFactor updates in the DM
// Assert that the AVS is an operator set AVS. | ||
require(isOperatorSetAVS[msg.sender], InvalidAVS()); |
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.
so new AVSs will need to call becomeOperatorSetAVS
before they can register any operators?
I think this is OK but definitely a potential mistake for people to make / something to call out in docs
require(_operatorSetsMemberOf[operator].add(encodedOperatorSet), InvalidOperator()); | ||
|
||
_operatorSetMembers[encodedOperatorSet].add(operator); |
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.
is there a reason for this asymmetry?
it feels like the second line should also be wrapped in a require statement -- am I missing something?
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.
ah, at least it seems these are always meant to mirror one another, so if the first addition returns 'true' (indicating the addition occurred) then the second should also return 'true', and if the first returns 'false' then execution has already reverted
// Mutate `operatorSaltIsSpent` to `true` to prevent future respending. | ||
operatorSaltIsSpent[operator][operatorSignature.salt] = true; | ||
} | ||
_deregisterFromOperatorSets(avs, operator, operatorSetIds); |
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.
given that this function (forceDeregisterFromOperatorSets
) has the same end result as deregisterOperatorFromOperatorSets
should these maybe be combined? or this would just be too confusing with too many ways to call the same function?
* @param expiry Time after which the approver's signature becomes invalid | ||
* @notice Returns operatorSet an operator is registered to in the order they were registered. | ||
* @param operator The operator address to query. | ||
* @param index The index of the enumerated list of operator sets. |
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.
minor grammatical nit: index of
=> index in
. this occurs in a couple places
/// @dev BIPS factor for slashable bips | ||
uint256 internal constant BIPS_FACTOR = 10_000; |
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.
can we move this definition to something like libraries/SlashingConstants.sol ?
/// @dev The minimum number of blocks to complete a withdrawal of a strategy. 50400 * 12 seconds = 1 week | ||
uint256 public constant LEGACY_MIN_WITHDRAWAL_DELAY_BLOCKS = 50_400; |
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.
any chance renaming this breaks any integrations?
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.
great question, we should keep around a view function with the same signature, at the least
string calldata metadataURI | ||
) external { | ||
require(!isDelegated(msg.sender), "DelegationManager.registerAsOperator: caller is already actively delegated"); | ||
require(!isDelegated(msg.sender), ActivelyDelegated()); | ||
allocationManager.setAllocationDelay(msg.sender, allocationDelay); |
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.
is this only called when initially registering as an operator for the moment?
do we have a transition plan for existing operators?
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.
nevermind, I see that there are other codepaths for adjusting.
* add share lib * feat: share lib cleanup (#778) * refactor: review changes * chore: forge fmt src/contracts * refactor: more explicit share names * rename share * fmt --------- Co-authored-by: gpsanant <[email protected]> * remove unused * remove unused --------- Co-authored-by: clandestine.eth <[email protected]>
* and adds all completable deallocations for each strategy, updating the freeMagnitudes of the operator | ||
* | ||
* @param operator address to complete deallocations for | ||
* @param strategies a list of strategies to complete deallocations for | ||
* @param numToComplete a list of number of pending free magnitude deallocations to complete for each strategy |
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.
two nits:
adds all
is somewhat technically inaccurate, since there is anumToComplete
input- this should probably specify that it's fine (i.e. function won't revert) if
numToComplete
exceeds the actual completable number -- e.g. just using type(uint16).max as the input shouldn't revert except for running out of gas
/** | ||
* @notice Called by the delagation manager to set delay when operators register. | ||
* @param operator The operator to set the delay on behalf of. | ||
* @param delay The allocation delay in seconds. | ||
* @dev msg.sender is assumed to be the delegation manager. | ||
*/ | ||
function setAllocationDelay(address operator, uint32 delay) external { | ||
require(msg.sender == address(delegation), OnlyDelegationManager()); | ||
_setAllocationDelay(operator, delay); | ||
} | ||
|
||
/** | ||
* @notice Called by operators to set their allocation delay. | ||
* @param delay the allocation delay in seconds | ||
* @dev msg.sender is assumed to be the operator | ||
*/ | ||
function setAllocationDelay( | ||
uint32 delay | ||
) external { | ||
require(delegation.isOperator(msg.sender), OperatorNotRegistered()); | ||
_setAllocationDelay(msg.sender, delay); | ||
} |
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.
should these two (overloaded) functions perhaps be combined? or is that too confusing? I'm imagining something like
function setAllocationDelay(address operator, uint32 delay) external {
if (msg.sender != address(delegation)) {
require(msg.sender == operator && delegation.isOperator(msg.sender))
}
_setAllocationDelay(operator, delay);
}
* @dev Must be called by the operator or with a valid operator signature | ||
* @dev For each allocation, allocation.operatorSets MUST be ordered in ascending order according to the | ||
* encoding of the operatorSet. This is to prevent duplicate operatorSets being passed in. The easiest way to ensure | ||
* ordering is to sort allocated operatorSets by address first, and then sort for each avs by ascending operatorSetIds. |
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.
nit: sort by address first
should probably specify that this is the AVS address
// 2. Check current totalMagnitude matches expected value. This is to check for slashing race conditions | ||
// where an operator gets slashed from an operatorSet and as a result all the configured allocations have larger | ||
// proprtional magnitudes relative to eachother. This check prevents any surprising behavior. | ||
uint64 currentTotalMagnitude = _getLatestTotalMagnitude(operator, allocations[i].strategy); | ||
require(currentTotalMagnitude == allocations[i].expectedTotalMagnitude, InvalidExpectedTotalMagnitude()); |
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.
can this check be done before (1)? it's generally better to revert earlier, all else being equal
/// TODO: add wrapping library for rounding up for slashing accounting | ||
slashedMagnitude = uint64(uint256(bipsToSlash) * uint256(currentMagnitude) / BIPS_FACTOR); |
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.
pretty sure this will round up properly if you just add 9999
(i.e. BIPS_FACTOR-1
) prior to dividing
so make it:
slashedMagnitude = uint64((uint256(bipsToSlash) * uint256(currentMagnitude) + BIPS_FACTOR - 1) / BIPS_FACTOR);
* | ||
* @return operatorSets the operator sets the operator is a member of and the current slashable magnitudes for each strategy | ||
*/ | ||
function getCurrentSlashableMagnitudes( |
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.
this is basically a view function lib from here on.
@shrimalmadhur what methods are u using here, so we can move them to a periphery contract?
Updated 9/20/24
Contract Descriptions
DONT REVIEW THE REWARDS COORDINATOR
AVSDirectory
New Core contract: AllocationManager! (basically the Slasher contract)
modifyAllocations
can be called to configure updated allocations for a strategy per opSet. Allocations/deallocations are handled differently in the underlying storage.slashOperator
, from the perspective of an (operator, Strategy) tuple, slashes from current magnitude as well as queued deallocations. Whatever magnitude is slashed is also decremented from the totalMagnitude from the (operator, Strategy) tuple.freeMagnitude
in storage.StrategyManager/EigenPodManager/DelegationManager - Changes to Deposits/Withdrawals
This code comment explains a bunch:
Withdrawal
struct in the DelegationManager has been changed fromstartBlock
tostartTimestamp
. To account for legacy M2 withdrawals, we check that this field is less than a specific timestamp and handle accordingly.IShareManager
interfacedepositScalingFactor
is the k value used in the accounting docsstakerStrategyShares
is soperatorDelegatedShares
is optotalMagnitude
is m read from the AllocationManagerRewardsCoordinator
Misc Notes:
thirdPartyTransfersForbidden
is removed entirely. This mapping will be deprecated and never read from again.TODOs:
Deposits/Withdraws of Shares [WIP]
Allocator Configuration [WIP]