Skip to content
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

Open
wants to merge 47 commits into
base: custom-errors
Choose a base branch
from
Open

feat: slashing release #679

wants to merge 47 commits into from

Conversation

8sunyuan
Copy link
Collaborator

@8sunyuan 8sunyuan commented Aug 12, 2024

Updated 9/20/24

Contract Descriptions

DONT REVIEW THE REWARDS COORDINATOR

AVSDirectory

  • The OperatorSets release has been combined into the Slashing release. This includes interfaces for AVS migration to operatorSets and register/deregister functionality.
  • Slashing and magnitude allocations revolve around the introduction of these operatorSets. More on this below...

New Core contract: AllocationManager! (basically the Slasher contract)

  • This is the Slasher contract, pending decision on rename or just changing back to Slasher.
  • This implements "unique security", stake is not reused across AVSs/operatorSets and operators allocate slashable proportions to each operatorSet. Imagine this as a pie chart where each slice gets given to a different operatorSet.
  • 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.
  • Deallocations are slashable while pending. Pending allocations on the other hand are not slashable (referring to the increased amount) because allocations are increases that are also decrementing from the non-allocated/non-slashable magnitude. We refer to this as freeMagnitude in storage.
  • Deallocations are all on a fixed 17.5 day delay
  • Allocation delays are configurable on a per operator basis. Updating the allocationDelay has a delay for it to take effect itself. Open PR for this is here. feat: allocation delay #740

StrategyManager/EigenPodManager/DelegationManager - Changes to Deposits/Withdrawals

This code comment explains a bunch:

   /*
 * There are 3 types of shares:
 *      1. shares
 *          - These can be converted to an amount of tokens given a strategy
 *              - by calling `sharesToUnderlying` on the strategy address (they're already tokens 
 *              in the case of EigenPods)
 *          - These are comparable between operators and stakers.
 *          - These live in the storage of StrategyManager strategies: 
 *              - `totalShares` is the total amount of shares delegated to a strategy
 *      2. delegatedShares
 *          - These can be converted to shares given an operator and a strategy
 *              - by multiplying by the operator's totalMagnitude for the strategy
 *          - These values automatically update their conversion into tokens
 *              - when the operator's total magnitude for the strategy is decreased upon slashing
 *          - These live in the storage of the DelegationManager:
 *              - `delegatedShares` is the total amount of delegatedShares delegated to an operator for a strategy
 *              - `withdrawal.delegatedShares` is the amount of delegatedShares in a withdrawal          
 *      3. principalShares 
 *          - These can be converted into delegatedShares given a staker and a strategy
 *              - by multiplying by the staker's depositScalingFactor for the strategy
 *          - These values automatically update their conversion into tokens
 *             - when the staker's depositScalingFactor for the strategy is increased upon new deposits
 *             - or when the staker's operator's total magnitude for the strategy is decreased upon slashing
 *          - These represent the total amount of shares the staker would have of a strategy if they were never slashed
 *          - These live in the storage of the StrategyManager/EigenPodManager
 *              - `stakerStrategyShares` in the SM is the staker's principalShares that have not been queued for withdrawal in a strategy
 *              - `podOwnerShares` in the EPM is the staker's principalShares that have not been queued for withdrawal in the beaconChainETHStrategy
 */
  • Minimal changes to SM/EPM since the
  • Withdrawal struct in the DelegationManager has been changed from startBlock to startTimestamp. To account for legacy M2 withdrawals, we check that this field is less than a specific timestamp and handle accordingly.
  • DM: EIGEN strategy delay is currently set to 7 days so all the strategies currently have the same delay. We remove strategy specific delays entirely here but all withdrawals will be on a 17.5 day delay (same as deallocation delay)
  • DM: We've abstracted the EPM and SM behind a unified IShareManager interface
  • Accounting NOTEs:
    • depositScalingFactor is the k value used in the accounting docs
    • stakerStrategyShares is s
    • operatorDelegatedShares is op
    • totalMagnitude is m read from the AllocationManager

RewardsCoordinator

  • Likely undo all changes here as the scope for rewards has changed

Misc Notes:

  • Using OZ CheckpointsUpgradeable library in the AllocationManager to keep track of historically timestamped magnitude values. This is required for staker withdrawal completion where the timestamped totalMagnitude value at completion may not be the current so we have a lookup for that. We renamed the Checkpoints library to Snapshots to avoid confusion with EigenPod checkpoints.
  • 0.8.27 custom errors with requires (saves a lot of bytecode size)
  • thirdPartyTransfersForbidden is removed entirely. This mapping will be deprecated and never read from again.
  • We will likely deploy with optimizer-runs = 0-10 to save on contract size

TODOs:

  • burning of slashed shares: requires scoping
  • BytecodeSize (if applicable)
  • EigenPodManager stuff: beaconchain slashing, negative shares

Deposits/Withdraws of Shares [WIP]

  • Confirm no overflow scenarios as part of scaling factor calculations in deposits/withdraws. Note that permissionless staking limits the number of shares in Strategies
  • Events

Allocator Configuration [WIP]

  • Figure out a tolerable rounding scheme for slashing magnitude allocations
  • Events

This comment was marked as spam.

@Layr-Labs Layr-Labs deleted a comment from github-actions bot Aug 19, 2024
@Layr-Labs Layr-Labs deleted a comment from github-actions bot Aug 19, 2024
src/contracts/core/AVSDirectory.sol Fixed Show fixed Hide fixed
src/contracts/core/AVSDirectory.sol Fixed Show fixed Hide fixed
src/contracts/core/AVSDirectory.sol Fixed Show fixed Hide fixed
Copy link
Collaborator

@ypatil12 ypatil12 left a 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 Show resolved Hide resolved
src/contracts/core/AVSDirectory.sol Outdated Show resolved Hide resolved
src/contracts/core/AVSDirectory.sol Outdated Show resolved Hide resolved
src/contracts/core/AVSDirectory.sol Outdated Show resolved Hide resolved
src/contracts/core/AVSDirectory.sol Outdated Show resolved Hide resolved
src/contracts/core/AVSDirectory.sol Outdated Show resolved Hide resolved
src/contracts/core/AVSDirectory.sol Outdated Show resolved Hide resolved
src/contracts/core/AVSDirectory.sol Outdated Show resolved Hide resolved
);
}
// 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({
Copy link
Collaborator

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?

Copy link
Collaborator

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

// 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
Copy link
Collaborator

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?

@MinisculeTarantula
Copy link
Contributor

require allocationDelay being set when calling modifyAllocations. Based on current implementation of a default allocation delay, if a shorter delay is set, it is possible to create a pending magnitudeUpdate that has a timestamp thats earlier than a already existing pending update. We need the checkpoints history to be asc sorted and figuring out some insertion method instead of pushing the history seems like too much overhead complexity.

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).

Copy link
Collaborator

@ypatil12 ypatil12 left a comment

Choose a reason for hiding this comment

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

Minor slashing comments

// 3. update totalMagnitude, get total magnitude and subtract slashedMagnitude
_totalMagnitudeUpdate[operator][strategies[i]].push({
key: uint32(block.timestamp),
value: _getLatestTotalMagnitude(operator, strategies[i]) - slashedMagnitude
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

{
uint256 queuedDeallocationIndicesLen =
_queuedDeallocationIndices[operator][strategies[i]][msg.sender][operatorSetId].length;
for (uint256 j = queuedDeallocationIndicesLen; j > 0; --j) {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

Copy link
Contributor

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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Needs update here too

@ypatil12 ypatil12 mentioned this pull request Aug 29, 2024
@ypatil12 ypatil12 changed the base branch from feat/operator-set-release to dev August 29, 2024 20:24
shrimalmadhur and others added 8 commits September 17, 2024 16:12
* 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
Comment on lines +153 to +154
// Assert that the AVS is an operator set AVS.
require(isOperatorSetAVS[msg.sender], InvalidAVS());
Copy link
Contributor

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

Comment on lines +360 to +362
require(_operatorSetsMemberOf[operator].add(encodedOperatorSet), InvalidOperator());

_operatorSetMembers[encodedOperatorSet].add(operator);
Copy link
Contributor

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?

Copy link
Contributor

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);
Copy link
Contributor

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.
Copy link
Contributor

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

Comment on lines +22 to +23
/// @dev BIPS factor for slashable bips
uint256 internal constant BIPS_FACTOR = 10_000;
Copy link
Contributor

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 ?

Comment on lines +41 to +42
/// @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;
Copy link
Contributor

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?

Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Contributor

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]>
Comment on lines +95 to +99
* 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
Copy link
Contributor

Choose a reason for hiding this comment

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

two nits:

  1. adds all is somewhat technically inaccurate, since there is a numToComplete input
  2. 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

Comment on lines +69 to +90
/**
* @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);
}
Copy link
Contributor

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.
Copy link
Contributor

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

Comment on lines +153 to +157
// 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());
Copy link
Contributor

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

Comment on lines +200 to +201
/// TODO: add wrapping library for rounding up for slashing accounting
slashedMagnitude = uint64(uint256(bipsToSlash) * uint256(currentMagnitude) / BIPS_FACTOR);
Copy link
Contributor

@MinisculeTarantula MinisculeTarantula Sep 21, 2024

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(
Copy link
Contributor

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants