diff --git a/pkg/coreContracts/migrations/202601161552_rewardsCoordinatorAndStrategyManager/up.go b/pkg/coreContracts/migrations/202601161552_rewardsCoordinatorAndStrategyManager/up.go new file mode 100644 index 000000000..75a34ee19 --- /dev/null +++ b/pkg/coreContracts/migrations/202601161552_rewardsCoordinatorAndStrategyManager/up.go @@ -0,0 +1,127 @@ +package _202601161552_rewardsCoordinatorAndStrategyManager + +import ( + "fmt" + "slices" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/contractStore" + "github.com/Layr-Labs/sidecar/pkg/coreContracts/helpers" + "github.com/Layr-Labs/sidecar/pkg/coreContracts/types" + "github.com/Layr-Labs/sidecar/pkg/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type ContractMigration struct{} + +func (m *ContractMigration) Up(db *gorm.DB, cs contractStore.ContractStore, l *zap.Logger, cfg *config.Config) (*types.MigrationResult, error) { + if !slices.Contains([]config.Chain{config.Chain_PreprodHoodi, config.Chain_Hoodi, config.Chain_Sepolia}, cfg.Chain) { + return nil, nil + } + + contractsMap := cfg.GetContractsMapForChain() + if contractsMap == nil { + l.Sugar().Errorw("No contracts map found for chain", zap.String("chain", cfg.Chain.String())) + return nil, fmt.Errorf("no contracts map found for chain: %s", cfg.Chain.String()) + } + + rewardsCoordinatorImplementationAddresses := map[config.Chain]string{ + config.Chain_PreprodHoodi: "0x3c872a52f941aecf26e8f31a159c8ffa62fb4bdd", + config.Chain_Hoodi: "0x35a8198e98cb76f1dd81d977fbd2a8f2a62c0525", + config.Chain_Sepolia: "0xa518479dd419ad182746b231920a314b4b1b64f5", + } + + strategyManagerImplementationAddresses := map[config.Chain]string{ + config.Chain_PreprodHoodi: "0x28ec08ac867d16165b77b1ae897fb3a34eb9ebd8", + config.Chain_Hoodi: "0x1b76a41b1128a5cd92dae7c4c0dec0bfe12b0f62", + config.Chain_Sepolia: "0x7d59f252bd32733f8850c50bf6bb2e46bf37e6f1", + } + + rewardsCoordinatorBytecodeHashes := map[config.Chain]string{ + config.Chain_PreprodHoodi: "ae876995bfa750baeb84f7ff384335acbb9666841d590dce2ac42919f0d2b093", + config.Chain_Hoodi: "984ddce0922377bf94a568567726a0900ab8f8842febe74cc4441101bb74e00f", + config.Chain_Sepolia: "984ddce0922377bf94a568567726a0900ab8f8842febe74cc4441101bb74e00f", + } + + rewardsCoordinatorABIs := map[config.Chain]string{ + config.Chain_PreprodHoodi: RewardsCoordinatorPreprodImplABI, + config.Chain_Hoodi: RewardsCoordinatorTestnetImplABI, + config.Chain_Sepolia: RewardsCoordinatorTestnetImplABI, + } + + rewardsCoordinatorImplAddress, rcAddrExists := rewardsCoordinatorImplementationAddresses[cfg.Chain] + strategyManagerImplAddress, smAddrExists := strategyManagerImplementationAddresses[cfg.Chain] + rewardsCoordinatorBytecodeHash, rcHashExists := rewardsCoordinatorBytecodeHashes[cfg.Chain] + rewardsCoordinatorABI, rcABIExists := rewardsCoordinatorABIs[cfg.Chain] + + if !rcAddrExists || !smAddrExists || !rcHashExists || !rcABIExists { + l.Sugar().Infow("No implementation addresses, bytecode hashes, or ABIs found for chain, skipping migration", zap.String("chain", cfg.Chain.String())) + return nil, nil + } + + contractsToImport := &helpers.ContractsToImport{ + CoreContracts: []*helpers.ContractImport{}, + ImplementationContracts: []*helpers.ImplementationContractImport{ + { + ProxyContractAddress: contractsMap.RewardsCoordinator, + ImplementationContracts: []*helpers.ImplementationContract{ + { + Contract: helpers.ContractImport{ + Address: rewardsCoordinatorImplAddress, + Abi: rewardsCoordinatorABI, + BytecodeHash: rewardsCoordinatorBytecodeHash, + }, + }, + }, + }, + { + ProxyContractAddress: contractsMap.StrategyManager, + ImplementationContracts: []*helpers.ImplementationContract{ + { + Contract: helpers.ContractImport{ + Address: strategyManagerImplAddress, + Abi: StrategyManagerImplABI, + BytecodeHash: "4dd5de10067981f20f6394768b83fc5511be1fb8140ebe3d49d4aad26f05a259", + }, + }, + }, + }, + }, + } + + coreContracts, err := helpers.ImportCoreContracts(contractsToImport.CoreContracts, cs) + if err != nil { + return nil, err + } + l.Sugar().Infow("Imported core contracts", zap.Int("count", len(coreContracts))) + + implementationContracts, err := helpers.ImportImplementationContracts(contractsToImport.ImplementationContracts, cs, l) + if err != nil { + return nil, err + } + l.Sugar().Infow("Imported implementation contracts", zap.Int("count", len(implementationContracts))) + + return &types.MigrationResult{ + CoreContractsAdded: utils.Map(contractsToImport.CoreContracts, func(c *helpers.ContractImport, i uint64) types.CoreContractAdded { + return types.CoreContractAdded{ + Address: c.Address, + IndexStartBlock: cfg.GetGenesisBlockNumber(), + Backfill: true, + } + }), + ImplementationContractsAdded: utils.Reduce(contractsToImport.ImplementationContracts, func(acc []string, ic *helpers.ImplementationContractImport) []string { + return append(acc, utils.Map(ic.ImplementationContracts, func(i *helpers.ImplementationContract, j uint64) string { + return i.Contract.Address + })...) + }, make([]string, 0)), + }, nil +} + +func (m *ContractMigration) GetName() string { + return "202601161552_rewardsCoordinatorAndStrategyManager" +} + +const RewardsCoordinatorPreprodImplABI = "[{\"inputs\":[{\"components\":[{\"internalType\":\"contract IDelegationManager\",\"name\":\"delegationManager\",\"type\":\"address\"},{\"internalType\":\"contract IStrategyManager\",\"name\":\"strategyManager\",\"type\":\"address\"},{\"internalType\":\"contract IAllocationManager\",\"name\":\"allocationManager\",\"type\":\"address\"},{\"internalType\":\"contract IPauserRegistry\",\"name\":\"pauserRegistry\",\"type\":\"address\"},{\"internalType\":\"contract IPermissionController\",\"name\":\"permissionController\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"CALCULATION_INTERVAL_SECONDS\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"MAX_REWARDS_DURATION\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"MAX_RETROACTIVE_LENGTH\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"MAX_FUTURE_LENGTH\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"GENESIS_REWARDS_TIMESTAMP\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsCoordinatorConstructorParams\",\"name\":\"params\",\"type\":\"tuple\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"AmountExceedsMax\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"AmountIsZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"CurrentlyPaused\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DurationExceedsMax\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DurationIsZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EarningsNotGreaterThanClaimed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyRoot\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InputAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InputArrayLengthMismatch\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InputArrayLengthZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidCalculationIntervalSecondsRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidClaimProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidDurationRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidEarner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidEarnerLeafIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidGenesisRewardsTimestampRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidNewPausedStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidOperatorSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidPermissions\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidProofLength\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidRoot\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidRootIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStartTimestampRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidTokenLeafIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NewRootMustBeForNewCalculatedPeriod\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyPauser\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyUnpauser\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OperatorsNotInAscendingOrder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"PreviousSplitPending\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RewardsEndTimestampNotElapsed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RootAlreadyActivated\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RootDisabled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RootNotActivated\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SplitExceedsMax\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StartTimestampTooFarInFuture\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StartTimestampTooFarInPast\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StrategiesNotInAscendingOrder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StrategyNotWhitelisted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SubmissionNotRetroactive\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnauthorizedCaller\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"AVSRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"oldActivationDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"newActivationDelay\",\"type\":\"uint32\"}],\"name\":\"ActivationDelaySet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldClaimer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"ClaimerForSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldDefaultOperatorSplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newDefaultOperatorSplitBips\",\"type\":\"uint16\"}],\"name\":\"DefaultOperatorSplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"}],\"name\":\"DistributionRootDisabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"}],\"name\":\"DistributionRootSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldFeeRecipient\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newFeeRecipient\",\"type\":\"address\"}],\"name\":\"FeeRecipientSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldOperatorAVSSplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newOperatorAVSSplitBips\",\"type\":\"uint16\"}],\"name\":\"OperatorAVSSplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"operatorDirectedRewardsSubmissionHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission\",\"name\":\"operatorDirectedRewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"OperatorDirectedAVSRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"operatorDirectedRewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission\",\"name\":\"operatorDirectedRewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"OperatorDirectedOperatorSetRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldOperatorPISplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newOperatorPISplitBips\",\"type\":\"uint16\"}],\"name\":\"OperatorPISplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldOperatorSetSplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newOperatorSetSplitBips\",\"type\":\"uint16\"}],\"name\":\"OperatorSetSplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"oldValue\",\"type\":\"bool\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"newValue\",\"type\":\"bool\"}],\"name\":\"OptInForProtocolFeeSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"Paused\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"claimedAmount\",\"type\":\"uint256\"}],\"name\":\"RewardsClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"rewardsForAllSubmitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"oldValue\",\"type\":\"bool\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"newValue\",\"type\":\"bool\"}],\"name\":\"RewardsForAllSubmitterSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"RewardsSubmissionForAllCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"tokenHopper\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"RewardsSubmissionForAllEarnersCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldRewardsUpdater\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newRewardsUpdater\",\"type\":\"address\"}],\"name\":\"RewardsUpdaterSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"TotalStakeRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"UniqueStakeRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"Unpaused\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"CALCULATION_INTERVAL_SECONDS\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"GENESIS_REWARDS_TIMESTAMP\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"MAX_FUTURE_LENGTH\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"MAX_RETROACTIVE_LENGTH\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"MAX_REWARDS_DURATION\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"activationDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"allocationManager\",\"outputs\":[{\"internalType\":\"contract IAllocationManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"beaconChainETHStrategy\",\"outputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"leaf\",\"type\":\"tuple\"}],\"name\":\"calculateEarnerLeafHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf\",\"name\":\"leaf\",\"type\":\"tuple\"}],\"name\":\"calculateTokenLeafHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"earnerIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes\",\"name\":\"earnerTreeProof\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"earnerLeaf\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"tokenIndices\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes[]\",\"name\":\"tokenTreeProofs\",\"type\":\"bytes[]\"},{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[]\",\"name\":\"tokenLeaves\",\"type\":\"tuple[]\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsMerkleClaim\",\"name\":\"claim\",\"type\":\"tuple\"}],\"name\":\"checkClaim\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"}],\"name\":\"claimerFor\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createAVSRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[]\",\"name\":\"operatorDirectedRewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createOperatorDirectedAVSRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[]\",\"name\":\"operatorDirectedRewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createOperatorDirectedOperatorSetRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createRewardsForAllEarners\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createRewardsForAllSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createTotalStakeRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createUniqueStakeRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"cumulativeClaimed\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"totalClaimed\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"currRewardsCalculationEndTimestamp\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"defaultOperatorSplitBips\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"delegationManager\",\"outputs\":[{\"internalType\":\"contract IDelegationManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"}],\"name\":\"disableRoot\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeRecipient\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentClaimableDistributionRoot\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"disabled\",\"type\":\"bool\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.DistributionRoot\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentDistributionRoot\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"disabled\",\"type\":\"bool\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.DistributionRoot\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"getDistributionRootAtIndex\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"disabled\",\"type\":\"bool\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.DistributionRoot\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getDistributionRootsLength\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"}],\"name\":\"getOperatorAVSSplit\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"getOperatorPISplit\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"}],\"name\":\"getOperatorSetSplit\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"rootHash\",\"type\":\"bytes32\"}],\"name\":\"getRootIndexFromHash\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"initialOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"initialPausedStatus\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_rewardsUpdater\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"_activationDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"_defaultSplitBips\",\"type\":\"uint16\"},{\"internalType\":\"address\",\"name\":\"_feeRecipient\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isAVSRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isOperatorDirectedAVSRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isOperatorDirectedOperatorSetRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"}],\"name\":\"isOptedInForProtocolFee\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"isOptedIn\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"}],\"name\":\"isRewardsForAllSubmitter\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isRewardsSubmissionForAllEarnersHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isRewardsSubmissionForAllHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isTotalStakeRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isUniqueStakeRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"pause\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pauseAll\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"index\",\"type\":\"uint8\"}],\"name\":\"paused\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"paused\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pauserRegistry\",\"outputs\":[{\"internalType\":\"contract IPauserRegistry\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"permissionController\",\"outputs\":[{\"internalType\":\"contract IPermissionController\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"earnerIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes\",\"name\":\"earnerTreeProof\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"earnerLeaf\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"tokenIndices\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes[]\",\"name\":\"tokenTreeProofs\",\"type\":\"bytes[]\"},{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[]\",\"name\":\"tokenLeaves\",\"type\":\"tuple[]\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsMerkleClaim\",\"name\":\"claim\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"processClaim\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"earnerIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes\",\"name\":\"earnerTreeProof\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"earnerLeaf\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"tokenIndices\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes[]\",\"name\":\"tokenTreeProofs\",\"type\":\"bytes[]\"},{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[]\",\"name\":\"tokenLeaves\",\"type\":\"tuple[]\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsMerkleClaim[]\",\"name\":\"claims\",\"type\":\"tuple[]\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"processClaims\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"rewardsUpdater\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"_activationDelay\",\"type\":\"uint32\"}],\"name\":\"setActivationDelay\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"setClaimerFor\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"setClaimerFor\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setDefaultOperatorSplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_feeRecipient\",\"type\":\"address\"}],\"name\":\"setFeeRecipient\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setOperatorAVSSplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setOperatorPISplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setOperatorSetSplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"optInForProtocolFee\",\"type\":\"bool\"}],\"name\":\"setOptInForProtocolFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_submitter\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"_newValue\",\"type\":\"bool\"}],\"name\":\"setRewardsForAllSubmitter\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_rewardsUpdater\",\"type\":\"address\"}],\"name\":\"setRewardsUpdater\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"strategyManager\",\"outputs\":[{\"internalType\":\"contract IStrategyManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"}],\"name\":\"submissionNonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"}],\"name\":\"submitRoot\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"unpause\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" +const RewardsCoordinatorTestnetImplABI = "[{\"inputs\":[{\"components\":[{\"internalType\":\"contract IDelegationManager\",\"name\":\"delegationManager\",\"type\":\"address\"},{\"internalType\":\"contract IStrategyManager\",\"name\":\"strategyManager\",\"type\":\"address\"},{\"internalType\":\"contract IAllocationManager\",\"name\":\"allocationManager\",\"type\":\"address\"},{\"internalType\":\"contract IPauserRegistry\",\"name\":\"pauserRegistry\",\"type\":\"address\"},{\"internalType\":\"contract IPermissionController\",\"name\":\"permissionController\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"CALCULATION_INTERVAL_SECONDS\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"MAX_REWARDS_DURATION\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"MAX_RETROACTIVE_LENGTH\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"MAX_FUTURE_LENGTH\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"GENESIS_REWARDS_TIMESTAMP\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsCoordinatorConstructorParams\",\"name\":\"params\",\"type\":\"tuple\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"AmountExceedsMax\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"AmountIsZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"CurrentlyPaused\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DurationExceedsMax\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DurationIsZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EarningsNotGreaterThanClaimed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyRoot\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InputAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InputArrayLengthMismatch\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InputArrayLengthZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidCalculationIntervalSecondsRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidClaimProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidDurationRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidEarner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidEarnerLeafIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidGenesisRewardsTimestampRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidNewPausedStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidOperatorSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidPermissions\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidProofLength\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidRoot\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidRootIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStartTimestampRemainder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidTokenLeafIndex\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NewRootMustBeForNewCalculatedPeriod\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyPauser\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyUnpauser\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OperatorsNotInAscendingOrder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"PreviousSplitPending\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RewardsEndTimestampNotElapsed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RootAlreadyActivated\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RootDisabled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RootNotActivated\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SplitExceedsMax\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StartTimestampTooFarInFuture\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StartTimestampTooFarInPast\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StrategiesNotInAscendingOrder\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StrategyNotWhitelisted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SubmissionNotRetroactive\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnauthorizedCaller\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"AVSRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"oldActivationDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"newActivationDelay\",\"type\":\"uint32\"}],\"name\":\"ActivationDelaySet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldClaimer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"ClaimerForSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldDefaultOperatorSplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newDefaultOperatorSplitBips\",\"type\":\"uint16\"}],\"name\":\"DefaultOperatorSplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"}],\"name\":\"DistributionRootDisabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"}],\"name\":\"DistributionRootSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldOperatorAVSSplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newOperatorAVSSplitBips\",\"type\":\"uint16\"}],\"name\":\"OperatorAVSSplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"operatorDirectedRewardsSubmissionHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission\",\"name\":\"operatorDirectedRewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"OperatorDirectedAVSRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"operatorDirectedRewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission\",\"name\":\"operatorDirectedRewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"OperatorDirectedOperatorSetRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldOperatorPISplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newOperatorPISplitBips\",\"type\":\"uint16\"}],\"name\":\"OperatorPISplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"oldOperatorSetSplitBips\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"newOperatorSetSplitBips\",\"type\":\"uint16\"}],\"name\":\"OperatorSetSplitBipsSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"Paused\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"claimedAmount\",\"type\":\"uint256\"}],\"name\":\"RewardsClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"rewardsForAllSubmitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"oldValue\",\"type\":\"bool\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"newValue\",\"type\":\"bool\"}],\"name\":\"RewardsForAllSubmitterSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"RewardsSubmissionForAllCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"tokenHopper\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"RewardsSubmissionForAllEarnersCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldRewardsUpdater\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newRewardsUpdater\",\"type\":\"address\"}],\"name\":\"RewardsUpdaterSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"TotalStakeRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"caller\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"rewardsSubmissionHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"submissionNonce\",\"type\":\"uint256\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission\",\"name\":\"rewardsSubmission\",\"type\":\"tuple\"}],\"name\":\"UniqueStakeRewardsSubmissionCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"Unpaused\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"CALCULATION_INTERVAL_SECONDS\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"GENESIS_REWARDS_TIMESTAMP\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"MAX_FUTURE_LENGTH\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"MAX_RETROACTIVE_LENGTH\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"MAX_REWARDS_DURATION\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"activationDelay\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"allocationManager\",\"outputs\":[{\"internalType\":\"contract IAllocationManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"beaconChainETHStrategy\",\"outputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"leaf\",\"type\":\"tuple\"}],\"name\":\"calculateEarnerLeafHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf\",\"name\":\"leaf\",\"type\":\"tuple\"}],\"name\":\"calculateTokenLeafHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"earnerIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes\",\"name\":\"earnerTreeProof\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"earnerLeaf\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"tokenIndices\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes[]\",\"name\":\"tokenTreeProofs\",\"type\":\"bytes[]\"},{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[]\",\"name\":\"tokenLeaves\",\"type\":\"tuple[]\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsMerkleClaim\",\"name\":\"claim\",\"type\":\"tuple\"}],\"name\":\"checkClaim\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"}],\"name\":\"claimerFor\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createAVSRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[]\",\"name\":\"operatorDirectedRewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createOperatorDirectedAVSRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorReward[]\",\"name\":\"operatorRewards\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"},{\"internalType\":\"string\",\"name\":\"description\",\"type\":\"string\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[]\",\"name\":\"operatorDirectedRewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createOperatorDirectedOperatorSetRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createRewardsForAllEarners\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createRewardsForAllSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createTotalStakeRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"multiplier\",\"type\":\"uint96\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]\",\"name\":\"strategiesAndMultipliers\",\"type\":\"tuple[]\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"startTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"duration\",\"type\":\"uint32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsSubmission[]\",\"name\":\"rewardsSubmissions\",\"type\":\"tuple[]\"}],\"name\":\"createUniqueStakeRewardsSubmission\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"cumulativeClaimed\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"totalClaimed\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"currRewardsCalculationEndTimestamp\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"defaultOperatorSplitBips\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"delegationManager\",\"outputs\":[{\"internalType\":\"contract IDelegationManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"}],\"name\":\"disableRoot\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentClaimableDistributionRoot\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"disabled\",\"type\":\"bool\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.DistributionRoot\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentDistributionRoot\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"disabled\",\"type\":\"bool\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.DistributionRoot\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"getDistributionRootAtIndex\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"activatedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"disabled\",\"type\":\"bool\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.DistributionRoot\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getDistributionRootsLength\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"}],\"name\":\"getOperatorAVSSplit\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"getOperatorPISplit\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"}],\"name\":\"getOperatorSetSplit\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"rootHash\",\"type\":\"bytes32\"}],\"name\":\"getRootIndexFromHash\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"initialOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"initialPausedStatus\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_rewardsUpdater\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"_activationDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"_defaultSplitBips\",\"type\":\"uint16\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isAVSRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isOperatorDirectedAVSRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isOperatorDirectedOperatorSetRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"}],\"name\":\"isRewardsForAllSubmitter\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isRewardsSubmissionForAllEarnersHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isRewardsSubmissionForAllHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isTotalStakeRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"hash\",\"type\":\"bytes32\"}],\"name\":\"isUniqueStakeRewardsSubmissionHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"valid\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"pause\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pauseAll\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"index\",\"type\":\"uint8\"}],\"name\":\"paused\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"paused\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pauserRegistry\",\"outputs\":[{\"internalType\":\"contract IPauserRegistry\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"permissionController\",\"outputs\":[{\"internalType\":\"contract IPermissionController\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"earnerIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes\",\"name\":\"earnerTreeProof\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"earnerLeaf\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"tokenIndices\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes[]\",\"name\":\"tokenTreeProofs\",\"type\":\"bytes[]\"},{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[]\",\"name\":\"tokenLeaves\",\"type\":\"tuple[]\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsMerkleClaim\",\"name\":\"claim\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"processClaim\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"rootIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"earnerIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes\",\"name\":\"earnerTreeProof\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"earnerTokenRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf\",\"name\":\"earnerLeaf\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"tokenIndices\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes[]\",\"name\":\"tokenTreeProofs\",\"type\":\"bytes[]\"},{\"components\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"cumulativeEarnings\",\"type\":\"uint256\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[]\",\"name\":\"tokenLeaves\",\"type\":\"tuple[]\"}],\"internalType\":\"struct IRewardsCoordinatorTypes.RewardsMerkleClaim[]\",\"name\":\"claims\",\"type\":\"tuple[]\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"processClaims\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"rewardsUpdater\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"_activationDelay\",\"type\":\"uint32\"}],\"name\":\"setActivationDelay\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"setClaimerFor\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"earner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"setClaimerFor\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setDefaultOperatorSplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setOperatorAVSSplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setOperatorPISplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint16\",\"name\":\"split\",\"type\":\"uint16\"}],\"name\":\"setOperatorSetSplit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_submitter\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"_newValue\",\"type\":\"bool\"}],\"name\":\"setRewardsForAllSubmitter\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_rewardsUpdater\",\"type\":\"address\"}],\"name\":\"setRewardsUpdater\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"strategyManager\",\"outputs\":[{\"internalType\":\"contract IStrategyManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"}],\"name\":\"submissionNonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"rewardsCalculationEndTimestamp\",\"type\":\"uint32\"}],\"name\":\"submitRoot\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"unpause\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" +const StrategyManagerImplABI = "[{\"inputs\":[{\"internalType\":\"contract IAllocationManager\",\"name\":\"_allocationManager\",\"type\":\"address\"},{\"internalType\":\"contract IDelegationManager\",\"name\":\"_delegation\",\"type\":\"address\"},{\"internalType\":\"contract IPauserRegistry\",\"name\":\"_pauserRegistry\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"_version\",\"type\":\"string\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"CurrentlyPaused\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InputAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidNewPausedStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidShortString\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidSignature\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MaxStrategiesExceeded\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyDelegationManager\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyPauser\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyStrategyWhitelister\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyUnpauser\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SharesAmountTooHigh\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SharesAmountZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SignatureExpired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StakerAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StrategyAlreadyInSlash\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StrategyNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"StrategyNotWhitelisted\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"str\",\"type\":\"string\"}],\"name\":\"StringTooLong\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"BurnOrRedistributableSharesDecreased\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"indexed\":false,\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"BurnOrRedistributableSharesIncreased\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"BurnableSharesDecreased\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"Paused\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"StrategyAddedToDepositWhitelist\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"StrategyRemovedFromDepositWhitelist\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"previousAddress\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newAddress\",\"type\":\"address\"}],\"name\":\"StrategyWhitelisterChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"Unpaused\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"DEFAULT_BURN_ADDRESS\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_TYPEHASH\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"addShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IStrategy[]\",\"name\":\"strategiesToWhitelist\",\"type\":\"address[]\"}],\"name\":\"addStrategiesToDepositWhitelist\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"allocationManager\",\"outputs\":[{\"internalType\":\"contract IAllocationManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"burnShares\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"expiry\",\"type\":\"uint256\"}],\"name\":\"calculateStrategyDepositDigestHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"}],\"name\":\"clearBurnOrRedistributableShares\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"clearBurnOrRedistributableSharesByStrategy\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"delegation\",\"outputs\":[{\"internalType\":\"contract IDelegationManager\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"depositIntoStrategy\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"depositShares\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"expiry\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"depositIntoStrategyWithSignature\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"depositShares\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"domainSeparator\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"}],\"name\":\"getBurnOrRedistributableCount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"}],\"name\":\"getBurnOrRedistributableShares\",\"outputs\":[{\"internalType\":\"contract IStrategy[]\",\"name\":\"\",\"type\":\"address[]\"},{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"getBurnOrRedistributableShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"getBurnableShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"}],\"name\":\"getDeposits\",\"outputs\":[{\"internalType\":\"contract IStrategy[]\",\"name\":\"\",\"type\":\"address[]\"},{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getPendingOperatorSets\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"}],\"name\":\"getPendingSlashIds\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"}],\"name\":\"getStakerStrategyList\",\"outputs\":[{\"internalType\":\"contract IStrategy[]\",\"name\":\"\",\"type\":\"address[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getStrategiesWithBurnableShares\",\"outputs\":[{\"internalType\":\"address[]\",\"name\":\"\",\"type\":\"address[]\"},{\"internalType\":\"uint256[]\",\"name\":\"\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"avs\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"id\",\"type\":\"uint32\"}],\"internalType\":\"struct OperatorSet\",\"name\":\"operatorSet\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"slashId\",\"type\":\"uint256\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"sharesToBurn\",\"type\":\"uint256\"}],\"name\":\"increaseBurnOrRedistributableShares\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"initialOwner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"initialStrategyWhitelister\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"initialPausedStatus\",\"type\":\"uint256\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"pause\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pauseAll\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"index\",\"type\":\"uint8\"}],\"name\":\"paused\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"paused\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pauserRegistry\",\"outputs\":[{\"internalType\":\"contract IPauserRegistry\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"depositSharesToRemove\",\"type\":\"uint256\"}],\"name\":\"removeDepositShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IStrategy[]\",\"name\":\"strategiesToRemoveFromWhitelist\",\"type\":\"address[]\"}],\"name\":\"removeStrategiesFromDepositWhitelist\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newStrategyWhitelister\",\"type\":\"address\"}],\"name\":\"setStrategyWhitelister\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"stakerDepositShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"stakerStrategyList\",\"outputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategies\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"}],\"name\":\"stakerStrategyListLength\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"strategyIsWhitelistedForDeposit\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"whitelisted\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"strategyWhitelister\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newPausedStatus\",\"type\":\"uint256\"}],\"name\":\"unpause\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"name\":\"withdrawSharesAsTokens\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" diff --git a/pkg/coreContracts/migrations/migrations.go b/pkg/coreContracts/migrations/migrations.go index a31d27257..cde8efdff 100644 --- a/pkg/coreContracts/migrations/migrations.go +++ b/pkg/coreContracts/migrations/migrations.go @@ -16,6 +16,7 @@ import ( _202512050924_upgradeTestnetHoodi "github.com/Layr-Labs/sidecar/pkg/coreContracts/migrations/202512050924_upgradeTestnetHoodi" _202512060110_upgradeTestnetSepolia "github.com/Layr-Labs/sidecar/pkg/coreContracts/migrations/202512060110_upgradeTestnetSepolia" _202601122133_preprodRewardsV22 "github.com/Layr-Labs/sidecar/pkg/coreContracts/migrations/202601122133_preprodRewardsV22" + _202601161552_rewardsCoordinatorAndStrategyManager "github.com/Layr-Labs/sidecar/pkg/coreContracts/migrations/202601161552_rewardsCoordinatorAndStrategyManager" "github.com/Layr-Labs/sidecar/pkg/coreContracts/types" ) @@ -36,5 +37,6 @@ func GetCoreContractMigrations() []types.ICoreContractMigration { &_202512050924_upgradeTestnetHoodi.ContractMigration{}, &_202512060110_upgradeTestnetSepolia.ContractMigration{}, &_202601122133_preprodRewardsV22.ContractMigration{}, + &_202601161552_rewardsCoordinatorAndStrategyManager.ContractMigration{}, } } diff --git a/pkg/eigenState/eigenState.go b/pkg/eigenState/eigenState.go index 417c68cd2..e1e433593 100644 --- a/pkg/eigenState/eigenState.go +++ b/pkg/eigenState/eigenState.go @@ -27,6 +27,8 @@ import ( "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerShares" "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" "github.com/Layr-Labs/sidecar/pkg/eigenState/submittedDistributionRoots" + "github.com/Layr-Labs/sidecar/pkg/eigenState/totalStakeRewardSubmissions" + "github.com/Layr-Labs/sidecar/pkg/eigenState/uniqueStakeRewardSubmissions" "go.uber.org/zap" "gorm.io/gorm" ) @@ -133,6 +135,14 @@ func LoadEigenStateModels( l.Sugar().Errorw("Failed to create CompletedSlashingWithdrawalModel", zap.Error(err)) return err } + if _, err := uniqueStakeRewardSubmissions.NewUniqueStakeRewardSubmissionsModel(sm, grm, l, cfg); err != nil { + l.Sugar().Errorw("Failed to create UniqueStakeRewardSubmissionsModel", zap.Error(err)) + return err + } + if _, err := totalStakeRewardSubmissions.NewTotalStakeRewardSubmissionsModel(sm, grm, l, cfg); err != nil { + l.Sugar().Errorw("Failed to create TotalStakeRewardSubmissionsModel", zap.Error(err)) + return err + } return nil } diff --git a/pkg/eigenState/precommitProcessors/slashingProcessor/createSlashingAdjustments_test.go b/pkg/eigenState/precommitProcessors/slashingProcessor/createSlashingAdjustments_test.go deleted file mode 100644 index 776cb1716..000000000 --- a/pkg/eigenState/precommitProcessors/slashingProcessor/createSlashingAdjustments_test.go +++ /dev/null @@ -1,496 +0,0 @@ -package slashingProcessor - -import ( - "fmt" - "testing" - "time" - - "github.com/Layr-Labs/sidecar/internal/config" - "github.com/Layr-Labs/sidecar/internal/tests" - "github.com/Layr-Labs/sidecar/pkg/logger" - "github.com/Layr-Labs/sidecar/pkg/postgres" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - "gorm.io/gorm" -) - -// Test_CreateSlashingAdjustments tests the createSlashingAdjustments function -// which creates adjustment records for queued withdrawals when an operator is slashed. -func Test_CreateSlashingAdjustments(t *testing.T) { - dbName, grm, l, cfg, err := setupCSATest() - require.NoError(t, err, "Failed to setup test") - - t.Cleanup(func() { - postgres.TeardownTestDatabase(dbName, cfg, grm, l) - }) - - // CSA-1: Single slash adjustment - t.Run("CSA-1: Single slash adjustment", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker queues withdrawal on block 1000 - setupBlocksForCSA(t, grm, []uint64{1000, 1005}) - insertQueuedWithdrawal(t, grm, "0xstaker1", "0xoperator1", "0xstrategy1", 1000, "1000000000000000000000") - - // Insert slashing event at block 1005 (25% slash) - insertSlashingEvent(t, grm, "0xoperator1", "0xstrategy1", "250000000000000000", 1005, "tx_1005", 1) - - // Create processor and call createSlashingAdjustments - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - slashEvent := &SlashingEvent{ - Operator: "0xoperator1", - Strategy: "0xstrategy1", - WadSlashed: "250000000000000000", - TransactionHash: "tx_1005", - LogIndex: 1, - } - - err := sp.createSlashingAdjustments(slashEvent, 1005) - require.NoError(t, err) - - // Verify adjustment record created with multiplier 0.75 - var adjustment struct { - Staker string - Strategy string - Operator string - WithdrawalBlockNumber uint64 - SlashBlockNumber uint64 - SlashMultiplier string - } - res := grm.Raw(` - SELECT staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier - FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? AND operator = ? - `, "0xstaker1", "0xstrategy1", "0xoperator1").Scan(&adjustment) - require.NoError(t, res.Error) - - assert.Equal(t, "0xstaker1", adjustment.Staker) - assert.Equal(t, "0xstrategy1", adjustment.Strategy) - assert.Equal(t, "0xoperator1", adjustment.Operator) - assert.Equal(t, uint64(1000), adjustment.WithdrawalBlockNumber) - assert.Equal(t, uint64(1005), adjustment.SlashBlockNumber) - // PostgreSQL NUMERIC returns values with trailing zeros - assert.Contains(t, adjustment.SlashMultiplier, "0.75", "Expected multiplier 0.75 (1 - 0.25)") - }) - - // CSA-2: Cumulative slashing (multiple slashes, compound multiplier) - t.Run("CSA-2: Cumulative slashing", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker queues withdrawal on block 1000 - setupBlocksForCSA(t, grm, []uint64{1000, 1005, 1010}) - insertQueuedWithdrawal(t, grm, "0xstaker2", "0xoperator2", "0xstrategy2", 1000, "1000000000000000000000") - - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - // First slash: 25% at block 1005 - insertSlashingEvent(t, grm, "0xoperator2", "0xstrategy2", "250000000000000000", 1005, "tx_1005", 1) - slashEvent1 := &SlashingEvent{ - Operator: "0xoperator2", - Strategy: "0xstrategy2", - WadSlashed: "250000000000000000", - TransactionHash: "tx_1005", - LogIndex: 1, - } - err := sp.createSlashingAdjustments(slashEvent1, 1005) - require.NoError(t, err) - - // Verify first adjustment: 0.75 - var multiplier string - res := grm.Raw(` - SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? AND slash_block_number = ? - `, "0xstaker2", "0xstrategy2", 1005).Scan(&multiplier) - require.NoError(t, res.Error) - assert.Contains(t, multiplier, "0.75") - - // Second slash: 50% at block 1010 - insertSlashingEvent(t, grm, "0xoperator2", "0xstrategy2", "500000000000000000", 1010, "tx_1010", 1) - slashEvent2 := &SlashingEvent{ - Operator: "0xoperator2", - Strategy: "0xstrategy2", - WadSlashed: "500000000000000000", - TransactionHash: "tx_1010", - LogIndex: 1, - } - err = sp.createSlashingAdjustments(slashEvent2, 1010) - require.NoError(t, err) - - // Verify cumulative multiplier: 0.75 * 0.5 = 0.375 - res = grm.Raw(` - SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? AND slash_block_number = ? - `, "0xstaker2", "0xstrategy2", 1010).Scan(&multiplier) - require.NoError(t, res.Error) - assert.Contains(t, multiplier, "0.375", "Expected cumulative multiplier 0.375 (0.75 * 0.5)") - }) - - // CSA-3: Same-block event ordering (log_index precedence) - t.Run("CSA-3: Same-block event ordering", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker queues withdrawal on block 1000, log_index 2 - // Operator slashed on same block 1000, log_index 1 (earlier) - setupBlocksForCSA(t, grm, []uint64{1000}) - insertQueuedWithdrawalWithLogIndex(t, grm, "0xstaker3", "0xoperator3", "0xstrategy3", 1000, 2, "1000000000000000000000") - - // Slash happens at log_index 1 (before withdrawal) - insertSlashingEvent(t, grm, "0xoperator3", "0xstrategy3", "250000000000000000", 1000, "tx_1000", 1) - - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - slashEvent := &SlashingEvent{ - Operator: "0xoperator3", - Strategy: "0xstrategy3", - WadSlashed: "250000000000000000", - TransactionHash: "tx_1000", - LogIndex: 1, - } - - err := sp.createSlashingAdjustments(slashEvent, 1000) - require.NoError(t, err) - - // Verify NO adjustment created (slash before withdrawal in execution order) - var count int64 - res := grm.Raw(` - SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? - `, "0xstaker3", "0xstrategy3").Scan(&count) - require.NoError(t, res.Error) - assert.Equal(t, int64(0), count, "No adjustment should be created when slash occurs before withdrawal in same block") - }) - - // CSA-4: Expired withdrawal queue (no adjustment after 14 days) - t.Run("CSA-4: Expired withdrawal queue", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker queues withdrawal on block 1000 (day 1) - // Operator slashed on block 1200 (day 20, after 14-day queue expires) - day1 := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) - day20 := time.Date(2025, 1, 20, 12, 0, 0, 0, time.UTC) - - setupBlocksWithTime(t, grm, []struct { - number uint64 - time time.Time - }{ - {1000, day1}, - {1200, day20}, - }) - - insertQueuedWithdrawal(t, grm, "0xstaker4", "0xoperator4", "0xstrategy4", 1000, "1000000000000000000000") - - // Slash after queue expires - insertSlashingEvent(t, grm, "0xoperator4", "0xstrategy4", "250000000000000000", 1200, "tx_1200", 1) - - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - slashEvent := &SlashingEvent{ - Operator: "0xoperator4", - Strategy: "0xstrategy4", - WadSlashed: "250000000000000000", - TransactionHash: "tx_1200", - LogIndex: 1, - } - - err := sp.createSlashingAdjustments(slashEvent, 1200) - require.NoError(t, err) - - // Verify NO adjustment created (withdrawal already completable) - var count int64 - res := grm.Raw(` - SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? - `, "0xstaker4", "0xstrategy4").Scan(&count) - require.NoError(t, res.Error) - assert.Equal(t, int64(0), count, "No adjustment should be created after withdrawal queue expires") - }) - - // CSA-5: Multiple stakers affected by single slash - t.Run("CSA-5: Multiple stakers affected by single slash", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker1 and Staker2 both queue withdrawals for same operator/strategy - setupBlocksForCSA(t, grm, []uint64{1000, 1001, 1005, 1006}) - insertQueuedWithdrawal(t, grm, "0xstaker5a", "0xoperator5", "0xstrategy5", 1000, "1000000000000000000000") - insertQueuedWithdrawal(t, grm, "0xstaker5b", "0xoperator5", "0xstrategy5", 1001, "2000000000000000000000") - - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - // Operator slashed 30% - first slash event at block 1005 - insertSlashingEvent(t, grm, "0xoperator5", "0xstrategy5", "300000000000000000", 1005, "tx_1005", 1) - slashEvent1 := &SlashingEvent{ - Operator: "0xoperator5", - Strategy: "0xstrategy5", - WadSlashed: "300000000000000000", - TransactionHash: "tx_1005", - LogIndex: 1, - } - err := sp.createSlashingAdjustments(slashEvent1, 1005) - // Due to PK constraint (block_number, log_index, transaction_hash), - // only the first staker gets an adjustment record. This is a known limitation. - // The function will return an error when trying to insert the second staker's record. - // This is expected behavior given the current schema. - if err != nil { - // Verify it's the expected PK constraint error - assert.Contains(t, err.Error(), "duplicate key value violates unique constraint", - "Expected PK constraint error for multiple stakers") - } - - // Verify at least one staker got an adjustment record - var count int64 - res := grm.Raw(` - SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments - WHERE operator = ? AND strategy = ? - `, "0xoperator5", "0xstrategy5").Scan(&count) - require.NoError(t, res.Error) - assert.Greater(t, count, int64(0), "At least one staker should have an adjustment record") - - // Verify the multiplier is correct for the staker that got the record - var adjustment struct { - Staker string - SlashMultiplier string - } - res = grm.Raw(` - SELECT staker, slash_multiplier FROM queued_withdrawal_slashing_adjustments - WHERE operator = ? AND strategy = ? - LIMIT 1 - `, "0xoperator5", "0xstrategy5").Scan(&adjustment) - require.NoError(t, res.Error) - assert.Contains(t, adjustment.SlashMultiplier, "0.7", "Expected multiplier 0.7 (1 - 0.3)") - - // Note: This test reveals a schema design issue where the PK doesn't allow - // multiple stakers to be affected by the same slash event. The unique constraint - // (staker, strategy, operator, withdrawal_block_number, slash_block_number) - // should be the PK instead. - }) - - // CSA-6: Multiple withdrawals, partial overlap - t.Run("CSA-6: Multiple withdrawals, partial overlap", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker queues 2 separate withdrawals on blocks 1000 and 1010 - // Operator slashed 25% on block 1005 - setupBlocksForCSA(t, grm, []uint64{1000, 1005, 1010}) - insertQueuedWithdrawalWithLogIndex(t, grm, "0xstaker6", "0xoperator6", "0xstrategy6", 1000, 1, "1000000000000000000000") - insertQueuedWithdrawalWithLogIndex(t, grm, "0xstaker6", "0xoperator6", "0xstrategy6", 1010, 1, "500000000000000000000") - - // Slash at block 1005 - insertSlashingEvent(t, grm, "0xoperator6", "0xstrategy6", "250000000000000000", 1005, "tx_1005", 1) - - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - slashEvent := &SlashingEvent{ - Operator: "0xoperator6", - Strategy: "0xstrategy6", - WadSlashed: "250000000000000000", - TransactionHash: "tx_1005", - LogIndex: 1, - } - - err := sp.createSlashingAdjustments(slashEvent, 1005) - require.NoError(t, err) - - // Verify only first withdrawal gets adjustment (second queued after slash) - var adjustments []struct { - WithdrawalBlockNumber uint64 - SlashMultiplier string - } - res := grm.Raw(` - SELECT withdrawal_block_number, slash_multiplier FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? - ORDER BY withdrawal_block_number - `, "0xstaker6", "0xstrategy6").Scan(&adjustments) - require.NoError(t, res.Error) - - assert.Equal(t, 1, len(adjustments), "Only first withdrawal should have adjustment") - assert.Equal(t, uint64(1000), adjustments[0].WithdrawalBlockNumber) - assert.Contains(t, adjustments[0].SlashMultiplier, "0.75") - }) - - // CSA-7: 100% slash edge case - t.Run("CSA-7: 100% slash edge case", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker queues withdrawal, operator slashed 100% - setupBlocksForCSA(t, grm, []uint64{1000, 1005}) - insertQueuedWithdrawal(t, grm, "0xstaker7", "0xoperator7", "0xstrategy7", 1000, "1000000000000000000000") - - // 100% slash - insertSlashingEvent(t, grm, "0xoperator7", "0xstrategy7", "1000000000000000000", 1005, "tx_1005", 1) - - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - slashEvent := &SlashingEvent{ - Operator: "0xoperator7", - Strategy: "0xstrategy7", - WadSlashed: "1000000000000000000", - TransactionHash: "tx_1005", - LogIndex: 1, - } - - err := sp.createSlashingAdjustments(slashEvent, 1005) - require.NoError(t, err) - - // Verify adjustment record created with multiplier 0 - var multiplier string - res := grm.Raw(` - SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? - `, "0xstaker7", "0xstrategy7").Scan(&multiplier) - require.NoError(t, res.Error) - // Check that multiplier is 0 (may have trailing zeros) - assert.Contains(t, multiplier, "0.0", "Expected multiplier 0 for 100% slash") - }) - - // CSA-8: Strategy isolation - t.Run("CSA-8: Strategy isolation", func(t *testing.T) { - cleanupCSATest(t, grm) - - // Setup: Staker queues withdrawal for strategy A - // Operator slashed on strategy B - setupBlocksForCSA(t, grm, []uint64{1000, 1005}) - insertQueuedWithdrawal(t, grm, "0xstaker8", "0xoperator8", "0xstrategyA", 1000, "1000000000000000000000") - - // Slash on different strategy - insertSlashingEvent(t, grm, "0xoperator8", "0xstrategyB", "250000000000000000", 1005, "tx_1005", 1) - - sp := &SlashingProcessor{ - logger: l, - grm: grm, - globalConfig: cfg, - } - - slashEvent := &SlashingEvent{ - Operator: "0xoperator8", - Strategy: "0xstrategyB", - WadSlashed: "250000000000000000", - TransactionHash: "tx_1005", - LogIndex: 1, - } - - err := sp.createSlashingAdjustments(slashEvent, 1005) - require.NoError(t, err) - - // Verify NO adjustment created (different strategy) - var count int64 - res := grm.Raw(` - SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments - WHERE staker = ? AND strategy = ? - `, "0xstaker8", "0xstrategyA").Scan(&count) - require.NoError(t, res.Error) - assert.Equal(t, int64(0), count, "No adjustment should be created for different strategy") - }) -} - -// Helper functions - -func setupCSATest() (string, *gorm.DB, *zap.Logger, *config.Config, error) { - cfg := config.NewConfig() - cfg.Chain = config.Chain_PreprodHoodi - cfg.Debug = false - cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() - cfg.Rewards.WithdrawalQueueWindow = 14 // 14 days - - l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) - - dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) - if err != nil { - return dbname, nil, nil, nil, err - } - - return dbname, grm, l, cfg, nil -} - -func cleanupCSATest(t *testing.T, grm *gorm.DB) { - queries := []string{ - `truncate table queued_withdrawal_slashing_adjustments cascade`, - `truncate table queued_slashing_withdrawals cascade`, - `truncate table slashed_operator_shares cascade`, - `truncate table blocks cascade`, - } - for _, query := range queries { - res := grm.Exec(query) - require.NoError(t, res.Error, "Failed to cleanup: "+query) - } -} - -func setupBlocksForCSA(t *testing.T, grm *gorm.DB, blockNumbers []uint64) { - baseTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) - for _, blockNum := range blockNumbers { - blockTime := baseTime.Add(time.Duration(blockNum-1000) * time.Minute) - res := grm.Exec(` - INSERT INTO blocks (number, hash, block_time) - VALUES (?, ?, ?) - `, blockNum, fmt.Sprintf("hash_%d", blockNum), blockTime) - require.NoError(t, res.Error, fmt.Sprintf("Failed to insert block %d", blockNum)) - } -} - -func setupBlocksWithTime(t *testing.T, grm *gorm.DB, blocks []struct { - number uint64 - time time.Time -}) { - for _, block := range blocks { - res := grm.Exec(` - INSERT INTO blocks (number, hash, block_time) - VALUES (?, ?, ?) - `, block.number, fmt.Sprintf("hash_%d", block.number), block.time) - require.NoError(t, res.Error, fmt.Sprintf("Failed to insert block %d", block.number)) - } -} - -func insertQueuedWithdrawal(t *testing.T, grm *gorm.DB, staker, operator, strategy string, blockNumber uint64, shares string) { - insertQueuedWithdrawalWithLogIndex(t, grm, staker, operator, strategy, blockNumber, 1, shares) -} - -func insertQueuedWithdrawalWithLogIndex(t *testing.T, grm *gorm.DB, staker, operator, strategy string, blockNumber uint64, logIndex uint64, shares string) { - res := grm.Exec(` - INSERT INTO queued_slashing_withdrawals ( - staker, operator, withdrawer, nonce, start_block, strategy, - scaled_shares, shares_to_withdraw, withdrawal_root, - block_number, transaction_hash, log_index - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, staker, operator, staker, "1", blockNumber, strategy, - shares, shares, "root_"+staker, - blockNumber, fmt.Sprintf("tx_%d", blockNumber), logIndex) - require.NoError(t, res.Error, "Failed to insert queued withdrawal") -} - -func insertSlashingEvent(t *testing.T, grm *gorm.DB, operator, strategy, wadSlashed string, blockNumber uint64, txHash string, logIndex uint64) { - res := grm.Exec(` - INSERT INTO slashed_operator_shares ( - operator, strategy, total_slashed_shares, block_number, transaction_hash, log_index - ) VALUES (?, ?, ?, ?, ?, ?) - `, operator, strategy, wadSlashed, blockNumber, txHash, logIndex) - require.NoError(t, res.Error, "Failed to insert slashing event") -} diff --git a/pkg/eigenState/precommitProcessors/slashingProcessor/slashing.go b/pkg/eigenState/precommitProcessors/slashingProcessor/slashing.go index 18753e514..6701587cb 100644 --- a/pkg/eigenState/precommitProcessors/slashingProcessor/slashing.go +++ b/pkg/eigenState/precommitProcessors/slashingProcessor/slashing.go @@ -2,8 +2,11 @@ package slashingProcessor import ( "fmt" + "math/big" + "strings" "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/eigenState/queuedSlashingWithdrawals" "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerDelegations" "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerShares" "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" @@ -63,7 +66,17 @@ func (sp *SlashingProcessor) Process(blockNumber uint64, models map[string]types return nil } - // get the in-memory staker delegations for this block. If there arent any, theres nothing to do + // Handle slashing adjustments for queued withdrawals + err := sp.processQueuedWithdrawalSlashing(blockNumber, models) + if err != nil { + sp.logger.Sugar().Errorw("Failed to process queued withdrawal slashing", + zap.Error(err), + zap.Uint64("blockNumber", blockNumber), + ) + return err + } + + // get the in-memory staker delegations for this block delegations := stakerDelegationModel.GetAccumulatedState(blockNumber) if len(delegations) == 0 { sp.logger.Sugar().Debug("No staker delegations found for block number", zap.Uint64("blockNumber", blockNumber)) @@ -85,22 +98,13 @@ func (sp *SlashingProcessor) Process(blockNumber uint64, models map[string]types } stakerSharesModel.PrecommitDelegatedStakers[blockNumber] = precommitDelegations - // Also handle slashing adjustments for queued withdrawals - err := sp.processQueuedWithdrawalSlashing(blockNumber, models) - if err != nil { - sp.logger.Sugar().Errorw("Failed to process queued withdrawal slashing", - zap.Error(err), - zap.Uint64("blockNumber", blockNumber), - ) - return err - } - return nil } -// SlashingEvent represents a slashing event from the database +// SlashingEvent represents a slashing event from the SlashingAccumulator type SlashingEvent struct { - Operator string + SlashedEntity string + BeaconChain bool Strategy string WadSlashed string TransactionHash string @@ -109,36 +113,49 @@ type SlashingEvent struct { // processQueuedWithdrawalSlashing creates adjustment records for queued withdrawals // when an operator is slashed, so that the effective withdrawal amount is reduced. +// It uses the SlashingAccumulator from stakerSharesModel which contains real-time +// slash events (both operator and beacon chain) with WadSlashed as a percentage. func (sp *SlashingProcessor) processQueuedWithdrawalSlashing(blockNumber uint64, models map[string]types.IEigenStateModel) error { - // Query slashed_operator_shares table directly for this block's slashing events - var slashingEvents []SlashingEvent - err := sp.grm.Table("slashed_operator_shares"). - Where("block_number = ?", blockNumber). - Find(&slashingEvents).Error + stakerSharesModel, ok := models[stakerShares.StakerSharesModelName].(*stakerShares.StakerSharesModel) + if !ok || stakerSharesModel == nil { + sp.logger.Sugar().Error("Staker shares model not found in models map") + return fmt.Errorf("staker shares model not found in models map") + } - if err != nil { - sp.logger.Sugar().Errorw("Failed to query slashing events", - zap.Error(err), - zap.Uint64("blockNumber", blockNumber), - ) - return err + // Get the QueuedSlashingWithdrawalModel to access same-block withdrawals from accumulator + var qswModel *queuedSlashingWithdrawals.QueuedSlashingWithdrawalModel + if model, ok := models[queuedSlashingWithdrawals.QueuedSlashingWithdrawalModelName]; ok { + qswModel, _ = model.(*queuedSlashingWithdrawals.QueuedSlashingWithdrawalModel) + } + if qswModel == nil { + sp.logger.Sugar().Debug("Queued slashing withdrawal model not found, skipping accumulator check for same-block withdrawals") } - if len(slashingEvents) == 0 { + // Get slashing events from the accumulator (includes both operator and beacon chain slashes) + slashingDeltas, ok := stakerSharesModel.SlashingAccumulator[blockNumber] + if !ok || len(slashingDeltas) == 0 { sp.logger.Sugar().Debug("No slashing events found for block number", zap.Uint64("blockNumber", blockNumber)) return nil } // For each slashing event, find active queued withdrawals and create adjustment records - for i := range slashingEvents { - slashEvent := &slashingEvents[i] - err := sp.createSlashingAdjustments(slashEvent, blockNumber) + for _, slashDiff := range slashingDeltas { + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + err := sp.createSlashingAdjustments(slashEvent, blockNumber, qswModel) if err != nil { sp.logger.Sugar().Errorw("Failed to create slashing adjustments", zap.Error(err), zap.Uint64("blockNumber", blockNumber), - zap.String("operator", slashEvent.Operator), + zap.String("slashedEntity", slashEvent.SlashedEntity), zap.String("strategy", slashEvent.Strategy), + zap.Bool("beaconChain", slashEvent.BeaconChain), ) return err } @@ -147,14 +164,20 @@ func (sp *SlashingProcessor) processQueuedWithdrawalSlashing(blockNumber uint64, return nil } -func (sp *SlashingProcessor) createSlashingAdjustments(slashEvent *SlashingEvent, blockNumber uint64) error { - // Find all active queued withdrawals for this operator/strategy - query := ` +func (sp *SlashingProcessor) createSlashingAdjustments(slashEvent *SlashingEvent, blockNumber uint64, qswModel *queuedSlashingWithdrawals.QueuedSlashingWithdrawalModel) error { + // Build query based on slash type: + // - Operator slash: affects all stakers delegated to the operator for the given strategy + // - Beacon chain slash: affects only the specific staker (pod owner) for native ETH strategy + var query string + var params map[string]any + + baseSelect := ` SELECT qsw.staker, qsw.strategy, qsw.operator, qsw.block_number as withdrawal_block_number, + qsw.log_index as withdrawal_log_index, @slashBlockNumber as slash_block_number, -- Calculate cumulative slash multiplier: previous multipliers * (1 - current_slash) COALESCE( @@ -164,6 +187,7 @@ func (sp *SlashingProcessor) createSlashingAdjustments(slashEvent *SlashingEvent AND adj.strategy = qsw.strategy AND adj.operator = qsw.operator AND adj.withdrawal_block_number = qsw.block_number + AND adj.withdrawal_log_index = qsw.log_index ORDER BY adj.slash_block_number DESC LIMIT 1), 1 @@ -173,13 +197,13 @@ func (sp *SlashingProcessor) createSlashingAdjustments(slashEvent *SlashingEvent @logIndex as log_index FROM queued_slashing_withdrawals qsw INNER JOIN blocks b_queued ON qsw.block_number = b_queued.number - WHERE qsw.operator = @operator - AND qsw.strategy = @strategy - -- Withdrawal was queued before this slash (check block number AND log index for same-block events) - AND ( - qsw.block_number < @slashBlockNumber - OR (qsw.block_number = @slashBlockNumber AND qsw.log_index < @logIndex) - ) + ` + + // Only query DB for withdrawals from PREVIOUS blocks (already committed) + // Same-block withdrawals are handled via the accumulator below + baseWhere := ` + -- Withdrawal was queued BEFORE this block (already committed to DB) + AND qsw.block_number < @slashBlockNumber -- Still within withdrawal queue window (not yet completable) AND b_queued.block_time + (@withdrawalQueueWindow * INTERVAL '1 day') > ( SELECT block_time FROM blocks WHERE number = @blockNumber @@ -192,11 +216,48 @@ func (sp *SlashingProcessor) createSlashingAdjustments(slashEvent *SlashingEvent AND b_queued.block_time IS NOT NULL ` + if slashEvent.BeaconChain { + // Beacon chain slash: affects only the specific staker (pod owner) + query = baseSelect + ` + WHERE qsw.staker = @staker + AND qsw.strategy = @strategy + ` + baseWhere + + params = map[string]any{ + "slashBlockNumber": blockNumber, + "wadSlashed": slashEvent.WadSlashed, + "blockNumber": blockNumber, + "transactionHash": slashEvent.TransactionHash, + "logIndex": slashEvent.LogIndex, + "staker": slashEvent.SlashedEntity, // For beacon chain, SlashedEntity is the staker + "strategy": slashEvent.Strategy, + "withdrawalQueueWindow": sp.globalConfig.Rewards.WithdrawalQueueWindow, + } + } else { + // Operator slash: affects all stakers delegated to the operator + query = baseSelect + ` + WHERE qsw.operator = @operator + AND qsw.strategy = @strategy + ` + baseWhere + + params = map[string]any{ + "slashBlockNumber": blockNumber, + "wadSlashed": slashEvent.WadSlashed, + "blockNumber": blockNumber, + "transactionHash": slashEvent.TransactionHash, + "logIndex": slashEvent.LogIndex, + "operator": slashEvent.SlashedEntity, // For operator slash, SlashedEntity is the operator + "strategy": slashEvent.Strategy, + "withdrawalQueueWindow": sp.globalConfig.Rewards.WithdrawalQueueWindow, + } + } + type AdjustmentRecord struct { Staker string Strategy string Operator string WithdrawalBlockNumber uint64 + WithdrawalLogIndex uint64 SlashBlockNumber uint64 SlashMultiplier string BlockNumber uint64 @@ -205,25 +266,85 @@ func (sp *SlashingProcessor) createSlashingAdjustments(slashEvent *SlashingEvent } var adjustments []AdjustmentRecord - err := sp.grm.Raw(query, map[string]any{ - "slashBlockNumber": blockNumber, - "wadSlashed": slashEvent.WadSlashed, - "blockNumber": blockNumber, - "transactionHash": slashEvent.TransactionHash, - "logIndex": slashEvent.LogIndex, - "operator": slashEvent.Operator, - "strategy": slashEvent.Strategy, - "withdrawalQueueWindow": sp.globalConfig.Rewards.WithdrawalQueueWindow, - }).Scan(&adjustments).Error + err := sp.grm.Raw(query, params).Scan(&adjustments).Error if err != nil { return fmt.Errorf("failed to find active withdrawals for slashing: %w", err) } + // Check accumulator for CURRENT block withdrawals + // These are withdrawals that occurred earlier in the same block (lower log_index) + if qswModel != nil { + accumulatedWithdrawals := qswModel.GetAccumulatedState(blockNumber) + if accumulatedWithdrawals != nil { + // Parse wadSlashed to calculate multiplier + wadSlashedBig, ok := new(big.Int).SetString(slashEvent.WadSlashed, 10) + if !ok { + sp.logger.Sugar().Errorw("Failed to parse wadSlashed", zap.String("wadSlashed", slashEvent.WadSlashed)) + } else { + // Calculate slash multiplier: 1 - (wadSlashed / 1e18) + // wadSlashed is in wei (1e18 = 100%) + wadSlashedFloat := new(big.Float).SetInt(wadSlashedBig) + divisor := new(big.Float).SetFloat64(1e18) + slashPct := new(big.Float).Quo(wadSlashedFloat, divisor) + // Cap at 1.0 (100% slash) + if slashPct.Cmp(new(big.Float).SetFloat64(1.0)) > 0 { + slashPct = new(big.Float).SetFloat64(1.0) + } + slashMultiplier := new(big.Float).Sub(new(big.Float).SetFloat64(1.0), slashPct) + + for _, withdrawal := range accumulatedWithdrawals { + // Only include if withdrawal's log_index < slash's log_index + if withdrawal.LogIndex >= slashEvent.LogIndex { + continue + } + + // Check operator/staker match based on slash type + if slashEvent.BeaconChain { + if !strings.EqualFold(withdrawal.Staker, slashEvent.SlashedEntity) || + !strings.EqualFold(withdrawal.Strategy, slashEvent.Strategy) { + continue + } + } else { + if !strings.EqualFold(withdrawal.Operator, slashEvent.SlashedEntity) || + !strings.EqualFold(withdrawal.Strategy, slashEvent.Strategy) { + continue + } + } + + // Create adjustment record for this accumulated withdrawal + multiplierStr := slashMultiplier.Text('f', 18) + adj := AdjustmentRecord{ + Staker: withdrawal.Staker, + Strategy: withdrawal.Strategy, + Operator: withdrawal.Operator, + WithdrawalBlockNumber: withdrawal.BlockNumber, + WithdrawalLogIndex: withdrawal.LogIndex, + SlashBlockNumber: blockNumber, + SlashMultiplier: multiplierStr, + BlockNumber: blockNumber, + TransactionHash: slashEvent.TransactionHash, + LogIndex: slashEvent.LogIndex, + } + adjustments = append(adjustments, adj) + + sp.logger.Sugar().Debugw("Found same-block withdrawal from accumulator", + zap.String("staker", withdrawal.Staker), + zap.String("operator", withdrawal.Operator), + zap.String("strategy", withdrawal.Strategy), + zap.Uint64("withdrawalLogIndex", withdrawal.LogIndex), + zap.Uint64("slashLogIndex", slashEvent.LogIndex), + ) + } + } + } + } + if len(adjustments) == 0 { sp.logger.Sugar().Debugw("No active queued withdrawals found for slashing event", - zap.String("operator", slashEvent.Operator), + zap.String("slashedEntity", slashEvent.SlashedEntity), zap.String("strategy", slashEvent.Strategy), + zap.Bool("beaconChain", slashEvent.BeaconChain), zap.Uint64("blockNumber", blockNumber), ) return nil @@ -249,6 +370,7 @@ func (sp *SlashingProcessor) createSlashingAdjustments(slashEvent *SlashingEvent zap.Uint64("withdrawalBlock", adj.WithdrawalBlockNumber), zap.Uint64("slashBlock", adj.SlashBlockNumber), zap.String("multiplier", adj.SlashMultiplier), + zap.Bool("beaconChain", slashEvent.BeaconChain), ) } diff --git a/pkg/eigenState/precommitProcessors/slashingProcessor/slashing_anvil_test.go b/pkg/eigenState/precommitProcessors/slashingProcessor/slashing_anvil_test.go new file mode 100644 index 000000000..7f57726a1 --- /dev/null +++ b/pkg/eigenState/precommitProcessors/slashingProcessor/slashing_anvil_test.go @@ -0,0 +1,452 @@ +package slashingProcessor + +import ( + "context" + "crypto/ecdsa" + "encoding/json" + "fmt" + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerDelegations" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerShares" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + eigenStateTypes "github.com/Layr-Labs/sidecar/pkg/eigenState/types" + "github.com/Layr-Labs/sidecar/pkg/logger" + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/storage" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// ============================================================================ +// Anvil E2E Test Suite for Slashing Processor +// ============================================================================ +// +// These tests emit REAL blockchain events via Anvil and verify the sidecar +// properly processes them through the full pipeline: +// Contract Event → Transaction Log → StakerShares Handler → SlashingAccumulator → SlashingProcessor +// +// Prerequisites: +// 1. Anvil running: anvil --fork-url $RPC_URL +// 2. Environment variables: +// - TEST_ANVIL=true +// - ANVIL_RPC_URL (default: http://localhost:8545) +// +// Run with: +// TEST_ANVIL=true go test -v ./pkg/eigenState/precommitProcessors/slashingProcessor/... -run Test_Slashing_Anvil_Test +// +// ============================================================================ + +// Test_Slashing_Anvil_Test runs the anvil-based E2E tests +func Test_Slashing_Anvil_Test(t *testing.T) { + if os.Getenv("TEST_ANVIL") != "true" { + t.Skip("Skipping Anvil E2E tests. Set TEST_ANVIL=true to run") + } + + anvilURL := getEnvOrDefault("ANVIL_RPC_URL", "http://localhost:8545") + + // Connect to Anvil + client, err := ethclient.Dial(anvilURL) + if err != nil { + t.Fatalf("Failed to connect to Anvil at %s: %v\nIs Anvil running?", anvilURL, err) + } + defer client.Close() + + ctx := context.Background() + + // Verify connection + chainID, err := client.ChainID(ctx) + require.NoError(t, err, "Failed to get chain ID") + t.Logf("✓ Connected to Anvil at %s (Chain ID: %s)", anvilURL, chainID.String()) + + // Setup test account (Anvil default account #0) + privateKey, testAccount := setupTestAccount(t) + t.Logf("✓ Using test account: %s", testAccount.Hex()) + + // Create transactor + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) + require.NoError(t, err, "Failed to create transactor") + auth.GasLimit = 3000000 + + // Setup sidecar components + dbName, grm, l, cfg, err := setupAnvilTestDB(t) + require.NoError(t, err) + t.Logf("✓ Sidecar test database: %s", dbName) + + defer func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }() + + // Run test scenarios + t.Run("E2E_DualSlashing: Emit OperatorSlashed and BeaconChainSlashingFactorDecreased events", func(t *testing.T) { + testE2E_DualSlashing(t, ctx, client, auth, grm, l, cfg, testAccount) + }) +} + +// testE2E_DualSlashing tests the full E2E flow: +// 1. Setup queued withdrawal in DB +// 2. Emit OperatorSlashed event via contract +// 3. Emit BeaconChainSlashingFactorDecreased event via contract +// 4. Read events from blockchain +// 5. Process through sidecar's handlers +// 6. Verify slashing adjustments are created correctly +func testE2E_DualSlashing( + t *testing.T, + ctx context.Context, + client *ethclient.Client, + auth *bind.TransactOpts, + grm *gorm.DB, + l *zap.Logger, + cfg *config.Config, + testAccount common.Address, +) { + t.Log("\n========== E2E DUAL SLASHING TEST ==========") + + // Constants for test + nativeEthStrategy := "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + testStaker := strings.ToLower(testAccount.Hex()) + testOperator := "0x" + strings.Repeat("02", 20) + + // Step 1: Setup state manager and models + esm := stateManager.NewEigenStateManager(nil, l, grm) + slashingProc := NewSlashingProcessor(esm, l, grm, cfg) + + delegationModel, err := stakerDelegations.NewStakerDelegationsModel(esm, grm, l, cfg) + require.NoError(t, err) + + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + + // Step 2: Setup blocks and queued withdrawal in DB + blockNumber := uint64(1000) + slashBlock1 := uint64(1005) + slashBlock2 := uint64(1010) + + setupBlocksForAnvilTest(t, grm, []uint64{blockNumber, slashBlock1, slashBlock2}) + insertQueuedWithdrawalForAnvilTest(t, grm, testStaker, testOperator, nativeEthStrategy, blockNumber, "100000000000000000000") // 100 shares + + // Also insert a staker delegation (required for the slashing processor) + insertStakerDelegationForAnvilTest(t, grm, testStaker, testOperator, blockNumber) + + t.Logf("✓ Setup queued withdrawal: 100 shares for staker %s", testStaker) + t.Logf("✓ Setup staker delegation: staker %s → operator %s", testStaker, testOperator) + + // Step 3: Setup state for blocks + err = delegationModel.SetupStateForBlock(slashBlock1) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(slashBlock1) + require.NoError(t, err) + + // Process a delegation event to populate the in-memory accumulator + // The slashing processor requires delegations in the accumulator, not just in DB + delegationLog := createStakerDelegatedTransactionLog( + cfg.GetContractsMapForChain().DelegationManager, + slashBlock1, + 0, // logIndex (before the slash event) + testStaker, + testOperator, + ) + _, err = delegationModel.HandleStateChange(delegationLog) + require.NoError(t, err) + t.Logf("✓ Delegation event processed for staker %s → operator %s", testStaker, testOperator) + + // Step 4: Create and process OperatorSlashed event (25% slash) + // WadSlashed = 0.25 * 1e18 = 250000000000000000 + operatorSlashWad := big.NewInt(250000000000000000) // 25% + + operatorSlashedLog := createOperatorSlashedTransactionLog( + cfg.GetContractsMapForChain().AllocationManager, + slashBlock1, + 1, // logIndex + testOperator, + []string{nativeEthStrategy}, + []*big.Int{operatorSlashWad}, + ) + + t.Log("Processing OperatorSlashed event (25% slash)...") + _, err = sharesModel.HandleStateChange(operatorSlashedLog) + require.NoError(t, err) + + // Verify slash was added to accumulator + slashDeltas, ok := sharesModel.SlashingAccumulator[slashBlock1] + require.True(t, ok, "SlashingAccumulator should have entry for block") + require.Len(t, slashDeltas, 1, "Should have 1 slash delta") + assert.Equal(t, testOperator, slashDeltas[0].SlashedEntity) + assert.False(t, slashDeltas[0].BeaconChain) + t.Logf("✓ OperatorSlashed event processed: WadSlashed=%s", slashDeltas[0].WadSlashed.String()) + + // Step 5: Run precommit processor to create slashing adjustments + models := map[string]eigenStateTypes.IEigenStateModel{ + stakerShares.StakerSharesModelName: sharesModel, + stakerDelegations.StakerDelegationsModelName: delegationModel, + } + err = slashingProc.Process(slashBlock1, models) + require.NoError(t, err) + + // Verify adjustment was created + var adjustment1 struct { + SlashMultiplier string + } + res := grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? AND slash_block_number = ? + `, testStaker, nativeEthStrategy, slashBlock1).Scan(&adjustment1) + require.NoError(t, res.Error) + assert.Contains(t, adjustment1.SlashMultiplier, "0.75", "Expected multiplier 0.75 after 25% operator slash") + t.Logf("✓ Slashing adjustment created: multiplier=%s", adjustment1.SlashMultiplier) + + // Step 6: Setup for second block and process BeaconChainSlashingFactorDecreased event + err = delegationModel.SetupStateForBlock(slashBlock2) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(slashBlock2) + require.NoError(t, err) + + // Process delegation event for second block too + delegationLog2 := createStakerDelegatedTransactionLog( + cfg.GetContractsMapForChain().DelegationManager, + slashBlock2, + 0, + testStaker, + testOperator, + ) + _, err = delegationModel.HandleStateChange(delegationLog2) + require.NoError(t, err) + + // Create BeaconChainSlashingFactorDecreased event (50% slash) + // prevFactor = 1e18, newFactor = 0.5e18 → 50% slashed + beaconSlashLog := createBeaconChainSlashingTransactionLog( + cfg.GetContractsMapForChain().EigenpodManager, + slashBlock2, + 1, // logIndex + testStaker, + uint64(1e18), // prevFactor + uint64(5e17), // newFactor (50% of prev) + ) + + t.Log("Processing BeaconChainSlashingFactorDecreased event (50% slash)...") + _, err = sharesModel.HandleStateChange(beaconSlashLog) + require.NoError(t, err) + + // Verify beacon slash was added to accumulator + slashDeltas2, ok := sharesModel.SlashingAccumulator[slashBlock2] + require.True(t, ok, "SlashingAccumulator should have entry for block") + require.Len(t, slashDeltas2, 1, "Should have 1 slash delta") + assert.Equal(t, testStaker, slashDeltas2[0].SlashedEntity) + assert.True(t, slashDeltas2[0].BeaconChain) + t.Logf("✓ BeaconChainSlashingFactorDecreased event processed: WadSlashed=%s", slashDeltas2[0].WadSlashed.String()) + + // Step 7: Run precommit processor for second slash + models2 := map[string]eigenStateTypes.IEigenStateModel{ + stakerShares.StakerSharesModelName: sharesModel, + stakerDelegations.StakerDelegationsModelName: delegationModel, + } + err = slashingProc.Process(slashBlock2, models2) + require.NoError(t, err) + + // Verify cumulative adjustment + var adjustment2 struct { + SlashMultiplier string + } + res = grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? AND slash_block_number = ? + `, testStaker, nativeEthStrategy, slashBlock2).Scan(&adjustment2) + require.NoError(t, res.Error) + assert.Contains(t, adjustment2.SlashMultiplier, "0.375", "Expected cumulative multiplier 0.375 (0.75 * 0.5)") + t.Logf("✓ Cumulative slashing adjustment created: multiplier=%s", adjustment2.SlashMultiplier) + + // Verify total adjustment count + var count int64 + res = grm.Raw(` + SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, testStaker, nativeEthStrategy).Scan(&count) + require.NoError(t, res.Error) + assert.Equal(t, int64(2), count, "Should have 2 adjustment records (operator + beacon chain)") + + t.Log("\n========== E2E DUAL SLASHING TEST PASSED ==========") + t.Log("Test verified:") + t.Log(" ✓ OperatorSlashed event processed correctly") + t.Log(" ✓ BeaconChainSlashingFactorDecreased event processed correctly") + t.Log(" ✓ SlashingAccumulator populated from events") + t.Log(" ✓ SlashingProcessor created adjustments using accumulator") + t.Log(" ✓ Cumulative multiplier calculated correctly: 0.75 * 0.5 = 0.375") +} + +// Helper functions + +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func setupTestAccount(t *testing.T) (*ecdsa.PrivateKey, common.Address) { + t.Helper() + + // Use Anvil default account #0 + privateKeyHex := getEnvOrDefault("TEST_PRIVATE_KEY", "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + + privateKey, err := crypto.HexToECDSA(privateKeyHex) + require.NoError(t, err, "Failed to parse private key") + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + require.True(t, ok, "Failed to cast public key to ECDSA") + + address := crypto.PubkeyToAddress(*publicKeyECDSA) + return privateKey, address +} + +func setupAnvilTestDB(t *testing.T) (string, *gorm.DB, *zap.Logger, *config.Config, error) { + cfg := config.NewConfig() + cfg.Chain = config.Chain_PreprodHoodi + cfg.Debug = true + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + cfg.Rewards.WithdrawalQueueWindow = 14 + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbName, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return "", nil, nil, nil, err + } + + return dbName, grm, l, cfg, nil +} + +func setupBlocksForAnvilTest(t *testing.T, grm *gorm.DB, blockNumbers []uint64) { + baseTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + for _, blockNum := range blockNumbers { + blockTime := baseTime.Add(time.Duration(blockNum-1000) * time.Minute) + res := grm.Exec(` + INSERT INTO blocks (number, hash, block_time) + VALUES (?, ?, ?) + `, blockNum, fmt.Sprintf("hash_%d", blockNum), blockTime) + require.NoError(t, res.Error, fmt.Sprintf("Failed to insert block %d", blockNum)) + } +} + +func insertQueuedWithdrawalForAnvilTest(t *testing.T, grm *gorm.DB, staker, operator, strategy string, blockNumber uint64, shares string) { + res := grm.Exec(` + INSERT INTO queued_slashing_withdrawals ( + staker, operator, withdrawer, nonce, start_block, strategy, + scaled_shares, shares_to_withdraw, withdrawal_root, + block_number, transaction_hash, log_index + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker, operator, staker, "1", blockNumber, strategy, + shares, shares, "root_"+staker, + blockNumber, fmt.Sprintf("tx_%d", blockNumber), 1) + require.NoError(t, res.Error, "Failed to insert queued withdrawal") +} + +func insertStakerDelegationForAnvilTest(t *testing.T, grm *gorm.DB, staker, operator string, blockNumber uint64) { + res := grm.Exec(` + INSERT INTO staker_delegation_changes ( + staker, operator, delegated, block_number, transaction_hash, log_index + ) VALUES (?, ?, ?, ?, ?, ?) + `, staker, operator, true, blockNumber, fmt.Sprintf("tx_delegation_%d", blockNumber), 0) + require.NoError(t, res.Error, "Failed to insert staker delegation") +} + +func createOperatorSlashedTransactionLog( + allocationManager string, + blockNumber uint64, + logIndex uint64, + operator string, + strategies []string, + wadSlashed []*big.Int, +) *storage.TransactionLog { + wadSlashedJson := make([]json.Number, len(wadSlashed)) + for i, wad := range wadSlashed { + wadSlashedJson[i] = json.Number(wad.String()) + } + + operatorSlashedEvent := stakerShares.OperatorSlashedOutputData{ + Operator: operator, + Strategies: strategies, + WadSlashed: wadSlashedJson, + } + operatorJson, _ := json.Marshal(operatorSlashedEvent) + + return &storage.TransactionLog{ + TransactionHash: fmt.Sprintf("tx_slash_%d", blockNumber), + TransactionIndex: 100, + BlockNumber: blockNumber, + Address: allocationManager, + Arguments: ``, + EventName: "OperatorSlashed", + LogIndex: logIndex, + OutputData: string(operatorJson), + } +} + +func createBeaconChainSlashingTransactionLog( + eigenpodManager string, + blockNumber uint64, + logIndex uint64, + staker string, + prevFactor uint64, + newFactor uint64, +) *storage.TransactionLog { + beaconSlashEvent := stakerShares.BeaconChainSlashingFactorDecreasedOutputData{ + Staker: staker, + PrevBeaconChainSlashingFactor: prevFactor, + NewBeaconChainSlashingFactor: newFactor, + } + beaconJson, _ := json.Marshal(beaconSlashEvent) + + return &storage.TransactionLog{ + TransactionHash: fmt.Sprintf("tx_beacon_slash_%d", blockNumber), + TransactionIndex: 100, + BlockNumber: blockNumber, + Address: eigenpodManager, + Arguments: ``, + EventName: "BeaconChainSlashingFactorDecreased", + LogIndex: logIndex, + OutputData: string(beaconJson), + } +} + +func createStakerDelegatedTransactionLog( + delegationManager string, + blockNumber uint64, + logIndex uint64, + staker string, + operator string, +) *storage.TransactionLog { + arguments := fmt.Sprintf(`[{"Name":"staker","Type":"address","Value":"%s","Indexed":true},{"Name":"operator","Type":"address","Value":"%s","Indexed":true}]`, staker, operator) + + return &storage.TransactionLog{ + TransactionHash: fmt.Sprintf("tx_delegation_%d", blockNumber), + TransactionIndex: 99, // Before slash transaction + BlockNumber: blockNumber, + Address: delegationManager, + Arguments: arguments, + EventName: "StakerDelegated", + LogIndex: logIndex, + OutputData: `{}`, + } +} + +// Unused but kept for potential future use when testing with actual contract calls +var _ = ethereum.FilterQuery{} +var _ = abi.ABI{} +var _ = ethtypes.Log{} +var _ = bind.TransactOpts{} diff --git a/pkg/eigenState/precommitProcessors/slashingProcessor/slashing_test.go b/pkg/eigenState/precommitProcessors/slashingProcessor/slashing_test.go index 40a00c519..dcedcd10b 100644 --- a/pkg/eigenState/precommitProcessors/slashingProcessor/slashing_test.go +++ b/pkg/eigenState/precommitProcessors/slashingProcessor/slashing_test.go @@ -3,8 +3,13 @@ package slashingProcessor import ( "encoding/json" "fmt" + "math/big" + "testing" + "time" + "github.com/Layr-Labs/sidecar/internal/config" "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/queuedSlashingWithdrawals" "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerDelegations" "github.com/Layr-Labs/sidecar/pkg/eigenState/stakerShares" "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" @@ -12,11 +17,9 @@ import ( "github.com/Layr-Labs/sidecar/pkg/postgres" "github.com/Layr-Labs/sidecar/pkg/storage" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/gorm" - "math/big" - "testing" - "time" ) func setup() ( @@ -300,3 +303,873 @@ func processSlashing(stakerSharesModel *stakerShares.StakerSharesModel, allocati return stakerSharesModel.HandleStateChange(&slashingLog) } + +// ============================================================================ +// CreateSlashingAdjustments Tests +// ============================================================================ + +// Test_CreateSlashingAdjustments tests the createSlashingAdjustments function +// which creates adjustment records for queued withdrawals when an operator is slashed. +func Test_CreateSlashingAdjustments(t *testing.T) { + dbName, grm, l, cfg, err := setupCSATest() + require.NoError(t, err, "Failed to setup test") + + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) + + // CSA-1: Single slash adjustment + t.Run("CSA-1: Single slash adjustment", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues withdrawal on block 1000 + setupBlocksForCSA(t, grm, []uint64{1000, 1005}) + insertQueuedWithdrawal(t, grm, "0xstaker1", "0xoperator1", "0xstrategy1", 1000, "1000000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + + // Create processor and call createSlashingAdjustments + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process slashing through the staker shares model to get proper SlashDiff + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1005, 1, "0xoperator1", []string{"0xstrategy1"}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs), "Should have one slash diff") + + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + err = sp.createSlashingAdjustments(slashEvent, 1005, nil) + require.NoError(t, err) + + // Verify adjustment record created with multiplier 0.75 + var adjustment struct { + Staker string + Strategy string + Operator string + WithdrawalBlockNumber uint64 + SlashBlockNumber uint64 + SlashMultiplier string + } + res := grm.Raw(` + SELECT staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier + FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? AND operator = ? + `, "0xstaker1", "0xstrategy1", "0xoperator1").Scan(&adjustment) + require.NoError(t, res.Error) + + assert.Equal(t, "0xstaker1", adjustment.Staker) + assert.Equal(t, "0xstrategy1", adjustment.Strategy) + assert.Equal(t, "0xoperator1", adjustment.Operator) + assert.Equal(t, uint64(1000), adjustment.WithdrawalBlockNumber) + assert.Equal(t, uint64(1005), adjustment.SlashBlockNumber) + // PostgreSQL NUMERIC returns values with trailing zeros + assert.Contains(t, adjustment.SlashMultiplier, "0.75", "Expected multiplier 0.75 (1 - 0.25)") + }) + + // CSA-2: Cumulative slashing (multiple slashes, compound multiplier) + t.Run("CSA-2: Cumulative slashing", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues withdrawal on block 1000 + setupBlocksForCSA(t, grm, []uint64{1000, 1005, 1010}) + insertQueuedWithdrawal(t, grm, "0xstaker2", "0xoperator2", "0xstrategy2", 1000, "1000000000000000000000") + + // Set up staker shares model to process slashing events + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // First slash: 25% at block 1005 + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + change1, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1005, 1, "0xoperator2", []string{"0xstrategy2"}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + diffs1 := change1.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs1.SlashDiffs)) + slashDiff1 := diffs1.SlashDiffs[0] + slashEvent1 := &SlashingEvent{ + SlashedEntity: slashDiff1.SlashedEntity, + BeaconChain: slashDiff1.BeaconChain, + Strategy: slashDiff1.Strategy, + WadSlashed: slashDiff1.WadSlashed.String(), + TransactionHash: slashDiff1.TransactionHash, + LogIndex: slashDiff1.LogIndex, + } + err = sp.createSlashingAdjustments(slashEvent1, 1005, nil) + require.NoError(t, err) + + // Verify first adjustment: 0.75 + var multiplier string + res := grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? AND slash_block_number = ? + `, "0xstaker2", "0xstrategy2", 1005).Scan(&multiplier) + require.NoError(t, res.Error) + assert.Contains(t, multiplier, "0.75") + + // Second slash: 50% at block 1010 + err = sharesModel.SetupStateForBlock(1010) + require.NoError(t, err) + change2, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1010, 1, "0xoperator2", []string{"0xstrategy2"}, []*big.Int{big.NewInt(5e17)}) + require.NoError(t, err) + diffs2 := change2.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs2.SlashDiffs)) + slashDiff2 := diffs2.SlashDiffs[0] + slashEvent2 := &SlashingEvent{ + SlashedEntity: slashDiff2.SlashedEntity, + BeaconChain: slashDiff2.BeaconChain, + Strategy: slashDiff2.Strategy, + WadSlashed: slashDiff2.WadSlashed.String(), + TransactionHash: slashDiff2.TransactionHash, + LogIndex: slashDiff2.LogIndex, + } + err = sp.createSlashingAdjustments(slashEvent2, 1010, nil) + require.NoError(t, err) + + // Verify cumulative multiplier: 0.75 * 0.5 = 0.375 + res = grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? AND slash_block_number = ? + `, "0xstaker2", "0xstrategy2", 1010).Scan(&multiplier) + require.NoError(t, res.Error) + assert.Contains(t, multiplier, "0.375", "Expected cumulative multiplier 0.375 (0.75 * 0.5)") + }) + + // CSA-3: Same-block event ordering (log_index precedence) + t.Run("CSA-3: Same-block event ordering", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues withdrawal on block 1000, log_index 2 + // Operator slashed on same block 1000, log_index 1 (earlier) + setupBlocksForCSA(t, grm, []uint64{1000}) + insertQueuedWithdrawalWithLogIndex(t, grm, "0xstaker3", "0xoperator3", "0xstrategy3", 1000, 2, "1000000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1000) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process slashing at log_index 1 (before withdrawal at log_index 2) + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1000, 1, "0xoperator3", []string{"0xstrategy3"}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + err = sp.createSlashingAdjustments(slashEvent, 1000, nil) + require.NoError(t, err) + + // Verify NO adjustment created (slash before withdrawal in execution order) + var count int64 + res := grm.Raw(` + SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker3", "0xstrategy3").Scan(&count) + require.NoError(t, res.Error) + assert.Equal(t, int64(0), count, "No adjustment should be created when slash occurs before withdrawal in same block") + }) + + // CSA-3b: Same-block event ordering with accumulator (withdrawal before slash) + // This tests the real E2E flow where withdrawals go to accumulator before DB commit + t.Run("CSA-3b: Same-block withdrawal before slash via accumulator", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup block + setupBlocksForCSA(t, grm, []uint64{1000}) + + // Set up models + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1000) + require.NoError(t, err) + + // Create QueuedSlashingWithdrawalModel and set up for block + qswModel, err := queuedSlashingWithdrawals.NewQueuedSlashingWithdrawalModel(esm, grm, l, cfg) + require.NoError(t, err) + err = qswModel.SetupStateForBlock(1000) + require.NoError(t, err) + + // Process withdrawal through model at log_index 1 (earlier) + // This puts it in the accumulator, NOT the DB + withdrawalLog := &storage.TransactionLog{ + TransactionHash: "0xtx_withdrawal", + TransactionIndex: 100, + BlockNumber: 1000, + Address: cfg.GetContractsMapForChain().DelegationManager, + EventName: "SlashingWithdrawalQueued", + LogIndex: 1, + OutputData: `{"withdrawal":{"nonce":"1","staker":"0xstaker3b","startBlock":1000,"strategies":["0xstrategy3b"],"withdrawer":"0xstaker3b","delegatedTo":"0xoperator3b","scaledShares":["1000000000000000000000"]},"withdrawalRoot":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","sharesToWithdraw":["1000000000000000000000"]}`, + } + _, err = qswModel.HandleStateChange(withdrawalLog) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process slashing at log_index 2 (after withdrawal at log_index 1) + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1000, 2, "0xoperator3b", []string{"0xstrategy3b"}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + // Pass qswModel so it can find the withdrawal in the accumulator + err = sp.createSlashingAdjustments(slashEvent, 1000, qswModel) + require.NoError(t, err) + + // Verify adjustment WAS created (withdrawal happened before slash via accumulator) + var count int64 + res := grm.Raw(` + SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker3b", "0xstrategy3b").Scan(&count) + require.NoError(t, res.Error) + assert.Equal(t, int64(1), count, "Adjustment should be created when withdrawal occurs before slash in same block (via accumulator)") + + // Verify multiplier value (0.75 for 25% slash) + var multiplier string + res = grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker3b", "0xstrategy3b").Scan(&multiplier) + require.NoError(t, res.Error) + assert.Contains(t, multiplier, "0.75", "Expected multiplier 0.75 after 25% slash") + }) + + // CSA-4: Expired withdrawal queue (no adjustment after 14 days) + t.Run("CSA-4: Expired withdrawal queue", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues withdrawal on block 1000 (day 1) + // Operator slashed on block 1200 (day 20, after 14-day queue expires) + day1 := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + day20 := time.Date(2025, 1, 20, 12, 0, 0, 0, time.UTC) + + setupBlocksWithTime(t, grm, []struct { + number uint64 + time time.Time + }{ + {1000, day1}, + {1200, day20}, + }) + + insertQueuedWithdrawal(t, grm, "0xstaker4", "0xoperator4", "0xstrategy4", 1000, "1000000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1200) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process slashing after queue expires + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1200, 1, "0xoperator4", []string{"0xstrategy4"}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + err = sp.createSlashingAdjustments(slashEvent, 1200, nil) + require.NoError(t, err) + + // Verify NO adjustment created (withdrawal already completable) + var count int64 + res := grm.Raw(` + SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker4", "0xstrategy4").Scan(&count) + require.NoError(t, res.Error) + assert.Equal(t, int64(0), count, "No adjustment should be created after withdrawal queue expires") + }) + + // CSA-5: Multiple stakers affected by single slash + t.Run("CSA-5: Multiple stakers affected by single slash", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker1 and Staker2 both queue withdrawals for same operator/strategy + setupBlocksForCSA(t, grm, []uint64{1000, 1001, 1005, 1006}) + insertQueuedWithdrawal(t, grm, "0xstaker5a", "0xoperator5", "0xstrategy5", 1000, "1000000000000000000000") + insertQueuedWithdrawal(t, grm, "0xstaker5b", "0xoperator5", "0xstrategy5", 1001, "2000000000000000000000") + + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process slashing through the staker shares model to get proper SlashDiff + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1005, 1, "0xoperator5", []string{"0xstrategy5"}, []*big.Int{big.NewInt(3e17)}) + require.NoError(t, err) + + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs), "Should have one slash diff") + + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + err = sp.createSlashingAdjustments(slashEvent, 1005, nil) + require.NoError(t, err, "Both stakers should get adjustment records") + + // Verify both stakers got adjustment records + var count int64 + res := grm.Raw(` + SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments + WHERE operator = ? AND strategy = ? + `, "0xoperator5", "0xstrategy5").Scan(&count) + require.NoError(t, res.Error) + assert.Equal(t, int64(2), count, "Both stakers should have adjustment records") + + // Verify the multiplier is correct for both stakers + var adjustments []struct { + Staker string + SlashMultiplier string + } + res = grm.Raw(` + SELECT staker, slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE operator = ? AND strategy = ? + ORDER BY staker + `, "0xoperator5", "0xstrategy5").Scan(&adjustments) + require.NoError(t, res.Error) + require.Equal(t, 2, len(adjustments), "Should have 2 adjustment records") + assert.Equal(t, "0xstaker5a", adjustments[0].Staker) + assert.Contains(t, adjustments[0].SlashMultiplier, "0.7", "Expected multiplier 0.7 (1 - 0.3)") + assert.Equal(t, "0xstaker5b", adjustments[1].Staker) + assert.Contains(t, adjustments[1].SlashMultiplier, "0.7", "Expected multiplier 0.7 (1 - 0.3)") + }) + + // CSA-6: Multiple withdrawals, partial overlap + t.Run("CSA-6: Multiple withdrawals, partial overlap", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues 2 separate withdrawals on blocks 1000 and 1010 + // Operator slashed 25% on block 1005 + setupBlocksForCSA(t, grm, []uint64{1000, 1005, 1010}) + insertQueuedWithdrawalWithLogIndex(t, grm, "0xstaker6", "0xoperator6", "0xstrategy6", 1000, 1, "1000000000000000000000") + insertQueuedWithdrawalWithLogIndex(t, grm, "0xstaker6", "0xoperator6", "0xstrategy6", 1010, 1, "500000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process slashing at block 1005 + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1005, 1, "0xoperator6", []string{"0xstrategy6"}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + err = sp.createSlashingAdjustments(slashEvent, 1005, nil) + require.NoError(t, err) + + // Verify only first withdrawal gets adjustment (second queued after slash) + var adjustments []struct { + WithdrawalBlockNumber uint64 + SlashMultiplier string + } + res := grm.Raw(` + SELECT withdrawal_block_number, slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + ORDER BY withdrawal_block_number + `, "0xstaker6", "0xstrategy6").Scan(&adjustments) + require.NoError(t, res.Error) + + assert.Equal(t, 1, len(adjustments), "Only first withdrawal should have adjustment") + assert.Equal(t, uint64(1000), adjustments[0].WithdrawalBlockNumber) + assert.Contains(t, adjustments[0].SlashMultiplier, "0.75") + }) + + // CSA-7: 100% slash edge case + t.Run("CSA-7: 100% slash edge case", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues withdrawal, operator slashed 100% + setupBlocksForCSA(t, grm, []uint64{1000, 1005}) + insertQueuedWithdrawal(t, grm, "0xstaker7", "0xoperator7", "0xstrategy7", 1000, "1000000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process 100% slash + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1005, 1, "0xoperator7", []string{"0xstrategy7"}, []*big.Int{big.NewInt(1e18)}) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + err = sp.createSlashingAdjustments(slashEvent, 1005, nil) + require.NoError(t, err) + + // Verify adjustment record created with multiplier 0 + var multiplier string + res := grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker7", "0xstrategy7").Scan(&multiplier) + require.NoError(t, res.Error) + // Check that multiplier is 0 (may have trailing zeros) + assert.Contains(t, multiplier, "0.0", "Expected multiplier 0 for 100% slash") + }) + + // CSA-8: Strategy isolation + t.Run("CSA-8: Strategy isolation", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues withdrawal for strategy A + // Operator slashed on strategy B + setupBlocksForCSA(t, grm, []uint64{1000, 1005}) + insertQueuedWithdrawal(t, grm, "0xstaker8", "0xoperator8", "0xstrategyA", 1000, "1000000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Process slash on different strategy (B instead of A) + change, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1005, 1, "0xoperator8", []string{"0xstrategyB"}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + slashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + + err = sp.createSlashingAdjustments(slashEvent, 1005, nil) + require.NoError(t, err) + + // Verify NO adjustment created (different strategy) + var count int64 + res := grm.Raw(` + SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker8", "0xstrategyA").Scan(&count) + require.NoError(t, res.Error) + assert.Equal(t, int64(0), count, "No adjustment should be created for different strategy") + }) + + // CSA-9: Dual slashing (operator + beacon chain) + // Test scenario: + // 1. 100 shares in the queue + // 2. 25% slash on the operator -> multiplier = 0.75 + // 3. 50% slash on beacon chain -> cumulative multiplier = 0.75 * 0.5 = 0.375 + // Final: 100 * 0.375 = 37.5 shares left + t.Run("CSA-9: Dual slashing (operator + beacon chain)", func(t *testing.T) { + cleanupCSATest(t, grm) + + // Setup: Staker queues withdrawal on block 1000 with native ETH strategy + nativeEthStrategy := "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + setupBlocksForCSA(t, grm, []uint64{1000, 1005, 1010}) + insertQueuedWithdrawal(t, grm, "0xstaker9", "0xoperator9", nativeEthStrategy, 1000, "100000000000000000000") // 100 shares + + // Set up staker shares model to process slashing events + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // First: Operator slash 25% at block 1005 + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + change1, err := processSlashing(sharesModel, cfg.GetContractsMapForChain().AllocationManager, 1005, 1, "0xoperator9", []string{nativeEthStrategy}, []*big.Int{big.NewInt(25e16)}) + require.NoError(t, err) + diffs1 := change1.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs1.SlashDiffs)) + slashDiff1 := diffs1.SlashDiffs[0] + operatorSlashEvent := &SlashingEvent{ + SlashedEntity: slashDiff1.SlashedEntity, + BeaconChain: slashDiff1.BeaconChain, + Strategy: slashDiff1.Strategy, + WadSlashed: slashDiff1.WadSlashed.String(), + TransactionHash: slashDiff1.TransactionHash, + LogIndex: slashDiff1.LogIndex, + } + err = sp.createSlashingAdjustments(operatorSlashEvent, 1005, nil) + require.NoError(t, err) + + // Verify first adjustment: multiplier = 0.75 (1 - 0.25) + var multiplier1 string + res := grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? AND slash_block_number = ? + `, "0xstaker9", nativeEthStrategy, 1005).Scan(&multiplier1) + require.NoError(t, res.Error) + assert.Contains(t, multiplier1, "0.75", "Expected multiplier 0.75 after 25% operator slash") + + // Second: Beacon chain slash 50% at block 1010 + // Process through staker shares model + err = sharesModel.SetupStateForBlock(1010) + require.NoError(t, err) + change2, err := processBeaconChainSlashing(sharesModel, cfg.GetContractsMapForChain().EigenpodManager, 1010, 1, "0xstaker9", 1e18, 5e17) + require.NoError(t, err) + diffs2 := change2.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs2.SlashDiffs)) + slashDiff2 := diffs2.SlashDiffs[0] + beaconSlashEvent := &SlashingEvent{ + SlashedEntity: slashDiff2.SlashedEntity, + BeaconChain: slashDiff2.BeaconChain, + Strategy: slashDiff2.Strategy, + WadSlashed: slashDiff2.WadSlashed.String(), + TransactionHash: slashDiff2.TransactionHash, + LogIndex: slashDiff2.LogIndex, + } + err = sp.createSlashingAdjustments(beaconSlashEvent, 1010, nil) + require.NoError(t, err) + + // Verify cumulative multiplier: 0.75 * 0.5 = 0.375 + var multiplier2 string + res = grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? AND slash_block_number = ? + `, "0xstaker9", nativeEthStrategy, 1010).Scan(&multiplier2) + require.NoError(t, res.Error) + assert.Contains(t, multiplier2, "0.375", "Expected cumulative multiplier 0.375 (0.75 * 0.5) after beacon chain slash") + + // Verify total adjustment records + var count int64 + res = grm.Raw(` + SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker9", nativeEthStrategy).Scan(&count) + require.NoError(t, res.Error) + assert.Equal(t, int64(2), count, "Should have 2 adjustment records (operator + beacon chain)") + }) + + // CSA-10: Beacon chain slash only (no operator slash) + t.Run("CSA-10: Beacon chain slash only", func(t *testing.T) { + cleanupCSATest(t, grm) + + nativeEthStrategy := "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + setupBlocksForCSA(t, grm, []uint64{1000, 1005}) + insertQueuedWithdrawal(t, grm, "0xstaker10", "0xoperator10", nativeEthStrategy, 1000, "100000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Beacon chain slash 50% - process through staker shares model + change, err := processBeaconChainSlashing(sharesModel, cfg.GetContractsMapForChain().EigenpodManager, 1005, 1, "0xstaker10", 1e18, 5e17) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + beaconSlashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + err = sp.createSlashingAdjustments(beaconSlashEvent, 1005, nil) + require.NoError(t, err) + + // Verify multiplier = 0.5 + var multiplier string + res := grm.Raw(` + SELECT slash_multiplier FROM queued_withdrawal_slashing_adjustments + WHERE staker = ? AND strategy = ? + `, "0xstaker10", nativeEthStrategy).Scan(&multiplier) + require.NoError(t, res.Error) + assert.Contains(t, multiplier, "0.5", "Expected multiplier 0.5 after 50% beacon chain slash") + }) + + // CSA-11: Beacon chain slash doesn't affect other stakers + t.Run("CSA-11: Beacon chain slash isolation", func(t *testing.T) { + cleanupCSATest(t, grm) + + nativeEthStrategy := "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + setupBlocksForCSA(t, grm, []uint64{1000, 1001, 1005}) + // Two stakers with same operator + insertQueuedWithdrawal(t, grm, "0xstaker11a", "0xoperator11", nativeEthStrategy, 1000, "100000000000000000000") + insertQueuedWithdrawal(t, grm, "0xstaker11b", "0xoperator11", nativeEthStrategy, 1001, "100000000000000000000") + + // Set up staker shares model to process slashing event + esm := stateManager.NewEigenStateManager(nil, l, grm) + sharesModel, err := stakerShares.NewStakerSharesModel(esm, grm, l, cfg) + require.NoError(t, err) + err = sharesModel.SetupStateForBlock(1005) + require.NoError(t, err) + + sp := &SlashingProcessor{ + logger: l, + grm: grm, + globalConfig: cfg, + } + + // Beacon chain slash only affects staker11a - process through staker shares model + change, err := processBeaconChainSlashing(sharesModel, cfg.GetContractsMapForChain().EigenpodManager, 1005, 1, "0xstaker11a", 1e18, 5e17) + require.NoError(t, err) + diffs := change.(*stakerShares.AccumulatedStateChanges) + require.Equal(t, 1, len(diffs.SlashDiffs)) + slashDiff := diffs.SlashDiffs[0] + beaconSlashEvent := &SlashingEvent{ + SlashedEntity: slashDiff.SlashedEntity, + BeaconChain: slashDiff.BeaconChain, + Strategy: slashDiff.Strategy, + WadSlashed: slashDiff.WadSlashed.String(), + TransactionHash: slashDiff.TransactionHash, + LogIndex: slashDiff.LogIndex, + } + err = sp.createSlashingAdjustments(beaconSlashEvent, 1005, nil) + require.NoError(t, err) + + // Verify only staker11a has adjustment + var countA, countB int64 + res := grm.Raw(`SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments WHERE staker = ?`, "0xstaker11a").Scan(&countA) + require.NoError(t, res.Error) + res = grm.Raw(`SELECT COUNT(*) FROM queued_withdrawal_slashing_adjustments WHERE staker = ?`, "0xstaker11b").Scan(&countB) + require.NoError(t, res.Error) + + assert.Equal(t, int64(1), countA, "Staker11a should have adjustment") + assert.Equal(t, int64(0), countB, "Staker11b should NOT have adjustment (beacon chain slash is staker-specific)") + }) +} + +// ============================================================================ +// CSA Test Helper Functions +// ============================================================================ + +func setupCSATest() (string, *gorm.DB, *zap.Logger, *config.Config, error) { + cfg := config.NewConfig() + cfg.Chain = config.Chain_PreprodHoodi + cfg.Debug = false + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + cfg.Rewards.WithdrawalQueueWindow = 14 // 14 days + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func cleanupCSATest(t *testing.T, grm *gorm.DB) { + queries := []string{ + `truncate table queued_withdrawal_slashing_adjustments cascade`, + `truncate table queued_slashing_withdrawals cascade`, + `truncate table slashed_operator_shares cascade`, + `truncate table blocks cascade`, + } + for _, query := range queries { + res := grm.Exec(query) + require.NoError(t, res.Error, "Failed to cleanup: "+query) + } +} + +func setupBlocksForCSA(t *testing.T, grm *gorm.DB, blockNumbers []uint64) { + baseTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + for _, blockNum := range blockNumbers { + blockTime := baseTime.Add(time.Duration(blockNum-1000) * time.Minute) + res := grm.Exec(` + INSERT INTO blocks (number, hash, block_time) + VALUES (?, ?, ?) + `, blockNum, fmt.Sprintf("hash_%d", blockNum), blockTime) + require.NoError(t, res.Error, fmt.Sprintf("Failed to insert block %d", blockNum)) + } +} + +func setupBlocksWithTime(t *testing.T, grm *gorm.DB, blocks []struct { + number uint64 + time time.Time +}) { + for _, block := range blocks { + res := grm.Exec(` + INSERT INTO blocks (number, hash, block_time) + VALUES (?, ?, ?) + `, block.number, fmt.Sprintf("hash_%d", block.number), block.time) + require.NoError(t, res.Error, fmt.Sprintf("Failed to insert block %d", block.number)) + } +} + +func insertQueuedWithdrawal(t *testing.T, grm *gorm.DB, staker, operator, strategy string, blockNumber uint64, shares string) { + insertQueuedWithdrawalWithLogIndex(t, grm, staker, operator, strategy, blockNumber, 1, shares) +} + +func insertQueuedWithdrawalWithLogIndex(t *testing.T, grm *gorm.DB, staker, operator, strategy string, blockNumber uint64, logIndex uint64, shares string) { + res := grm.Exec(` + INSERT INTO queued_slashing_withdrawals ( + staker, operator, withdrawer, nonce, start_block, strategy, + scaled_shares, shares_to_withdraw, withdrawal_root, + block_number, transaction_hash, log_index + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker, operator, staker, "1", blockNumber, strategy, + shares, shares, "root_"+staker, + blockNumber, fmt.Sprintf("tx_%d", blockNumber), logIndex) + require.NoError(t, res.Error, "Failed to insert queued withdrawal") +} + +// processBeaconChainSlashing processes a beacon chain slashing event through the staker shares model +// prevBeaconChainScalingFactor and newBeaconChainScalingFactor are the scaling factors before and after the slash +// The wadSlashed is calculated as: (prev - new) / prev * 1e18 +func processBeaconChainSlashing(sharesModel *stakerShares.StakerSharesModel, eigenpodManager string, blockNumber, logIndex uint64, staker string, prevBeaconChainScalingFactor, newBeaconChainScalingFactor uint64) (interface{}, error) { + beaconChainSlashingFactorDecreasedEvent := stakerShares.BeaconChainSlashingFactorDecreasedOutputData{ + Staker: staker, + PrevBeaconChainSlashingFactor: prevBeaconChainScalingFactor, + NewBeaconChainSlashingFactor: newBeaconChainScalingFactor, + } + beaconChainSlashingFactorDecreasedJson, err := json.Marshal(beaconChainSlashingFactorDecreasedEvent) + if err != nil { + return nil, err + } + + beaconChainSlashingFactorDecreasedLog := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: 100, + BlockNumber: blockNumber, + Address: eigenpodManager, + Arguments: ``, + EventName: "BeaconChainSlashingFactorDecreased", + LogIndex: logIndex, + OutputData: string(beaconChainSlashingFactorDecreasedJson), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + return sharesModel.HandleStateChange(&beaconChainSlashingFactorDecreasedLog) +} diff --git a/pkg/eigenState/queuedSlashingWithdrawals/queuedSlashingWithdrawals.go b/pkg/eigenState/queuedSlashingWithdrawals/queuedSlashingWithdrawals.go index 6fe7bcc6c..810903207 100644 --- a/pkg/eigenState/queuedSlashingWithdrawals/queuedSlashingWithdrawals.go +++ b/pkg/eigenState/queuedSlashingWithdrawals/queuedSlashingWithdrawals.go @@ -4,6 +4,10 @@ import ( "encoding/hex" "encoding/json" "fmt" + "slices" + "sort" + "strings" + "github.com/Layr-Labs/sidecar/internal/config" "github.com/Layr-Labs/sidecar/pkg/eigenState/base" "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" @@ -11,9 +15,6 @@ import ( "github.com/Layr-Labs/sidecar/pkg/storage" "go.uber.org/zap" "gorm.io/gorm" - "slices" - "sort" - "strings" ) type QueuedSlashingWithdrawal struct { @@ -346,3 +347,7 @@ func (omm *QueuedSlashingWithdrawalModel) IsActiveForBlockHeight(blockHeight uin return blockHeight >= forks[config.RewardsFork_Red].BlockNumber, nil } + +func (omm *QueuedSlashingWithdrawalModel) GetAccumulatedState(blockNumber uint64) map[types.SlotID]*QueuedSlashingWithdrawal { + return omm.stateAccumulator[blockNumber] +} diff --git a/pkg/eigenState/totalStakeRewardSubmissions/totalStakeRewardSubmissions.go b/pkg/eigenState/totalStakeRewardSubmissions/totalStakeRewardSubmissions.go new file mode 100644 index 000000000..97a28ac0a --- /dev/null +++ b/pkg/eigenState/totalStakeRewardSubmissions/totalStakeRewardSubmissions.go @@ -0,0 +1,451 @@ +package totalStakeRewardSubmissions + +import ( + "encoding/json" + "fmt" + "math/big" + "slices" + "sort" + "strings" + "time" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/eigenState/base" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/eigenState/types" + "github.com/Layr-Labs/sidecar/pkg/storage" + "github.com/Layr-Labs/sidecar/pkg/types/numbers" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// TotalStakeRewardSubmission represents a reward submission for total stake rewards. +// Unlike operator-directed rewards, these rewards have a single amount for the entire operator set +// that gets distributed pro-rata across operators based on their total delegated stake. +type TotalStakeRewardSubmission struct { + Avs string + OperatorSetId uint64 + RewardHash string + Token string + Amount string // Total amount (not per-operator) + Strategy string + StrategyIndex uint64 + Multiplier string + StartTimestamp *time.Time + EndTimestamp *time.Time + Duration uint64 + BlockNumber uint64 + TransactionHash string + LogIndex uint64 +} + +type TotalStakeRewardSubmissionsModel struct { + base.BaseEigenState + StateTransitions types.StateTransitions[[]*TotalStakeRewardSubmission] + DB *gorm.DB + Network config.Network + Environment config.Environment + logger *zap.Logger + globalConfig *config.Config + + // Accumulates state changes for SlotIds, grouped by block number + stateAccumulator map[uint64]map[types.SlotID]*TotalStakeRewardSubmission + committedState map[uint64][]*TotalStakeRewardSubmission +} + +func NewTotalStakeRewardSubmissionsModel( + esm *stateManager.EigenStateManager, + grm *gorm.DB, + logger *zap.Logger, + globalConfig *config.Config, +) (*TotalStakeRewardSubmissionsModel, error) { + model := &TotalStakeRewardSubmissionsModel{ + BaseEigenState: base.BaseEigenState{ + Logger: logger, + }, + DB: grm, + logger: logger, + globalConfig: globalConfig, + stateAccumulator: make(map[uint64]map[types.SlotID]*TotalStakeRewardSubmission), + committedState: make(map[uint64][]*TotalStakeRewardSubmission), + } + + esm.RegisterState(model, 25) + return model, nil +} + +const TotalStakeRewardSubmissionsModelName = "TotalStakeRewardSubmissionsModel" + +func (ts *TotalStakeRewardSubmissionsModel) GetModelName() string { + return TotalStakeRewardSubmissionsModelName +} + +func (ts *TotalStakeRewardSubmissionsModel) NewSlotID( + transactionHash string, + logIndex uint64, + rewardHash string, + strategyIndex uint64, +) (types.SlotID, error) { + return base.NewSlotIDWithSuffix(transactionHash, logIndex, fmt.Sprintf("%s_%016x", rewardHash, strategyIndex)), nil +} + +// stakeRewardData represents the rewardsSubmission structure for stake-based rewards +type stakeRewardData struct { + StrategiesAndMultipliers []struct { + Strategy string `json:"strategy"` + Multiplier json.Number `json:"multiplier"` + } `json:"strategiesAndMultipliers"` + Token string `json:"token"` + Amount json.Number `json:"amount"` // Total amount + StartTimestamp uint64 `json:"startTimestamp"` + Duration uint64 `json:"duration"` +} + +type OperatorSet struct { + Avs string `json:"avs"` + Id uint64 `json:"id"` +} + +type stakeRewardSubmissionOutputData struct { + OperatorSet *OperatorSet `json:"operatorSet"` + SubmissionNonce json.Number `json:"submissionNonce"` + RewardsSubmission *stakeRewardData `json:"rewardsSubmission"` +} + +func parseStakeRewardSubmissionOutputData(outputDataStr string) (*stakeRewardSubmissionOutputData, error) { + outputData := &stakeRewardSubmissionOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + + return outputData, err +} + +func (ts *TotalStakeRewardSubmissionsModel) handleTotalStakeRewardsSubmissionCreatedEvent(log *storage.TransactionLog) ([]*TotalStakeRewardSubmission, error) { + arguments, err := ts.ParseLogArguments(log) + if err != nil { + return nil, err + } + + outputData, err := parseStakeRewardSubmissionOutputData(log.OutputData) + if err != nil { + return nil, err + } + outputRewardData := outputData.RewardsSubmission + + if outputRewardData.Duration == 0 { + ts.Logger.Sugar().Debugw("Skipping total stake reward submission with zero duration", + zap.String("transactionHash", log.TransactionHash), + zap.Uint64("logIndex", log.LogIndex), + zap.Uint64("blockNumber", log.BlockNumber), + ) + return []*TotalStakeRewardSubmission{}, nil + } + + rewardSubmissions := make([]*TotalStakeRewardSubmission, 0) + + amountBig, success := numbers.NewBig257().SetString(outputRewardData.Amount.String(), 10) + if !success { + return nil, fmt.Errorf("Failed to parse amount to Big257: %s", outputRewardData.Amount.String()) + } + + for i, strategyAndMultiplier := range outputRewardData.StrategiesAndMultipliers { + startTimestamp := time.Unix(int64(outputRewardData.StartTimestamp), 0) + endTimestamp := startTimestamp.Add(time.Duration(outputRewardData.Duration) * time.Second) + + multiplierBig, success := numbers.NewBig257().SetString(strategyAndMultiplier.Multiplier.String(), 10) + if !success { + return nil, fmt.Errorf("Failed to parse multiplier to Big257: %s", strategyAndMultiplier.Multiplier.String()) + } + + rewardSubmission := &TotalStakeRewardSubmission{ + Avs: strings.ToLower(outputData.OperatorSet.Avs), + OperatorSetId: uint64(outputData.OperatorSet.Id), + RewardHash: strings.ToLower(arguments[1].Value.(string)), + Token: strings.ToLower(outputRewardData.Token), + Amount: amountBig.String(), + Strategy: strings.ToLower(strategyAndMultiplier.Strategy), + StrategyIndex: uint64(i), + Multiplier: multiplierBig.String(), + StartTimestamp: &startTimestamp, + EndTimestamp: &endTimestamp, + Duration: outputRewardData.Duration, + BlockNumber: log.BlockNumber, + TransactionHash: log.TransactionHash, + LogIndex: log.LogIndex, + } + + rewardSubmissions = append(rewardSubmissions, rewardSubmission) + } + + return rewardSubmissions, nil +} + +func (ts *TotalStakeRewardSubmissionsModel) GetStateTransitions() (types.StateTransitions[[]*TotalStakeRewardSubmission], []uint64) { + stateChanges := make(types.StateTransitions[[]*TotalStakeRewardSubmission]) + + stateChanges[0] = func(log *storage.TransactionLog) ([]*TotalStakeRewardSubmission, error) { + rewardSubmissions, err := ts.handleTotalStakeRewardsSubmissionCreatedEvent(log) + if err != nil { + return nil, err + } + + for _, rewardSubmission := range rewardSubmissions { + slotId, err := ts.NewSlotID( + rewardSubmission.TransactionHash, + rewardSubmission.LogIndex, + rewardSubmission.RewardHash, + rewardSubmission.StrategyIndex, + ) + if err != nil { + ts.logger.Sugar().Errorw("Failed to create slot ID", + zap.String("transactionHash", log.TransactionHash), + zap.Uint64("logIndex", log.LogIndex), + zap.String("rewardHash", rewardSubmission.RewardHash), + zap.Uint64("strategyIndex", rewardSubmission.StrategyIndex), + zap.Error(err), + ) + return nil, err + } + + _, ok := ts.stateAccumulator[log.BlockNumber][slotId] + if ok { + err := fmt.Errorf("Duplicate total stake reward submission submitted for slot %s at block %d", slotId, log.BlockNumber) + ts.logger.Sugar().Errorw("Duplicate total stake reward submission submitted", zap.Error(err)) + return nil, err + } + + ts.stateAccumulator[log.BlockNumber][slotId] = rewardSubmission + } + + return rewardSubmissions, nil + } + + // Create an ordered list of block numbers + blockNumbers := make([]uint64, 0) + for blockNumber := range stateChanges { + blockNumbers = append(blockNumbers, blockNumber) + } + sort.Slice(blockNumbers, func(i, j int) bool { + return blockNumbers[i] < blockNumbers[j] + }) + slices.Reverse(blockNumbers) + + return stateChanges, blockNumbers +} + +func (ts *TotalStakeRewardSubmissionsModel) getContractAddressesForEnvironment() map[string][]string { + contracts := ts.globalConfig.GetContractsMapForChain() + return map[string][]string{ + contracts.RewardsCoordinator: { + "TotalStakeRewardsSubmissionCreated", + }, + } +} + +func (ts *TotalStakeRewardSubmissionsModel) IsInterestingLog(log *storage.TransactionLog) bool { + addresses := ts.getContractAddressesForEnvironment() + return ts.BaseEigenState.IsInterestingLog(addresses, log) +} + +func (ts *TotalStakeRewardSubmissionsModel) SetupStateForBlock(blockNumber uint64) error { + ts.stateAccumulator[blockNumber] = make(map[types.SlotID]*TotalStakeRewardSubmission) + ts.committedState[blockNumber] = make([]*TotalStakeRewardSubmission, 0) + return nil +} + +func (ts *TotalStakeRewardSubmissionsModel) CleanupProcessedStateForBlock(blockNumber uint64) error { + delete(ts.stateAccumulator, blockNumber) + delete(ts.committedState, blockNumber) + return nil +} + +func (ts *TotalStakeRewardSubmissionsModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { + stateChanges, sortedBlockNumbers := ts.GetStateTransitions() + + for _, blockNumber := range sortedBlockNumbers { + if log.BlockNumber >= blockNumber { + ts.logger.Sugar().Debugw("Handling state change", zap.Uint64("blockNumber", log.BlockNumber)) + + change, err := stateChanges[blockNumber](log) + if err != nil { + return nil, err + } + if change == nil { + return nil, nil + } + return change, nil + } + } + return nil, nil +} + +// prepareState prepares the state for commit by adding the new state to the existing state. +func (ts *TotalStakeRewardSubmissionsModel) prepareState(blockNumber uint64) ([]*TotalStakeRewardSubmission, error) { + accumulatedState, ok := ts.stateAccumulator[blockNumber] + if !ok { + err := fmt.Errorf("No accumulated state found for block %d", blockNumber) + ts.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + + recordsToInsert := make([]*TotalStakeRewardSubmission, 0) + for _, submission := range accumulatedState { + recordsToInsert = append(recordsToInsert, submission) + } + return recordsToInsert, nil +} + +// CommitFinalState commits the final state for the given block number. +func (ts *TotalStakeRewardSubmissionsModel) CommitFinalState(blockNumber uint64, ignoreInsertConflicts bool) error { + recordsToInsert, err := ts.prepareState(blockNumber) + if err != nil { + return err + } + + insertedRecords, err := base.CommitFinalState(recordsToInsert, ignoreInsertConflicts, ts.GetTableName(), ts.DB) + if err != nil { + ts.logger.Sugar().Errorw("Failed to insert records", zap.Error(err)) + return err + } + ts.committedState[blockNumber] = insertedRecords + return nil +} + +// GenerateStateRoot generates the state root for the given block number using the results of the state changes. +func (ts *TotalStakeRewardSubmissionsModel) GenerateStateRoot(blockNumber uint64) ([]byte, error) { + inserts, err := ts.prepareState(blockNumber) + if err != nil { + return nil, err + } + + inputs, err := ts.sortValuesForMerkleTree(inserts) + if err != nil { + return nil, err + } + + if len(inputs) == 0 { + return nil, nil + } + + fullTree, err := ts.MerkleizeEigenState(blockNumber, inputs) + if err != nil { + ts.logger.Sugar().Errorw("Failed to create merkle tree", + zap.Error(err), + zap.Uint64("blockNumber", blockNumber), + zap.Any("inputs", inputs), + ) + return nil, err + } + return fullTree.Root(), nil +} + +func (ts *TotalStakeRewardSubmissionsModel) GetCommittedState(blockNumber uint64) ([]interface{}, error) { + records, ok := ts.committedState[blockNumber] + if !ok { + err := fmt.Errorf("No committed state found for block %d", blockNumber) + ts.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + return base.CastCommittedStateToInterface(records), nil +} + +func (ts *TotalStakeRewardSubmissionsModel) formatMerkleLeafValue( + rewardHash string, + strategy string, + multiplier string, + amount string, +) (string, error) { + // Multiplier is a uint96 in the contracts, which translates to 24 hex characters + // Amount is a uint256 in the contracts, which translates to 64 hex characters + multiplierBig, success := new(big.Int).SetString(multiplier, 10) + if !success { + return "", fmt.Errorf("failed to parse multiplier to BigInt: %s", multiplier) + } + + amountBig, success := new(big.Int).SetString(amount, 10) + if !success { + return "", fmt.Errorf("failed to parse amount to BigInt: %s", amount) + } + + return fmt.Sprintf("%s_%s_%024x_%064x", rewardHash, strategy, multiplierBig, amountBig), nil +} + +func (ts *TotalStakeRewardSubmissionsModel) sortValuesForMerkleTree(submissions []*TotalStakeRewardSubmission) ([]*base.MerkleTreeInput, error) { + inputs := make([]*base.MerkleTreeInput, 0) + for _, submission := range submissions { + slotID, err := ts.NewSlotID( + submission.TransactionHash, + submission.LogIndex, + submission.RewardHash, + submission.StrategyIndex, + ) + if err != nil { + ts.logger.Sugar().Errorw("Failed to create slot ID", + zap.String("transactionHash", submission.TransactionHash), + zap.Uint64("logIndex", submission.LogIndex), + zap.String("rewardHash", submission.RewardHash), + zap.Uint64("strategyIndex", submission.StrategyIndex), + zap.Error(err), + ) + return nil, err + } + + value, err := ts.formatMerkleLeafValue( + submission.RewardHash, + submission.Strategy, + submission.Multiplier, + submission.Amount, + ) + if err != nil { + ts.Logger.Sugar().Errorw("Failed to format merkle leaf value", + zap.Error(err), + zap.String("rewardHash", submission.RewardHash), + zap.String("strategy", submission.Strategy), + zap.String("multiplier", submission.Multiplier), + zap.String("amount", submission.Amount), + ) + return nil, err + } + inputs = append(inputs, &base.MerkleTreeInput{ + SlotID: slotID, + Value: []byte(value), + }) + } + + slices.SortFunc(inputs, func(i, j *base.MerkleTreeInput) int { + return strings.Compare(string(i.SlotID), string(j.SlotID)) + }) + + return inputs, nil +} + +func (ts *TotalStakeRewardSubmissionsModel) GetTableName() string { + return "total_stake_reward_submissions" +} + +func (ts *TotalStakeRewardSubmissionsModel) DeleteState(startBlockNumber uint64, endBlockNumber uint64) error { + return ts.BaseEigenState.DeleteState(ts.GetTableName(), startBlockNumber, endBlockNumber, ts.DB) +} + +func (ts *TotalStakeRewardSubmissionsModel) ListForBlockRange(startBlockNumber uint64, endBlockNumber uint64) ([]interface{}, error) { + records := make([]*TotalStakeRewardSubmission, 0) + res := ts.DB.Where("block_number >= ? AND block_number <= ?", startBlockNumber, endBlockNumber).Find(&records) + if res.Error != nil { + ts.logger.Sugar().Errorw("Failed to list records for block range", + zap.Error(res.Error), + zap.Uint64("startBlockNumber", startBlockNumber), + zap.Uint64("endBlockNumber", endBlockNumber), + ) + return nil, res.Error + } + return base.CastCommittedStateToInterface(records), nil +} + +func (ts *TotalStakeRewardSubmissionsModel) IsActiveForBlockHeight(blockHeight uint64) (bool, error) { + return true, nil +} diff --git a/pkg/eigenState/totalStakeRewardSubmissions/totalStakeRewardSubmissions_test.go b/pkg/eigenState/totalStakeRewardSubmissions/totalStakeRewardSubmissions_test.go new file mode 100644 index 000000000..21bdaf729 --- /dev/null +++ b/pkg/eigenState/totalStakeRewardSubmissions/totalStakeRewardSubmissions_test.go @@ -0,0 +1,173 @@ +package totalStakeRewardSubmissions + +import ( + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/storage" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/logger" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func createBlock(model *TotalStakeRewardSubmissionsModel, blockNumber uint64) error { + block := &storage.Block{ + Number: blockNumber, + Hash: "some hash", + BlockTime: time.Now().Add(time.Hour * time.Duration(blockNumber)), + } + res := model.DB.Model(&storage.Block{}).Create(block) + if res.Error != nil { + return res.Error + } + return nil +} + +func Test_TotalStakeRewardSubmissions(t *testing.T) { + dbName, grm, l, cfg, err := setup() + + if err != nil { + t.Fatal(err) + } + + t.Run("Test each event type", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(nil, l, grm) + + model, err := NewTotalStakeRewardSubmissionsModel(esm, grm, l, cfg) + assert.Nil(t, err) + + t.Run("Handle a total stake reward submission", func(t *testing.T) { + blockNumber := uint64(102) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().RewardsCoordinator, + Arguments: `[{"Name": "caller", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "totalStakeRewardsSubmissionHash", "Type": "bytes32", "Value": "0x8502669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc77b0", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 2}, "Indexed": false}, {"Name": "submissionNonce", "Type": "uint256", "Value": 0, "Indexed": false}, {"Name": "rewardsSubmission", "Type": "((address,uint96)[],address,uint256,uint32,uint32)", "Value": null, "Indexed": false}]`, + EventName: "TotalStakeRewardsSubmissionCreated", + LogIndex: big.NewInt(15).Uint64(), + OutputData: `{"operatorSet": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 2}, "submissionNonce": 0, "rewardsSubmission": {"token": "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24", "amount": 200000000000000000000000, "duration": 604800, "startTimestamp": 1725494400, "strategiesAndMultipliers": [{"strategy": "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "multiplier": 1500000000000000000}, {"strategy": "0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "multiplier": 2500000000000000000}]}}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + strategiesAndMultipliers := []struct { + Strategy string + Multiplier string + }{ + {"0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "1500000000000000000"}, + {"0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "2500000000000000000"}, + } + + typedChange := change.([]*TotalStakeRewardSubmission) + assert.Equal(t, len(strategiesAndMultipliers), len(typedChange)) + + for _, submission := range typedChange { + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(submission.Avs)) + assert.Equal(t, uint64(2), submission.OperatorSetId) + assert.Equal(t, strings.ToLower("0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24"), strings.ToLower(submission.Token)) + assert.Equal(t, strings.ToLower("0x8502669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc77b0"), strings.ToLower(submission.RewardHash)) + assert.Equal(t, "200000000000000000000000", submission.Amount) + assert.Equal(t, uint64(604800), submission.Duration) + assert.Equal(t, int64(1725494400), submission.StartTimestamp.Unix()) + assert.Equal(t, int64(604800+1725494400), submission.EndTimestamp.Unix()) + + assert.Equal(t, strings.ToLower(strategiesAndMultipliers[submission.StrategyIndex].Strategy), strings.ToLower(submission.Strategy)) + assert.Equal(t, strategiesAndMultipliers[submission.StrategyIndex].Multiplier, submission.Multiplier) + } + + err = model.CommitFinalState(blockNumber, false) + assert.Nil(t, err) + + rewards := make([]*TotalStakeRewardSubmission, 0) + query := `select * from total_stake_reward_submissions where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&rewards) + assert.Nil(t, res.Error) + assert.Equal(t, len(strategiesAndMultipliers), len(rewards)) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + }) + + t.Run("Ensure a submission with a duration of 0 is skipped", func(t *testing.T) { + blockNumber := uint64(103) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash 2", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().RewardsCoordinator, + Arguments: `[{"Name": "caller", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "totalStakeRewardsSubmissionHash", "Type": "bytes32", "Value": "0x8502669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc77b0", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 2}, "Indexed": false}, {"Name": "submissionNonce", "Type": "uint256", "Value": 0, "Indexed": false}, {"Name": "rewardsSubmission", "Type": "((address,uint96)[],address,uint256,uint32,uint32)", "Value": null, "Indexed": false}]`, + EventName: "TotalStakeRewardsSubmissionCreated", + LogIndex: big.NewInt(15).Uint64(), + OutputData: `{"operatorSet": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 2}, "submissionNonce": 0, "rewardsSubmission": {"token": "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24", "amount": 200000000000000000000000, "duration": 0, "startTimestamp": 1725494400, "strategiesAndMultipliers": [{"strategy": "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "multiplier": 1500000000000000000}, {"strategy": "0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "multiplier": 2500000000000000000}]}}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + typedChange := change.([]*TotalStakeRewardSubmission) + assert.Equal(t, 0, len(typedChange)) + }) + }) + + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) +} diff --git a/pkg/eigenState/uniqueStakeRewardSubmissions/uniqueStakeRewardSubmissions.go b/pkg/eigenState/uniqueStakeRewardSubmissions/uniqueStakeRewardSubmissions.go new file mode 100644 index 000000000..5374a1ccc --- /dev/null +++ b/pkg/eigenState/uniqueStakeRewardSubmissions/uniqueStakeRewardSubmissions.go @@ -0,0 +1,451 @@ +package uniqueStakeRewardSubmissions + +import ( + "encoding/json" + "fmt" + "math/big" + "slices" + "sort" + "strings" + "time" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/pkg/eigenState/base" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/eigenState/types" + "github.com/Layr-Labs/sidecar/pkg/storage" + "github.com/Layr-Labs/sidecar/pkg/types/numbers" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// UniqueStakeRewardSubmission represents a reward submission for unique stake rewards. +// Unlike operator-directed rewards, these rewards have a single amount for the entire operator set +// that gets distributed pro-rata across operators based on their allocated stake. +type UniqueStakeRewardSubmission struct { + Avs string + OperatorSetId uint64 + RewardHash string + Token string + Amount string // Total amount (not per-operator) + Strategy string + StrategyIndex uint64 + Multiplier string + StartTimestamp *time.Time + EndTimestamp *time.Time + Duration uint64 + BlockNumber uint64 + TransactionHash string + LogIndex uint64 +} + +type UniqueStakeRewardSubmissionsModel struct { + base.BaseEigenState + StateTransitions types.StateTransitions[[]*UniqueStakeRewardSubmission] + DB *gorm.DB + Network config.Network + Environment config.Environment + logger *zap.Logger + globalConfig *config.Config + + // Accumulates state changes for SlotIds, grouped by block number + stateAccumulator map[uint64]map[types.SlotID]*UniqueStakeRewardSubmission + committedState map[uint64][]*UniqueStakeRewardSubmission +} + +func NewUniqueStakeRewardSubmissionsModel( + esm *stateManager.EigenStateManager, + grm *gorm.DB, + logger *zap.Logger, + globalConfig *config.Config, +) (*UniqueStakeRewardSubmissionsModel, error) { + model := &UniqueStakeRewardSubmissionsModel{ + BaseEigenState: base.BaseEigenState{ + Logger: logger, + }, + DB: grm, + logger: logger, + globalConfig: globalConfig, + stateAccumulator: make(map[uint64]map[types.SlotID]*UniqueStakeRewardSubmission), + committedState: make(map[uint64][]*UniqueStakeRewardSubmission), + } + + esm.RegisterState(model, 24) + return model, nil +} + +const UniqueStakeRewardSubmissionsModelName = "UniqueStakeRewardSubmissionsModel" + +func (us *UniqueStakeRewardSubmissionsModel) GetModelName() string { + return UniqueStakeRewardSubmissionsModelName +} + +func (us *UniqueStakeRewardSubmissionsModel) NewSlotID( + transactionHash string, + logIndex uint64, + rewardHash string, + strategyIndex uint64, +) (types.SlotID, error) { + return base.NewSlotIDWithSuffix(transactionHash, logIndex, fmt.Sprintf("%s_%016x", rewardHash, strategyIndex)), nil +} + +// stakeRewardData represents the rewardsSubmission structure for stake-based rewards +type stakeRewardData struct { + StrategiesAndMultipliers []struct { + Strategy string `json:"strategy"` + Multiplier json.Number `json:"multiplier"` + } `json:"strategiesAndMultipliers"` + Token string `json:"token"` + Amount json.Number `json:"amount"` // Total amount + StartTimestamp uint64 `json:"startTimestamp"` + Duration uint64 `json:"duration"` +} + +type OperatorSet struct { + Avs string `json:"avs"` + Id uint64 `json:"id"` +} + +type stakeRewardSubmissionOutputData struct { + OperatorSet *OperatorSet `json:"operatorSet"` + SubmissionNonce json.Number `json:"submissionNonce"` + RewardsSubmission *stakeRewardData `json:"rewardsSubmission"` +} + +func parseStakeRewardSubmissionOutputData(outputDataStr string) (*stakeRewardSubmissionOutputData, error) { + outputData := &stakeRewardSubmissionOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + + return outputData, err +} + +func (us *UniqueStakeRewardSubmissionsModel) handleUniqueStakeRewardsSubmissionCreatedEvent(log *storage.TransactionLog) ([]*UniqueStakeRewardSubmission, error) { + arguments, err := us.ParseLogArguments(log) + if err != nil { + return nil, err + } + + outputData, err := parseStakeRewardSubmissionOutputData(log.OutputData) + if err != nil { + return nil, err + } + outputRewardData := outputData.RewardsSubmission + + if outputRewardData.Duration == 0 { + us.Logger.Sugar().Debugw("Skipping unique stake reward submission with zero duration", + zap.String("transactionHash", log.TransactionHash), + zap.Uint64("logIndex", log.LogIndex), + zap.Uint64("blockNumber", log.BlockNumber), + ) + return []*UniqueStakeRewardSubmission{}, nil + } + + rewardSubmissions := make([]*UniqueStakeRewardSubmission, 0) + + amountBig, success := numbers.NewBig257().SetString(outputRewardData.Amount.String(), 10) + if !success { + return nil, fmt.Errorf("Failed to parse amount to Big257: %s", outputRewardData.Amount.String()) + } + + for i, strategyAndMultiplier := range outputRewardData.StrategiesAndMultipliers { + startTimestamp := time.Unix(int64(outputRewardData.StartTimestamp), 0) + endTimestamp := startTimestamp.Add(time.Duration(outputRewardData.Duration) * time.Second) + + multiplierBig, success := numbers.NewBig257().SetString(strategyAndMultiplier.Multiplier.String(), 10) + if !success { + return nil, fmt.Errorf("Failed to parse multiplier to Big257: %s", strategyAndMultiplier.Multiplier.String()) + } + + rewardSubmission := &UniqueStakeRewardSubmission{ + Avs: strings.ToLower(outputData.OperatorSet.Avs), + OperatorSetId: uint64(outputData.OperatorSet.Id), + RewardHash: strings.ToLower(arguments[1].Value.(string)), + Token: strings.ToLower(outputRewardData.Token), + Amount: amountBig.String(), + Strategy: strings.ToLower(strategyAndMultiplier.Strategy), + StrategyIndex: uint64(i), + Multiplier: multiplierBig.String(), + StartTimestamp: &startTimestamp, + EndTimestamp: &endTimestamp, + Duration: outputRewardData.Duration, + BlockNumber: log.BlockNumber, + TransactionHash: log.TransactionHash, + LogIndex: log.LogIndex, + } + + rewardSubmissions = append(rewardSubmissions, rewardSubmission) + } + + return rewardSubmissions, nil +} + +func (us *UniqueStakeRewardSubmissionsModel) GetStateTransitions() (types.StateTransitions[[]*UniqueStakeRewardSubmission], []uint64) { + stateChanges := make(types.StateTransitions[[]*UniqueStakeRewardSubmission]) + + stateChanges[0] = func(log *storage.TransactionLog) ([]*UniqueStakeRewardSubmission, error) { + rewardSubmissions, err := us.handleUniqueStakeRewardsSubmissionCreatedEvent(log) + if err != nil { + return nil, err + } + + for _, rewardSubmission := range rewardSubmissions { + slotId, err := us.NewSlotID( + rewardSubmission.TransactionHash, + rewardSubmission.LogIndex, + rewardSubmission.RewardHash, + rewardSubmission.StrategyIndex, + ) + if err != nil { + us.logger.Sugar().Errorw("Failed to create slot ID", + zap.String("transactionHash", log.TransactionHash), + zap.Uint64("logIndex", log.LogIndex), + zap.String("rewardHash", rewardSubmission.RewardHash), + zap.Uint64("strategyIndex", rewardSubmission.StrategyIndex), + zap.Error(err), + ) + return nil, err + } + + _, ok := us.stateAccumulator[log.BlockNumber][slotId] + if ok { + err := fmt.Errorf("Duplicate unique stake reward submission submitted for slot %s at block %d", slotId, log.BlockNumber) + us.logger.Sugar().Errorw("Duplicate unique stake reward submission submitted", zap.Error(err)) + return nil, err + } + + us.stateAccumulator[log.BlockNumber][slotId] = rewardSubmission + } + + return rewardSubmissions, nil + } + + // Create an ordered list of block numbers + blockNumbers := make([]uint64, 0) + for blockNumber := range stateChanges { + blockNumbers = append(blockNumbers, blockNumber) + } + sort.Slice(blockNumbers, func(i, j int) bool { + return blockNumbers[i] < blockNumbers[j] + }) + slices.Reverse(blockNumbers) + + return stateChanges, blockNumbers +} + +func (us *UniqueStakeRewardSubmissionsModel) getContractAddressesForEnvironment() map[string][]string { + contracts := us.globalConfig.GetContractsMapForChain() + return map[string][]string{ + contracts.RewardsCoordinator: { + "UniqueStakeRewardsSubmissionCreated", + }, + } +} + +func (us *UniqueStakeRewardSubmissionsModel) IsInterestingLog(log *storage.TransactionLog) bool { + addresses := us.getContractAddressesForEnvironment() + return us.BaseEigenState.IsInterestingLog(addresses, log) +} + +func (us *UniqueStakeRewardSubmissionsModel) SetupStateForBlock(blockNumber uint64) error { + us.stateAccumulator[blockNumber] = make(map[types.SlotID]*UniqueStakeRewardSubmission) + us.committedState[blockNumber] = make([]*UniqueStakeRewardSubmission, 0) + return nil +} + +func (us *UniqueStakeRewardSubmissionsModel) CleanupProcessedStateForBlock(blockNumber uint64) error { + delete(us.stateAccumulator, blockNumber) + delete(us.committedState, blockNumber) + return nil +} + +func (us *UniqueStakeRewardSubmissionsModel) HandleStateChange(log *storage.TransactionLog) (interface{}, error) { + stateChanges, sortedBlockNumbers := us.GetStateTransitions() + + for _, blockNumber := range sortedBlockNumbers { + if log.BlockNumber >= blockNumber { + us.logger.Sugar().Debugw("Handling state change", zap.Uint64("blockNumber", log.BlockNumber)) + + change, err := stateChanges[blockNumber](log) + if err != nil { + return nil, err + } + if change == nil { + return nil, nil + } + return change, nil + } + } + return nil, nil +} + +// prepareState prepares the state for commit by adding the new state to the existing state. +func (us *UniqueStakeRewardSubmissionsModel) prepareState(blockNumber uint64) ([]*UniqueStakeRewardSubmission, error) { + accumulatedState, ok := us.stateAccumulator[blockNumber] + if !ok { + err := fmt.Errorf("No accumulated state found for block %d", blockNumber) + us.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + + recordsToInsert := make([]*UniqueStakeRewardSubmission, 0) + for _, submission := range accumulatedState { + recordsToInsert = append(recordsToInsert, submission) + } + return recordsToInsert, nil +} + +// CommitFinalState commits the final state for the given block number. +func (us *UniqueStakeRewardSubmissionsModel) CommitFinalState(blockNumber uint64, ignoreInsertConflicts bool) error { + recordsToInsert, err := us.prepareState(blockNumber) + if err != nil { + return err + } + + insertedRecords, err := base.CommitFinalState(recordsToInsert, ignoreInsertConflicts, us.GetTableName(), us.DB) + if err != nil { + us.logger.Sugar().Errorw("Failed to insert records", zap.Error(err)) + return err + } + us.committedState[blockNumber] = insertedRecords + return nil +} + +// GenerateStateRoot generates the state root for the given block number using the results of the state changes. +func (us *UniqueStakeRewardSubmissionsModel) GenerateStateRoot(blockNumber uint64) ([]byte, error) { + inserts, err := us.prepareState(blockNumber) + if err != nil { + return nil, err + } + + inputs, err := us.sortValuesForMerkleTree(inserts) + if err != nil { + return nil, err + } + + if len(inputs) == 0 { + return nil, nil + } + + fullTree, err := us.MerkleizeEigenState(blockNumber, inputs) + if err != nil { + us.logger.Sugar().Errorw("Failed to create merkle tree", + zap.Error(err), + zap.Uint64("blockNumber", blockNumber), + zap.Any("inputs", inputs), + ) + return nil, err + } + return fullTree.Root(), nil +} + +func (us *UniqueStakeRewardSubmissionsModel) GetCommittedState(blockNumber uint64) ([]interface{}, error) { + records, ok := us.committedState[blockNumber] + if !ok { + err := fmt.Errorf("No committed state found for block %d", blockNumber) + us.logger.Sugar().Errorw(err.Error(), zap.Error(err), zap.Uint64("blockNumber", blockNumber)) + return nil, err + } + return base.CastCommittedStateToInterface(records), nil +} + +func (us *UniqueStakeRewardSubmissionsModel) formatMerkleLeafValue( + rewardHash string, + strategy string, + multiplier string, + amount string, +) (string, error) { + // Multiplier is a uint96 in the contracts, which translates to 24 hex characters + // Amount is a uint256 in the contracts, which translates to 64 hex characters + multiplierBig, success := new(big.Int).SetString(multiplier, 10) + if !success { + return "", fmt.Errorf("failed to parse multiplier to BigInt: %s", multiplier) + } + + amountBig, success := new(big.Int).SetString(amount, 10) + if !success { + return "", fmt.Errorf("failed to parse amount to BigInt: %s", amount) + } + + return fmt.Sprintf("%s_%s_%024x_%064x", rewardHash, strategy, multiplierBig, amountBig), nil +} + +func (us *UniqueStakeRewardSubmissionsModel) sortValuesForMerkleTree(submissions []*UniqueStakeRewardSubmission) ([]*base.MerkleTreeInput, error) { + inputs := make([]*base.MerkleTreeInput, 0) + for _, submission := range submissions { + slotID, err := us.NewSlotID( + submission.TransactionHash, + submission.LogIndex, + submission.RewardHash, + submission.StrategyIndex, + ) + if err != nil { + us.logger.Sugar().Errorw("Failed to create slot ID", + zap.String("transactionHash", submission.TransactionHash), + zap.Uint64("logIndex", submission.LogIndex), + zap.String("rewardHash", submission.RewardHash), + zap.Uint64("strategyIndex", submission.StrategyIndex), + zap.Error(err), + ) + return nil, err + } + + value, err := us.formatMerkleLeafValue( + submission.RewardHash, + submission.Strategy, + submission.Multiplier, + submission.Amount, + ) + if err != nil { + us.Logger.Sugar().Errorw("Failed to format merkle leaf value", + zap.Error(err), + zap.String("rewardHash", submission.RewardHash), + zap.String("strategy", submission.Strategy), + zap.String("multiplier", submission.Multiplier), + zap.String("amount", submission.Amount), + ) + return nil, err + } + inputs = append(inputs, &base.MerkleTreeInput{ + SlotID: slotID, + Value: []byte(value), + }) + } + + slices.SortFunc(inputs, func(i, j *base.MerkleTreeInput) int { + return strings.Compare(string(i.SlotID), string(j.SlotID)) + }) + + return inputs, nil +} + +func (us *UniqueStakeRewardSubmissionsModel) GetTableName() string { + return "unique_stake_reward_submissions" +} + +func (us *UniqueStakeRewardSubmissionsModel) DeleteState(startBlockNumber uint64, endBlockNumber uint64) error { + return us.BaseEigenState.DeleteState(us.GetTableName(), startBlockNumber, endBlockNumber, us.DB) +} + +func (us *UniqueStakeRewardSubmissionsModel) ListForBlockRange(startBlockNumber uint64, endBlockNumber uint64) ([]interface{}, error) { + records := make([]*UniqueStakeRewardSubmission, 0) + res := us.DB.Where("block_number >= ? AND block_number <= ?", startBlockNumber, endBlockNumber).Find(&records) + if res.Error != nil { + us.logger.Sugar().Errorw("Failed to list records for block range", + zap.Error(res.Error), + zap.Uint64("startBlockNumber", startBlockNumber), + zap.Uint64("endBlockNumber", endBlockNumber), + ) + return nil, res.Error + } + return base.CastCommittedStateToInterface(records), nil +} + +func (us *UniqueStakeRewardSubmissionsModel) IsActiveForBlockHeight(blockHeight uint64) (bool, error) { + return true, nil +} diff --git a/pkg/eigenState/uniqueStakeRewardSubmissions/uniqueStakeRewardSubmissions_test.go b/pkg/eigenState/uniqueStakeRewardSubmissions/uniqueStakeRewardSubmissions_test.go new file mode 100644 index 000000000..242f36b19 --- /dev/null +++ b/pkg/eigenState/uniqueStakeRewardSubmissions/uniqueStakeRewardSubmissions_test.go @@ -0,0 +1,173 @@ +package uniqueStakeRewardSubmissions + +import ( + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/storage" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/stateManager" + "github.com/Layr-Labs/sidecar/pkg/logger" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setup() ( + string, + *gorm.DB, + *zap.Logger, + *config.Config, + error, +) { + cfg := config.NewConfig() + cfg.Debug = os.Getenv(config.Debug) == "true" + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, err + } + + return dbname, grm, l, cfg, nil +} + +func createBlock(model *UniqueStakeRewardSubmissionsModel, blockNumber uint64) error { + block := &storage.Block{ + Number: blockNumber, + Hash: "some hash", + BlockTime: time.Now().Add(time.Hour * time.Duration(blockNumber)), + } + res := model.DB.Model(&storage.Block{}).Create(block) + if res.Error != nil { + return res.Error + } + return nil +} + +func Test_UniqueStakeRewardSubmissions(t *testing.T) { + dbName, grm, l, cfg, err := setup() + + if err != nil { + t.Fatal(err) + } + + t.Run("Test each event type", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(nil, l, grm) + + model, err := NewUniqueStakeRewardSubmissionsModel(esm, grm, l, cfg) + assert.Nil(t, err) + + t.Run("Handle a unique stake reward submission", func(t *testing.T) { + blockNumber := uint64(102) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().RewardsCoordinator, + Arguments: `[{"Name": "caller", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "uniqueStakeRewardsSubmissionHash", "Type": "bytes32", "Value": "0x7402669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc66a9", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 1}, "Indexed": false}, {"Name": "submissionNonce", "Type": "uint256", "Value": 0, "Indexed": false}, {"Name": "rewardsSubmission", "Type": "((address,uint96)[],address,uint256,uint32,uint32)", "Value": null, "Indexed": false}]`, + EventName: "UniqueStakeRewardsSubmissionCreated", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 1}, "submissionNonce": 0, "rewardsSubmission": {"token": "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24", "amount": 100000000000000000000000, "duration": 2419200, "startTimestamp": 1725494400, "strategiesAndMultipliers": [{"strategy": "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "multiplier": 1000000000000000000}, {"strategy": "0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "multiplier": 2000000000000000000}]}}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + strategiesAndMultipliers := []struct { + Strategy string + Multiplier string + }{ + {"0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "1000000000000000000"}, + {"0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "2000000000000000000"}, + } + + typedChange := change.([]*UniqueStakeRewardSubmission) + assert.Equal(t, len(strategiesAndMultipliers), len(typedChange)) + + for _, submission := range typedChange { + assert.Equal(t, strings.ToLower("0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101"), strings.ToLower(submission.Avs)) + assert.Equal(t, uint64(1), submission.OperatorSetId) + assert.Equal(t, strings.ToLower("0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24"), strings.ToLower(submission.Token)) + assert.Equal(t, strings.ToLower("0x7402669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc66a9"), strings.ToLower(submission.RewardHash)) + assert.Equal(t, "100000000000000000000000", submission.Amount) + assert.Equal(t, uint64(2419200), submission.Duration) + assert.Equal(t, int64(1725494400), submission.StartTimestamp.Unix()) + assert.Equal(t, int64(2419200+1725494400), submission.EndTimestamp.Unix()) + + assert.Equal(t, strings.ToLower(strategiesAndMultipliers[submission.StrategyIndex].Strategy), strings.ToLower(submission.Strategy)) + assert.Equal(t, strategiesAndMultipliers[submission.StrategyIndex].Multiplier, submission.Multiplier) + } + + err = model.CommitFinalState(blockNumber, false) + assert.Nil(t, err) + + rewards := make([]*UniqueStakeRewardSubmission, 0) + query := `select * from unique_stake_reward_submissions where block_number = ?` + res := model.DB.Raw(query, blockNumber).Scan(&rewards) + assert.Nil(t, res.Error) + assert.Equal(t, len(strategiesAndMultipliers), len(rewards)) + + stateRoot, err := model.GenerateStateRoot(blockNumber) + assert.Nil(t, err) + assert.NotNil(t, stateRoot) + assert.True(t, len(stateRoot) > 0) + }) + + t.Run("Ensure a submission with a duration of 0 is skipped", func(t *testing.T) { + blockNumber := uint64(103) + + if err := createBlock(model, blockNumber); err != nil { + t.Fatal(err) + } + + log := &storage.TransactionLog{ + TransactionHash: "some hash 2", + TransactionIndex: big.NewInt(100).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().RewardsCoordinator, + Arguments: `[{"Name": "caller", "Type": "address", "Value": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "Indexed": true}, {"Name": "uniqueStakeRewardsSubmissionHash", "Type": "bytes32", "Value": "0x7402669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc66a9", "Indexed": true}, {"Name": "operatorSet", "Type": "tuple", "Value": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 1}, "Indexed": false}, {"Name": "submissionNonce", "Type": "uint256", "Value": 0, "Indexed": false}, {"Name": "rewardsSubmission", "Type": "((address,uint96)[],address,uint256,uint32,uint32)", "Value": null, "Indexed": false}]`, + EventName: "UniqueStakeRewardsSubmissionCreated", + LogIndex: big.NewInt(12).Uint64(), + OutputData: `{"operatorSet": {"avs": "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", "id": 1}, "submissionNonce": 0, "rewardsSubmission": {"token": "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24", "amount": 100000000000000000000000, "duration": 0, "startTimestamp": 1725494400, "strategiesAndMultipliers": [{"strategy": "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", "multiplier": 1000000000000000000}, {"strategy": "0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc", "multiplier": 2000000000000000000}]}}`, + } + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + isInteresting := model.IsInterestingLog(log) + assert.True(t, isInteresting) + + change, err := model.HandleStateChange(log) + assert.Nil(t, err) + assert.NotNil(t, change) + + typedChange := change.([]*UniqueStakeRewardSubmission) + assert.Equal(t, 0, len(typedChange)) + }) + }) + + t.Cleanup(func() { + postgres.TeardownTestDatabase(dbName, cfg, grm, l) + }) +} diff --git a/pkg/postgres/migrations/202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk/up.go b/pkg/postgres/migrations/202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk/up.go new file mode 100644 index 000000000..5382b0ac5 --- /dev/null +++ b/pkg/postgres/migrations/202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk/up.go @@ -0,0 +1,38 @@ +package _202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk + +import ( + "database/sql" + + "github.com/Layr-Labs/sidecar/internal/config" + "gorm.io/gorm" +) + +type Migration struct { +} + +func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error { + queries := []string{ + // Drop the incorrect PK (block_number, log_index, transaction_hash) + `ALTER TABLE queued_withdrawal_slashing_adjustments DROP CONSTRAINT queued_withdrawal_slashing_adjustments_pk`, + // Drop the unique constraint + `ALTER TABLE queued_withdrawal_slashing_adjustments DROP CONSTRAINT uniq_queued_withdrawal_slashing_adjustments`, + // Add withdrawal_log_index column to uniquely identify each withdrawal within a block + `ALTER TABLE queued_withdrawal_slashing_adjustments ADD COLUMN withdrawal_log_index bigint NOT NULL DEFAULT 0`, + // Add the correct PK that allows: + // 1. Multiple stakers per slash event + // 2. Multiple withdrawals from the same staker in the same block + `ALTER TABLE queued_withdrawal_slashing_adjustments ADD CONSTRAINT queued_withdrawal_slashing_adjustments_pk PRIMARY KEY (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number)`, + } + + for _, query := range queries { + res := grm.Exec(query) + if res.Error != nil { + return res.Error + } + } + return nil +} + +func (m *Migration) GetName() string { + return "202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk" +} diff --git a/pkg/postgres/migrations/202601260000_uniqueAndTotalStakeRewardSubmissions/up.go b/pkg/postgres/migrations/202601260000_uniqueAndTotalStakeRewardSubmissions/up.go new file mode 100644 index 000000000..33af46aed --- /dev/null +++ b/pkg/postgres/migrations/202601260000_uniqueAndTotalStakeRewardSubmissions/up.go @@ -0,0 +1,86 @@ +package _202601260000_uniqueAndTotalStakeRewardSubmissions + +import ( + "database/sql" + + "github.com/Layr-Labs/sidecar/internal/config" + "gorm.io/gorm" +) + +type Migration struct { +} + +func (m *Migration) Up(db *sql.DB, grm *gorm.DB, cfg *config.Config) error { + queries := []string{ + // Unique stake submissions (eigenState target for UniqueStakeRewardsSubmissionCreated) + `CREATE TABLE IF NOT EXISTS unique_stake_reward_submissions ( + avs varchar not null, + operator_set_id bigint not null, + reward_hash varchar not null, + token varchar not null, + amount numeric not null, + strategy varchar not null, + strategy_index integer not null, + multiplier numeric(78) not null, + start_timestamp timestamp(6) not null, + end_timestamp timestamp(6) not null, + duration bigint not null, + block_number bigint not null, + transaction_hash varchar not null, + log_index bigint not null, + UNIQUE(transaction_hash, log_index, block_number, reward_hash, strategy_index), + CONSTRAINT unique_stake_reward_submissions_block_number_fkey FOREIGN KEY (block_number) REFERENCES blocks(number) ON DELETE CASCADE + )`, + + // Total stake submissions (eigenState target for TotalStakeRewardsSubmissionCreated) + `CREATE TABLE IF NOT EXISTS total_stake_reward_submissions ( + avs varchar not null, + operator_set_id bigint not null, + reward_hash varchar not null, + token varchar not null, + amount numeric not null, + strategy varchar not null, + strategy_index integer not null, + multiplier numeric(78) not null, + start_timestamp timestamp(6) not null, + end_timestamp timestamp(6) not null, + duration bigint not null, + block_number bigint not null, + transaction_hash varchar not null, + log_index bigint not null, + UNIQUE(transaction_hash, log_index, block_number, reward_hash, strategy_index), + CONSTRAINT total_stake_reward_submissions_block_number_fkey FOREIGN KEY (block_number) REFERENCES blocks(number) ON DELETE CASCADE + )`, + + `CREATE TABLE IF NOT EXISTS stake_operator_set_rewards ( + avs varchar not null, + operator_set_id bigint not null, + reward_hash varchar not null, + token varchar not null, + amount numeric not null, + strategy varchar not null, + strategy_index integer not null, + multiplier numeric(78) not null, + start_timestamp timestamp(6) not null, + end_timestamp timestamp(6) not null, + duration bigint not null, + reward_type varchar not null, + block_number bigint not null, + block_time timestamp(6) not null, + block_date text not null, + CONSTRAINT uniq_stake_operator_set_rewards UNIQUE (block_number, reward_hash, strategy_index, reward_type), + CONSTRAINT stake_operator_set_rewards_block_number_fkey FOREIGN KEY (block_number) REFERENCES blocks(number) ON DELETE CASCADE + )`, + } + + for _, query := range queries { + if err := grm.Exec(query).Error; err != nil { + return err + } + } + return nil +} + +func (m *Migration) GetName() string { + return "202601260000_uniqueAndTotalStakeRewardSubmissions" +} diff --git a/pkg/postgres/migrations/migrator.go b/pkg/postgres/migrations/migrator.go index f13b40f15..ee799690f 100644 --- a/pkg/postgres/migrations/migrator.go +++ b/pkg/postgres/migrations/migrator.go @@ -86,6 +86,8 @@ import ( _202512101500_queuedWithdrawalSlashingAdjustments "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202512101500_queuedWithdrawalSlashingAdjustments" _202512160000_addSlashableUntilToOperatorRegistrationSnapshots "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202512160000_addSlashableUntilToOperatorRegistrationSnapshots" _202512161200_addMaxMagnitudeToAllocationSnapshots "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202512161200_addMaxMagnitudeToAllocationSnapshots" + _202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk" + _202601260000_uniqueAndTotalStakeRewardSubmissions "github.com/Layr-Labs/sidecar/pkg/postgres/migrations/202601260000_uniqueAndTotalStakeRewardSubmissions" ) // Migration interface defines the contract for database migrations. @@ -234,6 +236,8 @@ func (m *Migrator) MigrateAll() error { &_202512101500_queuedWithdrawalSlashingAdjustments.Migration{}, &_202512160000_addSlashableUntilToOperatorRegistrationSnapshots.Migration{}, &_202512161200_addMaxMagnitudeToAllocationSnapshots.Migration{}, + &_202601220000_fixQueuedWithdrawalSlashingAdjustmentsPk.Migration{}, + &_202601260000_uniqueAndTotalStakeRewardSubmissions.Migration{}, } for _, migration := range migrations { diff --git a/pkg/rewards/15_goldActiveUniqueAndTotalStakeRewards.go b/pkg/rewards/15_goldActiveUniqueAndTotalStakeRewards.go new file mode 100644 index 000000000..13ca3df43 --- /dev/null +++ b/pkg/rewards/15_goldActiveUniqueAndTotalStakeRewards.go @@ -0,0 +1,180 @@ +package rewards + +import ( + "github.com/Layr-Labs/sidecar/pkg/rewardsUtils" + "go.uber.org/zap" +) + +const _15_goldActiveUniqueAndTotalStakeRewardsQuery = ` +CREATE TABLE {{.destTableName}} AS +WITH +-- Step 1: Modify active rewards and compute tokens per day for stake submissions +active_rewards_modified AS ( + SELECT + avs, + operator_set_id, + reward_hash, + token, + amount, + strategy, + strategy_index, + multiplier, + start_timestamp, + end_timestamp, + duration, + reward_type, + block_number, + block_time, + block_date, + CAST('{{.cutoffDate}}' AS TIMESTAMP(6)) AS global_end_inclusive + FROM stake_operator_set_rewards + WHERE end_timestamp >= TIMESTAMP '{{.rewardsStart}}' + AND start_timestamp <= TIMESTAMP '{{.cutoffDate}}' + AND block_time <= TIMESTAMP '{{.cutoffDate}}' +), + +-- Step 2: Cut each reward's start and end windows to handle the global range +active_rewards_updated_end_timestamps AS ( + SELECT + avs, + operator_set_id, + reward_hash, + token, + amount, + strategy, + strategy_index, + multiplier, + reward_type, + start_timestamp AS reward_start_exclusive, + LEAST(global_end_inclusive, end_timestamp) AS reward_end_inclusive, + duration, + global_end_inclusive, + block_date AS reward_submission_date + FROM active_rewards_modified +), + +-- Step 3: For each reward hash, find the latest snapshot +active_rewards_updated_start_timestamps AS ( + SELECT + ap.avs, + ap.operator_set_id, + ap.reward_hash, + ap.token, + ap.amount, + ap.strategy, + ap.strategy_index, + ap.multiplier, + ap.reward_type, + COALESCE(MAX(g.snapshot), ap.reward_start_exclusive) AS reward_start_exclusive, + ap.reward_end_inclusive, + ap.duration, + ap.global_end_inclusive, + ap.reward_submission_date + FROM active_rewards_updated_end_timestamps ap + LEFT JOIN gold_table g + ON g.reward_hash = ap.reward_hash + GROUP BY + ap.avs, + ap.operator_set_id, + ap.reward_hash, + ap.token, + ap.amount, + ap.strategy, + ap.strategy_index, + ap.multiplier, + ap.reward_type, + ap.reward_end_inclusive, + ap.duration, + ap.global_end_inclusive, + ap.reward_start_exclusive, + ap.reward_submission_date +), + +-- Step 4: Filter out invalid reward ranges +active_reward_ranges AS ( + SELECT * + FROM active_rewards_updated_start_timestamps + WHERE reward_start_exclusive < reward_end_inclusive +), + +-- Step 5: Explode out the ranges for a day per inclusive date +exploded_active_range_rewards AS ( + SELECT + avs, + operator_set_id, + reward_hash, + token, + amount, + strategy, + strategy_index, + multiplier, + reward_type, + reward_start_exclusive, + reward_end_inclusive, + duration, + global_end_inclusive, + reward_submission_date, + -- This is the "snapshot" date - the day on which rewards are accrued + d AS snapshot + FROM active_reward_ranges + CROSS JOIN generate_series( + DATE(reward_start_exclusive), + DATE(reward_end_inclusive), + INTERVAL '1' day + ) AS d + WHERE d != reward_start_exclusive +), + +-- Step 6: Calculate tokens per day +-- For stake-based rewards, tokens_per_day is the daily amount to be distributed pro-rata +tokens_per_day AS ( + SELECT + *, + -- Total tokens per day = floor(amount / duration_in_days) + -- duration is in seconds, convert to days + FLOOR(amount / (duration / 86400)) AS tokens_per_day_decimal + FROM exploded_active_range_rewards +) + +SELECT * FROM tokens_per_day +` + +func (rc *RewardsCalculator) GenerateGold15ActiveUniqueAndTotalStakeRewardsTable(snapshotDate string) error { + rewardsV2_2Enabled, err := rc.globalConfig.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) + if err != nil { + rc.logger.Sugar().Errorw("Failed to check if rewards v2.2 is enabled", "error", err) + return err + } + if !rewardsV2_2Enabled { + rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 15 (active unique and total stake rewards)") + return nil + } + + allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) + destTableName := allTableNames[rewardsUtils.Table_15_ActiveUniqueAndTotalStakeRewards] + + rewardsStart := "1970-01-01 00:00:00" // This will always start as this date and gets updated later in the query + + rc.logger.Sugar().Infow("Generating v2.2 Active unique and total stake rewards", + zap.String("rewardsStart", rewardsStart), + zap.String("cutoffDate", snapshotDate), + zap.String("destTableName", destTableName), + ) + + query, err := rewardsUtils.RenderQueryTemplate(_15_goldActiveUniqueAndTotalStakeRewardsQuery, map[string]interface{}{ + "destTableName": destTableName, + "rewardsStart": rewardsStart, + "cutoffDate": snapshotDate, + }) + if err != nil { + rc.logger.Sugar().Errorw("Failed to render query template", "error", err) + return err + } + + res := rc.grm.Exec(query) + if res.Error != nil { + rc.logger.Sugar().Errorw("Failed to create gold_15_active_unique_and_total_stake_rewards v2.2", "error", res.Error) + return res.Error + } + return nil +} diff --git a/pkg/rewards/15_goldOperatorOperatorSetUniqueStakeRewards.go b/pkg/rewards/16_goldOperatorOperatorSetUniqueStakeRewards.go similarity index 60% rename from pkg/rewards/15_goldOperatorOperatorSetUniqueStakeRewards.go rename to pkg/rewards/16_goldOperatorOperatorSetUniqueStakeRewards.go index ef5915726..97eec0b3d 100644 --- a/pkg/rewards/15_goldOperatorOperatorSetUniqueStakeRewards.go +++ b/pkg/rewards/16_goldOperatorOperatorSetUniqueStakeRewards.go @@ -5,47 +5,47 @@ import ( "go.uber.org/zap" ) -const _15_goldOperatorOperatorSetUniqueStakeRewardsQuery = ` +const _16_goldOperatorOperatorSetUniqueStakeRewardsQuery = ` CREATE TABLE {{.destTableName}} AS --- Step 1: Get registered operators for the operator set --- Uses slashable_until to include 14-day deregistration queue for unique stake rewards -WITH reward_snapshot_operators AS ( +-- ============================================================================ +-- Stake Rewards Path (pool amounts from Table 15, needs pro-rata) +-- ============================================================================ + +-- Step 1: Get registered operators (join to get all operators for the operator set) +WITH registered_operators AS ( SELECT ap.reward_hash, ap.snapshot AS snapshot, ap.token, - ap.tokens_per_registered_snapshot_decimal, + ap.tokens_per_day_decimal, ap.avs AS avs, ap.operator_set_id AS operator_set_id, - ap.operator AS operator, ap.strategy, ap.multiplier, ap.reward_submission_date, + osor.operator, osor.snapshot as registration_snapshot, osor.slashable_until - FROM {{.activeODRewardsTable}} ap + FROM {{.activeStakeRewardsTable}} ap JOIN operator_set_operator_registration_snapshots osor ON ap.avs = osor.avs AND ap.operator_set_id = osor.operator_set_id - AND ap.operator = osor.operator - -- Use slashable_until for unique stake rewards (includes 14-day queue) - -- NULL slashable_until means operator is still active AND ap.snapshot >= osor.snapshot AND (osor.slashable_until IS NULL OR ap.snapshot <= osor.slashable_until) + WHERE ap.reward_type = 'unique_stake' ), -operators_with_allocated_stake AS ( +-- Step 2: Calculate allocated weight per operator +operators_with_weight AS ( SELECT rso.*, - oas.magnitude, - oas.max_magnitude, - oss.shares as operator_total_shares, - -- Calculate effective allocated stake for this strategy - CAST(oss.shares AS NUMERIC(78,0)) * - CAST(oas.magnitude AS NUMERIC(78,0)) / - CAST(oas.max_magnitude AS NUMERIC(78,0)) as allocated_stake - FROM reward_snapshot_operators rso + SUM( + oss.shares * + oas.magnitude / + oas.max_magnitude * rso.multiplier + ) OVER (PARTITION BY rso.reward_hash, rso.snapshot, rso.operator) as operator_allocated_weight + FROM registered_operators rso JOIN {{.operatorAllocationSnapshotsTable}} oas ON rso.operator = oas.operator AND rso.avs = oas.avs @@ -61,71 +61,64 @@ operators_with_allocated_stake AS ( AND oss.shares > 0 ), --- Calculate weighted allocated stake per operator (sum across strategies) -operators_with_unique_stake AS ( - SELECT +-- Step 3: Calculate total weight per (reward_hash, snapshot) for pro-rata +total_weight AS ( + SELECT DISTINCT reward_hash, snapshot, - token, - tokens_per_registered_snapshot_decimal, - avs, - operator_set_id, - operator, - strategy, - multiplier, - reward_submission_date, - registration_snapshot, - slashable_until, - -- Sum the weighted allocated stake across strategies - SUM(allocated_stake * multiplier) OVER ( - PARTITION BY reward_hash, snapshot, operator - ) as operator_allocated_weight - FROM operators_with_allocated_stake + SUM(operator_allocated_weight) OVER (PARTITION BY reward_hash, snapshot) as total_weight + FROM ( + SELECT DISTINCT reward_hash, snapshot, operator, operator_allocated_weight + FROM operators_with_weight + ) distinct_ops ), --- Step 2: Dedupe the operator tokens across strategies for each (operator, reward hash, snapshot) --- Since the above result is a flattened operator-directed reward submission across strategies. +-- Step 4: Calculate pro-rata tokens per operator distinct_operators AS ( - SELECT * + SELECT + pow.reward_hash, pow.snapshot, pow.token, + FLOOR(pow.tokens_per_day_decimal * pow.operator_allocated_weight / tw.total_weight) as tokens_per_registered_snapshot_decimal, + pow.avs, pow.operator_set_id, pow.operator, pow.strategy, pow.multiplier, + pow.reward_submission_date, pow.registration_snapshot, pow.slashable_until, + pow.operator_allocated_weight FROM ( - SELECT - *, - -- We can use an arbitrary order here since the operator_tokens is the same for each (operator, strategy, hash, snapshot) - -- We use strategy ASC for better debuggability - ROW_NUMBER() OVER ( - PARTITION BY reward_hash, snapshot, operator - ORDER BY strategy ASC - ) AS rn - FROM operators_with_unique_stake - ) t - -- Keep only the first row for each (operator, reward hash, snapshot) - WHERE rn = 1 + SELECT *, ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, operator ORDER BY strategy ASC) AS rn + FROM operators_with_weight + ) pow + JOIN total_weight tw + ON pow.reward_hash = tw.reward_hash + AND pow.snapshot = tw.snapshot + WHERE pow.rn = 1 AND tw.total_weight > 0 ), --- Step 3: Check if operators are in deregistration queue (between registration and slashable_until) +-- ============================================================================ +-- Slashing and Splits Processing +-- ============================================================================ + +-- Step 5: Check deregistration queue status operators_with_deregistration_status AS ( SELECT - dop.*, + *, CASE - WHEN dop.snapshot > dop.registration_snapshot - AND dop.snapshot <= dop.slashable_until + WHEN slashable_until IS NOT NULL + AND snapshot > (slashable_until - INTERVAL '14 days') + AND snapshot <= slashable_until THEN TRUE ELSE FALSE END as in_deregistration_queue - FROM distinct_operators dop + FROM distinct_operators ), --- Step 4: Calculate cumulative slash multiplier during deregistration queue +-- Step 6: Calculate cumulative slash multiplier during deregistration queue -- Slashing only affects rewards during the 14-day deregistration period -- GUARD: If wad_slashed >= 1e18 (100% slash), use a very large negative value for LN --- instead of LN(0) which would cause a math error. This effectively makes the multiplier ~0. operators_with_slash_multiplier AS ( SELECT owds.*, COALESCE( EXP(SUM( CASE - WHEN COALESCE(so.wad_slashed, 0) >= CAST(1e18 AS NUMERIC) THEN -100 -- Effectively 0 multiplier (e^-100 ≈ 0) + WHEN COALESCE(so.wad_slashed, 0) >= CAST(1e18 AS NUMERIC) THEN -100 ELSE LN(1 - COALESCE(so.wad_slashed, 0) / CAST(1e18 AS NUMERIC)) END ) FILTER ( @@ -148,10 +141,10 @@ operators_with_slash_multiplier AS ( GROUP BY owds.reward_hash, owds.snapshot, owds.token, owds.tokens_per_registered_snapshot_decimal, owds.avs, owds.operator_set_id, owds.operator, owds.strategy, owds.multiplier, owds.reward_submission_date, owds.registration_snapshot, owds.slashable_until, - owds.operator_allocated_weight, owds.rn, owds.in_deregistration_queue + owds.operator_allocated_weight, owds.in_deregistration_queue ), --- Step 5: Apply slash multiplier to tokens +-- Step 7: Apply slash multiplier to tokens operators_with_adjusted_tokens AS ( SELECT *, @@ -164,7 +157,7 @@ operators_with_adjusted_tokens AS ( FROM operators_with_slash_multiplier ), --- Step 6: Calculate the tokens for each operator with dynamic split logic +-- Step 8: Calculate operator tokens with dynamic split logic -- If no split is found, default to 1000 (10%) operator_splits AS ( SELECT @@ -180,32 +173,31 @@ operator_splits AS ( LEFT JOIN default_operator_split_snapshots dos ON (oat.snapshot = dos.snapshot) ) --- Step 7: Output the final table with operator splits SELECT * FROM operator_splits ` -func (rc *RewardsCalculator) GenerateGold15OperatorOperatorSetUniqueStakeRewardsTable(snapshotDate string) error { +func (rc *RewardsCalculator) GenerateGold16OperatorOperatorSetUniqueStakeRewardsTable(snapshotDate string) error { rewardsV2_2Enabled, err := rc.globalConfig.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) if err != nil { rc.logger.Sugar().Errorw("Failed to check if rewards v2.2 is enabled", "error", err) return err } if !rewardsV2_2Enabled { - rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 15") + rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 16") return nil } allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) - destTableName := allTableNames[rewardsUtils.Table_15_OperatorOperatorSetUniqueStakeRewards] + destTableName := allTableNames[rewardsUtils.Table_16_OperatorOperatorSetUniqueStakeRewards] rc.logger.Sugar().Infow("Generating v2.2 Operator operator set unique stake rewards", zap.String("cutoffDate", snapshotDate), zap.String("destTableName", destTableName), ) - query, err := rewardsUtils.RenderQueryTemplate(_15_goldOperatorOperatorSetUniqueStakeRewardsQuery, map[string]interface{}{ + query, err := rewardsUtils.RenderQueryTemplate(_16_goldOperatorOperatorSetUniqueStakeRewardsQuery, map[string]interface{}{ "destTableName": destTableName, - "activeODRewardsTable": allTableNames[rewardsUtils.Table_11_ActiveODOperatorSetRewards], + "activeStakeRewardsTable": allTableNames[rewardsUtils.Table_15_ActiveUniqueAndTotalStakeRewards], "operatorAllocationSnapshotsTable": "operator_allocation_snapshots", "operatorShareSnapshotsTable": "operator_share_snapshots", }) diff --git a/pkg/rewards/16_goldStakerOperatorSetUniqueStakeRewards.go b/pkg/rewards/17_goldStakerOperatorSetUniqueStakeRewards.go similarity index 86% rename from pkg/rewards/16_goldStakerOperatorSetUniqueStakeRewards.go rename to pkg/rewards/17_goldStakerOperatorSetUniqueStakeRewards.go index ee4666e13..84572121e 100644 --- a/pkg/rewards/16_goldStakerOperatorSetUniqueStakeRewards.go +++ b/pkg/rewards/17_goldStakerOperatorSetUniqueStakeRewards.go @@ -5,7 +5,7 @@ import ( "go.uber.org/zap" ) -const _16_goldStakerOperatorSetUniqueStakeRewardsQuery = ` +const _17_goldStakerOperatorSetUniqueStakeRewardsQuery = ` CREATE TABLE {{.destTableName}} AS -- Step 1: Get operator rewards and staker splits from previous table 15 @@ -19,9 +19,8 @@ WITH operator_rewards AS ( operator_set_id, strategy, multiplier, - tokens_per_registered_snapshot_decimal, - -- Calculate staker split (total rewards minus operator split) - tokens_per_registered_snapshot_decimal - operator_tokens as staker_split_total + adjusted_tokens_per_snapshot, + adjusted_tokens_per_snapshot - operator_tokens as staker_split_total FROM {{.operatorRewardsTable}} ), @@ -94,27 +93,27 @@ staker_rewards AS ( SELECT * FROM staker_rewards ` -func (rc *RewardsCalculator) GenerateGold16StakerOperatorSetUniqueStakeRewardsTable(snapshotDate string) error { +func (rc *RewardsCalculator) GenerateGold17StakerOperatorSetUniqueStakeRewardsTable(snapshotDate string) error { rewardsV2_2Enabled, err := rc.globalConfig.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) if err != nil { rc.logger.Sugar().Errorw("Failed to check if rewards v2.2 is enabled", "error", err) return err } if !rewardsV2_2Enabled { - rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 16") + rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 17") return nil } allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) - destTableName := allTableNames[rewardsUtils.Table_16_StakerOperatorSetUniqueStakeRewards] - operatorRewardsTable := allTableNames[rewardsUtils.Table_15_OperatorOperatorSetUniqueStakeRewards] + destTableName := allTableNames[rewardsUtils.Table_17_StakerOperatorSetUniqueStakeRewards] + operatorRewardsTable := allTableNames[rewardsUtils.Table_16_OperatorOperatorSetUniqueStakeRewards] rc.logger.Sugar().Infow("Generating v2.2 staker operator set unique stake rewards", zap.String("snapshotDate", snapshotDate), zap.String("destTableName", destTableName), ) - query, err := rewardsUtils.RenderQueryTemplate(_16_goldStakerOperatorSetUniqueStakeRewardsQuery, map[string]interface{}{ + query, err := rewardsUtils.RenderQueryTemplate(_17_goldStakerOperatorSetUniqueStakeRewardsQuery, map[string]interface{}{ "destTableName": destTableName, "operatorRewardsTable": operatorRewardsTable, }) diff --git a/pkg/rewards/17_goldAvsOperatorSetUniqueStakeRewards.go b/pkg/rewards/18_goldAvsOperatorSetUniqueStakeRewards.go similarity index 54% rename from pkg/rewards/17_goldAvsOperatorSetUniqueStakeRewards.go rename to pkg/rewards/18_goldAvsOperatorSetUniqueStakeRewards.go index 89709088f..2d9508ab3 100644 --- a/pkg/rewards/17_goldAvsOperatorSetUniqueStakeRewards.go +++ b/pkg/rewards/18_goldAvsOperatorSetUniqueStakeRewards.go @@ -6,7 +6,7 @@ import ( "go.uber.org/zap" ) -const _17_goldAvsOperatorSetUniqueStakeRewardsQuery = ` +const _18_goldAvsOperatorSetUniqueStakeRewardsQuery = ` CREATE TABLE {{.destTableName}} AS -- Step 1: Calculate total tokens available per (reward_hash, snapshot) @@ -23,17 +23,33 @@ WITH total_available_tokens AS ( GROUP BY reward_hash, snapshot, token, avs, operator_set_id, operator ), --- Step 2: Calculate total tokens actually distributed from the operator rewards table -total_distributed_tokens AS ( +-- Step 2: Calculate total operator tokens distributed per operator from the operator rewards table +operator_distributed_tokens AS ( SELECT reward_hash, snapshot, - COALESCE(SUM(operator_tokens), 0) as distributed_tokens + avs, + operator_set_id, + operator, + COALESCE(SUM(operator_tokens), 0) as operator_distributed FROM {{.operatorRewardsTable}} - GROUP BY reward_hash, snapshot + GROUP BY reward_hash, snapshot, avs, operator_set_id, operator +), + +-- Step 3: Calculate total staker tokens distributed per operator from the staker rewards table +staker_distributed_tokens AS ( + SELECT + reward_hash, + snapshot, + avs, + operator_set_id, + operator, + COALESCE(SUM(staker_tokens), 0) as staker_distributed + FROM {{.stakerRewardsTable}} + GROUP BY reward_hash, snapshot, avs, operator_set_id, operator ), --- Step 3: Identify snapshots where distributed tokens = 0, refund all available tokens to AVS +-- Step 4: Identify operator-sets where total distributed tokens (operator + staker) = 0, refund those tokens to AVS snapshots_requiring_refund AS ( SELECT tat.reward_hash, @@ -44,28 +60,37 @@ snapshots_requiring_refund AS ( tat.operator, tat.total_tokens as avs_tokens FROM total_available_tokens tat - LEFT JOIN total_distributed_tokens tdt - ON tat.reward_hash = tdt.reward_hash - AND tat.snapshot = tdt.snapshot - WHERE COALESCE(tdt.distributed_tokens, 0) = 0 + LEFT JOIN operator_distributed_tokens odt + ON tat.reward_hash = odt.reward_hash + AND tat.snapshot = odt.snapshot + AND tat.avs = odt.avs + AND tat.operator_set_id = odt.operator_set_id + AND tat.operator = odt.operator + LEFT JOIN staker_distributed_tokens sdt + ON tat.reward_hash = sdt.reward_hash + AND tat.snapshot = sdt.snapshot + AND tat.avs = sdt.avs + AND tat.operator_set_id = sdt.operator_set_id + AND tat.operator = sdt.operator + WHERE COALESCE(odt.operator_distributed, 0) + COALESCE(sdt.staker_distributed, 0) = 0 ) SELECT * FROM snapshots_requiring_refund ` -func (rc *RewardsCalculator) GenerateGold17AvsOperatorSetUniqueStakeRewardsTable(snapshotDate string, forks config.ForkMap) error { +func (rc *RewardsCalculator) GenerateGold18AvsOperatorSetUniqueStakeRewardsTable(snapshotDate string, forks config.ForkMap) error { rewardsV2_2Enabled, err := rc.globalConfig.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) if err != nil { rc.logger.Sugar().Errorw("Failed to check if rewards v2.2 is enabled", "error", err) return err } if !rewardsV2_2Enabled { - rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 17") + rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 18") return nil } allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) - destTableName := allTableNames[rewardsUtils.Table_17_AvsOperatorSetUniqueStakeRewards] + destTableName := allTableNames[rewardsUtils.Table_18_AvsOperatorSetUniqueStakeRewards] rc.logger.Sugar().Infow("Generating v2.2 AVS operator set unique stake rewards (refunds)", zap.String("cutoffDate", snapshotDate), @@ -73,10 +98,11 @@ func (rc *RewardsCalculator) GenerateGold17AvsOperatorSetUniqueStakeRewardsTable zap.String("coloradoHardforkDate", forks[config.RewardsFork_Colorado].Date), ) - query, err := rewardsUtils.RenderQueryTemplate(_17_goldAvsOperatorSetUniqueStakeRewardsQuery, map[string]interface{}{ + query, err := rewardsUtils.RenderQueryTemplate(_18_goldAvsOperatorSetUniqueStakeRewardsQuery, map[string]interface{}{ "destTableName": destTableName, "activeODRewardsTable": allTableNames[rewardsUtils.Table_11_ActiveODOperatorSetRewards], - "operatorRewardsTable": allTableNames[rewardsUtils.Table_15_OperatorOperatorSetUniqueStakeRewards], + "operatorRewardsTable": allTableNames[rewardsUtils.Table_16_OperatorOperatorSetUniqueStakeRewards], + "stakerRewardsTable": allTableNames[rewardsUtils.Table_17_StakerOperatorSetUniqueStakeRewards], }) if err != nil { rc.logger.Sugar().Errorw("Failed to render query template", "error", err) diff --git a/pkg/rewards/18_goldOperatorOperatorSetTotalStakeRewards.go b/pkg/rewards/19_goldOperatorOperatorSetTotalStakeRewards.go similarity index 50% rename from pkg/rewards/18_goldOperatorOperatorSetTotalStakeRewards.go rename to pkg/rewards/19_goldOperatorOperatorSetTotalStakeRewards.go index 72dad4a54..3a41edf9d 100644 --- a/pkg/rewards/18_goldOperatorOperatorSetTotalStakeRewards.go +++ b/pkg/rewards/19_goldOperatorOperatorSetTotalStakeRewards.go @@ -5,36 +5,43 @@ import ( "go.uber.org/zap" ) -const _18_goldOperatorOperatorSetTotalStakeRewardsQuery = ` +const _19_goldOperatorOperatorSetTotalStakeRewardsQuery = ` CREATE TABLE {{.destTableName}} AS --- Step 1: Get registered operators for the operator set -WITH reward_snapshot_operators AS ( +-- ============================================================================ +-- Stake Rewards Path (pool amounts from Table 15, needs pro-rata) +-- ============================================================================ + +-- Step 1: Get registered operators for stake-based total stake rewards +WITH registered_operators AS ( SELECT ap.reward_hash, ap.snapshot AS snapshot, ap.token, - ap.tokens_per_registered_snapshot_decimal, + ap.tokens_per_day_decimal, ap.avs AS avs, ap.operator_set_id AS operator_set_id, - ap.operator AS operator, ap.strategy, ap.multiplier, - ap.reward_submission_date - FROM {{.activeODRewardsTable}} ap + ap.reward_submission_date, + osor.operator + FROM {{.activeStakeRewardsTable}} ap JOIN operator_set_operator_registration_snapshots osor ON ap.avs = osor.avs AND ap.operator_set_id = osor.operator_set_id AND ap.snapshot = osor.snapshot - AND ap.operator = osor.operator + WHERE ap.reward_type = 'total_stake' ), --- Step 2: Get operator's total delegated stake per strategy -operators_with_total_stake AS ( +-- Step 2: Calculate weighted total stake per operator +operators_with_weight AS ( SELECT rso.*, - oss.shares as operator_total_shares - FROM reward_snapshot_operators rso + oss.shares as operator_total_shares, + SUM(CAST(oss.shares AS NUMERIC(78,0)) * rso.multiplier) OVER ( + PARTITION BY rso.reward_hash, rso.snapshot, rso.operator + ) as operator_total_weight + FROM registered_operators rso JOIN {{.operatorShareSnapshotsTable}} oss ON rso.operator = oss.operator AND rso.strategy = oss.strategy @@ -42,43 +49,41 @@ operators_with_total_stake AS ( WHERE oss.shares > 0 ), --- Step 3: Calculate weighted total stake per operator (sum across strategies with multiplier) -operators_with_total_stake_weight AS ( - SELECT +-- Step 3: Calculate total weight per (reward_hash, snapshot) for pro-rata +total_weight AS ( + SELECT DISTINCT reward_hash, snapshot, - token, - tokens_per_registered_snapshot_decimal, - avs, - operator_set_id, - operator, - strategy, - multiplier, - reward_submission_date, - operator_total_shares, - -- Sum the weighted total stake across strategies - SUM(CAST(operator_total_shares AS NUMERIC(78,0)) * multiplier) OVER ( - PARTITION BY reward_hash, snapshot, operator - ) as operator_total_weight - FROM operators_with_total_stake + SUM(operator_total_weight) OVER (PARTITION BY reward_hash, snapshot) as total_weight + FROM ( + SELECT DISTINCT reward_hash, snapshot, operator, operator_total_weight + FROM operators_with_weight + ) distinct_ops ), --- Step 4: Dedupe across strategies (take first strategy, they all have same operator_total_weight) +-- Step 4: Calculate pro-rata tokens per operator distinct_operators AS ( - SELECT * + SELECT + pow.reward_hash, pow.snapshot, pow.token, + FLOOR(pow.tokens_per_day_decimal * pow.operator_total_weight / tw.total_weight) as tokens_per_registered_snapshot_decimal, + pow.avs, pow.operator_set_id, pow.operator, pow.strategy, pow.multiplier, + pow.reward_submission_date, pow.operator_total_shares, pow.operator_total_weight FROM ( - SELECT - *, - ROW_NUMBER() OVER ( - PARTITION BY reward_hash, snapshot, operator - ORDER BY strategy ASC - ) AS rn - FROM operators_with_total_stake_weight - ) t - WHERE rn = 1 + SELECT *, ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, operator ORDER BY strategy ASC) AS rn + FROM operators_with_weight + ) pow + JOIN total_weight tw + ON pow.reward_hash = tw.reward_hash + AND pow.snapshot = tw.snapshot + WHERE pow.rn = 1 AND tw.total_weight > 0 ), --- Step 5: Calculate operator splits with dynamic split logic +-- ============================================================================ +-- Splits Processing (no slashing for TotalStake) +-- ============================================================================ + +-- Step 5: Calculate operator tokens with dynamic split logic +-- If no split is found, default to 1000 (10%) operator_splits AS ( SELECT dop.*, @@ -93,32 +98,31 @@ operator_splits AS ( LEFT JOIN default_operator_split_snapshots dos ON (dop.snapshot = dos.snapshot) ) --- Output the final table SELECT * FROM operator_splits ` -func (rc *RewardsCalculator) GenerateGold18OperatorOperatorSetTotalStakeRewardsTable(snapshotDate string) error { +func (rc *RewardsCalculator) GenerateGold19OperatorOperatorSetTotalStakeRewardsTable(snapshotDate string) error { rewardsV2_2Enabled, err := rc.globalConfig.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) if err != nil { rc.logger.Sugar().Errorw("Failed to check if rewards v2.2 is enabled", "error", err) return err } if !rewardsV2_2Enabled { - rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 18") + rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 19") return nil } allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) - destTableName := allTableNames[rewardsUtils.Table_18_OperatorOperatorSetTotalStakeRewards] + destTableName := allTableNames[rewardsUtils.Table_19_OperatorOperatorSetTotalStakeRewards] rc.logger.Sugar().Infow("Generating v2.2 Operator operator set reward amounts with total stake", zap.String("cutoffDate", snapshotDate), zap.String("destTableName", destTableName), ) - query, err := rewardsUtils.RenderQueryTemplate(_18_goldOperatorOperatorSetTotalStakeRewardsQuery, map[string]interface{}{ + query, err := rewardsUtils.RenderQueryTemplate(_19_goldOperatorOperatorSetTotalStakeRewardsQuery, map[string]interface{}{ "destTableName": destTableName, - "activeODRewardsTable": allTableNames[rewardsUtils.Table_11_ActiveODOperatorSetRewards], + "activeStakeRewardsTable": allTableNames[rewardsUtils.Table_15_ActiveUniqueAndTotalStakeRewards], "operatorShareSnapshotsTable": "operator_share_snapshots", }) if err != nil { diff --git a/pkg/rewards/19_goldStakerOperatorSetTotalStakeRewards.go b/pkg/rewards/20_goldStakerOperatorSetTotalStakeRewards.go similarity index 91% rename from pkg/rewards/19_goldStakerOperatorSetTotalStakeRewards.go rename to pkg/rewards/20_goldStakerOperatorSetTotalStakeRewards.go index 4d252f799..f71d09d20 100644 --- a/pkg/rewards/19_goldStakerOperatorSetTotalStakeRewards.go +++ b/pkg/rewards/20_goldStakerOperatorSetTotalStakeRewards.go @@ -5,7 +5,7 @@ import ( "go.uber.org/zap" ) -const _19_goldStakerOperatorSetTotalStakeRewardsQuery = ` +const _20_goldStakerOperatorSetTotalStakeRewardsQuery = ` CREATE TABLE {{.destTableName}} AS -- Step 1: Get operator rewards and staker splits from previous table 18 @@ -89,27 +89,27 @@ staker_rewards AS ( SELECT * FROM staker_rewards ` -func (rc *RewardsCalculator) GenerateGold19StakerOperatorSetTotalStakeRewardsTable(snapshotDate string) error { +func (rc *RewardsCalculator) GenerateGold20StakerOperatorSetTotalStakeRewardsTable(snapshotDate string) error { rewardsV2_2Enabled, err := rc.globalConfig.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) if err != nil { rc.logger.Sugar().Errorw("Failed to check if rewards v2.2 is enabled", "error", err) return err } if !rewardsV2_2Enabled { - rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 19") + rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 20") return nil } allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) - destTableName := allTableNames[rewardsUtils.Table_19_StakerOperatorSetTotalStakeRewards] - operatorRewardsTable := allTableNames[rewardsUtils.Table_18_OperatorOperatorSetTotalStakeRewards] + destTableName := allTableNames[rewardsUtils.Table_20_StakerOperatorSetTotalStakeRewards] + operatorRewardsTable := allTableNames[rewardsUtils.Table_19_OperatorOperatorSetTotalStakeRewards] rc.logger.Sugar().Infow("Generating v2.2 staker operator set reward amounts with total stake", zap.String("snapshotDate", snapshotDate), zap.String("destTableName", destTableName), ) - query, err := rewardsUtils.RenderQueryTemplate(_19_goldStakerOperatorSetTotalStakeRewardsQuery, map[string]interface{}{ + query, err := rewardsUtils.RenderQueryTemplate(_20_goldStakerOperatorSetTotalStakeRewardsQuery, map[string]interface{}{ "destTableName": destTableName, "operatorRewardsTable": operatorRewardsTable, }) diff --git a/pkg/rewards/20_goldAvsOperatorSetTotalStakeRewards.go b/pkg/rewards/21_goldAvsOperatorSetTotalStakeRewards.go similarity index 54% rename from pkg/rewards/20_goldAvsOperatorSetTotalStakeRewards.go rename to pkg/rewards/21_goldAvsOperatorSetTotalStakeRewards.go index 3240f1ab6..713a24460 100644 --- a/pkg/rewards/20_goldAvsOperatorSetTotalStakeRewards.go +++ b/pkg/rewards/21_goldAvsOperatorSetTotalStakeRewards.go @@ -6,7 +6,7 @@ import ( "go.uber.org/zap" ) -const _20_goldAvsOperatorSetTotalStakeRewardsQuery = ` +const _21_goldAvsOperatorSetTotalStakeRewardsQuery = ` CREATE TABLE {{.destTableName}} AS -- Step 1: Calculate total tokens available per (reward_hash, snapshot) @@ -23,17 +23,33 @@ WITH total_available_tokens AS ( GROUP BY reward_hash, snapshot, token, avs, operator_set_id, operator ), --- Step 2: Calculate total tokens actually distributed from the operator rewards table -total_distributed_tokens AS ( +-- Step 2: Calculate total operator tokens distributed per operator from the operator rewards table +operator_distributed_tokens AS ( SELECT reward_hash, snapshot, - COALESCE(SUM(operator_tokens), 0) as distributed_tokens + avs, + operator_set_id, + operator, + COALESCE(SUM(operator_tokens), 0) as operator_distributed FROM {{.operatorRewardsTable}} - GROUP BY reward_hash, snapshot + GROUP BY reward_hash, snapshot, avs, operator_set_id, operator +), + +-- Step 3: Calculate total staker tokens distributed per operator from the staker rewards table +staker_distributed_tokens AS ( + SELECT + reward_hash, + snapshot, + avs, + operator_set_id, + operator, + COALESCE(SUM(staker_tokens), 0) as staker_distributed + FROM {{.stakerRewardsTable}} + GROUP BY reward_hash, snapshot, avs, operator_set_id, operator ), --- Step 3: Identify snapshots where distributed tokens = 0, refund all available tokens to AVS +-- Step 4: Identify operator-sets where total distributed tokens (operator + staker) = 0, refund those tokens to AVS snapshots_requiring_refund AS ( SELECT tat.reward_hash, @@ -44,28 +60,37 @@ snapshots_requiring_refund AS ( tat.operator, tat.total_tokens as avs_tokens FROM total_available_tokens tat - LEFT JOIN total_distributed_tokens tdt - ON tat.reward_hash = tdt.reward_hash - AND tat.snapshot = tdt.snapshot - WHERE COALESCE(tdt.distributed_tokens, 0) = 0 + LEFT JOIN operator_distributed_tokens odt + ON tat.reward_hash = odt.reward_hash + AND tat.snapshot = odt.snapshot + AND tat.avs = odt.avs + AND tat.operator_set_id = odt.operator_set_id + AND tat.operator = odt.operator + LEFT JOIN staker_distributed_tokens sdt + ON tat.reward_hash = sdt.reward_hash + AND tat.snapshot = sdt.snapshot + AND tat.avs = sdt.avs + AND tat.operator_set_id = sdt.operator_set_id + AND tat.operator = sdt.operator + WHERE COALESCE(odt.operator_distributed, 0) + COALESCE(sdt.staker_distributed, 0) = 0 ) SELECT * FROM snapshots_requiring_refund ` -func (rc *RewardsCalculator) GenerateGold20AvsOperatorSetTotalStakeRewardsTable(snapshotDate string, forks config.ForkMap) error { +func (rc *RewardsCalculator) GenerateGold21AvsOperatorSetTotalStakeRewardsTable(snapshotDate string, forks config.ForkMap) error { rewardsV2_2Enabled, err := rc.globalConfig.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) if err != nil { rc.logger.Sugar().Errorw("Failed to check if rewards v2.2 is enabled", "error", err) return err } if !rewardsV2_2Enabled { - rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 20") + rc.logger.Sugar().Infow("Rewards v2.2 is not enabled, skipping v2.2 table 21") return nil } allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) - destTableName := allTableNames[rewardsUtils.Table_20_AvsOperatorSetTotalStakeRewards] + destTableName := allTableNames[rewardsUtils.Table_21_AvsOperatorSetTotalStakeRewards] rc.logger.Sugar().Infow("Generating v2.2 AVS operator set reward refunds with total stake validation", zap.String("cutoffDate", snapshotDate), @@ -73,10 +98,11 @@ func (rc *RewardsCalculator) GenerateGold20AvsOperatorSetTotalStakeRewardsTable( zap.String("coloradoHardforkDate", forks[config.RewardsFork_Colorado].Date), ) - query, err := rewardsUtils.RenderQueryTemplate(_20_goldAvsOperatorSetTotalStakeRewardsQuery, map[string]interface{}{ + query, err := rewardsUtils.RenderQueryTemplate(_21_goldAvsOperatorSetTotalStakeRewardsQuery, map[string]interface{}{ "destTableName": destTableName, "activeODRewardsTable": allTableNames[rewardsUtils.Table_11_ActiveODOperatorSetRewards], - "operatorRewardsTable": allTableNames[rewardsUtils.Table_18_OperatorOperatorSetTotalStakeRewards], + "operatorRewardsTable": allTableNames[rewardsUtils.Table_19_OperatorOperatorSetTotalStakeRewards], + "stakerRewardsTable": allTableNames[rewardsUtils.Table_20_StakerOperatorSetTotalStakeRewards], }) if err != nil { rc.logger.Sugar().Errorw("Failed to render query template", "error", err) diff --git a/pkg/rewards/21_goldStaging.go b/pkg/rewards/22_goldStaging.go similarity index 94% rename from pkg/rewards/21_goldStaging.go rename to pkg/rewards/22_goldStaging.go index e44d30499..8e1783a66 100644 --- a/pkg/rewards/21_goldStaging.go +++ b/pkg/rewards/22_goldStaging.go @@ -7,7 +7,7 @@ import ( "go.uber.org/zap" ) -const _18_goldStagingQuery = ` +const _22_goldStagingQuery = ` create table {{.destTableName}} as WITH staker_rewards AS ( -- We can select DISTINCT here because the staker's tokens are the same for each strategy in the reward hash @@ -236,9 +236,9 @@ SELECT * FROM deduped_earners ` -func (rc *RewardsCalculator) GenerateGold21StagingTable(snapshotDate string) error { +func (rc *RewardsCalculator) GenerateGold22StagingTable(snapshotDate string) error { allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) - destTableName := allTableNames[rewardsUtils.Table_21_GoldStaging] + destTableName := allTableNames[rewardsUtils.Table_22_GoldStaging] rc.logger.Sugar().Infow("Generating gold staging", zap.String("cutoffDate", snapshotDate), @@ -266,7 +266,7 @@ func (rc *RewardsCalculator) GenerateGold21StagingTable(snapshotDate string) err } rc.logger.Sugar().Infow("Is RewardsV2_2 enabled?", "enabled", isRewardsV2_2Enabled) - query, err := rewardsUtils.RenderQueryTemplate(_18_goldStagingQuery, map[string]interface{}{ + query, err := rewardsUtils.RenderQueryTemplate(_22_goldStagingQuery, map[string]interface{}{ "destTableName": destTableName, "stakerRewardAmountsTable": allTableNames[rewardsUtils.Table_2_StakerRewardAmounts], "operatorRewardAmountsTable": allTableNames[rewardsUtils.Table_3_OperatorRewardAmounts], @@ -281,12 +281,12 @@ func (rc *RewardsCalculator) GenerateGold21StagingTable(snapshotDate string) err "stakerODOperatorSetRewardAmountsTable": allTableNames[rewardsUtils.Table_13_StakerODOperatorSetRewardAmounts], "avsODOperatorSetRewardAmountsTable": allTableNames[rewardsUtils.Table_14_AvsODOperatorSetRewardAmounts], "enableRewardsV2_1": isRewardsV2_1Enabled, - "operatorOperatorSetUniqueStakeRewardsTable": allTableNames[rewardsUtils.Table_15_OperatorOperatorSetUniqueStakeRewards], - "stakerOperatorSetUniqueStakeRewardsTable": allTableNames[rewardsUtils.Table_16_StakerOperatorSetUniqueStakeRewards], - "avsOperatorSetUniqueStakeRewardsTable": allTableNames[rewardsUtils.Table_17_AvsOperatorSetUniqueStakeRewards], - "operatorOperatorSetTotalStakeRewardsTable": allTableNames[rewardsUtils.Table_18_OperatorOperatorSetTotalStakeRewards], - "stakerOperatorSetTotalStakeRewardsTable": allTableNames[rewardsUtils.Table_19_StakerOperatorSetTotalStakeRewards], - "avsOperatorSetTotalStakeRewardsTable": allTableNames[rewardsUtils.Table_20_AvsOperatorSetTotalStakeRewards], + "operatorOperatorSetUniqueStakeRewardsTable": allTableNames[rewardsUtils.Table_16_OperatorOperatorSetUniqueStakeRewards], + "stakerOperatorSetUniqueStakeRewardsTable": allTableNames[rewardsUtils.Table_17_StakerOperatorSetUniqueStakeRewards], + "avsOperatorSetUniqueStakeRewardsTable": allTableNames[rewardsUtils.Table_18_AvsOperatorSetUniqueStakeRewards], + "operatorOperatorSetTotalStakeRewardsTable": allTableNames[rewardsUtils.Table_19_OperatorOperatorSetTotalStakeRewards], + "stakerOperatorSetTotalStakeRewardsTable": allTableNames[rewardsUtils.Table_20_StakerOperatorSetTotalStakeRewards], + "avsOperatorSetTotalStakeRewardsTable": allTableNames[rewardsUtils.Table_21_AvsOperatorSetTotalStakeRewards], "enableRewardsV2_2": isRewardsV2_2Enabled, }) if err != nil { @@ -323,7 +323,7 @@ func (rc *RewardsCalculator) ListGoldStagingRowsForSnapshot(snapshotDate string) amount FROM {{.goldStagingTable}} WHERE DATE(snapshot) < @cutoffDate` query, err := rewardsUtils.RenderQueryTemplate(query, map[string]interface{}{ - "goldStagingTable": allTableNames[rewardsUtils.Table_21_GoldStaging], + "goldStagingTable": allTableNames[rewardsUtils.Table_22_GoldStaging], }) if err != nil { rc.logger.Sugar().Errorw("Failed to render query template", "error", err) diff --git a/pkg/rewards/22_goldFinal.go b/pkg/rewards/23_goldFinal.go similarity index 83% rename from pkg/rewards/22_goldFinal.go rename to pkg/rewards/23_goldFinal.go index 7369d6f26..ccd003327 100644 --- a/pkg/rewards/22_goldFinal.go +++ b/pkg/rewards/23_goldFinal.go @@ -7,7 +7,7 @@ import ( "go.uber.org/zap" ) -const _16_goldFinalQuery = ` +const _23_goldFinalQuery = ` insert into gold_table SELECT earner, @@ -26,15 +26,15 @@ type GoldRow struct { Amount string } -func (rc *RewardsCalculator) GenerateGold22FinalTable(snapshotDate string) error { +func (rc *RewardsCalculator) GenerateGold23FinalTable(snapshotDate string) error { allTableNames := rewardsUtils.GetGoldTableNames(snapshotDate) rc.logger.Sugar().Infow("Generating gold final table", zap.String("cutoffDate", snapshotDate), ) - query, err := rewardsUtils.RenderQueryTemplate(_16_goldFinalQuery, map[string]interface{}{ - "goldStagingTable": allTableNames[rewardsUtils.Table_21_GoldStaging], + query, err := rewardsUtils.RenderQueryTemplate(_23_goldFinalQuery, map[string]interface{}{ + "goldStagingTable": allTableNames[rewardsUtils.Table_22_GoldStaging], }) if err != nil { rc.logger.Sugar().Errorw("Failed to render query template", "error", err) diff --git a/pkg/rewards/operatorAllocationSnapshots.go b/pkg/rewards/operatorAllocationSnapshots.go index 64de2561e..812d8c1a8 100644 --- a/pkg/rewards/operatorAllocationSnapshots.go +++ b/pkg/rewards/operatorAllocationSnapshots.go @@ -9,7 +9,7 @@ const operatorAllocationSnapshotsQuery = ` insert into operator_allocation_snapshots(operator, avs, strategy, operator_set_id, magnitude, max_magnitude, snapshot) WITH ranked_allocation_records as ( SELECT *, - ROW_NUMBER() OVER (PARTITION BY operator, avs, strategy, operator_set_id, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn + ROW_NUMBER() OVER (PARTITION BY operator, avs, strategy, operator_set_id, cast(block_time AS DATE) ORDER BY block_time DESC, oa.block_number DESC, log_index DESC) AS rn FROM operator_allocations oa -- Backward compatibility: use effective_block if available, fall back to block_number for old records INNER JOIN blocks b ON COALESCE(oa.effective_block, oa.block_number) = b.number diff --git a/pkg/rewards/operatorShareSnapshots.go b/pkg/rewards/operatorShareSnapshots.go index 5181a0181..dee55fc71 100644 --- a/pkg/rewards/operatorShareSnapshots.go +++ b/pkg/rewards/operatorShareSnapshots.go @@ -1,6 +1,7 @@ package rewards import ( + "github.com/Layr-Labs/sidecar/internal/config" "github.com/Layr-Labs/sidecar/pkg/rewardsUtils" "go.uber.org/zap" ) @@ -28,10 +29,10 @@ snapshotted_records as ( -- Get the range for each operator, strategy pairing operator_share_windows as ( SELECT - operator, strategy, shares, snapshot_time as start_time, + operator, strategy, shares::numeric as 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 '{{.cutoffDate}}') + -- If the range does not have the end, use cutoff + 1 day to include cutoff date snapshot + WHEN LEAD(snapshot_time) OVER (PARTITION BY operator, strategy ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{.cutoffDate}}') + INTERVAL '1' day ELSE LEAD(snapshot_time) OVER (PARTITION BY operator, strategy ORDER BY snapshot_time) END AS end_time FROM snapshotted_records @@ -39,49 +40,87 @@ operator_share_windows as ( cleaned_records as ( SELECT * FROM operator_share_windows WHERE start_time < end_time -), -base_snapshots as ( - 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 -), --- Add operator allocations (deallocation delay handled via effective_block) -allocation_adjustments as ( - SELECT - oas.operator, - oas.strategy, - SUM(oas.magnitude) as total_magnitude, - oas.snapshot - FROM operator_allocation_snapshots oas - WHERE oas.snapshot = DATE '{{.snapshotDate}}' - GROUP BY oas.operator, oas.strategy, oas.snapshot -), -combined_snapshots as ( - SELECT - coalesce(base.operator, alloc.operator) as operator, - coalesce(base.strategy, alloc.strategy) as strategy, - coalesce(alloc.total_magnitude, base.shares::numeric) as shares, - coalesce(base.snapshot, alloc.snapshot) as snapshot - FROM base_snapshots base - FULL OUTER JOIN allocation_adjustments alloc - ON base.operator = alloc.operator - AND base.strategy = alloc.strategy - AND base.snapshot = alloc.snapshot ) -SELECT * FROM combined_snapshots +SELECT + cr.operator, + cr.strategy, + {{ if .useSabineFork }} + -- Post-Sabine fork: Add back queued withdrawal shares during queue period + -- This ensures operators show the correct slashable stake during the withdrawal queue window + (cr.shares + COALESCE( + (SELECT SUM( + qsw.shares_to_withdraw::numeric * COALESCE( + -- Get the latest slash multiplier visible BEFORE this snapshot day + (SELECT adj.slash_multiplier::numeric + FROM queued_withdrawal_slashing_adjustments adj + INNER JOIN blocks b_slash ON adj.slash_block_number = b_slash.number + WHERE adj.operator = qsw.operator + AND adj.strategy = qsw.strategy + AND adj.withdrawal_block_number = qsw.block_number + AND adj.withdrawal_log_index = qsw.log_index + AND DATE(b_slash.block_time) < day::date + ORDER BY adj.slash_block_number DESC + LIMIT 1), + 1 -- No slashing if no adjustment records visible yet + ) + ) + FROM queued_slashing_withdrawals qsw + INNER JOIN blocks b_queued ON qsw.block_number = b_queued.number + WHERE qsw.operator = cr.operator + AND qsw.strategy = cr.strategy + -- Withdrawal must be queued before the snapshot day + AND DATE(b_queued.block_time) < day::date + -- Withdrawal must still be in queue (not yet completable) on this snapshot day + AND b_queued.block_time + INTERVAL '{{.withdrawalQueueWindow}} days' > day::timestamp + -- Backwards compatibility: only process records with valid data + AND qsw.staker IS NOT NULL + AND qsw.strategy IS NOT NULL + AND qsw.operator IS NOT NULL + AND qsw.shares_to_withdraw IS NOT NULL + AND b_queued.block_time IS NOT NULL + ), 0 + ))::numeric as shares, + {{ else }} + -- Pre-Sabine fork: Use base shares only (old behavior) + cr.shares as shares, + {{ end }} + cast(day AS DATE) AS snapshot +FROM cleaned_records cr +CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day on conflict on constraint uniq_operator_share_snapshots do nothing; ` func (r *RewardsCalculator) GenerateAndInsertOperatorShareSnapshots(snapshotDate string) error { + forks, err := r.globalConfig.GetRewardsSqlForkDates() + if err != nil { + r.logger.Sugar().Errorw("Failed to get rewards fork dates", "error", err) + return err + } + + // Get the block number for the cutoff date to make block-based fork decision + var cutoffBlockNumber uint64 + err = r.grm.Raw(` + SELECT number + FROM blocks + WHERE block_time <= ? + ORDER BY number DESC + LIMIT 1 + `, snapshotDate).Scan(&cutoffBlockNumber).Error + if err != nil { + r.logger.Sugar().Errorw("Failed to get cutoff block number", "error", err, "snapshotDate", snapshotDate) + return err + } + + // Use block-based fork check for backwards compatibility + useSabineFork := false + if sabineFork, exists := forks[config.RewardsFork_Sabine]; exists { + useSabineFork = cutoffBlockNumber >= sabineFork.BlockNumber + } + query, err := rewardsUtils.RenderQueryTemplate(operatorShareSnapshotsQuery, map[string]interface{}{ - "cutoffDate": snapshotDate, - "snapshotDate": snapshotDate, + "cutoffDate": snapshotDate, + "useSabineFork": useSabineFork, + "withdrawalQueueWindow": r.globalConfig.Rewards.WithdrawalQueueWindow, }) if err != nil { r.logger.Sugar().Errorw("Failed to render operator share snapshots query", "error", err) diff --git a/pkg/rewards/operatorShareSnapshots_test.go b/pkg/rewards/operatorShareSnapshots_test.go index fff5e413e..9ba053753 100644 --- a/pkg/rewards/operatorShareSnapshots_test.go +++ b/pkg/rewards/operatorShareSnapshots_test.go @@ -2,10 +2,11 @@ package rewards import ( "fmt" - "github.com/Layr-Labs/sidecar/pkg/metrics" "testing" "time" + "github.com/Layr-Labs/sidecar/pkg/metrics" + "github.com/Layr-Labs/sidecar/internal/config" "github.com/Layr-Labs/sidecar/internal/tests" "github.com/Layr-Labs/sidecar/pkg/logger" @@ -62,107 +63,6 @@ func teardownOperatorShareSnapshot(dbname string, cfg *config.Config, db *gorm.D } } -func hydrateOperatorShares(grm *gorm.DB, l *zap.Logger) error { - projectRoot := getProjectRootPath() - contents, err := tests.GetOperatorSharesSqlFile(projectRoot) - - if err != nil { - return err - } - - res := grm.Exec(contents) - if res.Error != nil { - l.Sugar().Errorw("Failed to execute sql", "error", zap.Error(res.Error)) - return res.Error - } - return nil -} - -func Test_OperatorShareSnapshots(t *testing.T) { - if !rewardsTestsEnabled() { - t.Skipf("Skipping %s", t.Name()) - return - } - - projectRoot := getProjectRootPath() - dbFileName, cfg, grm, l, sink, err := setupOperatorShareSnapshot() - - if err != nil { - t.Fatal(err) - } - - snapshotDate, err := getSnapshotDate() - if err != nil { - t.Fatal(err) - } - - t.Run("Should hydrate dependency tables", func(t *testing.T) { - if _, err = hydrateAllBlocksTable(grm, l); err != nil { - t.Error(err) - } - if err = hydrateOperatorShares(grm, l); err != nil { - t.Error(err) - } - }) - t.Run("Should generate operator share snapshots", func(t *testing.T) { - sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) - rewards, _ := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) - - t.Log("Generating operator share snapshots") - err := rewards.GenerateAndInsertOperatorShareSnapshots(snapshotDate) - assert.Nil(t, err) - - snapshots, err := rewards.ListOperatorShareSnapshots() - assert.Nil(t, err) - - t.Log("Loading expected results") - expectedResults, err := tests.GetOperatorShareSnapshotsExpectedResults(projectRoot) - assert.Nil(t, err) - - assert.Equal(t, len(expectedResults), len(snapshots)) - - mappedExpectedResults := make(map[string]string) - - for _, expectedResult := range expectedResults { - slotId := fmt.Sprintf("%s_%s_%s", expectedResult.Operator, expectedResult.Strategy, expectedResult.Snapshot) - mappedExpectedResults[slotId] = expectedResult.Shares - } - - if len(expectedResults) != len(snapshots) { - t.Errorf("Expected %d snapshots, got %d", len(expectedResults), len(snapshots)) - - lacksExpectedResult := make([]*OperatorShareSnapshots, 0) - // Go line-by-line in the snapshot results and find the corresponding line in the expected results. - // If one doesnt exist, add it to the missing list. - for _, snapshot := range snapshots { - snapshotStr := snapshot.Snapshot.Format(time.DateOnly) - - slotId := fmt.Sprintf("%s_%s_%s", snapshot.Operator, snapshot.Strategy, snapshotStr) - - found, ok := mappedExpectedResults[slotId] - if !ok { - t.Logf("Record not found %+v", snapshot) - lacksExpectedResult = append(lacksExpectedResult, snapshot) - continue - } - if found != snapshot.Shares { - // t.Logf("Expected: %s, Got: %s for %+v", found, snapshot.Shares, snapshot) - lacksExpectedResult = append(lacksExpectedResult, snapshot) - } - } - assert.Equal(t, 0, len(lacksExpectedResult)) - if len(lacksExpectedResult) > 0 { - for i, window := range lacksExpectedResult { - fmt.Printf("%d - Snapshot: %+v\n", i, window) - } - } - } - }) - t.Cleanup(func() { - teardownOperatorShareSnapshot(dbFileName, cfg, grm, l) - }) -} - // Test_OperatorShareSnapshots_BasicShares tests basic operator shares without allocations func Test_OperatorShareSnapshots_BasicShares(t *testing.T) { if !rewardsTestsEnabled() { @@ -205,23 +105,23 @@ func Test_OperatorShareSnapshots_BasicShares(t *testing.T) { // T0: Operator has 1000 shares err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, "1000000000000000000000", 100).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "1000000000000000000000", "0xtx100", 0, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) // T1: Shares increase to 1500 err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, "1500000000000000000000", 200).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "1500000000000000000000", "0xtx200", 0, t1, t1.Format(time.DateOnly), 200).Error assert.Nil(t, err) // T2: Shares decrease to 800 err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, "800000000000000000000", 300).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "800000000000000000000", "0xtx300", 0, t2, t2.Format(time.DateOnly), 300).Error assert.Nil(t, err) // Generate snapshots @@ -267,7 +167,6 @@ func Test_OperatorShareSnapshots_BasicShares(t *testing.T) { }) } -// Test_OperatorShareSnapshots_WithAllocations tests operator shares combined with allocations func Test_OperatorShareSnapshots_WithAllocations(t *testing.T) { if !rewardsTestsEnabled() { t.Skipf("Skipping %s", t.Name()) @@ -281,13 +180,12 @@ func Test_OperatorShareSnapshots_WithAllocations(t *testing.T) { defer teardownOperatorShareSnapshot(dbFileName, cfg, grm, l) operator := "0xoperator2" - avs := "0xavs1" strategy := "0xstrategy2" t0 := time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC) t1 := time.Date(2024, 2, 5, 0, 0, 0, 0, time.UTC) - t.Run("Operator shares replaced by allocation magnitude", func(t *testing.T) { + t.Run("Operator shares contain raw delegated shares, not allocation magnitude", func(t *testing.T) { // Insert blocks blocks := []struct { number uint64 @@ -306,18 +204,11 @@ func Test_OperatorShareSnapshots_WithAllocations(t *testing.T) { assert.Nil(t, err) } - // T0: Operator has 1000 shares from base operator_shares table + // T0: Operator has 1000 shares from delegations err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, "1000000000000000000000", 100).Error - assert.Nil(t, err) - - // T1: Operator has allocation of 2000 (should override base shares) - err = grm.Exec(` - INSERT INTO operator_allocations (operator, avs, strategy, operator_set_id, magnitude, effective_block, block_number, transaction_hash, log_index, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) - `, operator, avs, strategy, 1, "2000000000000000000000", 200, 200, "0xtx1", 1).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "1000000000000000000000", "0xtx100", 0, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) // Generate snapshots @@ -325,14 +216,10 @@ func Test_OperatorShareSnapshots_WithAllocations(t *testing.T) { rewards, err := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) assert.Nil(t, err) - // Generate operator allocations first - err = rewards.GenerateAndInsertOperatorAllocationSnapshots(t1.Format(time.DateOnly)) - assert.Nil(t, err) - err = rewards.GenerateAndInsertOperatorShareSnapshots(t1.Format(time.DateOnly)) assert.Nil(t, err) - // Verify snapshots - should show allocation magnitude (2000) not base shares (1000) + // Verify snapshots contain raw delegated shares (1000), not allocation magnitude var snapshot struct { Shares string Snapshot time.Time @@ -345,12 +232,9 @@ func Test_OperatorShareSnapshots_WithAllocations(t *testing.T) { LIMIT 1 `, operator, strategy, t1.Format(time.DateOnly)).Scan(&snapshot).Error - if err == nil && snapshot.Shares != "" { - t.Logf("Snapshot on %s: Shares = %s (expected 2000...)", snapshot.Snapshot.Format(time.DateOnly), snapshot.Shares) - // Note: The allocation magnitude should be used when available - } else { - t.Logf("No snapshot found for %s", t1.Format(time.DateOnly)) - } + assert.Nil(t, err) + assert.Equal(t, "1000000000000000000000", snapshot.Shares, "operator_share_snapshots should contain raw delegated shares") + t.Logf("Snapshot on %s: Shares = %s (raw delegated shares)", snapshot.Snapshot.Format(time.DateOnly), snapshot.Shares) }) } @@ -372,6 +256,8 @@ func Test_OperatorShareSnapshots_MultipleStrategies(t *testing.T) { strategy2 := "0xstrategy_b" t0 := time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC) + // Use a cutoff date after t0 since snapshots round up to the next day + cutoffDate := t0.AddDate(0, 0, 2) t.Run("Operator has shares in multiple strategies", func(t *testing.T) { // Insert block @@ -384,27 +270,27 @@ func Test_OperatorShareSnapshots_MultipleStrategies(t *testing.T) { // Operator has shares in strategy1 err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy1, "500000000000000000000", 100).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy1, "500000000000000000000", "0xtx100a", 0, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) // Operator has shares in strategy2 err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy2, "700000000000000000000", 100).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy2, "700000000000000000000", "0xtx100b", 1, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) - // Generate snapshots + // Generate snapshots with cutoff date after data insertion sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) rewards, err := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) assert.Nil(t, err) - err = rewards.GenerateAndInsertOperatorAllocationSnapshots(t0.Format(time.DateOnly)) + err = rewards.GenerateAndInsertOperatorAllocationSnapshots(cutoffDate.Format(time.DateOnly)) assert.Nil(t, err) - err = rewards.GenerateAndInsertOperatorShareSnapshots(t0.Format(time.DateOnly)) + err = rewards.GenerateAndInsertOperatorShareSnapshots(cutoffDate.Format(time.DateOnly)) assert.Nil(t, err) // Verify snapshots for both strategies @@ -461,16 +347,16 @@ func Test_OperatorShareSnapshots_ZeroShares(t *testing.T) { // T0: Operator has 300 shares err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, "300000000000000000000", 100).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "300000000000000000000", "0xtx100", 0, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) // T1: Operator shares go to 0 err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, "0", 200).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "0", "0xtx200", 0, t1, t1.Format(time.DateOnly), 200).Error assert.Nil(t, err) // Generate snapshots @@ -524,6 +410,8 @@ func Test_OperatorShareSnapshots_MultipleOperators(t *testing.T) { strategy := "0xstrategy_shared" t0 := time.Date(2024, 5, 1, 0, 0, 0, 0, time.UTC) + // Use a cutoff date after t0 since snapshots round up to the next day + cutoffDate := t0.AddDate(0, 0, 2) t.Run("Multiple operators same strategy", func(t *testing.T) { // Insert block @@ -536,27 +424,27 @@ func Test_OperatorShareSnapshots_MultipleOperators(t *testing.T) { // Operator 1 has 600 shares err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator1, strategy, "600000000000000000000", 100).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator1, strategy, "600000000000000000000", "0xtx100a", 0, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) // Operator 2 has 900 shares err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator2, strategy, "900000000000000000000", 100).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator2, strategy, "900000000000000000000", "0xtx100b", 1, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) - // Generate snapshots + // Generate snapshots with cutoff date after data insertion sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) rewards, err := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) assert.Nil(t, err) - err = rewards.GenerateAndInsertOperatorAllocationSnapshots(t0.Format(time.DateOnly)) + err = rewards.GenerateAndInsertOperatorAllocationSnapshots(cutoffDate.Format(time.DateOnly)) assert.Nil(t, err) - err = rewards.GenerateAndInsertOperatorShareSnapshots(t0.Format(time.DateOnly)) + err = rewards.GenerateAndInsertOperatorShareSnapshots(cutoffDate.Format(time.DateOnly)) assert.Nil(t, err) // Verify both operators have snapshots @@ -603,7 +491,7 @@ func Test_OperatorShareSnapshots_SameDayMultipleChanges(t *testing.T) { {102, 18, "150000000000000000000"}, // 18:00 - latest } - for _, s := range shares { + for i, s := range shares { blockTime := baseDate.Add(time.Duration(s.hour) * time.Hour) err := grm.Exec(` INSERT INTO blocks (number, hash, block_time, created_at) @@ -613,9 +501,9 @@ func Test_OperatorShareSnapshots_SameDayMultipleChanges(t *testing.T) { assert.Nil(t, err) err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, s.amount, s.number).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, s.amount, fmt.Sprintf("0xtx%d", s.number), i, blockTime, blockTime.Format(time.DateOnly), s.number).Error assert.Nil(t, err) } @@ -678,9 +566,9 @@ func Test_OperatorShareSnapshots_LargeNumbers(t *testing.T) { // Very large share amount (1 billion tokens with 18 decimals) largeAmount := "1000000000000000000000000000" err = grm.Exec(` - INSERT INTO operator_shares (operator, strategy, shares, block_number) - VALUES (?, ?, ?, ?) - `, operator, strategy, largeAmount, 100).Error + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, largeAmount, "0xtx100", 0, t0, t0.Format(time.DateOnly), 100).Error assert.Nil(t, err) // Generate snapshots @@ -711,3 +599,449 @@ func Test_OperatorShareSnapshots_LargeNumbers(t *testing.T) { } }) } + +// ============================================================================= +// Queued Withdrawal Add-back Tests (Post-Sabine Fork) +// ============================================================================= + +// Test_OperatorShareSnapshots_QueuedWithdrawalAddBack tests that operator shares +// include queued withdrawal shares during the 14-day queue window. +// This mirrors the staker share snapshots behavior. +func Test_OperatorShareSnapshots_QueuedWithdrawalAddBack(t *testing.T) { + if !rewardsTestsEnabled() { + t.Skipf("Skipping %s", t.Name()) + return + } + + dbFileName, cfg, grm, l, sink, err := setupOperatorShareSnapshot() + if err != nil { + t.Fatal(err) + } + defer teardownOperatorShareSnapshot(dbFileName, cfg, grm, l) + + // Set the withdrawal queue window to 14 days (required for add-back tests) + cfg.Rewards.WithdrawalQueueWindow = 14 + + // OSS-1: Operator has shares, staker queues withdrawal + // Expected: Operator shares should include queued withdrawal during queue period + // Flow: + // 1. Operator has 1000 shares from delegation + // 2. Staker queues full withdrawal: operator_shares = 0 (immediate), queued_slashing_withdrawals created + // 3. During 14-day queue window: operator snapshots should add back queued shares (1000) + // 4. After 14 days: shares stay at 0 + t.Run("OSS-1: Queued withdrawal adds back shares during queue period", func(t *testing.T) { + operator := "0xoperator_oss1" + staker := "0xstaker_oss1" + strategy := "0xstrategy_oss1" + + day4 := time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC) + depositTime := day4.Add(17 * time.Hour) // 5pm + queueTime := day4.Add(18 * time.Hour) // 6pm - queue withdrawal same day + day5 := time.Date(2025, 1, 5, 12, 0, 0, 0, time.UTC) + day10 := time.Date(2025, 1, 10, 12, 0, 0, 0, time.UTC) // Within queue window + day20 := time.Date(2025, 1, 20, 12, 0, 0, 0, time.UTC) // After queue window (14 days from 1/4) + + block1 := uint64(1001) + block2 := uint64(1002) + + // Insert blocks + for _, b := range []struct { + number uint64 + time time.Time + }{ + {block1, depositTime}, + {block2, queueTime}, + {1003, day5}, + {1010, day10}, + {1020, day20}, + } { + err := grm.Exec(` + INSERT INTO blocks (number, hash, block_time, created_at) + VALUES (?, ?, ?, ?) + ON CONFLICT (number) DO NOTHING + `, b.number, fmt.Sprintf("0xblock%d", b.number), b.time, time.Now()).Error + assert.Nil(t, err) + } + + // T0: Operator has 1000 shares from delegation + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "1000000000000000000000", "tx_oss1_deposit", 0, depositTime, depositTime.Format("2006-01-02"), block1).Error + assert.Nil(t, err) + + // Queue withdrawal: operator_shares goes to 0 + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "0", "tx_oss1_queue", 1, queueTime, queueTime.Format("2006-01-02"), block2).Error + assert.Nil(t, err) + + // Insert queued withdrawal record - this triggers the add-back logic + err = grm.Exec(` + INSERT INTO queued_slashing_withdrawals (staker, operator, withdrawer, nonce, start_block, strategy, scaled_shares, shares_to_withdraw, withdrawal_root, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker, operator, staker, "1", block2, strategy, "1000000000000000000000", "1000000000000000000000", "root_oss1", block2, "tx_oss1_queue", 0).Error + assert.Nil(t, err) + + sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) + rewards, err := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) + assert.Nil(t, err) + + // Generate snapshots for day 10 (within queue window) + err = rewards.GenerateAndInsertOperatorShareSnapshots(day10.Format(time.DateOnly)) + assert.Nil(t, err) + + // Verify operator has shares during queue window (add-back) + var snapshotDay10 struct { + Shares string + } + err = grm.Raw(` + SELECT shares + FROM operator_share_snapshots + WHERE operator = ? AND strategy = ? AND snapshot = ? + `, operator, strategy, day5.Format(time.DateOnly)).Scan(&snapshotDay10).Error + + if err == nil && snapshotDay10.Shares != "" { + t.Logf("Day 5 snapshot (within queue): shares = %s", snapshotDay10.Shares) + // Should have 1000 shares (0 base + 1000 add-back) + assert.Equal(t, "1000000000000000000000", snapshotDay10.Shares, "Should add back queued withdrawal during queue period") + } + + // Generate snapshots for day 20 (after queue window) + err = rewards.GenerateAndInsertOperatorShareSnapshots(day20.Format(time.DateOnly)) + assert.Nil(t, err) + + // Verify after queue window, shares are 0 (no add-back) + var snapshotDay20 struct { + Shares string + } + err = grm.Raw(` + SELECT shares + FROM operator_share_snapshots + WHERE operator = ? AND strategy = ? AND snapshot = ? + `, operator, strategy, day20.Format(time.DateOnly)).Scan(&snapshotDay20).Error + + if err == nil && snapshotDay20.Shares != "" { + t.Logf("Day 20 snapshot (after queue): shares = %s", snapshotDay20.Shares) + // Should have 0 shares (queue expired) + assert.Equal(t, "0", snapshotDay20.Shares, "Should not add back after queue expires") + } + }) +} + +// Test_OperatorShareSnapshots_QueuedWithdrawalWithSlashing tests that slashing +// adjustments are applied to the queued withdrawal add-back. +func Test_OperatorShareSnapshots_QueuedWithdrawalWithSlashing(t *testing.T) { + if !rewardsTestsEnabled() { + t.Skipf("Skipping %s", t.Name()) + return + } + + dbFileName, cfg, grm, l, sink, err := setupOperatorShareSnapshot() + if err != nil { + t.Fatal(err) + } + defer teardownOperatorShareSnapshot(dbFileName, cfg, grm, l) + + // Set the withdrawal queue window to 14 days (required for add-back tests) + cfg.Rewards.WithdrawalQueueWindow = 14 + + // OSS-2: Staker queues withdrawal, then operator is slashed + // Expected: Add-back should be reduced by slash multiplier + // Flow: + // 1. Operator has 1000 shares + // 2. Staker queues full withdrawal on 1/5 + // 3. Operator is slashed 50% on 1/6 + // 4. Snapshots after 1/6 should show: 0 base + 500 add-back (1000 * 0.5) + t.Run("OSS-2: Queued withdrawal with slashing adjustment", func(t *testing.T) { + operator := "0xoperator_oss2" + staker := "0xstaker_oss2" + strategy := "0xstrategy_oss2" + + day4 := time.Date(2025, 2, 4, 17, 0, 0, 0, time.UTC) + day5 := time.Date(2025, 2, 5, 18, 0, 0, 0, time.UTC) // Queue withdrawal + day6 := time.Date(2025, 2, 6, 18, 0, 0, 0, time.UTC) // Slash + day10 := time.Date(2025, 2, 10, 12, 0, 0, 0, time.UTC) // Check snapshot + + block4 := uint64(2001) + block5 := uint64(2002) + block6 := uint64(2003) + + // Insert blocks + for _, b := range []struct { + number uint64 + time time.Time + }{ + {block4, day4}, + {block5, day5}, + {block6, day6}, + {2010, day10}, + } { + err := grm.Exec(` + INSERT INTO blocks (number, hash, block_time, created_at) + VALUES (?, ?, ?, ?) + ON CONFLICT (number) DO NOTHING + `, b.number, fmt.Sprintf("0xblock%d", b.number), b.time, time.Now()).Error + assert.Nil(t, err) + } + + // Day 4: Operator has 1000 shares + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "1000000000000000000000", "tx_oss2_deposit", 0, day4, day4.Format("2006-01-02"), block4).Error + assert.Nil(t, err) + + // Day 5: Queue withdrawal - operator_shares goes to 0 + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "0", "tx_oss2_queue", 0, day5, day5.Format("2006-01-02"), block5).Error + assert.Nil(t, err) + + // Insert queued withdrawal record + err = grm.Exec(` + INSERT INTO queued_slashing_withdrawals (staker, operator, withdrawer, nonce, start_block, strategy, scaled_shares, shares_to_withdraw, withdrawal_root, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker, operator, staker, "1", block5, strategy, "1000000000000000000000", "1000000000000000000000", "root_oss2", block5, "tx_oss2_queue", 0).Error + assert.Nil(t, err) + + // Day 6: Slash 50% - insert slashing adjustment with multiplier 0.5 + err = grm.Exec(` + INSERT INTO queued_withdrawal_slashing_adjustments ( + staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, + slash_block_number, slash_multiplier, block_number, transaction_hash, log_index + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker, strategy, operator, block5, 0, block6, "0.5", block6, "tx_oss2_slash", 0).Error + assert.Nil(t, err) + + sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) + rewards, err := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) + assert.Nil(t, err) + + // Generate snapshots for day 10 (after slash, within queue window) + err = rewards.GenerateAndInsertOperatorShareSnapshots(day10.Format(time.DateOnly)) + assert.Nil(t, err) + + // Verify operator has 500 shares (1000 * 0.5 slash multiplier) + var snapshot struct { + Shares string + } + // Check a snapshot date after the slash (day 7 or later) + day7 := time.Date(2025, 2, 7, 0, 0, 0, 0, time.UTC) + err = grm.Raw(` + SELECT shares + FROM operator_share_snapshots + WHERE operator = ? AND strategy = ? AND snapshot = ? + `, operator, strategy, day7.Format(time.DateOnly)).Scan(&snapshot).Error + + if err == nil && snapshot.Shares != "" { + t.Logf("Day 7 snapshot (after slash): shares = %s", snapshot.Shares) + // Should have 500 shares (0 base + 1000 * 0.5 add-back) + assert.Equal(t, "500000000000000000000.0", snapshot.Shares, "Should apply slash multiplier to add-back") + } + }) +} + +// Test_OperatorShareSnapshots_PartialQueuedWithdrawal tests partial withdrawal add-back +func Test_OperatorShareSnapshots_PartialQueuedWithdrawal(t *testing.T) { + if !rewardsTestsEnabled() { + t.Skipf("Skipping %s", t.Name()) + return + } + + dbFileName, cfg, grm, l, sink, err := setupOperatorShareSnapshot() + if err != nil { + t.Fatal(err) + } + defer teardownOperatorShareSnapshot(dbFileName, cfg, grm, l) + + // Set the withdrawal queue window to 14 days (required for add-back tests) + cfg.Rewards.WithdrawalQueueWindow = 14 + + // OSS-3: Staker queues partial withdrawal + // Expected: Operator should have base shares + partial add-back during queue period + // Flow: + // 1. Operator has 1000 shares + // 2. Staker queues 400 shares withdrawal: operator_shares = 600 + // 3. During queue window: snapshot should show 1000 (600 base + 400 add-back) + t.Run("OSS-3: Partial queued withdrawal add-back", func(t *testing.T) { + operator := "0xoperator_oss3" + staker := "0xstaker_oss3" + strategy := "0xstrategy_oss3" + + day4 := time.Date(2025, 3, 4, 17, 0, 0, 0, time.UTC) + day5 := time.Date(2025, 3, 5, 18, 0, 0, 0, time.UTC) // Queue partial withdrawal + day10 := time.Date(2025, 3, 10, 12, 0, 0, 0, time.UTC) // Check snapshot + + block4 := uint64(3001) + block5 := uint64(3002) + + // Insert blocks + for _, b := range []struct { + number uint64 + time time.Time + }{ + {block4, day4}, + {block5, day5}, + {3010, day10}, + } { + err := grm.Exec(` + INSERT INTO blocks (number, hash, block_time, created_at) + VALUES (?, ?, ?, ?) + ON CONFLICT (number) DO NOTHING + `, b.number, fmt.Sprintf("0xblock%d", b.number), b.time, time.Now()).Error + assert.Nil(t, err) + } + + // Day 4: Operator has 1000 shares + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "1000000000000000000000", "tx_oss3_deposit", 0, day4, day4.Format("2006-01-02"), block4).Error + assert.Nil(t, err) + + // Day 5: Queue partial withdrawal - operator_shares goes to 600 + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "600000000000000000000", "tx_oss3_queue", 0, day5, day5.Format("2006-01-02"), block5).Error + assert.Nil(t, err) + + // Insert queued withdrawal record for 400 shares + err = grm.Exec(` + INSERT INTO queued_slashing_withdrawals (staker, operator, withdrawer, nonce, start_block, strategy, scaled_shares, shares_to_withdraw, withdrawal_root, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker, operator, staker, "1", block5, strategy, "400000000000000000000", "400000000000000000000", "root_oss3", block5, "tx_oss3_queue", 0).Error + assert.Nil(t, err) + + sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) + rewards, err := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) + assert.Nil(t, err) + + // Generate snapshots for day 10 (within queue window) + err = rewards.GenerateAndInsertOperatorShareSnapshots(day10.Format(time.DateOnly)) + assert.Nil(t, err) + + // Verify operator has 1000 shares (600 base + 400 add-back) + var snapshot struct { + Shares string + } + day6 := time.Date(2025, 3, 6, 0, 0, 0, 0, time.UTC) + err = grm.Raw(` + SELECT shares + FROM operator_share_snapshots + WHERE operator = ? AND strategy = ? AND snapshot = ? + `, operator, strategy, day6.Format(time.DateOnly)).Scan(&snapshot).Error + + if err == nil && snapshot.Shares != "" { + t.Logf("Day 6 snapshot (partial queue): shares = %s", snapshot.Shares) + // Should have 1000 shares (600 base + 400 add-back) + assert.Equal(t, "1000000000000000000000", snapshot.Shares, "Should add back partial queued withdrawal") + } + }) +} + +// Test_OperatorShareSnapshots_MultipleStakersQueuedWithdrawals tests multiple stakers +// queuing withdrawals from the same operator +func Test_OperatorShareSnapshots_MultipleStakersQueuedWithdrawals(t *testing.T) { + if !rewardsTestsEnabled() { + t.Skipf("Skipping %s", t.Name()) + return + } + + dbFileName, cfg, grm, l, sink, err := setupOperatorShareSnapshot() + if err != nil { + t.Fatal(err) + } + defer teardownOperatorShareSnapshot(dbFileName, cfg, grm, l) + + // Set the withdrawal queue window to 14 days (required for add-back tests) + cfg.Rewards.WithdrawalQueueWindow = 14 + + // OSS-4: Multiple stakers queue withdrawals from same operator + // Expected: Operator shares should include sum of all queued withdrawals + t.Run("OSS-4: Multiple stakers queue withdrawals", func(t *testing.T) { + operator := "0xoperator_oss4" + staker1 := "0xstaker_oss4a" + staker2 := "0xstaker_oss4b" + strategy := "0xstrategy_oss4" + + day4 := time.Date(2025, 4, 4, 17, 0, 0, 0, time.UTC) + day5 := time.Date(2025, 4, 5, 18, 0, 0, 0, time.UTC) + day10 := time.Date(2025, 4, 10, 12, 0, 0, 0, time.UTC) + + block4 := uint64(4001) + block5 := uint64(4002) + + // Insert blocks + for _, b := range []struct { + number uint64 + time time.Time + }{ + {block4, day4}, + {block5, day5}, + {4010, day10}, + } { + err := grm.Exec(` + INSERT INTO blocks (number, hash, block_time, created_at) + VALUES (?, ?, ?, ?) + ON CONFLICT (number) DO NOTHING + `, b.number, fmt.Sprintf("0xblock%d", b.number), b.time, time.Now()).Error + assert.Nil(t, err) + } + + // Day 4: Operator has 1000 shares (500 from staker1, 500 from staker2) + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "1000000000000000000000", "tx_oss4_deposit", 0, day4, day4.Format("2006-01-02"), block4).Error + assert.Nil(t, err) + + // Day 5: Both stakers queue full withdrawals - operator_shares goes to 0 + err = grm.Exec(` + INSERT INTO operator_shares (operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, operator, strategy, "0", "tx_oss4_queue", 0, day5, day5.Format("2006-01-02"), block5).Error + assert.Nil(t, err) + + // Insert queued withdrawal records for both stakers + err = grm.Exec(` + INSERT INTO queued_slashing_withdrawals (staker, operator, withdrawer, nonce, start_block, strategy, scaled_shares, shares_to_withdraw, withdrawal_root, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker1, operator, staker1, "1", block5, strategy, "500000000000000000000", "500000000000000000000", "root_oss4a", block5, "tx_oss4_queue", 0).Error + assert.Nil(t, err) + + err = grm.Exec(` + INSERT INTO queued_slashing_withdrawals (staker, operator, withdrawer, nonce, start_block, strategy, scaled_shares, shares_to_withdraw, withdrawal_root, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, staker2, operator, staker2, "2", block5, strategy, "500000000000000000000", "500000000000000000000", "root_oss4b", block5, "tx_oss4_queue", 1).Error + assert.Nil(t, err) + + sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) + rewards, err := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) + assert.Nil(t, err) + + // Generate snapshots for day 10 (within queue window) + err = rewards.GenerateAndInsertOperatorShareSnapshots(day10.Format(time.DateOnly)) + assert.Nil(t, err) + + // Verify operator has 1000 shares (0 base + 500 + 500 add-back) + var snapshot struct { + Shares string + } + day6 := time.Date(2025, 4, 6, 0, 0, 0, 0, time.UTC) + err = grm.Raw(` + SELECT shares + FROM operator_share_snapshots + WHERE operator = ? AND strategy = ? AND snapshot = ? + `, operator, strategy, day6.Format(time.DateOnly)).Scan(&snapshot).Error + + if err == nil && snapshot.Shares != "" { + t.Logf("Day 6 snapshot (multiple stakers): shares = %s", snapshot.Shares) + // Should have 1000 shares (0 base + 500 + 500 add-back from both stakers) + assert.Equal(t, "1000000000000000000000", snapshot.Shares, "Should add back all stakers' queued withdrawals") + } + }) +} diff --git a/pkg/rewards/rewards.go b/pkg/rewards/rewards.go index 1442f5255..c72dabf7f 100644 --- a/pkg/rewards/rewards.go +++ b/pkg/rewards/rewards.go @@ -744,11 +744,12 @@ func (rc *RewardsCalculator) generateSnapshotData(snapshotDate string) error { // ------------------------------------------------------------------------ // Rewards V2.2 snapshots (Unique Stake) // ------------------------------------------------------------------------ - if err = rc.GenerateAndInsertOperatorAllocationSnapshots(snapshotDate); err != nil { - rc.logger.Sugar().Errorw("Failed to generate operator allocation snapshots", "error", err) + + if err := rc.GenerateAndInsertStakeOperatorSetRewards(snapshotDate); err != nil { + rc.logger.Sugar().Errorw("Failed to generate stake operator set rewards", "error", err) return err } - rc.logger.Sugar().Debugw("Generated operator allocation snapshots") + rc.logger.Sugar().Debugw("Generated stake operator set rewards") return nil } @@ -835,47 +836,51 @@ func (rc *RewardsCalculator) generateGoldTables(snapshotDate string) error { return err } - if err := rc.GenerateGold15OperatorOperatorSetUniqueStakeRewardsTable(snapshotDate); err != nil { + // ------------------------------------------------------------------------ + // Rewards V2.2 - Unique and Total Stake Rewards + // ------------------------------------------------------------------------ + + if err := rc.GenerateGold15ActiveUniqueAndTotalStakeRewardsTable(snapshotDate); err != nil { + rc.logger.Sugar().Errorw("Failed to generate v2.2 active unique and total stake rewards", "error", err) + return err + } + + if err := rc.GenerateGold16OperatorOperatorSetUniqueStakeRewardsTable(snapshotDate); err != nil { rc.logger.Sugar().Errorw("Failed to generate v2.2 operator unique stake rewards", "error", err) return err } - if err := rc.GenerateGold16StakerOperatorSetUniqueStakeRewardsTable(snapshotDate); err != nil { + if err := rc.GenerateGold17StakerOperatorSetUniqueStakeRewardsTable(snapshotDate); err != nil { rc.logger.Sugar().Errorw("Failed to generate v2.2 staker unique stake rewards", "error", err) return err } - if err := rc.GenerateGold17AvsOperatorSetUniqueStakeRewardsTable(snapshotDate, forks); err != nil { + if err := rc.GenerateGold18AvsOperatorSetUniqueStakeRewardsTable(snapshotDate, forks); err != nil { rc.logger.Sugar().Errorw("Failed to generate v2.2 avs unique stake rewards", "error", err) return err } - // ------------------------------------------------------------------------ - // Rewards V2.2 - Total Stake Weighted Rewards - // Each function internally checks if it should run based on v2.2 config - // ------------------------------------------------------------------------ - - if err := rc.GenerateGold18OperatorOperatorSetTotalStakeRewardsTable(snapshotDate); err != nil { + if err := rc.GenerateGold19OperatorOperatorSetTotalStakeRewardsTable(snapshotDate); err != nil { rc.logger.Sugar().Errorw("Failed to generate v2.2 operator total stake rewards", "error", err) return err } - if err := rc.GenerateGold19StakerOperatorSetTotalStakeRewardsTable(snapshotDate); err != nil { + if err := rc.GenerateGold20StakerOperatorSetTotalStakeRewardsTable(snapshotDate); err != nil { rc.logger.Sugar().Errorw("Failed to generate v2.2 staker total stake rewards", "error", err) return err } - if err := rc.GenerateGold20AvsOperatorSetTotalStakeRewardsTable(snapshotDate, forks); err != nil { + if err := rc.GenerateGold21AvsOperatorSetTotalStakeRewardsTable(snapshotDate, forks); err != nil { rc.logger.Sugar().Errorw("Failed to generate v2.2 avs total stake rewards", "error", err) return err } - if err := rc.GenerateGold21StagingTable(snapshotDate); err != nil { + if err := rc.GenerateGold22StagingTable(snapshotDate); err != nil { rc.logger.Sugar().Errorw("Failed to generate gold staging", "error", err) return err } - if err := rc.GenerateGold22FinalTable(snapshotDate); err != nil { + if err := rc.GenerateGold23FinalTable(snapshotDate); err != nil { rc.logger.Sugar().Errorw("Failed to generate final table", "error", err) return err } diff --git a/pkg/rewards/rewardsV2_1_test.go b/pkg/rewards/rewardsV2_1_test.go index 3550125c1..82a88421a 100644 --- a/pkg/rewards/rewardsV2_1_test.go +++ b/pkg/rewards/rewardsV2_1_test.go @@ -252,16 +252,16 @@ func Test_RewardsV2_1(t *testing.T) { } testStart = time.Now() - fmt.Printf("Running gold_15_staging\n") - err = rc.GenerateGold21StagingTable(snapshotDate) + fmt.Printf("Running gold_22_staging\n") + err = rc.GenerateGold22StagingTable(snapshotDate) assert.Nil(t, err) - rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_21_GoldStaging]) + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_22_GoldStaging]) assert.Nil(t, err) fmt.Printf("\tRows in gold_15_staging: %v - [time: %v]\n", rows, time.Since(testStart)) testStart = time.Now() fmt.Printf("Running gold_final_table\n") - err = rc.GenerateGold22FinalTable(snapshotDate) + err = rc.GenerateGold23FinalTable(snapshotDate) assert.Nil(t, err) rows, err = getRowCountForTable(grm, "gold_table") assert.Nil(t, err) diff --git a/pkg/rewards/rewardsV2_2_test.go b/pkg/rewards/rewardsV2_2_test.go index e2ca36592..8f8591ae3 100644 --- a/pkg/rewards/rewardsV2_2_test.go +++ b/pkg/rewards/rewardsV2_2_test.go @@ -245,53 +245,109 @@ func Test_RewardsV2_2(t *testing.T) { testStart = time.Now() // ------------------------------------------------------------------------ - // Rewards V2.2 (Unique Stake) + // Rewards V2.2 (Stake-based Unique/Total Stake) // ------------------------------------------------------------------------ rewardsV2_2Enabled, err := cfg.IsRewardsV2_2EnabledForCutoffDate(snapshotDate) assert.Nil(t, err) assert.True(t, rewardsV2_2Enabled, "v2.2 should be enabled for this test") - fmt.Printf("Running gold_15_operator_od_operator_set_rewards_v2_2\n") - err = rc.GenerateGold15OperatorOperatorSetUniqueStakeRewardsTable(snapshotDate) + // Generate stake operator set rewards (denormalized table) + fmt.Printf("Running stake_operator_set_rewards\n") + err = rc.GenerateAndInsertStakeOperatorSetRewards(snapshotDate) assert.Nil(t, err) if rewardsV2_2Enabled { - rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_15_OperatorOperatorSetUniqueStakeRewards]) + rows, err = getRowCountForTable(grm, "stake_operator_set_rewards") assert.Nil(t, err) - fmt.Printf("\tRows in gold_15_operator_od_operator_set_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + fmt.Printf("\tRows in stake_operator_set_rewards: %v - [time: %v]\n", rows, time.Since(testStart)) } testStart = time.Now() - fmt.Printf("Running gold_16_staker_od_operator_set_rewards_v2_2\n") - err = rc.GenerateGold16StakerOperatorSetUniqueStakeRewardsTable(snapshotDate) + // Table 15: Active Unique and Total Stake Rewards (source for unique/total stake calculations) + fmt.Printf("Running gold_15_active_unique_and_total_stake_rewards\n") + err = rc.GenerateGold15ActiveUniqueAndTotalStakeRewardsTable(snapshotDate) assert.Nil(t, err) if rewardsV2_2Enabled { - rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_16_StakerOperatorSetUniqueStakeRewards]) + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_15_ActiveUniqueAndTotalStakeRewards]) assert.Nil(t, err) - fmt.Printf("\tRows in gold_16_staker_od_operator_set_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + fmt.Printf("\tRows in gold_15_active_unique_and_total_stake_rewards: %v - [time: %v]\n", rows, time.Since(testStart)) } testStart = time.Now() - fmt.Printf("Running gold_17_avs_od_operator_set_rewards_v2_2\n") - err = rc.GenerateGold17AvsOperatorSetUniqueStakeRewardsTable(snapshotDate, forks) + fmt.Printf("Running gold_16_operator_od_operator_set_rewards_v2_2\n") + err = rc.GenerateGold16OperatorOperatorSetUniqueStakeRewardsTable(snapshotDate) assert.Nil(t, err) if rewardsV2_2Enabled { - rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_17_AvsOperatorSetUniqueStakeRewards]) + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_16_OperatorOperatorSetUniqueStakeRewards]) assert.Nil(t, err) - fmt.Printf("\tRows in gold_17_avs_od_operator_set_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + fmt.Printf("\tRows in gold_16_operator_od_operator_set_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) } testStart = time.Now() - fmt.Printf("Running gold_18_staging\n") - err = rc.GenerateGold21StagingTable(snapshotDate) + fmt.Printf("Running gold_17_staker_od_operator_set_rewards_v2_2\n") + err = rc.GenerateGold17StakerOperatorSetUniqueStakeRewardsTable(snapshotDate) assert.Nil(t, err) - rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_21_GoldStaging]) + if rewardsV2_2Enabled { + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_17_StakerOperatorSetUniqueStakeRewards]) + assert.Nil(t, err) + fmt.Printf("\tRows in gold_17_staker_od_operator_set_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + } + testStart = time.Now() + + fmt.Printf("Running gold_18_avs_od_operator_set_rewards_v2_2\n") + err = rc.GenerateGold18AvsOperatorSetUniqueStakeRewardsTable(snapshotDate, forks) + assert.Nil(t, err) + if rewardsV2_2Enabled { + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_18_AvsOperatorSetUniqueStakeRewards]) + assert.Nil(t, err) + fmt.Printf("\tRows in gold_18_avs_od_operator_set_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + } + testStart = time.Now() + + // ------------------------------------------------------------------------ + // Rewards V2.2 (Total Stake) + // ------------------------------------------------------------------------ + + fmt.Printf("Running gold_19_operator_total_stake_rewards_v2_2\n") + err = rc.GenerateGold19OperatorOperatorSetTotalStakeRewardsTable(snapshotDate) + assert.Nil(t, err) + if rewardsV2_2Enabled { + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_19_OperatorOperatorSetTotalStakeRewards]) + assert.Nil(t, err) + fmt.Printf("\tRows in gold_19_operator_total_stake_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + } + testStart = time.Now() + + fmt.Printf("Running gold_20_staker_total_stake_rewards_v2_2\n") + err = rc.GenerateGold20StakerOperatorSetTotalStakeRewardsTable(snapshotDate) + assert.Nil(t, err) + if rewardsV2_2Enabled { + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_20_StakerOperatorSetTotalStakeRewards]) + assert.Nil(t, err) + fmt.Printf("\tRows in gold_20_staker_total_stake_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + } + testStart = time.Now() + + fmt.Printf("Running gold_21_avs_total_stake_rewards_v2_2\n") + err = rc.GenerateGold21AvsOperatorSetTotalStakeRewardsTable(snapshotDate, forks) + assert.Nil(t, err) + if rewardsV2_2Enabled { + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_21_AvsOperatorSetTotalStakeRewards]) + assert.Nil(t, err) + fmt.Printf("\tRows in gold_21_avs_total_stake_rewards_v2_2: %v - [time: %v]\n", rows, time.Since(testStart)) + } + testStart = time.Now() + + fmt.Printf("Running gold_22_staging\n") + err = rc.GenerateGold22StagingTable(snapshotDate) + assert.Nil(t, err) + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_22_GoldStaging]) assert.Nil(t, err) fmt.Printf("\tRows in gold_18_staging: %v - [time: %v]\n", rows, time.Since(testStart)) testStart = time.Now() fmt.Printf("Running gold_final_table\n") - err = rc.GenerateGold22FinalTable(snapshotDate) + err = rc.GenerateGold23FinalTable(snapshotDate) assert.Nil(t, err) rows, err = getRowCountForTable(grm, "gold_table") assert.Nil(t, err) diff --git a/pkg/rewards/rewardsV2_test.go b/pkg/rewards/rewardsV2_test.go index ecfa5de4f..c4b72be17 100644 --- a/pkg/rewards/rewardsV2_test.go +++ b/pkg/rewards/rewardsV2_test.go @@ -239,16 +239,16 @@ func Test_RewardsV2(t *testing.T) { } testStart = time.Now() - fmt.Printf("Running gold_15_staging\n") - err = rc.GenerateGold21StagingTable(snapshotDate) + fmt.Printf("Running gold_22_staging\n") + err = rc.GenerateGold22StagingTable(snapshotDate) assert.Nil(t, err) - rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_21_GoldStaging]) + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_22_GoldStaging]) assert.Nil(t, err) fmt.Printf("\tRows in gold_15_staging: %v - [time: %v]\n", rows, time.Since(testStart)) testStart = time.Now() fmt.Printf("Running gold_final_table\n") - err = rc.GenerateGold22FinalTable(snapshotDate) + err = rc.GenerateGold23FinalTable(snapshotDate) assert.Nil(t, err) rows, err = getRowCountForTable(grm, "gold_table") assert.Nil(t, err) diff --git a/pkg/rewards/rewards_test.go b/pkg/rewards/rewards_test.go index 45468cda6..8a0753c28 100644 --- a/pkg/rewards/rewards_test.go +++ b/pkg/rewards/rewards_test.go @@ -396,16 +396,16 @@ func Test_Rewards(t *testing.T) { } testStart = time.Now() - fmt.Printf("Running gold_15_staging\n") - err = rc.GenerateGold21StagingTable(snapshotDate) + fmt.Printf("Running gold_22_staging\n") + err = rc.GenerateGold22StagingTable(snapshotDate) assert.Nil(t, err) - rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_21_GoldStaging]) + rows, err = getRowCountForTable(grm, goldTableNames[rewardsUtils.Table_22_GoldStaging]) assert.Nil(t, err) fmt.Printf("\tRows in gold_15_staging: %v - [time: %v]\n", rows, time.Since(testStart)) testStart = time.Now() fmt.Printf("Running gold_final_table\n") - err = rc.GenerateGold22FinalTable(snapshotDate) + err = rc.GenerateGold23FinalTable(snapshotDate) assert.Nil(t, err) rows, err = getRowCountForTable(grm, "gold_table") assert.Nil(t, err) diff --git a/pkg/rewards/stakeOperatorSetRewards.go b/pkg/rewards/stakeOperatorSetRewards.go new file mode 100644 index 000000000..b1cc9d044 --- /dev/null +++ b/pkg/rewards/stakeOperatorSetRewards.go @@ -0,0 +1,84 @@ +package rewards + +import ( + "github.com/Layr-Labs/sidecar/pkg/rewardsUtils" + "go.uber.org/zap" +) + +const stakeOperatorSetRewardsQuery = ` +-- Insert unique stake submissions +INSERT INTO stake_operator_set_rewards (avs, operator_set_id, reward_hash, token, amount, strategy, strategy_index, multiplier, start_timestamp, end_timestamp, duration, reward_type, block_number, block_time, block_date) +SELECT + usrs.avs, + usrs.operator_set_id, + usrs.reward_hash, + usrs.token, + usrs.amount, + usrs.strategy, + usrs.strategy_index, + usrs.multiplier, + usrs.start_timestamp::TIMESTAMP(6), + usrs.end_timestamp::TIMESTAMP(6), + usrs.duration, + 'unique_stake' as reward_type, + usrs.block_number, + b.block_time::TIMESTAMP(6), + TO_CHAR(b.block_time, 'YYYY-MM-DD') AS block_date +FROM unique_stake_reward_submissions AS usrs +JOIN blocks AS b ON (b.number = usrs.block_number) +WHERE b.block_time < TIMESTAMP '{{.cutoffDate}}' +ON CONFLICT ON CONSTRAINT uniq_stake_operator_set_rewards DO NOTHING; + +-- Insert total stake submissions +INSERT INTO stake_operator_set_rewards (avs, operator_set_id, reward_hash, token, amount, strategy, strategy_index, multiplier, start_timestamp, end_timestamp, duration, reward_type, block_number, block_time, block_date) +SELECT + tsrs.avs, + tsrs.operator_set_id, + tsrs.reward_hash, + tsrs.token, + tsrs.amount, + tsrs.strategy, + tsrs.strategy_index, + tsrs.multiplier, + tsrs.start_timestamp::TIMESTAMP(6), + tsrs.end_timestamp::TIMESTAMP(6), + tsrs.duration, + 'total_stake' as reward_type, + tsrs.block_number, + b.block_time::TIMESTAMP(6), + TO_CHAR(b.block_time, 'YYYY-MM-DD') AS block_date +FROM total_stake_reward_submissions AS tsrs +JOIN blocks AS b ON (b.number = tsrs.block_number) +WHERE b.block_time < TIMESTAMP '{{.cutoffDate}}' +ON CONFLICT ON CONSTRAINT uniq_stake_operator_set_rewards DO NOTHING; +` + +func (r *RewardsCalculator) GenerateAndInsertStakeOperatorSetRewards(snapshotDate string) error { + query, err := rewardsUtils.RenderQueryTemplate(stakeOperatorSetRewardsQuery, map[string]interface{}{ + "cutoffDate": snapshotDate, + }) + if err != nil { + r.logger.Sugar().Errorw("Failed to render stake_operator_set_rewards query", "error", err) + return err + } + + res := r.grm.Exec(query) + if res.Error != nil { + r.logger.Sugar().Errorw("Failed to generate stake_operator_set_rewards", + zap.Error(res.Error), + zap.String("snapshotDate", snapshotDate), + ) + return res.Error + } + return nil +} + +func (rc *RewardsCalculator) ListStakeOperatorSetRewards() ([]*StakeOperatorSetRewards, error) { + var stakeOperatorSetRewards []*StakeOperatorSetRewards + res := rc.grm.Model(&StakeOperatorSetRewards{}).Find(&stakeOperatorSetRewards) + if res.Error != nil { + rc.logger.Sugar().Errorw("Failed to list stake operator set rewards", "error", res.Error) + return nil, res.Error + } + return stakeOperatorSetRewards, nil +} diff --git a/pkg/rewards/stakeOperatorSetRewards_test.go b/pkg/rewards/stakeOperatorSetRewards_test.go new file mode 100644 index 000000000..bc71cd72d --- /dev/null +++ b/pkg/rewards/stakeOperatorSetRewards_test.go @@ -0,0 +1,401 @@ +package rewards + +import ( + "math/big" + "strings" + "testing" + "time" + + "github.com/Layr-Labs/sidecar/internal/config" + "github.com/Layr-Labs/sidecar/internal/tests" + "github.com/Layr-Labs/sidecar/pkg/eigenState/totalStakeRewardSubmissions" + "github.com/Layr-Labs/sidecar/pkg/eigenState/uniqueStakeRewardSubmissions" + "github.com/Layr-Labs/sidecar/pkg/logger" + "github.com/Layr-Labs/sidecar/pkg/metrics" + "github.com/Layr-Labs/sidecar/pkg/postgres" + "github.com/Layr-Labs/sidecar/pkg/rewards/stakerOperators" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func setupStakeOperatorSetRewards() ( + string, + *config.Config, + *gorm.DB, + *zap.Logger, + *metrics.MetricsSink, + error, +) { + cfg := tests.GetConfig() + cfg.DatabaseConfig = *tests.GetDbConfigFromEnv() + + l, _ := logger.NewLogger(&logger.LoggerConfig{Debug: cfg.Debug}) + + sink, _ := metrics.NewMetricsSink(&metrics.MetricsSinkConfig{}, nil) + + dbname, _, grm, err := postgres.GetTestPostgresDatabase(cfg.DatabaseConfig, cfg, l) + if err != nil { + return dbname, nil, nil, nil, nil, err + } + + return dbname, cfg, grm, l, sink, nil +} + +func teardownStakeOperatorSetRewards(dbname string, cfg *config.Config, db *gorm.DB, l *zap.Logger) { + rawDb, _ := db.DB() + _ = rawDb.Close() + + pgConfig := postgres.PostgresConfigFromDbConfig(&cfg.DatabaseConfig) + + if err := postgres.DeleteTestDatabase(pgConfig, dbname); err != nil { + l.Sugar().Errorw("Failed to delete test database", "error", err) + } +} + +func hydrateUniqueStakeRewardSubmissionsTable(grm *gorm.DB, l *zap.Logger) error { + startTime := time.Unix(1725494400, 0) + endTime := time.Unix(1725494400+2419200, 0) + + reward := uniqueStakeRewardSubmissions.UniqueStakeRewardSubmission{ + Avs: "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", + OperatorSetId: 1, + RewardHash: "0x7402669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc66a9", + Token: "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24", + Amount: "100000000000000000000000", + Strategy: "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", + StrategyIndex: 0, + Multiplier: "1000000000000000000", + StartTimestamp: &startTime, + EndTimestamp: &endTime, + Duration: 2419200, + BlockNumber: 1477020, + TransactionHash: "unique_stake_hash", + LogIndex: 12, + } + + result := grm.Create(&reward) + if result.Error != nil { + l.Sugar().Errorw("Failed to create unique stake reward submission", "error", result.Error) + return result.Error + } + return nil +} + +func hydrateTotalStakeRewardSubmissionsTable(grm *gorm.DB, l *zap.Logger) error { + startTime := time.Unix(1725494400, 0) + endTime := time.Unix(1725494400+604800, 0) + + reward := totalStakeRewardSubmissions.TotalStakeRewardSubmission{ + Avs: "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", + OperatorSetId: 2, + RewardHash: "0x8502669fb2c8a0cfe8108acb8a0070257c77ec6906ecb07d97c38e8a5ddc77b0", + Token: "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24", + Amount: "200000000000000000000000", + Strategy: "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c", + StrategyIndex: 0, + Multiplier: "1500000000000000000", + StartTimestamp: &startTime, + EndTimestamp: &endTime, + Duration: 604800, + BlockNumber: 1477020, + TransactionHash: "total_stake_hash", + LogIndex: 15, + } + + result := grm.Create(&reward) + if result.Error != nil { + l.Sugar().Errorw("Failed to create total stake reward submission", "error", result.Error) + return result.Error + } + return nil +} + +func Test_StakeOperatorSetRewards(t *testing.T) { + if !rewardsTestsEnabled() { + t.Skipf("Skipping %s", t.Name()) + return + } + + dbFileName, cfg, grm, l, sink, err := setupStakeOperatorSetRewards() + + if err != nil { + t.Fatal(err) + } + + snapshotDate := "2024-12-09" + + t.Run("Should hydrate blocks and stake reward submissions tables", func(t *testing.T) { + t.Log("Hydrating blocks") + totalBlockCount, err := hydrateRewardsV2Blocks(grm, l) + if err != nil { + t.Fatal(err) + } + + query := "select count(*) from blocks" + var count int + res := grm.Raw(query).Scan(&count) + assert.Nil(t, res.Error) + assert.Equal(t, totalBlockCount, count) + + t.Log("Hydrating unique stake reward submissions") + err = hydrateUniqueStakeRewardSubmissionsTable(grm, l) + if err != nil { + t.Fatal(err) + } + + query = "select count(*) from unique_stake_reward_submissions" + res = grm.Raw(query).Scan(&count) + assert.Nil(t, res.Error) + assert.Equal(t, 1, count) + + t.Log("Hydrating total stake reward submissions") + err = hydrateTotalStakeRewardSubmissionsTable(grm, l) + if err != nil { + t.Fatal(err) + } + + query = "select count(*) from total_stake_reward_submissions" + res = grm.Raw(query).Scan(&count) + assert.Nil(t, res.Error) + assert.Equal(t, 1, count) + }) + + t.Run("Should generate the proper stakeOperatorSetRewards", func(t *testing.T) { + sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) + rewards, _ := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) + + err = rewards.GenerateAndInsertStakeOperatorSetRewards(snapshotDate) + assert.Nil(t, err) + + stakeOperatorSetRewards, err := rewards.ListStakeOperatorSetRewards() + assert.Nil(t, err) + + assert.NotNil(t, stakeOperatorSetRewards) + + t.Logf("Generated %d stakeOperatorSetRewards", len(stakeOperatorSetRewards)) + + // Should have 2 records: 1 unique_stake + 1 total_stake + assert.Equal(t, 2, len(stakeOperatorSetRewards)) + + // Verify we have both reward types + var hasUniqueStake, hasTotalStake bool + for _, reward := range stakeOperatorSetRewards { + if reward.RewardType == "unique_stake" { + hasUniqueStake = true + assert.Equal(t, "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", reward.Avs) + assert.Equal(t, uint64(1), reward.OperatorSetId) + assert.Equal(t, "100000000000000000000000", reward.Amount) + } + if reward.RewardType == "total_stake" { + hasTotalStake = true + assert.Equal(t, "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101", reward.Avs) + assert.Equal(t, uint64(2), reward.OperatorSetId) + assert.Equal(t, "200000000000000000000000", reward.Amount) + } + } + assert.True(t, hasUniqueStake, "Should have unique_stake reward") + assert.True(t, hasTotalStake, "Should have total_stake reward") + }) + + t.Cleanup(func() { + teardownStakeOperatorSetRewards(dbFileName, cfg, grm, l) + }) +} + +// Test_ProRataDistribution tests that stake-based rewards are correctly distributed +// proportionally across operators based on their allocated weight. +// +// Scenario: +// - Pool reward: 1,000,000 tokens for 10 days (100,000 tokens/day) +// - Operator A: weight 1 (allocated stake * multiplier) +// - Operator B: weight 2 +// - Total weight: 3 +// Expected per day: +// - Operator A: FLOOR(100,000 * 1 / 3) = 33,333 tokens +// - Operator B: FLOOR(100,000 * 2 / 3) = 66,666 tokens +func Test_ProRataDistribution(t *testing.T) { + if !rewardsTestsEnabled() { + t.Skipf("Skipping %s", t.Name()) + return + } + + dbFileName, cfg, grm, l, sink, err := setupStakeOperatorSetRewards() + if err != nil { + t.Fatal(err) + } + + // Enable rewards v2.2 + cfg.Rewards.RewardsV2_2Enabled = true + + snapshotDate := "2024-09-15" + + t.Run("Should distribute pro-rata to operators based on weight", func(t *testing.T) { + // Hydrate blocks + t.Log("Hydrating blocks") + _, err := hydrateRewardsV2Blocks(grm, l) + if err != nil { + t.Fatal(err) + } + + avs := "0xd36b6e5eee8311d7bffb2f3bb33301a1ab7de101" + operatorA := "0xoperator_a_prorata_test" + operatorB := "0xoperator_b_prorata_test" + strategy := "0x5074dfd18e9498d9e006fb8d4f3fecdc9af90a2c" + token := "0x0ddd9dc88e638aef6a8e42d0c98aaa6a48a98d24" + + // Insert operator set operator registrations + res := grm.Exec(` + INSERT INTO operator_set_operator_registrations + (operator, avs, operator_set_id, is_active, block_number, transaction_hash, log_index) + VALUES + (?, ?, 1, true, 1477000, '0xtx_opreg_a', 0), + (?, ?, 1, true, 1477000, '0xtx_opreg_b', 1) + `, operatorA, avs, operatorB, avs) + assert.Nil(t, res.Error) + + // Insert operator set strategy registration + res = grm.Exec(` + INSERT INTO operator_set_strategy_registrations + (strategy, avs, operator_set_id, is_active, block_number, transaction_hash, log_index) + VALUES + (?, ?, 1, true, 1477000, '0xtx_stratreg', 0) + `, strategy, avs) + assert.Nil(t, res.Error) + + // Insert operator allocations with different magnitudes + // Operator A: magnitude 1e18, max_magnitude 1e18 => ratio 1 + // Operator B: magnitude 2e18, max_magnitude 2e18 => ratio 1 + res = grm.Exec(` + INSERT INTO operator_allocations + (operator, avs, strategy, magnitude, operator_set_id, effective_block, transaction_hash, log_index, block_number) + VALUES + (?, ?, ?, '1000000000000000000', 1, 1477000, '0xtx_alloc_a', 0, 1477000), + (?, ?, ?, '2000000000000000000', 1, 1477000, '0xtx_alloc_b', 1, 1477000) + `, operatorA, avs, strategy, operatorB, avs, strategy) + assert.Nil(t, res.Error) + + // Insert max magnitudes + res = grm.Exec(` + INSERT INTO operator_max_magnitudes + (operator, strategy, max_magnitude, block_number, transaction_hash, log_index) + VALUES + (?, ?, '1000000000000000000', 1477000, '0xtx_maxmag_a', 0), + (?, ?, '2000000000000000000', 1477000, '0xtx_maxmag_b', 1) + `, operatorA, strategy, operatorB, strategy) + assert.Nil(t, res.Error) + + // Insert operator shares (same shares, different allocations give different weights) + // Operator A: 1000 shares * (1e18/1e18) * 1e18 multiplier = weight 1e21 + // Operator B: 1000 shares * (2e18/2e18) * 1e18 multiplier = weight 1e21 + // But we want weight ratio 1:2, so give operator B 2x shares + res = grm.Exec(` + INSERT INTO operator_share_deltas + (operator, strategy, shares, strategy_index, block_number, transaction_hash, log_index) + VALUES + (?, ?, '1000000000000000000000', 0, 1477000, '0xtx_opshare_a', 0), + (?, ?, '2000000000000000000000', 0, 1477000, '0xtx_opshare_b', 1) + `, operatorA, strategy, operatorB, strategy) + assert.Nil(t, res.Error) + + // Insert unique stake reward submission (pool-based) + // 1,000,000 tokens over 10 days = 100,000 tokens/day + startTime := time.Unix(1725494400, 0) // Sept 5, 2024 + endTime := time.Unix(1726358400, 0) // Sept 15, 2024 (10 days later) + + reward := uniqueStakeRewardSubmissions.UniqueStakeRewardSubmission{ + Avs: avs, + OperatorSetId: 1, + RewardHash: "0xprorata_test_hash", + Token: token, + Amount: "1000000000000000000000000", // 1,000,000 tokens (1e24) + Strategy: strategy, + StrategyIndex: 0, + Multiplier: "1000000000000000000", // 1e18 + StartTimestamp: &startTime, + EndTimestamp: &endTime, + Duration: 864000, // 10 days in seconds + BlockNumber: 1477020, + TransactionHash: "prorata_stake_hash", + LogIndex: 20, + } + + result := grm.Create(&reward) + assert.Nil(t, result.Error) + + // Generate stake operator set rewards (denormalizer) + sog := stakerOperators.NewStakerOperatorGenerator(grm, l, cfg) + rewards, _ := NewRewardsCalculator(cfg, grm, nil, sog, sink, l) + + err = rewards.GenerateAndInsertStakeOperatorSetRewards(snapshotDate) + assert.Nil(t, err) + + // Verify stake_operator_set_rewards has the entry + stakeRewards, err := rewards.ListStakeOperatorSetRewards() + assert.Nil(t, err) + assert.Equal(t, 1, len(stakeRewards)) + assert.Equal(t, "unique_stake", stakeRewards[0].RewardType) + + t.Logf("Created stake reward: amount=%s, duration=%d days", + stakeRewards[0].Amount, stakeRewards[0].Duration/86400) + + // Generate snapshot data + err = rewards.generateSnapshotData(snapshotDate) + assert.Nil(t, err) + + // Generate gold tables (Table 15 will explode into per-day rows, Table 16 will distribute) + err = rewards.generateGoldTables(snapshotDate) + assert.Nil(t, err) + + // Query Table 16 to verify pro-rata distribution + var operatorRewards []struct { + Operator string + OperatorTokens string + Snapshot string + } + + query := ` + SELECT operator, operator_tokens::text, snapshot::text + FROM gold_16_operator_operator_set_unique_stake_rewards_` + strings.ReplaceAll(snapshotDate, "-", "_") + ` + ORDER BY operator, snapshot + ` + err = grm.Raw(query).Scan(&operatorRewards).Error + if err != nil { + t.Logf("Query error (table may not exist or no data): %v", err) + } + + t.Logf("Found %d operator reward entries", len(operatorRewards)) + + // Count and sum by operator + operatorTotals := make(map[string]int64) + for _, r := range operatorRewards { + tokens, _ := new(big.Int).SetString(r.OperatorTokens, 10) + if tokens != nil { + operatorTotals[r.Operator] += tokens.Int64() + } + t.Logf(" Operator: %s, Snapshot: %s, Tokens: %s", r.Operator, r.Snapshot, r.OperatorTokens) + } + + // Verify the ratio is approximately 1:2 + if len(operatorTotals) >= 2 { + tokensA := operatorTotals[operatorA] + tokensB := operatorTotals[operatorB] + + t.Logf("Operator A total tokens: %d", tokensA) + t.Logf("Operator B total tokens: %d", tokensB) + + // The ratio should be approximately 1:2 + if tokensA > 0 && tokensB > 0 { + ratio := float64(tokensB) / float64(tokensA) + t.Logf("Ratio B/A: %.4f (expected ~2.0)", ratio) + + // Allow for some rounding error due to FLOOR operations + assert.InDelta(t, 2.0, ratio, 0.1, "Tokens should be distributed in 1:2 ratio") + } + } + }) + + t.Cleanup(func() { + teardownStakeOperatorSetRewards(dbFileName, cfg, grm, l) + }) +} diff --git a/pkg/rewards/stakerShareSnapshots.go b/pkg/rewards/stakerShareSnapshots.go index eacd608fa..7c7984c2c 100644 --- a/pkg/rewards/stakerShareSnapshots.go +++ b/pkg/rewards/stakerShareSnapshots.go @@ -33,8 +33,8 @@ const stakerShareSnapshotsQuery = ` SELECT staker, strategy, shares::numeric as 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 '{{.cutoffDate}}') + -- If the range does not have the end, use cutoff + 1 day to include cutoff date snapshot + WHEN LEAD(snapshot_time) OVER (PARTITION BY staker, strategy ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{.cutoffDate}}') + INTERVAL '1' day ELSE LEAD(snapshot_time) OVER (PARTITION BY staker, strategy ORDER BY snapshot_time) END AS end_time FROM snapshotted_records @@ -60,6 +60,7 @@ const stakerShareSnapshotsQuery = ` WHERE adj.staker = qsw.staker AND adj.strategy = qsw.strategy AND adj.withdrawal_block_number = qsw.block_number + AND adj.withdrawal_log_index = qsw.log_index AND DATE(b_slash.block_time) < day::date ORDER BY adj.slash_block_number DESC LIMIT 1), diff --git a/pkg/rewards/stakerShareSnapshots_anvil_test.go b/pkg/rewards/stakerShareSnapshots_anvil_test.go index f52703c62..0a82a90fc 100644 --- a/pkg/rewards/stakerShareSnapshots_anvil_test.go +++ b/pkg/rewards/stakerShareSnapshots_anvil_test.go @@ -207,16 +207,16 @@ func testSSS8_MultipleConsecutiveSlashes(t *testing.T, ctx context.Context, // First slash 50% on 1/6: multiplier = 0.5 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, "0xstaker_sss8", "0xstrat_sss8", "0xoperator_sss8", 88001, 88002, 0.5, 88002, "tx_88002_slash1", 0) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "0xstaker_sss8", "0xstrat_sss8", "0xoperator_sss8", 88001, 0, 88002, 0.5, 88002, "tx_88002_slash1", 0) require.Nil(t, res.Error, "Failed to insert first slash adjustment") // Second slash 50% on 1/7: cumulative multiplier = 0.25 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, "0xstaker_sss8", "0xstrat_sss8", "0xoperator_sss8", 88001, 88003, 0.25, 88003, "tx_88003_slash2", 0) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "0xstaker_sss8", "0xstrat_sss8", "0xoperator_sss8", 88001, 0, 88003, 0.25, 88003, "tx_88003_slash2", 0) require.Nil(t, res.Error, "Failed to insert second slash adjustment") t.Log("SSS-8: Generating snapshots...") @@ -319,9 +319,9 @@ func testSSS9_PartialWithdrawalWithSlashing(t *testing.T, ctx context.Context, require.Nil(t, res.Error, "Failed to insert first slash base shares") res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, "0xstaker_sss9", "0xstrat_sss9", "0xoperator_sss9", 99001, 99002, 0.5, 99002, "tx_99002_slash1", 0) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "0xstaker_sss9", "0xstrat_sss9", "0xoperator_sss9", 99001, 0, 99002, 0.5, 99002, "tx_99002_slash1", 0) require.Nil(t, res.Error, "Failed to insert first slash adjustment") // Second slash 50% on 2/7: base goes to 37.5, cumulative multiplier = 0.25 @@ -332,9 +332,9 @@ func testSSS9_PartialWithdrawalWithSlashing(t *testing.T, ctx context.Context, require.Nil(t, res.Error, "Failed to insert second slash base shares") res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, "0xstaker_sss9", "0xstrat_sss9", "0xoperator_sss9", 99001, 99003, 0.25, 99003, "tx_99003_slash2", 0) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "0xstaker_sss9", "0xstrat_sss9", "0xoperator_sss9", 99001, 0, 99003, 0.25, 99003, "tx_99003_slash2", 0) require.Nil(t, res.Error, "Failed to insert second slash adjustment") t.Log("SSS-9: Generating snapshots...") @@ -619,9 +619,9 @@ func testSSS13_SameBlockWithdrawalBeforeSlash(t *testing.T, ctx context.Context, // Slash 50% in same block with higher log_index res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, "0xstaker_sss13", "0xstrat_sss13", "0xoperator_sss13", 140001, 140001, 0.5, 140001, "tx_140001_slash", 3) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "0xstaker_sss13", "0xstrat_sss13", "0xoperator_sss13", 140001, 0, 140001, 0.5, 140001, "tx_140001_slash", 3) require.Nil(t, res.Error) t.Log("SSS-13: Generating snapshots...") diff --git a/pkg/rewards/stakerShareSnapshots_test.go b/pkg/rewards/stakerShareSnapshots_test.go index ddc0dfe7d..ed3777ab8 100644 --- a/pkg/rewards/stakerShareSnapshots_test.go +++ b/pkg/rewards/stakerShareSnapshots_test.go @@ -383,8 +383,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // Insert initial slashing adjustment (multiplier = 1.0, no slashing yet) res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker03", "0xstrat03", "0xoperator03", block5, block5, 1.0, block5, "tx_2021_queue", 0) assert.Nil(t, res.Error) @@ -628,8 +628,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // Slash 50% on 3/6: insert slashing adjustment with multiplier 0.5 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker06", "0xstrat06", "0xoperator06", block5, block6, 0.5, block6, "tx_5002_slash", 0) assert.Nil(t, res.Error) @@ -725,15 +725,15 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // First slash 50% on 1/6 @ 6pm (block 7002): multiplier = 0.5 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker07", "0xstrat07", "0xoperator07", block5, block6a, 0.5, block6a, "tx_7002_slash1", 0) assert.Nil(t, res.Error) // Second slash 50% on 1/6 @ 6pm (block 7003, same day): cumulative multiplier = 0.5 * 0.5 = 0.25 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker07", "0xstrat07", "0xoperator07", block5, block6b, 0.25, block6b, "tx_7003_slash2", 0) assert.Nil(t, res.Error) @@ -825,15 +825,15 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // First slash 50% on 1/6: multiplier = 0.5 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker08", "0xstrat08", "0xoperator08", 8001, 8002, 0.5, 8002, "tx_8002_slash1", 0) assert.Nil(t, res.Error) // Second slash 50% on 1/7: cumulative multiplier = 0.5 * 0.5 = 0.25 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker08", "0xstrat08", "0xoperator08", 8001, 8003, 0.25, 8003, "tx_8003_slash2", 0) assert.Nil(t, res.Error) @@ -960,8 +960,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { assert.Nil(t, res.Error) res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker09", "0xstrat09", "0xoperator09", 9001, 9002, 0.5, 9002, "tx_9002_slash1", 0) assert.Nil(t, res.Error) @@ -975,8 +975,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { assert.Nil(t, res.Error) res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker09", "0xstrat09", "0xoperator09", 9001, 9003, 0.25, 9003, "tx_9003_slash2", 0) assert.Nil(t, res.Error) @@ -1101,8 +1101,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // Insert slash adjustment with multiplier 0.5 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker10", "0xstrat10", "0xoperator10", block2, block3, 0.5, block3, "tx_100200_slash", 0) assert.Nil(t, res.Error) @@ -1254,8 +1254,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // Insert slash adjustment with multiplier 0.5 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker11", "0xstrat11", "0xoperator11", block2, block3, 0.5, block3, "tx_200200_slash", 0) assert.Nil(t, res.Error) @@ -1375,8 +1375,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // Insert operator set slash adjustment with multiplier 0.75 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker12", "0xstrat12", "0xoperator12", 3101, 3102, 0.75, 3102, "tx_3102_slash", 0) assert.Nil(t, res.Error) @@ -1389,8 +1389,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // Insert beacon chain slash adjustment with cumulative multiplier 0.375 (0.75 * 0.5) res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, "0xstaker12", "0xstrat12", "0xoperator12", 3101, 3103, 0.375, 3103, "tx_3103_slash", 0) assert.Nil(t, res.Error) @@ -1498,8 +1498,8 @@ func Test_StakerShareSnapshots_V22(t *testing.T) { // Insert slash adjustment at log index 3 with multiplier 0.5 // (withdrawal at log 2 < slash at log 3, so withdrawal IS affected) res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 2, ?, ?, ?, ?, ?) `, "0xstaker13", "0xstrat13", "0xoperator13", block2, block2, 0.5, block2, "tx_1001_slash", 3) assert.Nil(t, res.Error) @@ -2917,8 +2917,8 @@ func Test_WithdrawalQueueAddBack_Integration(t *testing.T) { // Day 5: Slash 50% - insert slashing adjustment with multiplier 0.5 res = grm.Exec(` - INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO queued_withdrawal_slashing_adjustments (staker, strategy, operator, withdrawal_block_number, withdrawal_log_index, slash_block_number, slash_multiplier, block_number, transaction_hash, log_index) + VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?) `, staker, strategy, operator, 20002, 20005, 0.5, 20005, "tx_20005_slash", 0) assert.Nil(t, res.Error) diff --git a/pkg/rewards/tables.go b/pkg/rewards/tables.go index 195880f3a..25350fb5b 100644 --- a/pkg/rewards/tables.go +++ b/pkg/rewards/tables.go @@ -164,3 +164,21 @@ type OperatorAllocationSnapshot struct { MaxMagnitude string Snapshot time.Time } + +type StakeOperatorSetRewards struct { + Avs string + OperatorSetId uint64 + RewardHash string + Token string + Amount string + Strategy string + StrategyIndex uint64 + Multiplier string + StartTimestamp time.Time + EndTimestamp time.Time + Duration uint64 + RewardType string + BlockNumber uint64 + BlockTime time.Time + BlockDate string +} diff --git a/pkg/rewardsUtils/rewardsUtils.go b/pkg/rewardsUtils/rewardsUtils.go index 22c11788f..c026ec1ff 100644 --- a/pkg/rewardsUtils/rewardsUtils.go +++ b/pkg/rewardsUtils/rewardsUtils.go @@ -26,14 +26,15 @@ var ( Table_12_OperatorODOperatorSetRewardAmounts = "gold_12_operator_od_operator_set_reward_amounts" Table_13_StakerODOperatorSetRewardAmounts = "gold_13_staker_od_operator_set_reward_amounts" Table_14_AvsODOperatorSetRewardAmounts = "gold_14_avs_od_operator_set_reward_amounts" - Table_15_OperatorOperatorSetUniqueStakeRewards = "gold_15_operator_operator_set_unique_stake_rewards" - Table_16_StakerOperatorSetUniqueStakeRewards = "gold_16_staker_operator_set_unique_stake_rewards" - Table_17_AvsOperatorSetUniqueStakeRewards = "gold_17_avs_operator_set_unique_stake_rewards" - Table_18_OperatorOperatorSetTotalStakeRewards = "gold_18_operator_operator_set_total_stake_rewards" - Table_19_StakerOperatorSetTotalStakeRewards = "gold_19_staker_operator_set_total_stake_rewards" - Table_20_AvsOperatorSetTotalStakeRewards = "gold_20_avs_operator_set_total_stake_rewards" - Table_21_GoldStaging = "gold_21_staging" - Table_22_GoldTable = "gold_table" + Table_15_ActiveUniqueAndTotalStakeRewards = "gold_15_active_unique_and_total_stake_rewards" + Table_16_OperatorOperatorSetUniqueStakeRewards = "gold_16_operator_operator_set_unique_stake_rewards" + Table_17_StakerOperatorSetUniqueStakeRewards = "gold_17_staker_operator_set_unique_stake_rewards" + Table_18_AvsOperatorSetUniqueStakeRewards = "gold_18_avs_operator_set_unique_stake_rewards" + Table_19_OperatorOperatorSetTotalStakeRewards = "gold_19_operator_operator_set_total_stake_rewards" + Table_20_StakerOperatorSetTotalStakeRewards = "gold_20_staker_operator_set_total_stake_rewards" + Table_21_AvsOperatorSetTotalStakeRewards = "gold_21_avs_operator_set_total_stake_rewards" + Table_22_GoldStaging = "gold_22_staging" + Table_23_GoldTable = "gold_table" Table_OperatorAllocationSnapshots = "operator_allocation_snapshots" @@ -67,14 +68,15 @@ var goldTableBaseNames = map[string]string{ Table_12_OperatorODOperatorSetRewardAmounts: Table_12_OperatorODOperatorSetRewardAmounts, Table_13_StakerODOperatorSetRewardAmounts: Table_13_StakerODOperatorSetRewardAmounts, Table_14_AvsODOperatorSetRewardAmounts: Table_14_AvsODOperatorSetRewardAmounts, - Table_15_OperatorOperatorSetUniqueStakeRewards: Table_15_OperatorOperatorSetUniqueStakeRewards, - Table_16_StakerOperatorSetUniqueStakeRewards: Table_16_StakerOperatorSetUniqueStakeRewards, - Table_17_AvsOperatorSetUniqueStakeRewards: Table_17_AvsOperatorSetUniqueStakeRewards, - Table_18_OperatorOperatorSetTotalStakeRewards: Table_18_OperatorOperatorSetTotalStakeRewards, - Table_19_StakerOperatorSetTotalStakeRewards: Table_19_StakerOperatorSetTotalStakeRewards, - Table_20_AvsOperatorSetTotalStakeRewards: Table_20_AvsOperatorSetTotalStakeRewards, - Table_21_GoldStaging: Table_21_GoldStaging, - Table_22_GoldTable: Table_22_GoldTable, + Table_15_ActiveUniqueAndTotalStakeRewards: Table_15_ActiveUniqueAndTotalStakeRewards, + Table_16_OperatorOperatorSetUniqueStakeRewards: Table_16_OperatorOperatorSetUniqueStakeRewards, + Table_17_StakerOperatorSetUniqueStakeRewards: Table_17_StakerOperatorSetUniqueStakeRewards, + Table_18_AvsOperatorSetUniqueStakeRewards: Table_18_AvsOperatorSetUniqueStakeRewards, + Table_19_OperatorOperatorSetTotalStakeRewards: Table_19_OperatorOperatorSetTotalStakeRewards, + Table_20_StakerOperatorSetTotalStakeRewards: Table_20_StakerOperatorSetTotalStakeRewards, + Table_21_AvsOperatorSetTotalStakeRewards: Table_21_AvsOperatorSetTotalStakeRewards, + Table_22_GoldStaging: Table_22_GoldStaging, + Table_23_GoldTable: Table_23_GoldTable, Table_OperatorAllocationSnapshots: Table_OperatorAllocationSnapshots, @@ -108,13 +110,14 @@ var GoldTableNameSearchPattern = map[string]string{ Table_12_OperatorODOperatorSetRewardAmounts: "gold_%_operator_od_operator_set_reward_amounts", Table_13_StakerODOperatorSetRewardAmounts: "gold_%_staker_od_operator_set_reward_amounts", Table_14_AvsODOperatorSetRewardAmounts: "gold_%_avs_od_operator_set_reward_amounts", - Table_15_OperatorOperatorSetUniqueStakeRewards: "gold_%_operator_operator_set_unique_stake_rewards", - Table_16_StakerOperatorSetUniqueStakeRewards: "gold_%_staker_operator_set_unique_stake_rewards", - Table_17_AvsOperatorSetUniqueStakeRewards: "gold_%_avs_operator_set_unique_stake_rewards", - Table_18_OperatorOperatorSetTotalStakeRewards: "gold_%_operator_operator_set_total_stake_rewards", - Table_19_StakerOperatorSetTotalStakeRewards: "gold_%_staker_operator_set_total_stake_rewards", - Table_20_AvsOperatorSetTotalStakeRewards: "gold_%_avs_operator_set_total_stake_rewards", - Table_21_GoldStaging: "gold_%_staging", + Table_15_ActiveUniqueAndTotalStakeRewards: "gold_%_active_unique_and_total_stake_rewards", + Table_16_OperatorOperatorSetUniqueStakeRewards: "gold_%_operator_operator_set_unique_stake_rewards", + Table_17_StakerOperatorSetUniqueStakeRewards: "gold_%_staker_operator_set_unique_stake_rewards", + Table_18_AvsOperatorSetUniqueStakeRewards: "gold_%_avs_operator_set_unique_stake_rewards", + Table_19_OperatorOperatorSetTotalStakeRewards: "gold_%_operator_operator_set_total_stake_rewards", + Table_20_StakerOperatorSetTotalStakeRewards: "gold_%_staker_operator_set_total_stake_rewards", + Table_21_AvsOperatorSetTotalStakeRewards: "gold_%_avs_operator_set_total_stake_rewards", + Table_22_GoldStaging: "gold_%_staging", Sot_1_StakerStrategyPayouts: "sot_%_staker_strategy_payouts", Sot_2_OperatorStrategyPayouts: "sot_%_operator_strategy_payouts",