Skip to content

Commit

Permalink
Add claim rewards ica (#961)
Browse files Browse the repository at this point in the history
  • Loading branch information
riley-stride authored Dec 8, 2023
1 parent e61678b commit 32fd245
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 0 deletions.
15 changes: 15 additions & 0 deletions x/stakeibc/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
50 changes: 50 additions & 0 deletions x/stakeibc/keeper/msg_server_submit_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"))
Expand Down

0 comments on commit 32fd245

Please sign in to comment.