Skip to content

Commit 8aef2c4

Browse files
feat: claim rewards from latest active root (#188)
* feat: claim rewards from last active root * feat: cli support for using last active root for rewards * fix comment * readme update
1 parent 2f5daf3 commit 8aef2c4

File tree

4 files changed

+150
-20
lines changed

4 files changed

+150
-20
lines changed

pkg/rewards/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
```bash
55
eigenlayer rewards claim --help
66
NAME:
7-
eigenlayer rewards claim - Claim rewards for the operator
7+
eigenlayer rewards claim - Claim rewards for any earner
88

99
USAGE:
1010
eigenlayer rewards claim [command options]
1111

1212
OPTIONS:
1313
--broadcast, -b Use this flag to broadcast the transaction (default: false) [$BROADCAST]
14-
--claim-timestamp value, -c value Specify the timestamp. Only 'latest' is supported (default: "latest") [$CLAIM_TIMESTAMP]
14+
--claim-timestamp value, -c value Specify the timestamp. Only 'latest' and 'latest_active' are supported (default: "latest") [$CLAIM_TIMESTAMP]
1515
--earner-address value, --ea value Address of the earner [$REWARDS_EARNER_ADDRESS]
1616
--ecdsa-private-key value, -e value ECDSA private key hex to send transaction [$ECDSA_PRIVATE_KEY]
1717
--environment value, --env value Environment to use. Currently supports 'preprod' ,'testnet' and 'prod'. If not provided, it will be inferred based on network [$ENVIRONMENT]

pkg/rewards/claim.go

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package rewards
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -19,6 +20,7 @@ import (
1920
contractrewardscoordinator "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IRewardsCoordinator"
2021

2122
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/claimgen"
23+
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/proofDataFetcher"
2224
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/proofDataFetcher/httpProofDataFetcher"
2325

2426
"github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts"
@@ -35,8 +37,6 @@ import (
3537
"github.com/urfave/cli/v2"
3638
)
3739

38-
const LatestClaimTimestamp = "latest"
39-
4040
type ClaimConfig struct {
4141
Network string
4242
RPCUrl string
@@ -122,20 +122,11 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error {
122122
http.DefaultClient,
123123
)
124124

125-
latestSubmittedTimestamp, err := elReader.CurrRewardsCalculationEndTimestamp(&bind.CallOpts{})
126-
if err != nil {
127-
return eigenSdkUtils.WrapError("failed to get latest submitted timestamp", err)
128-
}
129-
claimDate := time.Unix(int64(latestSubmittedTimestamp), 0).UTC().Format(time.DateOnly)
130-
logger.Debugf("Latest submitted timestamp: %s", claimDate)
131-
132-
rootCount, err := elReader.GetDistributionRootsLength(&bind.CallOpts{})
125+
claimDate, rootIndex, err := getClaimDistributionRoot(ctx, config.ClaimTimestamp, df, elReader, logger)
133126
if err != nil {
134-
return eigenSdkUtils.WrapError("failed to get number of published roots", err)
127+
return eigenSdkUtils.WrapError("failed to get claim distribution root", err)
135128
}
136129

137-
rootIndex := uint32(rootCount.Uint64() - 1)
138-
139130
proofData, err := df.FetchClaimAmountsForDate(ctx, claimDate)
140131
if err != nil {
141132
return eigenSdkUtils.WrapError("failed to fetch claim amounts for date", err)
@@ -264,6 +255,64 @@ func Claim(cCtx *cli.Context, p utils.Prompter) error {
264255
return nil
265256
}
266257

258+
func getClaimDistributionRoot(
259+
ctx context.Context,
260+
claimTimestamp string,
261+
df *httpProofDataFetcher.HttpProofDataFetcher,
262+
elReader *elcontracts.ChainReader,
263+
logger logging.Logger,
264+
) (string, uint32, error) {
265+
if claimTimestamp == "latest" {
266+
latestSubmittedTimestamp, err := elReader.CurrRewardsCalculationEndTimestamp(&bind.CallOpts{})
267+
if err != nil {
268+
return "", 0, eigenSdkUtils.WrapError("failed to get latest submitted timestamp", err)
269+
}
270+
claimDate := time.Unix(int64(latestSubmittedTimestamp), 0).UTC().Format(time.DateOnly)
271+
logger.Debugf("Latest submitted timestamp: %s", claimDate)
272+
273+
rootCount, err := elReader.GetDistributionRootsLength(&bind.CallOpts{})
274+
if err != nil {
275+
return "", 0, eigenSdkUtils.WrapError("failed to get number of published roots", err)
276+
}
277+
278+
rootIndex := uint32(rootCount.Uint64() - 1)
279+
return claimDate, rootIndex, nil
280+
} else if claimTimestamp == "latest_active" {
281+
// Get the latest 10 roots
282+
postedRoots, err := df.FetchPostedRewards(ctx)
283+
if err != nil {
284+
return "", 0, eigenSdkUtils.WrapError("failed to fetch posted rewards", err)
285+
}
286+
287+
ts, rootIndex, err := getLatestActivePostedRoot(postedRoots)
288+
if err != nil {
289+
return "", 0, eigenSdkUtils.WrapError("failed to get latest active posted root", err)
290+
}
291+
logger.Debugf("Latest active posted root timestamp: %s, index: %d", ts, rootIndex)
292+
293+
return ts, rootIndex, nil
294+
}
295+
return "", 0, errors.New("invalid claim timestamp")
296+
}
297+
298+
// getLatestActivePostedRoot returns the latest active posted root by sorting the roots by the latest calculated end
299+
// timestamp in descending order and checking the latest timestamp which activated before the current time
300+
func getLatestActivePostedRoot(postedRoots []*proofDataFetcher.SubmittedRewardRoot) (string, uint32, error) {
301+
// sort by latest calculated end timestamp
302+
sort.Slice(postedRoots, func(i, j int) bool {
303+
return postedRoots[i].CalcEndTimestamp.After(postedRoots[j].CalcEndTimestamp)
304+
})
305+
306+
currTime := time.Now()
307+
for _, postedRoot := range postedRoots {
308+
if postedRoot.ActivatedAt.Before(currTime) {
309+
return postedRoot.GetRewardDate(), postedRoot.RootIndex, nil
310+
}
311+
// There is no else here because on of last 10 root be
312+
}
313+
return "", 0, errors.New("no active posted roots found")
314+
}
315+
267316
func convertClaimTokenLeaves(
268317
claimTokenLeaves []contractrewardscoordinator.IRewardsCoordinatorTokenTreeMerkleLeaf,
269318
) []rewardscoordinator.IRewardsCoordinatorTokenTreeMerkleLeaf {
@@ -300,9 +349,7 @@ func readAndValidateClaimConfig(cCtx *cli.Context, logger logging.Logger) (*Clai
300349
logger.Debugf("Using Rewards Coordinator address: %s", rewardsCoordinatorAddress)
301350

302351
claimTimestamp := cCtx.String(ClaimTimestampFlag.Name)
303-
if claimTimestamp != LatestClaimTimestamp {
304-
return nil, errors.New("claim-timestamp must be 'latest'")
305-
}
352+
logger.Debugf("Using claim timestamp from user: %s", claimTimestamp)
306353

307354
recipientAddress := gethcommon.HexToAddress(cCtx.String(RecipientAddressFlag.Name))
308355
if recipientAddress == utils.ZeroAddress {
@@ -357,6 +404,7 @@ func readAndValidateClaimConfig(cCtx *cli.Context, logger logging.Logger) (*Clai
357404
Environment: environment,
358405
RecipientAddress: recipientAddress,
359406
SignerConfig: signerConfig,
407+
ClaimTimestamp: claimTimestamp,
360408
}, nil
361409
}
362410

pkg/rewards/claim_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"flag"
55
"os"
66
"testing"
7+
"time"
78

8-
"github.com/Layr-Labs/eigenlayer-cli/pkg/internal/testutils"
9+
"github.com/Layr-Labs/eigenlayer-rewards-proofs/pkg/proofDataFetcher"
910

1011
"github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags"
12+
"github.com/Layr-Labs/eigenlayer-cli/pkg/internal/testutils"
1113

1214
"github.com/Layr-Labs/eigensdk-go/logging"
1315

@@ -53,3 +55,83 @@ func TestReadAndValidateConfig_RecipientProvided(t *testing.T) {
5355
assert.NoError(t, err)
5456
assert.Equal(t, common.HexToAddress(recipientAddress), config.RecipientAddress)
5557
}
58+
59+
func TestGetLatestActivePostedRoot(t *testing.T) {
60+
now := time.Now().UTC()
61+
var tests = []struct {
62+
name string
63+
postedRoots []*proofDataFetcher.SubmittedRewardRoot
64+
expectedRootIndex uint32
65+
rewardsTimestampString string
66+
}{
67+
{
68+
name: "found an activated root before current time",
69+
postedRoots: []*proofDataFetcher.SubmittedRewardRoot{
70+
{
71+
RootIndex: 1,
72+
ActivatedAt: now,
73+
CalcEndTimestamp: now.Add(-24 * time.Hour),
74+
},
75+
{
76+
RootIndex: 2,
77+
ActivatedAt: now.Add(-2 * time.Hour),
78+
CalcEndTimestamp: now.Add(-48 * time.Hour),
79+
},
80+
{
81+
RootIndex: 3,
82+
ActivatedAt: now.Add(1 * time.Hour),
83+
CalcEndTimestamp: now,
84+
},
85+
},
86+
expectedRootIndex: 1,
87+
rewardsTimestampString: now.Add(-24 * time.Hour).Format(time.DateOnly),
88+
},
89+
{
90+
name: "found no activated root before current time",
91+
postedRoots: []*proofDataFetcher.SubmittedRewardRoot{
92+
{
93+
RootIndex: 3,
94+
ActivatedAt: now.Add(1 * time.Hour),
95+
CalcEndTimestamp: now,
96+
},
97+
{
98+
RootIndex: 4,
99+
ActivatedAt: now.Add(2 * time.Hour),
100+
CalcEndTimestamp: now.Add(3 * time.Hour),
101+
},
102+
},
103+
expectedRootIndex: 0,
104+
},
105+
{
106+
name: "found an activated root before current time 2",
107+
postedRoots: []*proofDataFetcher.SubmittedRewardRoot{
108+
{
109+
RootIndex: 2,
110+
ActivatedAt: now.Add(-2 * time.Hour),
111+
CalcEndTimestamp: now.Add(-24 * time.Hour),
112+
},
113+
{
114+
RootIndex: 3,
115+
ActivatedAt: now.Add(1 * time.Hour),
116+
CalcEndTimestamp: now.Add(-48 * time.Hour),
117+
},
118+
},
119+
expectedRootIndex: 2,
120+
rewardsTimestampString: now.Add(-24 * time.Hour).Format(time.DateOnly),
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
actualRewardTimeString, actualRootIndex, err := getLatestActivePostedRoot(tt.postedRoots)
127+
if tt.expectedRootIndex == 0 {
128+
assert.Error(t, err)
129+
} else {
130+
assert.NoError(t, err)
131+
assert.Equal(t, tt.rewardsTimestampString, actualRewardTimeString)
132+
}
133+
assert.Equal(t, tt.expectedRootIndex, actualRootIndex)
134+
135+
})
136+
}
137+
}

pkg/rewards/flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var (
2121
ClaimTimestampFlag = cli.StringFlag{
2222
Name: "claim-timestamp",
2323
Aliases: []string{"c"},
24-
Usage: "Specify the timestamp. Only 'latest' is supported",
24+
Usage: "Specify the timestamp. Only 'latest' and 'latest_active' are supported",
2525
Value: "latest",
2626
EnvVars: []string{"CLAIM_TIMESTAMP"},
2727
}

0 commit comments

Comments
 (0)