EigenLayer 奖励计算 - 基础激励

该文档详细介绍了 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():

获取 utc 的当前时间

ts = datetime.now(timezone.utc)

向下舍入到当天的 00:00 UTC

ts = ts.replace(hour=0, minute=0, second=0, microsecond=0)

减去 1 天

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('结束日期必须小于或等于截止日期')

推送到 XCom

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") }}' ```

EigenPodShares

*注意: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。

Operator Shares

对于一个策略 $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 Delegation Status

在这里,我们将每个委托和取消委托聚合到一个单独的视图中。当一个 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 ```

Combined Rewards Submissions

将 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') }} ```

Operator AVS Statuses

将 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') }} ```

Part 2: Windows & Snapshots

一旦我们转换了链上状态,我们就会将状态聚合到状态处于活动状态的时间窗口中。最后,状态窗口被展开为每日快照。

正如我们在上面的考虑事项中解释的那样,关键的设计决策是:状态总是向上取整到最近的一天 0:00 UTC,除了 operator<>avs 注销

Windows

Staker Share Windows

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`) 组合的下一个记录。逻辑是:

  • 如果没有下一个记录(即 null),则将窗口的 `end_time` 设置为 `cutoff_date`
  • 如果有记录,则将当前记录窗口的 `end_time` 设置为下一个记录的 `start_time`

注意:上述逻辑可能存在 (`staker`, `strategy`) 组合,其中一个记录的 `end_record` 等于下一个记录的 `start_time`。当我们将窗口展开为快照时,会处理这个问题。

Operator Share Windows

```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 完全相同。

Staker Delegation 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 完全相同。

Operator AVS Registration 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 AVS Strategy 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 ```

Snapshots

一旦我们为核心合约状态的每个表创建了窗口,我们会将这些窗口展开为每日快照。在下面的每个查询中,我们将 `end_time` 向下舍入一天,因为新记录可以在同一天开始,或者它将在 `cutoff_date` 之后的单独管道运行中包含。

Staker Share Snapshot

```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` 值的记录。然后,我们展开整个范围。

Operator Share Snapshot

```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 ```

Staker Delegation Snapshots

```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 ```

Operator AVS Strategy Snapshots

```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 ```

Operator AVS Registration Snapshots

```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

Key Considerations

Calculation Ranges

奖励分配是根据状态的每日快照计算的。例如,如果我们有一个针对以下范围的奖励提交:

``` 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] 的最新状态。

State Entry/Exit

由于快照向上取整到最近一天,除了 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 天。

Multiplier Calculation

每个奖励提交都有两个长度相等的数组:`strategies` 和 `multiplier`。管道使用此值来计算快照奖励的 earner 的 stakeWeight。对于给定快照 $d$ 上的 staker $s$,stakeWeight 由下式给出:

$stakeWeight_{s, d} = multiplier_i \cdot shares_{i,s,d}$

该计算也在 AVS 的 `StakeRegistry` 合约中完成。参考 solidity 实现

Token Reward Amounts

一个关键的不变量是,对于给定奖励快照 $d$ 上的奖励提交 $r$, $Tokens_{r,d} >= \sum_{i=0}^{n=paidEarners} Earner_{i,r,d}$

换句话说,奖励提交的 `tokensPerDay` 不能小于 `rewardSnaphot` 的所有 earner 的奖励分配总和。我们将此称为从截断转换份额和乘数到 double 类型(最多可容纳 15 位有效数字)的关键考虑因素。

Reward Aggregation

`RewardsCoordinator` 要求 `CALCULATION_INTERVAL_SECONDS % SNAPSHOT_CADENCE == 0`,这保证了每个奖励快照都将位于奖励范围的边界内。

根据奖励更新程序定义的某个节奏,管道将聚合所有奖励分配快照到某个时间戳 $t$。为了使根是“全新的”,它必须 merklize 大于 `lastRewardTimestamp` 的 `rewardSnaphot` 之后的状态。

Lack of reward rollover

如果 AVS 为没有重新质押任何策略的快照提供了奖励,则该奖励将不会重新分配到奖励提交的未来快照。请参阅 奖励快照 operator 一个具体的例子。

1. Get Active Rewards

以下每个查询都是 `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") }}' ), ```

  • 我们将金额除以奖励提交持续时间的天数,以获得 `tokens_per_day`(每日代币数)。这是在给定的 `rewardSnapshot`(奖励快照)中分配给所有赚取者的代币数量。
  • 在给定的运行中可以有多个 `rewardsSnapshot`,因为对于每次奖励提交,我们可以在管道运行中拥有多个快照来计算奖励。
  • `global_end_inclusive`(全局包含结束时间)是此管道运行中用于计算奖励的最后一个快照。它与 `cutoff_date`(截止日期)相同。
  • `end_timestamp` (结束时间戳)大于 UNIX 时间的开始时间,但将在后续步骤中正确处理。
每日代币数 全局包含结束时间
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 ), ```

  • 奖励提交的原始 `start_timestamp`(开始时间戳)重命名为 `reward_start_exclusive`(奖励开始时间不包含)。它被标记为不包含,因为我们要么已经在之前的运行中在这个时间点拍摄了快照,要么它是奖励提交的第 0 天,并且不会拍摄快照(参见 [计算范围](https://hackmd.io/Fmjcckn1RoivWpPLRAPwBw?view#Calculation-Ranges))。
  • `reward_end_inclusive`(奖励结束时间包含)是 `MIN(global_end_inclsuive, end_timestamp)`。这是限制 `end_timestamp`(结束时间戳)不大于给定运行的 `cutoff_time`(截止时间)。
奖励开始时间不包含 奖励结束时间包含
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 日

2. Staker(质押者)奖励金额

在生成有效奖励后,管道接着计算奖励分配给 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 日

Staker 委托的运营商

```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

Staker 策略份额

```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` 份额,这就是为什么此视图少一行。

Staker 权重

```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

不同的 Staker

```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

Staker 权重总和

```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

Staker 比例

```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

Total Tokens

```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 次硬分叉:

  1. 向下舍入为文本
  2. 所有地方都使用 decimal 而不是 double。

此代码反映了处理此边缘情况,以使回填情况下的计算具有幂等性。

计算将支付给所有 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

Token Breakdowns

```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

3. Operator 奖励金额

我们可以通过其 Staker 的奖励分配总和来计算 Operator 奖励分配,因为 Operator $o$ 的份额由下式给出:

$Shares_{o} = \sum_{i=0}^{n=operatorStakers} Shares_i$

Operator Token 总和

```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

Dedupe Operators

```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

4. 所有Staker的奖励

此查询计算通过`RewardCoordinator`上的`createRewardsForAllSubmission`函数进行的奖励。此奖励直接给Staker。

注意:此功能当前已暂停且未使用,因此不存在硬分叉逻辑。

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的所有奖励

Staker权重 -> Staker Token

该计算与[步骤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 ```

5. 所有赚钱者的奖励-Staker

此奖励功能奖励所有已选择加入至少一个AVS的运营商(及其委派的Staker)。运营商可获得固定的10%佣金。

我们通过以下方式做到这一点:

  1. 获取所有已选择加入至少1个AVS的运营商
  2. 获取运营商的Staker
  3. 计算对Staker的支付
  4. 计算对运营商的支付(步骤7)

AVS选择的运营商

```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的快照运营商加入在一起。

Staker委派运营商

```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。

Staker策略股票 -> Token分解

其余的计算将按照 `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 ```

6. 所有赚钱者的奖励-运营商

与步骤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 ```

7. Gold Table暂存

此查询结合了步骤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 ), ```

Dedupe Earners

```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 求和余额。此步骤处理运营商也委派给自己的情况。

8. Gold Table合并

先前的查询都是视图。此步骤从 [步骤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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Fmjcckn1RoivWpPLRAPwBw
Fmjcckn1RoivWpPLRAPwBw
江湖只有他的大名,没有他的介绍。