From 32fd245d898acc9a1aa7604b942e3ca7d481b56d Mon Sep 17 00:00:00 2001 From: riley-stride <104941670+riley-stride@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:06:29 -0500 Subject: [PATCH] Add claim rewards ica (#961) --- x/stakeibc/keeper/hooks.go | 15 ++++ ...im_accrued_staking_rewards_on_host_test.go | 87 +++++++++++++++++++ x/stakeibc/keeper/msg_server_submit_tx.go | 50 +++++++++++ 3 files changed, 152 insertions(+) create mode 100644 x/stakeibc/keeper/msg_server_claim_accrued_staking_rewards_on_host_test.go diff --git a/x/stakeibc/keeper/hooks.go b/x/stakeibc/keeper/hooks.go index 1282481d8d..a11c126127 100644 --- a/x/stakeibc/keeper/hooks.go +++ b/x/stakeibc/keeper/hooks.go @@ -45,6 +45,9 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInf delegationInterval := k.GetParam(ctx, types.KeyDelegateInterval) reinvestInterval := k.GetParam(ctx, types.KeyReinvestInterval) + // Claim accrued staking rewards at the beginning of the epoch + k.ClaimAccruedStakingRewards(ctx) + // Create a new deposit record for each host zone and the grab all deposit records k.CreateDepositRecordsForEpoch(ctx, epochNumber) depositRecords := k.RecordsKeeper.GetAllDepositRecord(ctx) @@ -151,6 +154,18 @@ func (k Keeper) SetWithdrawalAddress(ctx sdk.Context) { } } +// Claim staking rewards for each host zone +func (k Keeper) ClaimAccruedStakingRewards(ctx sdk.Context) { + k.Logger(ctx).Info("Claiming Accrued Staking Rewards...") + + for _, hostZone := range k.GetAllActiveHostZone(ctx) { + err := k.ClaimAccruedStakingRewardsOnHost(ctx, hostZone) + if err != nil { + k.Logger(ctx).Error(fmt.Sprintf("Unable to claim accrued staking rewards on %s, err: %s", hostZone.ChainId, err)) + } + } +} + // Updates the redemption rate for each host zone // At a high level, the redemption rate is equal to the amount of native tokens locked divided by the stTokens in existence. // The equation is broken down further into the following sub-components: diff --git a/x/stakeibc/keeper/msg_server_claim_accrued_staking_rewards_on_host_test.go b/x/stakeibc/keeper/msg_server_claim_accrued_staking_rewards_on_host_test.go new file mode 100644 index 0000000000..94afe7b1ed --- /dev/null +++ b/x/stakeibc/keeper/msg_server_claim_accrued_staking_rewards_on_host_test.go @@ -0,0 +1,87 @@ +package keeper_test + +import ( + "fmt" + + "cosmossdk.io/math" + _ "github.com/stretchr/testify/suite" + + epochtypes "github.com/Stride-Labs/stride/v16/x/epochs/types" + stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" + types "github.com/Stride-Labs/stride/v16/x/stakeibc/types" +) + +// constant number of zero delegations +const numZeroDelegations = 37 + +func (s *KeeperTestSuite) ClaimAccruedStakingRewardsOnHost() { + // Create a delegation ICA channel for the ICA submission + owner := types.FormatICAAccountOwner(HostChainId, types.ICAAccountType_DELEGATION) + channelId, portId := s.CreateICAChannel(owner) + + // Create validators + validators := []*types.Validator{} + numberGTClaimRewardsBatchSize := int(50) + for i := 0; i < numberGTClaimRewardsBatchSize; i++ { + + // set most delegations to 5, some to 0 + valDelegation := math.NewInt(5) + if i > (numberGTClaimRewardsBatchSize - numZeroDelegations) { + valDelegation = math.NewInt(0) + } + validators = append(validators, &types.Validator{ + Address: fmt.Sprintf("val-%d", i), + Delegation: valDelegation, + }) + } + + // Create host zone + hostZone := types.HostZone{ + ChainId: HostChainId, + DelegationIcaAddress: "delegation", + WithdrawalIcaAddress: "withdrawal", + Validators: validators, + } + + // Create epoch tracker for ICA timeout + strideEpoch := types.EpochTracker{ + EpochIdentifier: epochtypes.STRIDE_EPOCH, + NextEpochStartTime: uint64(s.Coordinator.CurrentTime.UnixNano() + 30_000_000_000), // used for timeout + } + s.App.StakeibcKeeper.SetEpochTracker(s.Ctx, strideEpoch) + + // Get start sequence number to confirm ICA was set + startSequence, found := s.App.IBCKeeper.ChannelKeeper.GetNextSequenceSend(s.Ctx, portId, channelId) + s.Require().True(found) + + // Call claim accrued rewards to submit ICAs + err := s.App.StakeibcKeeper.ClaimAccruedStakingRewardsOnHost(s.Ctx, hostZone) + s.Require().NoError(err, "no error expected when accruing rewards") + + // Confirm sequence number incremented by the number of txs + // where the number of txs is equal: + // (total_validators - validators_with_zero_delegation) / batch_size + batchSize := (numberGTClaimRewardsBatchSize - numZeroDelegations) / stakeibckeeper.ClaimRewardsICABatchSize + expectedEndSequence := startSequence + uint64(batchSize) + actualEndSequence, found := s.App.IBCKeeper.ChannelKeeper.GetNextSequenceSend(s.Ctx, portId, channelId) + s.Require().True(found) + s.Require().Equal(expectedEndSequence, actualEndSequence, "sequence number should have incremented") + + // Attempt to call it with a host zone without a delegation ICA address, it should fail + invalidHostZone := hostZone + invalidHostZone.DelegationIcaAddress = "" + err = s.App.StakeibcKeeper.ClaimAccruedStakingRewardsOnHost(s.Ctx, hostZone) + s.Require().ErrorContains(err, "ICA account not found") + + // Attempt to call it with a host zone without a withdrawal ICA address, it should fail + invalidHostZone = hostZone + invalidHostZone.WithdrawalIcaAddress = "" + err = s.App.StakeibcKeeper.ClaimAccruedStakingRewardsOnHost(s.Ctx, hostZone) + s.Require().ErrorContains(err, "ICA account not found") + + // Attempt to call claim with an invalid connection ID on the host zone so the ica fails + invalidHostZone = hostZone + invalidHostZone.ConnectionId = "" + err = s.App.StakeibcKeeper.ClaimAccruedStakingRewardsOnHost(s.Ctx, hostZone) + s.Require().ErrorContains(err, "Failed to SubmitTxs") +} diff --git a/x/stakeibc/keeper/msg_server_submit_tx.go b/x/stakeibc/keeper/msg_server_submit_tx.go index 8bb88b83f8..25f9e3028a 100644 --- a/x/stakeibc/keeper/msg_server_submit_tx.go +++ b/x/stakeibc/keeper/msg_server_submit_tx.go @@ -31,6 +31,10 @@ import ( icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" ) +const ( + ClaimRewardsICABatchSize = 10 +) + func (k Keeper) DelegateOnHost(ctx sdk.Context, hostZone types.HostZone, amt sdk.Coin, depositRecord recordstypes.DepositRecord) error { // the relevant ICA is the delegate account owner := types.FormatICAAccountOwner(hostZone.ChainId, types.ICAAccountType_DELEGATION) @@ -153,6 +157,52 @@ func (k Keeper) SetWithdrawalAddressOnHost(ctx sdk.Context, hostZone types.HostZ return nil } +func (k Keeper) ClaimAccruedStakingRewardsOnHost(ctx sdk.Context, hostZone types.HostZone) error { + // Fetch the relevant ICA + if hostZone.DelegationIcaAddress == "" { + return errorsmod.Wrapf(types.ErrICAAccountNotFound, "delegation ICA not found for %s", hostZone.ChainId) + } + if hostZone.WithdrawalIcaAddress == "" { + return errorsmod.Wrapf(types.ErrICAAccountNotFound, "withdrawal ICA not found for %s", hostZone.ChainId) + } + k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Withdrawal Address: %s, Delegator Address: %s", + hostZone.WithdrawalIcaAddress, hostZone.DelegationIcaAddress)) + + validators := hostZone.Validators + + // Build multi-message transaction to withdraw rewards from each validator + // batching txs into groups of ClaimRewardsICABatchSize messages, to ensure they will fit in the host's blockSize + for start := 0; start < len(validators); start += ClaimRewardsICABatchSize { + end := start + ClaimRewardsICABatchSize + if end > len(validators) { + end = len(validators) + } + batch := validators[start:end] + msgs := []proto.Message{} + // Iterate over the items within the batch + for _, val := range batch { + // skip withdrawing rewards + if val.Delegation.IsZero() { + continue + } + msg := &distributiontypes.MsgWithdrawDelegatorReward{ + DelegatorAddress: hostZone.DelegationIcaAddress, + ValidatorAddress: val.Address, + } + msgs = append(msgs, msg) + } + + if len(msgs) > 0 { + _, err := k.SubmitTxsStrideEpoch(ctx, hostZone.ConnectionId, msgs, types.ICAAccountType_DELEGATION, "", nil) + if err != nil { + return errorsmod.Wrapf(err, "Failed to SubmitTxs for %s, %s, %s", hostZone.ConnectionId, hostZone.ChainId, msgs) + } + } + } + + return nil +} + // Submits an ICQ for the withdrawal account balance func (k Keeper) UpdateWithdrawalBalance(ctx sdk.Context, hostZone types.HostZone) error { k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Submitting ICQ for withdrawal account balance"))