-
Notifications
You must be signed in to change notification settings - Fork 3
feat: implement Rewards v2.2 - Operator Set Rewards with Unique & Total Stake #481
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
base: seri.choi/new-queue-calculation
Are you sure you want to change the base?
feat: implement Rewards v2.2 - Operator Set Rewards with Unique & Total Stake #481
Conversation
|
refer to this PR for previous comments: #466 |
fb368d8 to
8b8dbb4
Compare
8b8dbb4 to
eded96a
Compare
ypatil12
left a 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.
First pass on unique stake reward
| AND la.strategy = lmm.strategy | ||
| AND lmm.rn = 1 | ||
| WHERE la.rn = 1 | ||
| AND la.magnitude > 0 -- Only include active allocations |
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.
Curious why we're combining max magnitudes and allocations into one table here? Generally practice has been to separate out each data type. Max Magnitudes and allocations will update in separate events
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.
In the gold table, we need both max magnitudes and allocations together, so separating adds more complexity of adding more tables and unnecessarily complex query. We do ensure separating on the event level, but I think some snapshots has lightly combined events before.
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.
Im trying to remember which does but I don't think we've combined any snapshot events before, aside from delegation/undelegation and registration/deregistration and staker shares. Those need to be combined because those events are effectively pairs. Magnitude and allocations aren't though. When we snapshot the above, we take into account time series data on each event and then combine into a snapshot.
What we're doing here is creating snapshots and then joining these tables together. I'm a bit worried by the final join, because the allocations can always change irrespective of the max magnitude changing.
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.
Thought about this some more. One con of a separate table is state bloat. Max magnitudes update infrequently. However, we do need extensive test coverage on magnitudes not being affected by allocations/deallocation
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.
Also, maxMagnitude is initialized at 1e18 in the contracts and is only decreasing. In our queries we assume is is 0 if there is no event, which we shouldn't do
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.
Yes, updating maxMagnitude default to 1e18 when no MagnitudeUpdated event exists, matching contract initialization.
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 think keeping combined as is is the better approach, so I agree that we need an extensive test coverage to validate its correctness.
| AND ap.operator = osor.operator | ||
| ), | ||
| operators_with_allocated_stake AS ( |
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 should also check that the operator is registered to the operatorSet here. If the operator is not registered, then they should not earn rewards. Note that deregistrations have a 14 day queue (like deallocations).
The OperatorRemovedFromOperatorSet event emits the operator and operatorSet. However, the operator is still slashable for 14 days here. Likely need to create a rewards only registration table to handle this (similar to the shares table)?
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 update the event to add a slashableUntil parameter if that helps here. Note that for total stake rewards (and the other opSet rewards types) they should immediately stop earning for a deregistration but for slashable stake we need to wait 14 days.
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.
- Added a new column
slashable_untilto track 14 day queue after deregistration occurs - migration file backfills all previous registration snapshot rows -> do you think this is an overkill? i don't think this will take long too do
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.
That should be fine. One thing we will have to document is that we are adding 14 day in times but the contracts add 14 days in blocks (we do this for the withdrawal queue too). Blocks can be missed onchain but time is continuous. It's a fine edge case.
b9da0a9 to
59f0d93
Compare
| // Create index for max_magnitude queries for performance | ||
| `CREATE INDEX IF NOT EXISTS idx_operator_allocation_snapshots_max_magnitude | ||
| ON operator_allocation_snapshots(operator, strategy, snapshot) | ||
| WHERE max_magnitude > 0`, |
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.
Max magnitude can theoretically go down to 0 for an operator. Is it fine if the migration checks on this?
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.
Yes, the partial index is fine as max_magnitude = 0 is always filtered out by the query in gold table 15 (line 60). Operators with zero capacity correctly receive no rewards, whether they're in the index or not.
| -- Mark the next_block_time as the end_time for the range | ||
| -- Use coalesce because if the next_block_time for a registration is not closed, then we use cutoff_date | ||
| COALESCE(next_block_time, '{{.cutoffDate}}')::timestamp AS end_time, | ||
| -- Calculate slashable_until based on deregistration |
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 probably have to deal with backwards compatibility 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.
Backwards compatibility is already handled! When adding the slashable_until column, the migration backfills slashable_until by recalculating from historical deregistration events in the operator_set_operator_registrations table
| -- Still active (no deregistration event): NULL | ||
| NULL | ||
| ELSE | ||
| -- Re-registered: use next event 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.
A bit confused on the use case for re-registered? Is this when an operator deregisters then registers again? Is there a case when next_is_active is not false or null? How is that triggered?
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 was initially thinking of deregister then register again, but that case is correctly covered. Removing ELSE clause.
Now:
- next_is_active = FALSE → Deregistration with 14-day slashability
- next_is_active IS NULL → Still active (no deregistration)
|
|
||
| const operatorAllocationSnapshotsQuery = ` | ||
| insert into operator_allocation_snapshots(operator, avs, strategy, operator_set_id, magnitude, snapshot) | ||
| insert into operator_allocation_snapshots(operator, avs, strategy, operator_set_id, magnitude, max_magnitude, snapshot) |
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.
Noting that since this table itself is net-new as part of this feature, no need to add hard fork if cases
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.
Noted and updated!
| block_time, | ||
| LAG(magnitude) OVER (PARTITION BY operator, avs, strategy, operator_set_id ORDER BY block_time, block_number, log_index) as previous_magnitude, | ||
| -- Backward compatibility: Apply new rounding logic only after Sabine fork | ||
| -- Pre-Sabine: Always round down to current day (old behavior) |
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.
Wait, I thought this entire table was post sabine???
| AND la.strategy = lmm.strategy | ||
| AND lmm.rn = 1 | ||
| WHERE la.rn = 1 | ||
| AND la.magnitude > 0 -- Only include active allocations |
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.
Im trying to remember which does but I don't think we've combined any snapshot events before, aside from delegation/undelegation and registration/deregistration and staker shares. Those need to be combined because those events are effectively pairs. Magnitude and allocations aren't though. When we snapshot the above, we take into account time series data on each event and then combine into a snapshot.
What we're doing here is creating snapshots and then joining these tables together. I'm a bit worried by the final join, because the allocations can always change irrespective of the max magnitude changing.
0f3b753 to
7d776d0
Compare
| -- Step 4: Calculate cumulative slash multiplier during deregistration queue | ||
| -- Slashing only affects rewards during the 14-day deregistration period | ||
| operators_with_slash_multiplier AS ( |
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 is the purpose of the slash multiplier 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.
If a slash has occurred, it should already be reflected in the operator's shares. All we need to do is verify that the operator is still slashable (ie. registered OR slashable_until not hit)
| osor.avs, | ||
| osor.operator_set_id, | ||
| DATE(b.block_time) as deregistration_date, | ||
| DATE(b.block_time) + INTERVAL '14 days' as slashable_until_date |
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.
slashable_until_date is different on mainnet and other envs
| "github.com/Layr-Labs/sidecar/pkg/rewardsUtils" | ||
| "go.uber.org/zap" | ||
| ) | ||
|
|
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 also need to add the events to the EigenState so they are indexed. See OperatorDirectedOperatorSetRewardsSubmissionCreated for an example
9246213 to
8863a05
Compare
1c4e8ae to
5ba36c3
Compare
allocate unique stake to operator set add more v2.2 tables fix function call clean v2.1 delete unused file implement total stake rewards
8863a05 to
cefb196
Compare
Description
Implements Rewards v2.2 - Operator Set rewards with automatic stake-weighted distribution for both unique allocated stake and total delegated stake. This brings forward-looking reward capabilities (up to 2 years) to operator sets managed via AllocationManager, closing the architectural gap where only AVSDirectory-based AVSs could create forward-looking rewards.
Unique Stake Rewards (Tables 15-17)
Rewards based on allocated unique stake to operator sets:
Total Stake Rewards (Tables 18-20)
Rewards based on total delegated stake (operator set analog to Rewards v1):
Refund Logic
Tables 17 and 20 calculate separate refund buckets (not subtractions) for three scenarios:
These refunds are tracked separately and combined in final staging tables.
Type of change
How Has This Been Tested?
Integration testing
Checklist: