该文档详细介绍了 EigenLayer 的奖励计算方法,包括数据提取、数据转换和奖励计算三个阶段。重点包括快照的生成以及对 Staker 和 Operator 的奖励分配,尤其是AVS奖励分配和所有参与者的奖励分配。
# EigenLayer 奖励计算 - 程序化激励
之前的奖励计算链接是 [这里](https://hackmd.io/u-NHKEvtQ7m7CVDb4\_42bA)。Diff 在 [这里](https://www.diffchecker.com/zxczJFZr/)
[TOC]
# 概述
EigenLayer 奖励计算是一组 SQL 查询,用于计算通过 `RewardsCoordinator` 从 AVS 向 staker 和 operator 分发的奖励。数据被提取到 Postgres 表中,并经过转换以计算最终奖励。
这些查询在一个每日任务中运行,该任务使用核心合约状态的快照来计算来自任何有效奖励提交的奖励。
## 计算过程 计算过程分为 3 个阶段,并且每天运行
1. 数据提取:从事件日志中提取数据。这些被称为青铜表。作为此过程的一部分,我们会协调所有事件数据,包括 RPC 数据,以及额外的数据提供者,以确保一致性。 2. 数据转换:将青铜表中的数据转换为链上状态的每日快照。 3. 奖励计算:将快照与有效的奖励提交进行交叉引用,以计算给 staker 和 operator 的奖励。
然后,管道将所有奖励汇总到 `lastRewardTimestamp + calculationIntervalSeconds`,并提交一个 root,该 root 将每个 earner 的累积总和进行 Merkle 化,提交给 `RewardsCoordinator`。
## 任务排序 奖励计算是一个 airflow 管道,每天 UTC 时间 16:00 运行。对链上事件和事件计算的查询都向下舍入到 UTC 时间 0:00。也就是说,如果管道在 4 月 27 日 UTC 时间 16:00 运行,则 `cutoff_date` 参数设置为 4 月 26 日 UTC 时间 0:00。
我们通过在 UTC 时间 0:00 之后几个小时运行每日管道来处理重组(reorgs),从而使我们的重组处理程序有足够的时间来修复状态。 ## 关键考虑因素
以下三个部分中的每一部分都详细说明了在阅读查询和理解计算时需要注意的关键考虑因素。这些考虑因素的总结如下:
- 每 24 小时拍摄一次核心合约状态的快照:`RewardsCoordinator` 中的 `SNAPSHOT_CADENCE` - 来自链上状态的快照*向上舍入*到最近的一天 UTC 时间 0:00。唯一的例外是 operator<>avs 的取消注册,它会*向下舍入*到最近的一天 UTC 时间 0:00 - 由于快照是向上舍入的,因此我们只关心一天的*最新状态更新* - 分发给所有 earner 的奖励必须 <= 奖励提交的支付金额
## 词汇表 - `earner`:接收奖励的实体,即 staker 或 operator - `calculationIntervalSeconds`:奖励提交的持续时间必须是的倍数 - `SNAPSHOT_CADENCE`:拍摄 EigenLayer 核心合约状态快照的频率 - `typo rewardSnaphot -> rewardSnapshot`:快照时给 earner 的奖励 - `cutoff-date`:运行转换的日期。始终设置为*前一天*的 UTC 时间 0:00 - `run`:每日奖励管道任务的一次迭代 - `stakeWeight`:AVS 如何评估其 earner 的 stake,由奖励的每个策略的乘数给出 - `gold_table`:包含 `rewardSnapshots` 的表。其列为 `earner`、`amount`、`token`、`snapshot`、`reward_hash` # 数据提取 ## 关键考虑因素 Shares 被转换为 Decimal(78,0),这是一种可以容纳高达 uint256 的数据类型。为了进行存款而列入白名单的 token(所有 LST 和 Eigen)以及 Native ETH 不应存在截断问题。
## 截止日期 我们在每次运行开始时设置截止日期,逻辑如下:
```python= def get_cutoff_date():
ts = datetime.now(timezone.utc)
ts = ts.replace(hour=0, minute=0, second=0, microsecond=0)
ts = ts - timedelta(days=1)
return ts ```
## Airflow 变量 在管道的每日运行时,如果该运行是回填,我们会获取传入的变量。在回填运行时,我们强制执行开始和结束日期有效,即结束日期不得晚于截止日期,并且开始日期不得晚于结束日期。
如果在管道运行中遗漏了事件,则在最坏的情况下会运行回填。我们与多个数据供应商运行协调,以确保不必这样做。此外,我们在管道生成的最后运行一个健全性检查查询,以确保: 1. earner 的累积奖励永远不会减少 2. AVS 的每日 token 始终 >= 给定快照和奖励哈希的 sum(earner payouts) 3. (`earner`, `reward_hash`, 和 `snapshot`) 的行数永不减少 ```python= def get_gold_calculation_dates(**kwargs):
cutoff_date = get_cutoff_date() cutoff_date_str = cutoff_date.strftime('%Y-%m-%d %H:%M:%S')
dag_run = kwargs.get('dag_run') if dag_run is not None: start_date_str = dag_run.conf.get('start_date', '1970-01-01 00:00:00') end_date_str = dag_run.conf.get('end_date', cutoff_date_str) is_backfill = str.lower(dag_run.conf.get('is_backfill', 'false')) else: raise ValueError('Dag run is None')
start_datetime = datetime.strptime(start_date_str, '%Y-%m-%d %H:%M:%S') end_datetime = datetime.strptime(end_date_str, '%Y-%m-%d %H:%M:%S') cutoff_datetime = datetime.strptime(cutoff_date_str, '%Y-%m-%d %H:%M:%S')
if start_datetime >= end_datetime: raise ValueError('开始日期必须在结束日期之前')
if end_datetime > cutoff_datetime: raise ValueError('结束日期必须小于或等于截止日期')
kwargs['ti'].xcom_push(key='cutoff_date', value=end_date_str) kwargs['ti'].xcom_push(key='rewards_start', value=start_date_str) kwargs['ti'].xcom_push(key='is_backfill', value=is_backfill) ```
## 查询 这组查询从 EigenLayer Core 合约中提取事件数据。事件日志从合约 ABI 自动解码[这里](https://github.com/Layr-Labs/eigenlayer-contracts)。运行 `forge build` 将构建合约,并且 ABI 存储在 `/out` 文件夹中。
在以下查询中,`block_date` 是块的日期,而 `block_time` 是块的完整日期 + 时间。
### Staker 状态 #### 存款
```sql= SELECT lower(coalesce(t.output_data ->> 'depositor', t.output_data ->> 'staker')) as staker, lower(t.output_data ->> 'strategy') as strategy, (t.output_data ->> 'shares')::numeric(78,0) as shares, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs as t LEFT JOIN blocks as b ON (t.block_sequence_id = b.id) WHERE t.address = '{{ var('strategy_manager_address') }}' AND t.event_name = 'Deposit' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
*注意:Shares 可以为负数*
```sql= SELECT lower(t.arguments #>> '{0,Value}') AS staker, (t.output_data ->> 'sharesDelta')::numeric(78,0) as shares, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id WHERE t.address = '{{ var('eigen_pod_manager_address') }}' AND t.event_name = 'PodSharesUpdated' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
#### M1 提款 M1 中的提款通过 StrategyManager 路由。请注意,我们删除了作为 M1 中 shares 完成的单个提款,因为此代码路径没有存款事件。
```sql= SELECT lower(coalesce(t.output_data ->> 'depositor', t.output_data ->> 'staker')) as staker, lower(t.output_data ->> 'strategy') as strategy, (t.output_data ->> 'shares')::numeric(78,0) as shares, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id WHERE t.address = '{{ var('strategy_manager_address') }}' AND t.event_name = 'ShareWithdrawalQueued' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' -- 删除此交易哈希,因为它是 m1 上唯一作为 shares 完成的提款。没有相应的存款事件。提款已完成到相同的 staker 地址。 AND t.transaction_hash != '0x62eb0d0865b2636c74ed146e2d161e39e42b09bac7f86b8905fc7a830935dc1e' ```
#### M2 提款 与 M1 提款事件不同,M2 提款事件返回一个元组,其中包含策略和 shares 的列表。因此,我们将元组解包到单独的行中以创建 (staker, strategy$_0$, share$_0$)、(staker, strategy$_1$, share$_1$)。我们丢弃所有从 M1 迁移的 M2 提款,因此我们不会重复计算提款。
```sql= WITH migrations AS ( SELECT ( SELECT lower(string_agg(lpad(to_hex(elem::int), 2, '0'), '')) FROM jsonb_array_elements_text(t.output_data->'oldWithdrawalRoot') AS elem ) AS m1_withdrawal_root, ( SELECT lower(string_agg(lpad(to_hex(elem::int), 2, '0'), '')) FROM jsonb_array_elements_text(t.output_data->'newWithdrawalRoot') AS elem ) AS m2_withdrawal_root FROM transaction_logs t WHERE t.address = '{{ var('delegation_manager_address') }}' AND t.event_name = 'WithdrawalMigrated' ), full_m2_withdrawals AS ( SELECT lower(t.output_data #>> '{withdrawal}') as withdrawals, ( SELECT lower(string_agg(lpad(to_hex(elem::int), 2, '0'), '')) FROM jsonb_array_elements_text(t.output_data ->'withdrawalRoot') AS elem ) AS withdrawal_root, lower(t.output_data #>> '{withdrawal, staker}') AS staker, lower(t_strategy.strategy) AS strategy, (t_share.share)::numeric(78,0) AS shares, t_strategy.strategy_index, t_share.share_index, t.transaction_hash, t.log_index, b.block_time::timestamp(6), to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id, jsonb_array_elements_text(t.output_data #> '{withdrawal, strategies}') WITH ORDINALITY AS t_strategy(strategy, strategy_index), jsonb_array_elements_text(t.output_data #> '{withdrawal, shares}') WITH ORDINALITY AS t_share(share, share_index) WHERE t.address = '{{ var('delegation_manager_address') }}' AND t.event_name = 'WithdrawalQueued' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' AND t_strategy.strategy_index = t_share.share_index ) -- 解析出从 m1 迁移的 m2 提款 SELECT full_m2_withdrawals.* FROM full_m2_withdrawals LEFT JOIN migrations ON full_m2_withdrawals.withdrawal_root = migrations.m2_withdrawal_root WHERE migrations.m2_withdrawal_root IS NULL ```
### Operator 状态 Operator 状态由 staker 委托给他们的 stake 组成。
#### Operator Shares 增加
```sql= SELECT lower(t.arguments #>> '{0,Value}') as operator, lower(t.output_data ->> 'strategy') as strategy, (t.output_data ->> 'shares')::numeric(78,0) as shares, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id WHERE t.address = '{{ var('delegation_manager_address') }}' AND t.event_name = 'OperatorSharesIncreased' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
#### Operator Shares 减少
```sql= SELECT lower(t.arguments #>> '{0,Value}') as operator, lower(t.output_data ->> 'strategy') as strategy, (t.output_data ->> 'shares')::numeric(78,0) as shares, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id WHERE t.address = '{{ var('delegation_manager_address') }}' AND t.event_name = 'OperatorSharesDecreased' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
### Staker 委托
#### Staker 已委托
```sql= SELECT lower(t.arguments #>> '{0,Value}') AS staker, lower(t.arguments #>> '{1,Value}') AS operator, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id WHERE t.address = '{{ var('delegation_manager_address') }}' AND t.event_name = 'StakerDelegated' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
#### Staker 已取消委托
```sql= SELECT lower(t.arguments #>> '{0,Value}') AS staker, lower(t.arguments #>> '{1,Value}') AS operator, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id WHERE t.address = '{{ var('delegation_manager_address') }}' AND t.event_name = 'StakerUndelegated' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
### 奖励提交 协议中有两种类型的奖励提交: 1. AVS 奖励提交:任何 AVS 调用的无需许可的函数 2. 所有人的奖励:给协议的所有 staker 的具有许可的奖励
*注意:RewardsCoordinator 中的金额的最大值为 $1e38-1$,这允许我们将其截断为 DECIMAL(38,0)。*
#### AVS 奖励提交 对于每个奖励提交,我们在单独的行中提取每个 (strategy,multiplier),以便于记账
```sql= SELECT lower(tl.arguments #>> '{0,Value}') AS avs, lower(tl.arguments #>> '{2,Value}') AS reward_hash, coalesce(lower(tl.output_data #>> '{rewardsSubmission}'), lower(tl.output_data #>> '{rangePayment}')) as rewards_submission, coalesce(lower(tl.output_data #>> '{rewardsSubmission, token}'), lower(tl.output_data #>> '{rangePayment, token}')) as token, coalesce(tl.output_data #>> '{rewardsSubmission,amount}', tl.output_data #>> '{rangePayment,amount}')::numeric(78,0) as amount, to_timestamp(coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint)::timestamp(6) as start_timestamp, coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint as duration, to_timestamp( coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint + coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint )::timestamp(6) as end_timestamp, lower(t.entry ->> 'strategy') as strategy, (t.entry ->> 'multiplier')::numeric(78,0) as multiplier, t.strategy_index as strategy_index, tl.transaction_hash, tl.log_index, b.block_time::timestamp(6), to_char(b.block_time, 'YYYY-MM-DD') AS block_date, tl.block_number FROM transaction_logs tl LEFT JOIN blocks b ON (tl.block_sequence_id = b.id) CROSS JOIN LATERAL jsonb_array_elements( coalesce(tl.output_data #> '{rewardsSubmission,strategiesAndMultipliers}',tl.output_data #> '{rangePayment,strategiesAndMultipliers}') ) WITH ORDINALITY AS t(entry, strategy_index) WHERE address = '{{ var('rewards_coordinator_address') }}' AND event_name = 'AVSRewardsSubmissionCreated' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
#### 所有人的奖励提交
```sql=
SELECT lower(tl.arguments #>> '{0,Value}') AS avs, -- 保留为 AVS,以便与 range_payments 上的 unioning 兼容。 lower(tl.arguments #>> '{2,Value}') AS reward_hash, coalesce(lower(tl.output_data #>> '{rewardsSubmission}'), lower(tl.output_data #>> '{rangePayment}')) as rewards_submission, coalesce(lower(tl.output_data #>> '{rewardsSubmission, token}'), lower(tl.output_data #>> '{rangePayment, token}')) as token, coalesce(tl.output_data #>> '{rewardsSubmission,amount}', tl.output_data #>> '{rangePayment,amount}')::numeric(78,0) as amount, to_timestamp(coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint)::timestamp(6) as start_timestamp, coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint as duration, to_timestamp( coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint + coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint )::timestamp(6) as end_timestamp, lower(t.entry ->> 'strategy') as strategy, (t.entry ->> 'multiplier')::numeric(78,0) as multiplier, t.strategy_index as strategy_index, tl.transaction_hash, tl.log_index, b.block_time::timestamp(6), to_char(b.block_time, 'YYYY-MM-DD') AS block_date, tl.block_number FROM transaction_logs tl LEFT JOIN blocks b ON tl.block_sequence_id = b.id CROSS JOIN LATERAL jsonb_array_elements( coalesce(tl.output_data #> '{rewardsSubmission,strategiesAndMultipliers}',tl.output_data #> '{rangePayment,strategiesAndMultipliers}') ) WITH ORDINALITY AS t(entry, strategy_index) WHERE address = '{{ var('rewards_coordinator_address') }}' AND event_name = 'RewardsSubmissionForAllCreated' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
#### 所有 Earner 的奖励提交
```sql= SELECT lower(tl.arguments #>> '{0,Value}') AS avs, -- 保留为 AVS,以便与 range_payments 上的 unioning 兼容。 lower(tl.arguments #>> '{2,Value}') AS reward_hash, coalesce(lower(tl.output_data #>> '{rewardsSubmission}'), lower(tl.output_data #>> '{rangePayment}')) as rewards_submission, coalesce(lower(tl.output_data #>> '{rewardsSubmission, token}'), lower(tl.output_data #>> '{rangePayment, token}')) as token, coalesce(tl.output_data #>> '{rewardsSubmission,amount}', tl.output_data #>> '{rangePayment,amount}')::numeric(78,0) as amount, to_timestamp(coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint)::timestamp(6) as start_timestamp, coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint as duration, to_timestamp( coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint + coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint )::timestamp(6) as end_timestamp, lower(t.entry ->> 'strategy') as strategy, (t.entry ->> 'multiplier')::numeric(78,0) as multiplier, t.strategy_index as strategy_index, tl.transaction_hash, tl.log_index, b.block_time::timestamp(6), to_char(b.block_time, 'YYYY-MM-DD') AS block_date, tl.block_number FROM transaction_logs tl LEFT JOIN blocks b ON tl.block_sequence_id = b.id CROSS JOIN LATERAL jsonb_array_elements( coalesce(tl.output_data #> '{rewardsSubmission,strategiesAndMultipliers}',tl.output_data #> '{rangePayment,strategiesAndMultipliers}') ) WITH ORDINALITY AS t(entry, strategy_index) WHERE address = '{{ var('rewards_coordinator_address') }}' AND event_name = 'RewardsSubmissionForAllEarnersCreated' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
### Operator<>AVS 状态
从 Operator 到 AVS 的每次注销和注册都记录在 AVSDirectory 中
#### Operator 注册 ```sql= SELECT lower(t.arguments #>> '{0,Value}') as operator, lower(t.arguments #>> '{1,Value}') as avs, (t.output_data -> 'status')::int as status, t.transaction_hash, t.log_index, b.block_time, to_char(b.block_time, 'YYYY-MM-DD') AS block_date, t.block_number FROM transaction_logs t LEFT JOIN blocks b ON t.block_sequence_id = b.id WHERE t.address = '{{ var('avs_directory_address') }}' AND t.event_name = 'OperatorAVSRegistrationStatusUpdated' AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}' ```
#### Operator 重新质押的策略 AVS Directory **不会**针对 operator 在 AVS 上重新质押或取消质押的策略发出事件。为了检索此信息,我们每 3600 个块运行一个 cron 任务(即 `blockNum % 3600 = 0`),从部署 AVSDirectory 时开始,该任务: 1. 检索在 AVS 上重新质押的所有 operator 2. 在每个 AVS 的 serviceManager 合约上调用 `getOperatorRestakedStrategies(address operator) returns (address[])`
AVS 必须符合此接口的要求,如我们的 [文档](https://docs.eigenlayer.xyz/eigenlayer/avs-guides/avs-dashboard-onboarding) 中所述
假设 operator 在时间戳 $t$ 注册到 AVS,则此 cron 任务的示例输出为:
Operator | AVS | Strategy | Block Time |
---|---|---|---|
Operator1 | AVS-A | stETH | t |
Operator1 | AVS-A | rETH | t |
Operator2 | AVS-A | rETH | t |
Operator3 | AVS-B | cbETH | t |
# 数据转换 一旦我们提取了 EigenLayer 核心合约和 AVS 的所有日志和相关存储,我们就将其转换为创建状态的每日快照,分为两部分
1. 将提取数据聚合到链上合约状态 2. 将状态组合成范围并展开为每日快照 ## 关键考虑因素
在第 2 部分中,一旦状态被聚合,我们就将状态范围展开为每日快照。
***状态快照向上舍入到最近的一天,但 operator<>avs 注销除外,它会向下舍入***。假设我们有以下范围,其中事件 A 和 B 是 staker 的 shares 的更新。
``` GENESIS_TIMESTAMP---------------------Day1---------------------Day2 ^ ^ A=100 B=200 ``` 快照转换的输出应表示在 Day1,Staker 拥有 200 个 shares。更一般地,我们采用 [Day$_{i-1}$, Day$_i$] 范围内的*最新*更新,并将其设置为 Day$_i$ 的状态。我们将给定日期的奖励称为*奖励快照*。
### Operator<>AVS 注册/注销 在 operator 注册和注销的情况下:
``` GENESIS_TIMESTAMP---------------------Day1---------------------Day2 ^ ^ Register Deregister ```
最终状态是 operator 已在第 1 天注册和注销,导致没有对 operator 进行任何奖励。我们添加此机制是为了保护 operator,防止我们在向上舍入注销时获得额外的奖励天数。副作用如下:
``` --------------Day1--------------Day2--------------Day3--------------Day4 ^ ^ Register Deregister ```
在这种情况下,operator 将在 Day3 注销,导致 operator 在 [Day3,Day4] 范围内没有收到任何奖励,因为它正在保护 AVS。向下舍入注销是为什么 `cutoff_date` 必须是*前一天*的 UTC 时间 0:00 的原因。
## 第 1 部分:聚合
### Staker Shares staker $s$ 和策略 $y$ 的 LST shares 由下式给出:
Shares$_{s,y}$ = Deposits$_{s,y}$ $-$ M1Withdrawals$_{s,y}$ $-$ M2Withdrawals$_{s,y}$ 对于一个 staker 来说,其 Native ETH 份额是该 staker 所有的 `PodSharesUpdated` 事件的总和。请注意,在此事件中,份额 可能为负数。
NativeETHShares$_s$ = $\sum_{i=0}^{n}$ PodSharesUpdated$_i$- M1Withdrawals$_i$ - M2Withdrawals$_i$
将这两个部分结合起来,我们就可以得到每次更新时,每个策略下 staker 的份额。
这个查询的关键部分是:
```sql= SUM(shares) OVER (PARTITION BY staker, strategy ORDER BY block_time, log_index) AS shares, ``` 这将获得每个 (staker, strategy) 对在每次更新时的 running sum。
```sql= SELECT staker, strategy, -- Sum each share amount over the window to get total shares for each (staker, strategy) at every timestamp update */ SUM(shares) OVER (PARTITION BY staker, strategy ORDER BY block_time, log_index) AS shares, transaction_hash, log_index, strategy_index, block_time, block_date, block_number FROM ( SELECT staker, strategy, shares, 0 as strategy_index, transaction_hash, log_index, block_time, block_date, block_number FROM {{ ref('staker_deposits') }}
UNION ALL
-- Subtract m1 & m2 withdrawals SELECT staker, strategy, shares * -1, 0 as strategy_index, transaction_hash, log_index, block_time, block_date, block_number FROM {{ ref('m1_staker_withdrawals') }}
UNION ALL
SELECT staker, strategy, shares * -1, strategy_index, transaction_hash, log_index, block_time, block_date, block_number FROM {{ ref('m2_staker_withdrawals') }}
UNION all
-- Shares in eigenpod are positive or negative, so no need to multiply by -1 SELECT staker, '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' as strategy, shares, 0 as strategy_index, transaction_hash, log_index, block_time, block_date, block_number FROM {{ ref('eigenpod_shares') }} ) combined_staker_shares
```
注意:对于所有人的奖励将不会支付给那些尚未向执行层证明其信标链余额的 staker。
对于一个策略 $y$,一个 operator $o$ 的份额由下式给出: $Shares_{o,y} = ShareIncrease_{o,y} - ShareDecrease_{o,y}$
```sql= SELECT operator, strategy, -- Sum each share amount over the window to get total shares for each (operator, strategy) at every timestamp update */ SUM(shares) OVER (PARTITION BY operator, strategy ORDER BY block_time, log_index) AS shares, transaction_hash, log_index, block_time, block_date, block_number FROM ( SELECT operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number FROM {{ ref('operator_share_increases') }}
UNION ALL
SELECT operator, strategy, shares * -1 AS shares, transaction_hash, log_index, block_time, block_date, block_number FROM {{ ref('operator_share_decreases') }} ) combined_shares ```
在这里,我们将每个委托和取消委托聚合到一个单独的视图中。当一个 staker 被取消委托时,我们将其 operator 标记为 `0x0000000000000000000000000000000000000000`.
```sql= SELECT staker, CASE when src = 'undelegations' THEN '0x0000000000000000000000000000000000000000' ELSE operator END AS operator, transaction_hash, log_index, block_time, block_date, block_number FROM ( SELECT *, 'undelegations' AS src FROM {{ ref('staker_undelegations') }} UNION ALL SELECT *, 'delegations' AS src FROM {{ ref('staker_delegations') }} ) as delegations_combined ```
将 AVS 奖励提交和所有人的奖励提交合并到一个视图中,并添加一个 `reward_type` 参数。
```sql= SELECT *, 'avs' as reward_type from {{ ref('avs_reward_submissions') }}
UNION ALL
SELECT *, 'all_stakers' as reward_type from {{ ref('reward_submission_for_all') }}
UNION ALL
SELECT *, 'all_earners' as reward_type from {{ ref('reward_submission_for_all_earners') }} ```
将 AVSDirectory 中的 状态元组 格式化为 `true` 或 `false`。 ```sql= SELECT operator, avs, CASE WHEN status = 1 then true ELSE false END AS registered, transaction_hash, log_index, block_date, block_time, block_number FROM {{ ref('operator_avs_registrations') }} ```
一旦我们转换了链上状态,我们就会将状态聚合到状态处于活动状态的时间窗口中。最后,状态窗口被展开为每日快照。
正如我们在上面的考虑事项中解释的那样,关键的设计决策是:状态总是向上取整到最近的一天 0:00 UTC,除了 operator<>avs 注销。
1. Ranked_staker_records:对给定 staker、策略和日期的每个记录进行排名。排名越低,记录在窗口中的时间越晚
```sql= WITH ranked_staker_records as ( SELECT *, ROW_NUMBER() OVER (PARTITION BY staker, strategy, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn FROM {{ ref('staker_shares') }} ), ```
2. Snapshotted_records:选择每天的最新记录。向上取整记录以创建一个 `snapshot_time`,即每天的最新记录,并向上取整到快照日
```sql= snapshotted_records as ( SELECT staker, strategy, shares, block_time, date_trunc('day', block_time) + INTERVAL '1' day AS snapshot_time from ranked_staker_records where rn = 1 ), ``` 3. Staker_share_windows:获取每个 staker、策略、份额的范围
```sql= staker_share_windows as ( SELECT staker, strategy, shares, snapshot_time as start_time, CASE -- If the range does not have the end, use the current timestamp truncated to 0 UTC WHEN LEAD(snapshot_time) OVER (PARTITION BY staker, strategy ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{ var("cutoff_date") }}') ELSE LEAD(snapshot_time) OVER (PARTITION BY staker, strategy ORDER BY snapshot_time) END AS end_time FROM snapshotted_records ) SELECT * from staker_share_windows ```
我们在 `staker_share_windows` CTE 中使用了 `LEAD` 运算符。这个运算符找到给定 (`staker`, `strategy`) 组合的下一个记录。逻辑是:
注意:上述逻辑可能存在 (`staker`, `strategy`) 组合,其中一个记录的 `end_record` 等于下一个记录的 `start_time`。当我们将窗口展开为快照时,会处理这个问题。
```sql= WITH ranked_operator_records as ( SELECT *, ROW_NUMBER() OVER (PARTITION BY operator, strategy, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn FROM {{ ref('operator_shares') }} ), -- Get the latest record for each day & round up to the snapshot day snapshotted_records as ( SELECT operator, strategy, shares, block_time, date_trunc('day', block_time) + INTERVAL '1' day as snapshot_time from ranked_operator_records where rn = 1 ), -- Get the range for each operator, strategy pairing operator_share_windows as ( SELECT operator, strategy, shares, snapshot_time as start_time, CASE -- If the range does not have the end, use the current timestamp truncated to 0 UTC WHEN LEAD(snapshot_time) OVER (PARTITION BY operator, strategy ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{ var("cutoff_date") }}') ELSE LEAD(snapshot_time) OVER (PARTITION BY operator, strategy ORDER BY snapshot_time) END AS end_time FROM snapshotted_records ) SELECT * from operator_share_windows ```
这个逻辑与 Staker Share Windows 完全相同。
```sql= with ranked_delegations as ( SELECT *, ROW_NUMBER() OVER (PARTITION BY staker, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn FROM {{ ref('staker_delegation_status') }} ), -- Get the latest record for each day & round up to the snapshot day snapshotted_records as ( SELECT staker, operator, block_time, date_trunc('day', block_time) + INTERVAL '1' day AS snapshot_time from ranked_delegations where rn = 1 ), -- Get the range for each staker staker_delegation_windows as ( SELECT staker, operator, snapshot_time as start_time, CASE -- If the range does not have the end, use the cutoff date truncated to 0 UTC WHEN LEAD(snapshot_time) OVER (PARTITION BY staker ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{ var("cutoff_date") }}') ELSE LEAD(snapshot_time) OVER (PARTITION BY staker ORDER BY snapshot_time) END AS end_time FROM snapshotted_records ) SELECT * from staker_delegation_windows
```
这个逻辑与 Staker Share Windows 完全相同。
此计算与上述 3 个查询不同,因为注册窗口不能彼此延续。例如,如果一个 operator 具有以下状态:
Operator | AVS | Registered | Date |
---|---|---|---|
Operator1 | AVS1 | True | 4-24-2024 |
Operator1 | AVS1 | False | 4-26-2024 |
Operator1 | AVS1 | True | 4-29-2024 |
Operator1 | AVS1 | False | 5-1-2024 |
Operator1 | AVS1 | True | 5-1-2024 |
Operator1 | AVS1 | False | 5-1-2024 |
我们不在意第 2 行和第 3 行的(注销,注册)窗口。我们还需要丢弃第 5 行和第 6 行的(注册,注销窗口)。
1. 标记每个注册之间的链接
```sql= WITH marked_statuses AS ( SELECT operator, avs, registered, block_time, block_date, -- Mark the next action as next_block_time LEAD(block_time) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS next_block_time, -- The below lead/lag combinations are only used in the next CTE -- Get the next row's registered status and block_date LEAD(registered) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS next_registration_status, LEAD(block_date) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS next_block_date, -- Get the previous row's registered status and block_date LAG(registered) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS prev_registered, LAG(block_date) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS prev_block_date FROM {{ ref('operator_avs_status') }} ), ``` 2. 忽略同一天发生的(注册,注销)对。这是为了确保在我们将注销向下舍入并将注册向上舍入后,该分组不会被计算一次。
```sql= removed_same_day_deregistrations AS ( SELECT * from marked_statuses WHERE NOT ( -- Remove the registration part (registered = TRUE AND COALESCE(next_registration_status = FALSE, false) AND -- default to false if null COALESCE(block_date = next_block_date, false)) OR -- Remove the deregistration part (registered = FALSE AND COALESCE(prev_registered = TRUE, false) and COALESCE(block_date = prev_block_date, false) ) ) ), ``` 3. 将记录的 `end_time` 标记为下一个记录。如果不是这种情况,则将 `end_time` 标记为 `cutoff_date` ```sql= registration_periods AS ( SELECT operator, avs, block_time AS start_time, -- 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, TIMESTAMP '{{ var("cutoff_date") }}') AS end_time, registered FROM removed_same_day_deregistrations WHERE registered = TRUE ), ```
4. 向上取整每个 `start_time` 并向下取整每个 end_time ```sql= registration_windows_extra as ( SELECT operator, avs, date_trunc('day', start_time) + interval '1' day as start_time, -- End time is end time non inclusive becuase the operator is not registered on the AVS at the end time OR it is current timestamp rounded up date_trunc('day', end_time) as end_time FROM registration_periods ), ```
5. 去除具有相同 `start_time` 和 `end_time` 的记录:
```sql= operator_avs_registration_windows as ( SELECT * from registration_windows_extra WHERE start_time != end_time ) select * from operator_avs_registration_windows ```
此查询将来自 operator 重新质押的策略 cron 作业的条目聚合到窗口中。
1. 对所有记录排序。将 `block_time` 向上取整到 0 UTC。逻辑_index 是无关紧要的,因为此数据是从 RPC 调用生成的。 ```sql= with ranked_records AS ( SELECT lower(operator) as operator, lower(avs) as avs, lower(strategy) as strategy, block_time, date_trunc('day', CAST(block_time as timestamp(6))) + interval '1' day as start_time, ROW_NUMBER() OVER ( PARTITION BY operator, avs, strategy, date_trunc('day', CAST(block_time as timestamp(6))) + interval '1' day ORDER BY block_time DESC -- want latest records to be ranked highest ) AS rn -- Cannot use ref here because this table is not generated via DBT FROM public.operator_restaked_strategies -- testnet and holesky all exist together in the blocklake so the avs_directory_address allows us to filter WHERE avs_directory_address = lower('{{ var('avs_directory_address') }}') ), ```
2. 获取每个 (`operator`, `avs`, `strategy`, `day`) 组合的最新记录
```sql= latest_records AS ( SELECT operator, avs, strategy, start_time, block_time FROM ranked_records WHERE rn = 1 ), ```
3. 查找每个 (`operator`, `avs`, `strategy`) 分组的下一个条目。
```sql= grouped_records AS ( SELECT operator, avs, strategy, start_time, LEAD(start_time) OVER ( PARTITION BY operator, avs, strategy ORDER BY start_time ASC ) AS next_start_time FROM latest_records ), ```
4. 解析出任何漏洞(即任何 `next_start_times` 不是正好在当前记录的 `start_time` 之后的一天)。这是因为 operator 将已从 AVS 取消注册。
```sql= parsed_ranges AS ( SELECT operator, avs, strategy, start_time, -- If the next_start_time is not on the consecutive day, close off the end_time CASE WHEN next_start_time IS NULL OR next_start_time > start_time + INTERVAL '1' DAY THEN start_time ELSE next_start_time END AS end_time FROM grouped_records ), ```
5. 删除任何 `start_time` == `end_time` 的记录
```sql= active_windows as ( SELECT * FROM parsed_ranges WHERE start_time != end_time ), ```
6. 我们现在使用间隙和岛屿算法来查找 (`operator`, `avs`, `strategy`) 组合的连续组。首先,我们标记每行的 `prev_end_time`。如果有一个新的窗口,那么间隙是空的
```sql= gaps_and_islands AS ( SELECT operator, avs, strategy, start_time, end_time, LAG(end_time) OVER(PARTITION BY operator, avs, strategy ORDER BY start_time) as prev_end_time FROM active_windows ), ```
7. 接下来,我们检测岛屿。如果 `prev_end_time` 与 `start_time` 相同,则记录是同一连续分组的一部分。
```sql= island_detection AS ( SELECT operator, avs, strategy, start_time, end_time, prev_end_time, CASE -- If the previous end time is equal to the start time, then mark as part of the island, else create a new island WHEN prev_end_time = start_time THEN 0 ELSE 1 END as new_island FROM gaps_and_islands ), ```
8. 根据每个记录的 `new_island` 创建组。具有相同 `island_id` 总和的行属于同一分组
```sql= island_groups AS ( SELECT operator, avs, strategy, start_time, end_time, SUM(new_island) OVER ( PARTITION BY operator, avs, strategy ORDER BY start_time ) AS island_id FROM island_detection ), ```
9. 将组合并在一起
```sql= operator_avs_strategy_windows AS ( SELECT operator, avs, strategy, MIN(start_time) AS start_time, MAX(end_time) AS end_time FROM island_groups GROUP BY operator, avs, strategy, island_id ORDER BY operator, avs, strategy, start_time ) select * from operator_avs_strategy_windows ```
一旦我们为核心合约状态的每个表创建了窗口,我们会将这些窗口展开为每日快照。在下面的每个查询中,我们将 `end_time` 向下舍入一天,因为新记录可以在同一天开始,或者它将在 `cutoff_date` 之后的单独管道运行中包含。
```sql= WITH cleaned_records as ( SELECT * FROM {{ ref('staker_share_windows')}} WHERE start_time < end_time ) SELECT staker, strategy, shares, cast(day AS DATE) AS snapshot FROM cleaned_records CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day ```
我们删除任何具有错误的 `start_time` 和 `end_time` 值的记录。然后,我们展开整个范围。
```sql= WITH cleaned_records as ( SELECT * FROM {{ ref('operator_share_windows')}} WHERE start_time < end_time ) SELECT operator, strategy, shares, cast(day AS DATE) AS snapshot FROM cleaned_records CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day ```
```sql= WITH cleaned_records as ( SELECT * FROM {{ ref('staker_delegation_windows') }} WHERE start_time < end_time ) SELECT staker, operator, cast(day AS DATE) AS snapshot FROM cleaned_records CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day ```
```sql= WITH cleaned_records AS ( SELECT * FROM {{ ref('operator_avs_strategy_windows') }} WHERE start_time < end_time ) SELECT operator, avs, strategy, cast(day AS DATE) AS snapshot FROM cleaned_records CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day ```
```sql= WITH cleaned_records AS ( SELECT * FROM {{ ref('operator_avs_registration_windows') }} WHERE start_time < end_time ) SELECT operator, avs, day AS snapshot FROM cleaned_records CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day ```
# Reward Calculation
奖励分配是根据状态的每日快照计算的。例如,如果我们有一个针对以下范围的奖励提交:
``` Day0------Day1------Day2------Day3------Day4------Day5------Day6------Day7
```
奖励管道将从 7 个状态快照计算奖励分配:Day1, Day2,... Day7。我们包含最后一天 (Day7) 作为快照,而不是第一天 (Day0)。你可以将每个快照视为表示过去 24 小时内最新的状态更新。换句话说,$Day_i$ 表示来自 [$Day_{i-1}$ 00:00 UTC, $Day_{i-1}$ 23:59 UTC] 的最新状态。
由于快照向上取整到最近一天,除了 operator<>avs 注销,因此在同一天内的状态更新将收到完全相同的奖励金额。
例如,假设 staker A 和 B 具有等量的 stake,并且在以下时间选择加入同一 operator,则他们对 Day 2 `rewardSnaphot` 的奖励将相等:
``` Day1---------------------Day2 ^ ^ A B ```
这是一个已知的使用每日快照计算奖励的副作用。
此外,退出系统也具有相同的属性。如果 Operator J 和 K 在同一天内从同一 AVS 退出,他们都将不会收到来自 AVS 的奖励:
``` Day1---------------------Day2 ^ ^ J K ```
如上文转换部分所述,一个已知的副作用是,operator 可能会因在 2 个快照的边界从 AVS 注销而损失一天的奖励。例如:
``` --------Day1---------------------Day2---------------------Day3 ^ ^ Entry Exit ```
在上述场景中,operator 将被计为在 Day1 注册,并在 Day2 注销,即使他们仅验证了 AVS 将近 2 天。
每个奖励提交都有两个长度相等的数组:`strategies` 和 `multiplier`。管道使用此值来计算快照奖励的 earner 的 stakeWeight。对于给定快照 $d$ 上的 staker $s$,stakeWeight 由下式给出:
$stakeWeight_{s, d} = multiplier_i \cdot shares_{i,s,d}$
该计算也在 AVS 的 `StakeRegistry` 合约中完成。参考 solidity 实现。
一个关键的不变量是,对于给定奖励快照 $d$ 上的奖励提交 $r$, $Tokens_{r,d} >= \sum_{i=0}^{n=paidEarners} Earner_{i,r,d}$
换句话说,奖励提交的 `tokensPerDay` 不能小于 `rewardSnaphot` 的所有 earner 的奖励分配总和。我们将此称为从截断转换份额和乘数到 double 类型(最多可容纳 15 位有效数字)的关键考虑因素。
`RewardsCoordinator` 要求 `CALCULATION_INTERVAL_SECONDS % SNAPSHOT_CADENCE == 0`,这保证了每个奖励快照都将位于奖励范围的边界内。
根据奖励更新程序定义的某个节奏,管道将聚合所有奖励分配快照到某个时间戳 $t$。为了使根是“全新的”,它必须 merklize 大于 `lastRewardTimestamp` 的 `rewardSnaphot` 之后的状态。
如果 AVS 为没有重新质押任何策略的快照提供了奖励,则该奖励将不会重新分配到奖励提交的未来快照。请参阅 奖励快照 operator 一个具体的例子。
以下每个查询都是 `active_rewards` 视图的一部分的 CTE 集。 为了处理追溯奖励,我们寻找任何在 `cutoff_date` 之前开始的奖励。为了确保奖励不被重新计算,我们限制了用于计算有效奖励的快照范围。
下表将用于辅助可视化转换。假设管道的 `cutoff_date` 为 2024 年 4 月 29 日 0:00 UTC。此外,假设上次奖励快照是 2024 年 4 月 24 日。注意:由于 bronze 表查询和截止日期,奖励事件需要 2 天才能在当前快照的 gold 计算中显示出来。
AVS | 奖励哈希 | 开始时间戳 | 持续时间 | 结束时间戳 | 金额 | 策略 | 乘数 |
---|---|---|---|---|---|---|---|
AVS1 | 0xReward1 | 2024 年 4 月 21 日 | 21 天 | 2024 年 5 月 12 日 | 21e18 | stETH | 1e18 |
AVS1 | 0xReward1 | 2024 年 4 月 21 日 | 21 天 | 2024 年 5 月 12 日 | 21e18 | rETH | 2e18 |
为简洁起见,每个示例中仅显示表的相关行。
```sql= WITH active_rewards_modified as ( SELECT *, amount/(duration/86400) as tokens_per_day, cast('{{ var("cutoff_date") }}' AS TIMESTAMP(6)) as global_end_inclusive -- Inclusive means we DO USE this day as a snapshot FROM {{ ref('rewards_combined') }} WHERE end_timestamp >= TIMESTAMP '{{ var("rewards_start") }}' and start_timestamp <= TIMESTAMP '{{ var("cutoff_date") }}' ), ```
每日代币数 | 全局包含结束时间 |
---|---|
1e18 | 2024 年 4 月 27 日 |
```sql= active_rewards_updated_end_timestamps as ( SELECT avs, /** * Cut the start and end windows to handle * A. Retroactive rewards that came recently whose start date is less than start_timestamp * B. Don't make any rewards past end_timestamp for this run */ start_timestamp as reward_start_exclusive, LEAST(global_end_inclusive, end_timestamp) as reward_end_inclusive, tokens_per_day, token, multiplier, strategy, reward_type, reward_for_all, global_end_inclusive, block_date as reward_submission_date FROM active_rewards_modified ), ```
奖励开始时间不包含 | 奖励结束时间包含 |
---|---|
2024 年 4 月 21 日 | 2024 年 4 月 27 日 |
```sql= -- For each reward hash, find the latest snapshot active_rewards_updated_start_timestamps as ( SELECT ap.avs, CASE WHEN '{{ var("is_backfill") }}' = 'true' THEN ap.reward_start_exclusive ELSE COALESCE(MAX(g.snapshot), ap.reward_start_exclusive) END as reward_start_exclusive, ap.reward_end_inclusive, ap.token, ap.tokens_per_day as tokens_per_day_decimal, -- Round down to 15 sigfigs for double precision, ensuring know errouneous round up or down ap.tokens_per_day * ((POW(10, 15) - 1)/(POW(10, 15))) as tokens_per_day, ap.multiplier, ap.strategy, ap.reward_hash, ap.reward_type, ap.global_end_inclusive, ap.reward_submission_date FROM active_rewards_updated_end_timestamps ap LEFT JOIN {{ var('schema_name') }}.gold_table g ON g.reward_hash = ap.reward_hash GROUP BY ap.avs, ap.reward_end_inclusive, ap.token, ap.tokens_per_day, ap.multiplier, ap.strategy, ap.reward_hash, ap.global_end_inclusive, ap.reward_start_exclusive, ap.reward_for_all ), ```
根据最终 gold 表中 `reward_hash`(奖励哈希)的最新 `snapshot`(快照)限制 `reward_start_exclusive`(奖励开始时间不包含),该表包含每个 `snapshotReward`(快照奖励)。我们这样做是为了不重新计算奖励。如果 gold 表中没有快照,那么我们只使用奖励提交的原始 `reward_start_exclusive`(奖励开始时间不包含)。
此步骤还将 `tokens_per_day`(每日代币数)转换为 double 类型,该类型将用于奖励计算的其余部分。这使我们能够支持高达 1e38-1 的值,但代价是只有 15 位小数的精度。
最后,如果运行是回填,我们将开始时间戳设置为最早的 unix 时间戳`1970-01-01 00:00:00`。
假设 reward_hash 的最新 `snapshotReward`(快照奖励)是 2024 年 4 月 24 日。`reward_start_exclusive`(奖励开始时间不包含)的先前值为 2024 年 4 月 21 日。
奖励开始时间不包含 | 奖励结束时间包含 |
---|---|
2024 年 4 月 24 日 | 2024 年 4 月 28 日 |
```sql= active_reward_ranges AS ( SELECT * from active_rewards_updated_start_timestamps /** Take out (reward_start_exclusive, reward_end_inclusive) windows where * 1. reward_start_exclusive >= reward_end_inclusive: The reward period is done or we will handle on a subsequent run */ WHERE reward_start_exclusive < reward_end_inclusive ), ``` 解析出无效范围。如果当抢跑是回填,并且存在来自先前快照且大于我们正在回填的 `cutoff_time`(截止时间)的快照,则可能会发生这种情况。
```sql= exploded_active_range_rewards AS ( SELECT * FROM active_reward_ranges CROSS JOIN generate_series(DATE(reward_start_exclusive), DATE(reward_end_inclusive), INTERVAL '1' DAY) AS day ), ``` 为奖励范围内的每个快照创建一个行
AVS | 奖励哈希 | 日 | 策略 | 乘数 | ... |
---|---|---|---|---|---|
AVS1 | 0xReward1 | 2024 年 4 月 24 日 | stETH | 1e18 | |
AVS1 | 0xReward1 | 2024 年 4 月 25 日 | stETH | 1e18 | |
AVS1 | 0xReward1 | 2024 年 4 月 26 日 | stETH | 1e18 | |
AVS1 | 0xReward1 | 2024 年 4 月 27 日 | stETH | 1e18 | |
AVS1 | 0xReward1 | 2024 年 4 月 24 日 | rETH | 2e18 | |
AVS1 | 0xReward1 | 2024 年 4 月 25 日 | rETH | 2e18 | |
AVS1 | 0xReward1 | 2024 年 4 月 26 日 | rETH | 2e18 | |
AVS1 | 0xReward1 | 2024 年 4 月 27 日 | rETH | 2e18 |
```sql= active_rewards_final AS ( SELECT avs, cast(day as DATE) as snapshot, token, tokens_per_day, tokens_per_day_decimal, multiplier, strategy, reward_hash, reward_type, reward_submission_date FROM exploded_active_range_rewards -- Remove snapshots on the start day WHERE day != reward_start_exclusive ) select * from active_rewards_final ```
删除快照等于奖励的 reward_start_exclusive 的行。
AVS | 奖励哈希 | 快照 | 策略 | 乘数 | 奖励开始时间不包含 |
---|---|---|---|---|---|
<s>AVS1</s> | <s>0xReward1</s> | <s>2024 年 4 月 24 日</s> | <s>stETH</s> | <s>1e18</s> | <s>2024 年 4 月 24 日</s> |
AVS1 | 0xReward1 | 2024 年 4 月 25 日 | stETH | 1e18 | 2024 年 4 月 24 日 |
AVS1 | 0xReward1 | 2024 年 4 月 26 日 | stETH | 1e18 | 2024 年 4 月 24 日 |
AVS1 | 0xReward1 | 2024 年 4 月 27 日 | stETH | 1e18 | 2024 年 4 月 24 日 |
<s>AVS1</s> | <s>0xReward1</s> | <s>2024 年 4 月 24 日</s> | <s>rETH</s> | <s>2e18</s> | <s>2024 年 4 月 24 日</s> |
AVS1 | 0xReward1 | 2024 年 4 月 25 日 | rETH | 2e18 | 2024 年 4 月 24 日 |
AVS1 | 0xReward1 | 2024 年 4 月 26 日 | rETH | 2e18 | 2024 年 4 月 24 日 |
AVS1 | 0xReward1 | 2024 年 4 月 27 日 | rETH | 2e18 | 2024 年 4 月 24 日 |
在生成有效奖励后,管道接着计算奖励分配给 staker 运营商集合。
我们将奖励提交的 `multiplier`(乘数)和 staker 的 `shares`(份额)转换为 double 类型,以便对这些值进行计算。double 类型有 15 位有效数字,这种不精确性反映在计算 staker 的 `stakeWeight`(质押权重)、`staker_proportion`(质押者比例)、`total_staker_operator_reward`(总质押者运营商奖励)和 `staker_tokens`(质押者代币)的 CTE 中。必须存在以下情况:对于给定的奖励提交 $r$,$\sum_{i=0}^{n=avsStakerOperatorSet} stakeroperatorSetReward_{i, r} <= tokensPerDay_r$。
我们首先计算奖励分配给整个 staker 运营商集合,然后将其传递给运营商和 staker。
```sql= WITH reward_snapshot_operators as ( SELECT ap.reward_hash, ap.snapshot, ap.token, ap.tokens_per_day, ap.tokens_per_day_decimal, ap.avs, ap.strategy, ap.multiplier, ap.reward_type, ap.reward_submission_date, oar.operator FROM {{ ref('1_active_rewards') }} ap JOIN {{ ref('operator_avs_registration_snapshots') }} oar ON ap.avs = oar.avs and ap.snapshot = oar.snapshot WHERE ap.reward_type = 'avs' ), ``` 从 `active_rewards`(有效奖励)中,获取已注册 AVS 的运营商。我们过滤 `reward_for_all = false`,因为我们只关注 AVS 奖励提交。
假设 AVS 的运营商注册快照是。2024 年 4 月 27 日是有效快照,但 bronze 表不会包含来自此日期的事件(因为查询针对所有事件 < `cutoffDate`(截止日期))。在此运行中,不会为 2024 年 4 月 27 日支付任何 staker 或运营商。这就是两个快照延迟的由来。
对于没有运营商选择加入 AVS 的日期,1e18 的 `tokens_per_day`(每日代币数)不会重新分配给未来的 `rewardSnapshots`(奖励快照)。
运营商 | AVS | 快照 |
---|---|---|
Operator1 | AVS1 | 2024 年 4 月 25 日 |
Operator1 | AVS1 | 2024 年 4 月 26 日 |
Operator2 | AVS1 | 2024 年 4 月 25 日 |
Operator2 | AVS1 | 2024 年 4 月 26 日 |
```sql= _operator_restaked_strategies AS ( SELECT rso.* FROM reward_snapshot_operators rso JOIN {{ ref('operator_avs_strategy_snapshots') }} oas ON rso.operator = oas.operator AND rso.avs = oas.avs AND rso.strategy = oas.strategy AND rso.snapshot = oas.snapshot ), ```
从在 AVS 上重新质押的运营商中,获取每个 AVS 重新质押的策略和份额。
假设 AVS 上每个运营商的策略是:
运营商 | AVS | 策略 | 快照 |
---|---|---|---|
Operator1 | AVS1 | stETH | 2024 年 4 月 25 日 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 |
Operator1 | AVS1 | rETH | 2024 年 4 月 26 日 |
Operator2 | AVS1 | stETH | 2024 年 4 月 25 日 |
Operator2 | AVS1 | stETH | 2024 年 4 月 26 日 |
```sql= staker_delegated_operators AS ( SELECT ors.*, sds.staker FROM _operator_restaked_strategies ors JOIN {{ ref('staker_delegation_snapshots') }} sds ON ors.operator = sds.operator AND ors.snapshot = sds.snapshot ), ```
获取为快照委托给运营商的 staker。
运营商 | AVS | 策略 | 快照 | Staker |
---|---|---|---|---|
Operator1 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker1 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker1 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker2 |
Operator1 | AVS1 | rETH | 2024 年 4 月 26 日 | Staker1 |
Operator1 | AVS1 | rETH | 2024 年 4 月 26 日 | Staker2 |
Operator2 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker3 |
Operator2 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker3 |
```sql= staker_avs_strategy_shares AS ( SELECT sdo.*, sss.shares FROM staker_delegated_operators sdo JOIN {{ ref('staker_share_snapshots') }} sss ON sdo.staker = sss.staker AND sdo.snapshot = sss.snapshot AND sdo.strategy = sss.strategy -- Parse out negative shares and zero multiplier so there is no division by zero case WHERE sss.shares > 0 and sdo.multiplier != 0 ), ```
假设 staker 具有以下状态:
Staker | 策略 | 份额 | 快照 |
---|---|---|---|
Staker1 | stETH | 1e18 | 2024 年 4 月 25 日 |
Staker1 | stETH | 1e18 | 2024 年 4 月 26 日 |
Staker1 | rETH | 2e18 | 2024 年 4 月 26 日 |
Staker2 | stETH | 1e18 | 2024 年 4 月 26 日 |
Staker3 | stETH | 2e18 | 2024 年 4 月 25 日 |
Staker3 | stETH | 2e18 | 2024 年 4 月 26 日 |
连接将产生以下结果:
运营商 | AVS | 策略 | 快照 | Staker | 份额 |
---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker1 | 1e18 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker1 | 1e18 |
Operator1 | AVS1 | rETH | 2024 年 4 月 26 日 | Staker1 | 2e18 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker2 | 1e18 |
Operator2 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker3 | 2e18 |
Operator2 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker3 | 2e18 |
Staker2 没有 `rETH` 份额,这就是为什么此视图少一行。
```sql= staker_weights AS ( SELECT *, SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight FROM staker_avs_strategy_shares ), ```
计算 staker 的 `stakeWeight`(质押权重)。有关此计算的讨论请参见[上方](https://hackmd.io/Fmjcckn1RoivWpPLRAPwBw?view#Multiplier-Calculation)。
运营商 | AVS | 策略 | 快照 | Staker | 份额 | 乘数 | Staker Weight |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker1 | 1e18 | 1e18 | 1e36 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker1 | 1e18 | 1e18 | 3e36 |
Operator1 | AVS1 | rETH | 2024 年 4 月 26 日 | Staker1 | 1e18 | 2e18 | 3e36 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker2 | 1e18 | 1e18 | 1e36 |
Operator2 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker3 | 2e18 | 1e18 | 2e36 |
Operator2 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker3 | 2e18 | 1e18 | 2e36 |
```sql= distinct_stakers AS ( SELECT * FROM ( SELECT *, -- We can use an arbitrary order here since the staker_weight is the same for each (staker, strategy, hash, snapshot) -- We use strategy ASC for better debuggability ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, staker ORDER BY strategy ASC) as rn FROM staker_weights ) t WHERE rn = 1 ORDER BY reward_hash, snapshot, staker ), ```
在之前的计算中,质押权重是按(`reward_hash`,`staker`,`snapshot`)计算的。我们需要删除任何具有相同组合的行,因为下一步是计算每个快照的总质押权重。策略列无关紧要,因为最终奖励由 $tokensPerDay * stakerWeightProportion$ 给出。我们添加 order by 子句是为了使下一步中质押权重的总和是确定性的。
删除具有相同(`staker`,`reward_hash`,`snapshot`)的行
运营商 | AVS | 策略 | 快照 | Staker | 份额 | 乘数 | Staker Weight |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker1 | 1e18 | 1e18 | 1e36 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker1 | 1e18 | 1e18 | 3e36 |
<s>Operator1</s> | <s>AVS1</s> | <s>rETH</s> | <s>2024 年 4 月 26 日</s> | <s>Staker1</s> | <s>1e18</s> | <s>2e18</s> | <s>3e36</s> |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker2 | 1e18 | 1e18 | 1e36 |
Operator2 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker3 | 2e18 | 1e18 | 2e36 |
Operator2 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker3 | 2e18 | 1e18 | 2e36 |
```sql= staker_weight_sum AS ( SELECT *, SUM(staker_weight) OVER (PARTITION BY reward_hash, snapshot) as total_weight FROM distinct_stakers ), ```
获取给定(`reward_hash`,`snapshot`)的所有 staker 权重的总和
运营商 | AVS | 策略 | 快照 | Staker | 份额 | Staker Weight | Total Staker Weight |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker1 | 1e18 | 1e36 | 3e36 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker1 | 1e18 | 3e36 | 6e36 |
Operator1 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker2 | 1e18 | 1e36 | 6e36 |
Operator2 | AVS1 | stETH | 2024 年 4 月 25 日 | Staker3 | 2e18 | 2e36 | 3e36 |
Operator2 | AVS1 | stETH | 2024 年 4 月 26 日 | Staker3 | 2e18 | 2e36 | 6e36 |
```sql= staker_proportion AS ( SELECT *, FLOOR((staker_weight / total_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion FROM staker_weight_sum ), ```
计算 `snapshotReward`(快照奖励)的 staker 代币比例 我们向下舍入 `staker_proportion`(质押者比例),以确保 staker_proportions 的总和不大于 1。 | Operator | AVS | Strategy | Snapshot | Staker | Shares | Staker Weight | Total Staker Weight | Staker Proportion |
---|---|---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e36 | 3e36 | 0.333 | |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 3e36 | 6e36 | 0.5 | |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e36 | 6e36 | 0.166 | |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 2e36 | 3e36 | 0.666 | |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 2e36 | 6e36 | 0.333 |
```sql= staker_operator_total_tokens AS ( SELECT *, CASE -- 对于硬分叉之前的快照,以及在硬分叉之前提交的奖励,我们使用旧的计算方法 WHEN snapshot < '{{ var("amazon_hard_fork") }}' AND reward_submission_date < '{{ var("amazon_hard_fork") }}' THEN cast(staker_proportion * tokens_per_day AS DECIMAL(38,0)) WHEN snapshot < '{{ var("nile_hard_fork") }}' AND reward_submission_date < '{{ var("nile_hard_fork") }}' THEN (staker_proportion * tokens_per_day)::text::decimal(38,0) ELSE FLOOR(staker_proportion * tokens_per_day_decimal) END as total_staker_operator_payout FROM staker_proportion ), ```
我们已经对代码进行了 2 次硬分叉:
此代码反映了处理此边缘情况,以使回填情况下的计算具有幂等性。
计算将支付给所有 Staker 和 Operator 的 Token 数量。我们将结果从文本转换为 `DECIMAL(38,0)`,以避免损失任何精度。此外,对于给定的 Snapshot,Token 值适合此类型。
Operator | AVS | Strategy | Snapshot | Staker | Shares | Staker Weight | Total Staker Weight | Staker Proportion | Tokens Per Day | Total Staker Operator Reward |
---|---|---|---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e36 | 3e36 | 0.333 | 1e18 | 333333333333333000 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 3e36 | 6e36 | 0.5 | 1e18 | 5e17 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e36 | 6e36 | 0.166 | 1e18 | 166666666666666000 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 2e36 | 3e36 | 0.666 | 1e18 | 666666666666666000 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 2e36 | 6e36 | 0.333 | 1e18 | 333333333333333000 |
```sql= token_breakdowns AS ( SELECT *, CASE WHEN snapshot < '{{ var("amazon_hard_fork") }}' AND reward_submission_date < '{{ var("amazon_hard_fork") }}' THEN cast(total_staker_operator_payout * 0.10 AS DECIMAL(38,0)) WHEN snapshot < DATE '{{ var("nile_hard_fork") }}' AND reward_submission_date < '{{ var("nile_hard_fork") }}' THEN (total_staker_operator_payout * 0.10)::text::decimal(38,0) ELSE floor(total_staker_operator_payout * 0.10) END as operator_tokens, CASE WHEN snapshot < '{{ var("amazon_hard_fork") }}' AND reward_submission_date < '{{ var("amazon_hard_fork") }}' THEN total_staker_operator_payout - cast(total_staker_operator_payout * 0.10 as DECIMAL(38,0)) WHEN snapshot < '{{ var("nile_hard_fork") }}' AND reward_submission_date < '{{ var("nile_hard_fork") }}' THEN total_staker_operator_payout - ((total_staker_operator_payout * 0.10)::text::decimal(38,0)) ELSE total_staker_operator_payout - floor(total_staker_operator_payout * 0.10) END as staker_tokens FROM staker_operator_total_tokens ) SELECT * from token_breakdowns ORDER BY reward_hash, snapshot, staker, operator ``` 计算欠 Operator 和 Staker 的 Token 数量。Operator 获得固定的 10% 佣金。从文本转换为 `DECIMAL(38,0)` 将只会截断数字的小数部分(即,不会有四舍五入)。我们添加 order by 子句是为了使下一个查询中 Staker 权重的总和是确定性的。
Operator | AVS | Strategy | Snapshot | Staker | Shares | Staker Weight | Total Staker Weight | Staker Proportion | Tokens Per Day | Total Staker Operator Reward | Operator Tokens | Staker Tokens |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e36 | 3e36 | 0.333 | 1e18 | 333333333333333000 | 33333333333333300 | 299999999999999700 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 3e36 | 6e36 | 0.5 | 1e18 | 5e17 | 5e16 | 4.5e17 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e36 | 6e36 | 0.166 | 1e18 | 166666666666666000 | 16666666666666600 | 149999999999999400 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 2e36 | 3e36 | 0.666 | 1e18 | 666666666666666000 | 66666666666666600 | 599999999999999400 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 2e36 | 6e36 | 0.333 | 1e18 | 333333333333333000 | 33333333333333300 | 299999999999999700 |
我们可以通过其 Staker 的奖励分配总和来计算 Operator 奖励分配,因为 Operator $o$ 的份额由下式给出:
$Shares_{o} = \sum_{i=0}^{n=operatorStakers} Shares_i$
```sql= WITH operator_token_sums AS ( SELECT reward_hash, snapshot, token, tokens_per_day, avs, strategy, multiplier, reward_type, operator, SUM(operator_tokens) OVER (PARTITION BY operator, reward_hash, snapshot) AS operator_tokens FROM {{ ref('2_staker_reward_amounts') }} ), ```
获取给定 `reward_hash` 和 `snapshot` 的每个 Operator 的总和。每天的 Token 是 `1e18`。如果我们采用 `4-25-26` 上的 Operator 奖励分配,则大约是 1e18 的 10%。此外,第 2 行和第 3 行是等效的。
Operator | AVS | Strategy | Snapshot | Staker | Operator Tokens | Staker Tokens | Operator Tokens (Sum) |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 5e16 | 4.5e17 | 66666666666666600 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 16666666666666600 | 149999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 66666666666666600 | 599999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
```sql= distinct_operators AS ( SELECT * FROM ( SELECT *, -- 我们可以使用任意顺序,因为对于每个 (operator, strategy, hash, snapshot),staker_weight 都是相同的 -- 我们使用 strategy ASC 以获得更好的可调试性 ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, operator ORDER BY strategy ASC) as rn FROM operator_token_sums ) t WHERE rn = 1 ) SELECT * FROM distinct_operators ```
在上一步中,我们汇总了具有相同 `snapshot` 和 `reward_hash` 的 Operator 奖励。现在,我们删除这些行,以便在最终奖励分配中,每个 `reward_hash` 和 `snapshot` 仅计算一次 Operator。
Operator | AVS | Strategy | Snapshot | Staker | Operator Tokens | Staker Tokens | Operator Tokens (Sum) |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
<s> Operator1 </s> | <s> AVS1 </s> | <s> stETH </s> | <s> 4-26-2024 </s> | <s> Staker1 </s> | <s> 5e16 </s> | <s> 4.5e17 </s> | <s> 66666666666666600 </s> |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 16666666666666600 | 149999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 66666666666666600 | 599999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
此查询计算通过`RewardCoordinator`上的`createRewardsForAllSubmission`函数进行的奖励。此奖励直接给Staker。
注意:此功能当前已暂停且未使用,因此不存在硬分叉逻辑。
```sql= WITH reward_snapshot_stakers AS ( SELECT ap.reward_hash, ap.snapshot, ap.token, ap.tokens_per_day, ap.avs, ap.strategy, ap.multiplier, ap.reward_type, sss.staker, sss.shares FROM {{ ref('1_active_rewards') }} ap JOIN {{ ref('staker_share_snapshots') }} as sss ON ap.strategy = sss.strategy and ap.snapshot = sss.snapshot WHERE ap.reward_type = 'all_stakers' -- 解析出负的份额和零倍数,因此不存在被零除的情况 AND sss.shares > 0 and ap.multiplier != 0 ), ```
选择将`reward_for_all`设置为true的所有奖励
该计算与[步骤2](https://hackmd.io/Fmjcckn1RoivWpPLRAPwBw?view#2-Staker-Reward-Amounts)相同,除了我们不需要检查Operator a Staker 是否被委派
```sql= -- 计算Staker的权重 staker_weights AS ( SELECT *, SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight FROM reward_snapshot_stakers ), -- 获取不同的Staker,因为它们的权重已经计算出来 distinct_stakers AS ( SELECT * FROM ( SELECT *, -- 我们可以使用任意顺序,因为对于每个 (staker, strategy, hash, snapshot),staker_weight 都是相同的 -- 我们使用 strategy ASC 以获得更好的可调试性 ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, staker ORDER BY strategy ASC) as rn FROM staker_weights ) t WHERE rn = 1 ORDER BY reward_hash, snapshot, staker ), -- 计算所有Staker权重的总和 staker_weight_sum AS ( SELECT *, SUM(staker_weight) OVER (PARTITION BY reward_hash, snapshot) as total_staker_weight FROM distinct_stakers ), -- 计算Staker Token比例 staker_proportion AS ( SELECT *, FLOOR((staker_weight / total_staker_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion FROM staker_weight_sum ), -- 计算Staker的总Token staker_tokens AS ( SELECT *, (tokens_per_day * staker_proportion)::text::decimal(38,0) as staker_tokens FROM staker_proportion ) SELECT * from staker_tokens ```
此奖励功能奖励所有已选择加入至少一个AVS的运营商(及其委派的Staker)。运营商可获得固定的10%佣金。
我们通过以下方式做到这一点:
```sql= WITH avs_opted_operators AS ( SELECT DISTINCT snapshot, operator FROM {{ ref('operator_avs_registration_snapshots') }} ), ```
获取已为给定快照注册AVS的唯一运营商。这使用了`operator_avs_registration_snapshots` preclalculation视图。请注意,此表中不存在注销。
```sql= -- 获取将在给定快照中的奖励提交中获得奖励的运营商 reward_snapshot_operators as ( SELECT ap.reward_hash, ap.snapshot, ap.token, ap.tokens_per_day_decimal, ap.avs, ap.strategy, ap.multiplier, ap.reward_type, ap.reward_submission_date, aoo.operator FROM {{ ref('1_active_rewards') }} ap JOIN avs_opted_operators aoo ON ap.snapshot = aoo.snapshot WHERE ap.reward_type = 'all_earners' ), ```
我们将活动奖励与已注册到至少一个AVS的快照运营商加入在一起。
```sql= -- 获取为快照委派给运营商的Staker staker_delegated_operators AS ( SELECT rso.*, sds.staker FROM reward_snapshot_operators rso JOIN {{ ref('staker_delegation_snapshots') }} sds ON rso.operator = sds.operator AND rso.snapshot = sds.snapshot ), ```
获取为快照委派给已注册运营商的Staker。
其余的计算将按照 `2_staker_reward_amounts`进行,而没有进行硬分叉。
```sql= -- 获取Staker委派给运营商的每种策略的份额 staker_strategy_shares AS ( SELECT sdo.*, sss.shares FROM staker_delegated_operators sdo JOIN {{ ref('staker_share_snapshots') }} sss ON sdo.staker = sss.staker AND sdo.snapshot = sss.snapshot AND sdo.strategy = sss.strategy -- 解析出负的份额和零倍数,因此不存在被零除的情况 WHERE sss.shares > 0 and sdo.multiplier != 0 ), -- 计算Staker的权重 staker_weights AS ( SELECT *, SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight FROM staker_strategy_shares ), -- 获取不同的Staker,因为它们的权重已经计算出来 distinct_stakers AS ( SELECT * FROM ( SELECT *, -- 我们可以使用任意顺序,因为对于每个 (staker, strategy, hash, snapshot),staker_weight 都是相同的 -- 我们使用 strategy ASC 以获得更好的可调试性 ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, staker ORDER BY strategy ASC) as rn FROM staker_weights ) t WHERE rn = 1 ORDER BY reward_hash, snapshot, staker ), -- 计算每个奖励和快照的所有Staker权重的总和 staker_weight_sum AS ( SELECT *, SUM(staker_weight) OVER (PARTITION BY reward_hash, snapshot) as total_weight FROM distinct_stakers ), -- 计算每个奖励和快照的Staker Token比例 staker_proportion AS ( SELECT *, FLOOR((staker_weight / total_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion FROM staker_weight_sum ), -- 计算(Staker,运营商)对的总Token staker_operator_total_tokens AS ( SELECT *, FLOOR(staker_proportion * tokens_per_day_decimal) as total_staker_operator_payout FROM staker_proportion ), -- 计算每个(Staker,运营商)对的Token分解 token_breakdowns AS ( SELECT *, floor(total_staker_operator_payout * 0.10) as operator_tokens, total_staker_operator_payout - floor(total_staker_operator_payout * 0.10) as staker_tokens FROM staker_operator_total_tokens ) SELECT * from token_breakdowns ORDER BY reward_hash, snapshot, staker, operator ```
与步骤3类似,我们现在必须解析出对运营商的奖励。
```sql= -- 对于每个运营商,奖励哈希和快照,跨Staker汇总运营商Token WITH operator_token_sums AS ( SELECT reward_hash, snapshot, token, tokens_per_day_decimal, avs, strategy, multiplier, reward_type, operator, SUM(operator_tokens) OVER (PARTITION BY operator, reward_hash, snapshot) AS operator_tokens FROM {{ ref('5_rfae_stakers') }} ), -- 为每个运营商,奖励哈希和快照去重复跨策略的运营商Token distinct_operators AS ( SELECT * FROM ( SELECT *, -- 我们可以使用任意顺序,因为对于每个 (operator, strategy, hash, snapshot),staker_weight 都是相同的 -- 我们使用 strategy ASC 以获得更好的可调试性 ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, operator ORDER BY strategy ASC) as rn FROM operator_token_sums ) t WHERE rn = 1 ) SELECT * FROM distinct_operators ```
此查询结合了步骤2、3、4、5和6中的奖励,以生成具有以下列的表:
Earner | Snapshot | Reward Hash | Token | Amount |
---|
汇总步骤2、3和4中的奖励。对于给定的`reward_hash`和`snapshot`,我们使用`DISTINCT`作为完整性检查,以丢弃具有相同的`strategy`和`earner`的行。
```sql= WITH staker_rewards AS ( -- 我们可以选择 DISTINCT,因为对于奖励哈希中的每种策略,Staker的Token都相同 SELECT DISTINCT staker as earner, snapshot, reward_hash, token, staker_tokens as amount FROM {{ ref('2_staker_reward_amounts') }} ), operator_rewards AS ( SELECT DISTINCT -- 我们可以选择 DISTINCT,因为对于奖励哈希中的每种策略,运营商的Token都相同 operator as earner, snapshot, reward_hash, token, operator_tokens as amount FROM {{ ref('3_operator_reward_amounts') }} ), rewards_for_all AS ( SELECT DISTINCT staker as earner, snapshot, reward_hash, token, staker_tokens as amount FROM {{ ref('4_deprecated_rewards_for_all') }} ), rewards_for_all_earners_stakers AS ( SELECT DISTINCT staker as earner, snapshot, reward_hash, token, staker_tokens as amounts FROM {{ ref('5_rfae_stakers.sql')}} ), rewards_for_all_earners_operators AS ( SELECT DISTINCT operator as earner, snapshot, reward_hash, token, operator_tokens as amount FROM {{ ref('6_rfae_operators.sql')}} ) combined_rewards AS ( SELECT * FROM operator_rewards UNION ALL SELECT * FROM staker_rewards UNION ALL SELECT * FROM rewards_for_all UNION ALL SELECT * FROM rewards_for_all_earners_stakers UNION ALL SELECT * FROM rewards_for_all_earners_operators ), ```
```sql= -- Dedupe收款人,主要是也是他们自己的Staker的运营商。 deduped_earners AS ( SELECT earner, snapshot, reward_hash, token, SUM(amount) as amount FROM combined_rewards GROUP BY earner, snapshot, reward_hash, token ) SELECT * FROM deduped_earners ``` 对于给定的 `reward_hash` 和 `snapshot`,为具有多个行的 Earner 求和余额。此步骤处理运营商也委派给自己的情况。
先前的查询都是视图。此步骤从 [步骤7](#6-Gold-Table-Staging) 中选择行,并将它们附加到表中。
```sql= SELECT earner, snapshot, reward_hash, token, amount FROM {{ ref('gold_staging') }} ``` 此表通过 `merge` `incremental_strategy` 在 `['reward_hash', 'earner', 'snapshot']` 的唯一键上合并。有关更多信息,请参见 [dbt docs](https://docs.getdbt.com/docs/build/incremental-strategy)。
最后,Rewards Root Updater将查询此表以获取累积金额,并将以下收款人,Token,累积金额列进行 Merkelize。
```sql= SELECT earner, snapshot, reward_hash, token, amount FROM {{ ref('gold_staging') }} ```
- 原文链接: hackmd.io/Fmjcckn1RoivWp...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!