Skip to content

Commit

Permalink
batched undelegations (#1195)
Browse files Browse the repository at this point in the history
  • Loading branch information
sampocs authored Jun 13, 2024
1 parent 10a1abd commit 74c51cb
Show file tree
Hide file tree
Showing 36 changed files with 2,374 additions and 1,407 deletions.
8 changes: 4 additions & 4 deletions app/upgrades/v10/upgrades_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ func (s *UpgradeTestSuite) TestMigrateCallbackData() {
}
initialUndelegateCallbackArgs := stakeibctypes.UndelegateCallback{
HostZoneId: "host-0",
SplitDelegations: []*types.SplitDelegation{{
Validator: "val-0",
Amount: sdkmath.NewInt(1),
}},
// SplitDelegations: []*types.SplitDelegation{{
// Validator: "val-0",
// Amount: sdkmath.NewInt(1),
// }},
}
initialTransferCallbackArgs := recordstypes.TransferCallback{
DepositRecordId: 1,
Expand Down
4 changes: 2 additions & 2 deletions app/upgrades/v5/upgrades_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ func (s *UpgradeTestSuite) SetupOldUndelegateCallback(codec codec.Codec, callbac
err = proto.Unmarshal(undelegateCallbackData.CallbackArgs, &undelegateCallback)
s.Require().NoError(err, "unmarshaling undelegate callback args should not error")

s.Require().Equal(undelegateValidator, undelegateCallback.SplitDelegations[0].Validator, "undelegate callback validator")
s.Require().Equal(sdkmath.NewInt(3000000), undelegateCallback.SplitDelegations[0].Amount, "undelegate callback amount")
// s.Require().Equal(undelegateValidator, undelegateCallback.SplitDelegations[0].Validator, "undelegate callback validator")
// s.Require().Equal(sdkmath.NewInt(3000000), undelegateCallback.SplitDelegations[0].Amount, "undelegate callback amount")
}
}

Expand Down
17 changes: 17 additions & 0 deletions proto/stride/records/records.proto
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ message HostZoneUnbonding {
enum Status {
// tokens bonded on delegate account
UNBONDING_QUEUE = 0;
// unbonding ICA has been submitted
UNBONDING_IN_PROGRESS = 3;
// unbonding ICA failed for at least one batch and need to be retried
UNBONDING_RETRY_QUEUE = 5;
// unbonding completed on delegate account
EXIT_TRANSFER_QUEUE = 1;
// redemption sweep has been submitted
EXIT_TRANSFER_IN_PROGRESS = 4;
// transfer success
CLAIMABLE = 2;
Expand All @@ -73,6 +77,19 @@ message HostZoneUnbonding {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string st_tokens_to_burn = 8 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string native_tokens_to_unbond = 9 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string claimable_native_tokens = 10 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
uint64 undelegation_txs_in_progress = 11;
string denom = 3;
string host_zone_id = 4;
uint64 unbonding_time = 5;
Expand Down
10 changes: 9 additions & 1 deletion proto/stride/stakeibc/callbacks.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ message SplitDelegation {
];
}

message SplitUndelegation {
string validator = 1;
string native_token_amount = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
}

message DelegateCallback {
string host_zone_id = 1;
uint64 deposit_record_id = 2;
Expand All @@ -39,7 +47,7 @@ message ReinvestCallback {

message UndelegateCallback {
string host_zone_id = 1;
repeated SplitDelegation split_delegations = 2;
repeated SplitUndelegation split_undelegations = 2;
repeated uint64 epoch_unbonding_record_ids = 3;
}

Expand Down
20 changes: 10 additions & 10 deletions x/icacallbacks/migrations/v2/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ func convertDelegateCallback(oldDelegateCallback oldstakeibctypes.DelegateCallba
}

func convertUndelegateCallback(oldUndelegateCallback oldstakeibctypes.UndelegateCallback) stakeibctypes.UndelegateCallback {
newSplitDelegations := []*stakeibctypes.SplitDelegation{}
for _, oldSplitDelegation := range oldUndelegateCallback.SplitDelegations {
newSplitDelegation := stakeibctypes.SplitDelegation{
Validator: oldSplitDelegation.Validator,
Amount: sdkmath.NewIntFromUint64(oldSplitDelegation.Amount),
}
newSplitDelegations = append(newSplitDelegations, &newSplitDelegation)
}
// newSplitDelegations := []*stakeibctypes.SplitDelegation{}
// for _, oldSplitDelegation := range oldUndelegateCallback.SplitDelegations {
// newSplitDelegation := stakeibctypes.SplitDelegation{
// Validator: oldSplitDelegation.Validator,
// Amount: sdkmath.NewIntFromUint64(oldSplitDelegation.Amount),
// }
// newSplitDelegations = append(newSplitDelegations, &newSplitDelegation)
// }

return stakeibctypes.UndelegateCallback{
HostZoneId: oldUndelegateCallback.HostZoneId,
SplitDelegations: newSplitDelegations,
HostZoneId: oldUndelegateCallback.HostZoneId,
// SplitDelegations: newSplitDelegations,
EpochUnbondingRecordIds: oldUndelegateCallback.EpochUnbondingRecordIds,
}
}
Expand Down
8 changes: 4 additions & 4 deletions x/icacallbacks/migrations/v2/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ func TestConvertUndelegateCallback(t *testing.T) {
// Check unchanged fields
require.Equal(t, hostZoneId, newUndelegateCallback.HostZoneId, "host zone id")
require.Equal(t, epochUnbondingIds[0], newUndelegateCallback.EpochUnbondingRecordIds[0], "epoch unbonding record id")
require.Equal(t, val1, newUndelegateCallback.SplitDelegations[0].Validator, "validator 1 address")
require.Equal(t, val2, newUndelegateCallback.SplitDelegations[1].Validator, "validator 2 address")
// require.Equal(t, val1, newUndelegateCallback.SplitDelegations[0].Validator, "validator 1 address")
// require.Equal(t, val2, newUndelegateCallback.SplitDelegations[1].Validator, "validator 2 address")

// Check update fields
require.Equal(t, sdkmath.NewInt(1), newUndelegateCallback.SplitDelegations[0].Amount, "validator 1 amount")
require.Equal(t, sdkmath.NewInt(2), newUndelegateCallback.SplitDelegations[1].Amount, "validator 2 amount")
// require.Equal(t, sdkmath.NewInt(1), newUndelegateCallback.SplitDelegations[0].Amount, "validator 1 amount")
// require.Equal(t, sdkmath.NewInt(2), newUndelegateCallback.SplitDelegations[1].Amount, "validator 2 amount")
}

func TestConvertRebalanceCallback(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion x/interchainquery/keeper/msg_submit_query_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (s *KeeperTestSuite) SetupMsgSubmitQueryResponse() MsgSubmitQueryResponseTe

// define the query
goCtx := sdk.WrapSDKContext(s.Ctx)
h, err := s.App.StakeibcKeeper.GetLightClientHeightSafely(s.Ctx, s.TransferPath.EndpointA.ConnectionID)
h, err := s.App.StakeibcKeeper.GetLightClientHeight(s.Ctx, s.TransferPath.EndpointA.ConnectionID)
s.Require().NoError(err)
height := int64(h - 1) // start at the (LC height) - 1 height, which is the height the query executes at!
result := []byte("result-example")
Expand Down
54 changes: 28 additions & 26 deletions x/records/keeper/epoch_unbonding_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (

errorsmod "cosmossdk.io/errors"

stakeibctypes "github.com/Stride-Labs/stride/v22/x/stakeibc/types"

"github.com/Stride-Labs/stride/v22/x/records/types"
)

Expand Down Expand Up @@ -101,57 +99,61 @@ func (k Keeper) GetHostZoneUnbondingByChainId(ctx sdk.Context, epochNumber uint6
}

// Adds a HostZoneUnbonding to an EpochUnbondingRecord
// TODO [cleanup]: Return error instead of success
func (k Keeper) AddHostZoneToEpochUnbondingRecord(ctx sdk.Context, epochNumber uint64, chainId string, hzu *types.HostZoneUnbonding) (val *types.EpochUnbondingRecord, success bool) {
func (k Keeper) AddHostZoneToEpochUnbondingRecord(
ctx sdk.Context,
epochNumber uint64,
chainId string,
hzu types.HostZoneUnbonding,
) (eur types.EpochUnbondingRecord, err error) {
epochUnbondingRecord, found := k.GetEpochUnbondingRecord(ctx, epochNumber)
if !found {
return nil, false
return types.EpochUnbondingRecord{}, types.ErrEpochUnbondingRecordNotFound.Wrapf("epoch number %d", epochNumber)
}
wasSet := false

// Check if the hzu is already in the epoch unbonding record - if so, replace it
hzuAlreadyExists := false
for i, hostZoneUnbonding := range epochUnbondingRecord.HostZoneUnbondings {
if hostZoneUnbonding.GetHostZoneId() == chainId {
epochUnbondingRecord.HostZoneUnbondings[i] = hzu
wasSet = true
if hostZoneUnbonding.HostZoneId == chainId {
epochUnbondingRecord.HostZoneUnbondings[i] = &hzu
hzuAlreadyExists = true
break
}
}
if !wasSet {
// add new host zone unbonding record
epochUnbondingRecord.HostZoneUnbondings = append(epochUnbondingRecord.HostZoneUnbondings, hzu)

// If the hzu didn't already exist, add a new record
if !hzuAlreadyExists {
epochUnbondingRecord.HostZoneUnbondings = append(epochUnbondingRecord.HostZoneUnbondings, &hzu)
}
return &epochUnbondingRecord, true
return epochUnbondingRecord, nil
}

// Stores a host zone unbonding record - set via an epoch unbonding record
func (k Keeper) SetHostZoneUnbondingRecord(ctx sdk.Context, epochNumber uint64, chainId string, hostZoneUnbonding types.HostZoneUnbonding) error {
epochUnbondingRecord, success := k.AddHostZoneToEpochUnbondingRecord(ctx, epochNumber, chainId, &hostZoneUnbonding)
if !success {
return errorsmod.Wrapf(types.ErrEpochUnbondingRecordNotFound, "epoch unbonding record not found for epoch %d", epochNumber)
epochUnbondingRecord, err := k.AddHostZoneToEpochUnbondingRecord(ctx, epochNumber, chainId, hostZoneUnbonding)
if err != nil {
return err
}
k.SetEpochUnbondingRecord(ctx, *epochUnbondingRecord)
k.SetEpochUnbondingRecord(ctx, epochUnbondingRecord)
return nil
}

// Updates the status for a given host zone across relevant epoch unbonding record IDs
func (k Keeper) SetHostZoneUnbondingStatus(ctx sdk.Context, chainId string, epochUnbondingRecordIds []uint64, status types.HostZoneUnbonding_Status) error {
for _, epochUnbondingRecordId := range epochUnbondingRecordIds {
k.Logger(ctx).Info(fmt.Sprintf("Updating host zone unbondings on EpochUnbondingRecord %d to status %s", epochUnbondingRecordId, status.String()))

// fetch the host zone unbonding
hostZoneUnbonding, found := k.GetHostZoneUnbondingByChainId(ctx, epochUnbondingRecordId, chainId)
if !found {
errMsg := fmt.Sprintf("Error fetching host zone unbonding record for epoch: %d, host zone: %s", epochUnbondingRecordId, chainId)
k.Logger(ctx).Error(errMsg)
return errorsmod.Wrapf(stakeibctypes.ErrHostZoneNotFound, errMsg)
return errorsmod.Wrapf(types.ErrHostUnbondingRecordNotFound, "epoch number %d, chain %s",
epochUnbondingRecordId, chainId)
}
hostZoneUnbonding.Status = status

// save the updated hzu on the epoch unbonding record
updatedRecord, success := k.AddHostZoneToEpochUnbondingRecord(ctx, epochUnbondingRecordId, chainId, hostZoneUnbonding)
if !success {
errMsg := fmt.Sprintf("Error adding host zone unbonding record to epoch unbonding record: %d, host zone: %s", epochUnbondingRecordId, chainId)
k.Logger(ctx).Error(errMsg)
return errorsmod.Wrap(types.ErrAddingHostZone, errMsg)
if err := k.SetHostZoneUnbondingRecord(ctx, epochUnbondingRecordId, chainId, *hostZoneUnbonding); err != nil {
return err
}
k.SetEpochUnbondingRecord(ctx, *updatedRecord)
}
return nil
}
75 changes: 40 additions & 35 deletions x/records/keeper/epoch_unbonding_record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,25 @@ import (
"github.com/Stride-Labs/stride/v22/x/records/types"
)

// Helper function to create a new host zone unbonding record, filling in the sdkmath.Int's
// so that they can be compared
func newHostZoneUnbonding(chainId string, status types.HostZoneUnbonding_Status) types.HostZoneUnbonding {
return types.HostZoneUnbonding{
HostZoneId: chainId,
Status: status,
StTokenAmount: sdkmath.ZeroInt(),
NativeTokenAmount: sdkmath.ZeroInt(),
NativeTokensToUnbond: sdkmath.ZeroInt(),
StTokensToBurn: sdkmath.ZeroInt(),
ClaimableNativeTokens: sdkmath.ZeroInt(),
}
}

func createNEpochUnbondingRecord(keeper *keeper.Keeper, ctx sdk.Context, n int) ([]types.EpochUnbondingRecord, map[string]types.HostZoneUnbonding) {
hostZoneUnbondingsList := []types.HostZoneUnbonding{
{
HostZoneId: "host-A",
Status: types.HostZoneUnbonding_UNBONDING_QUEUE,
StTokenAmount: sdkmath.ZeroInt(),
NativeTokenAmount: sdkmath.ZeroInt(),
},
{
HostZoneId: "host-B",
Status: types.HostZoneUnbonding_UNBONDING_QUEUE,
StTokenAmount: sdkmath.ZeroInt(),
NativeTokenAmount: sdkmath.ZeroInt(),
},
{
HostZoneId: "host-C",
Status: types.HostZoneUnbonding_UNBONDING_QUEUE,
StTokenAmount: sdkmath.ZeroInt(),
NativeTokenAmount: sdkmath.ZeroInt(),
},
newHostZoneUnbonding("host-A", types.HostZoneUnbonding_UNBONDING_QUEUE),
newHostZoneUnbonding("host-B", types.HostZoneUnbonding_UNBONDING_QUEUE),
newHostZoneUnbonding("host-C", types.HostZoneUnbonding_UNBONDING_QUEUE),
}
hostZoneUnbondingsMap := make(map[string]types.HostZoneUnbonding)
for _, hostZoneUnbonding := range hostZoneUnbondingsList {
Expand Down Expand Up @@ -113,28 +112,34 @@ func TestGetHostZoneUnbondingByChainId(t *testing.T) {
)
}

func TestAddHostZoneToEpochUnbondingRecord(t *testing.T) {
keeper, ctx := keepertest.RecordsKeeper(t)
epochUnbondingRecords, _ := createNEpochUnbondingRecord(keeper, ctx, 3)
func (s *KeeperTestSuite) TestAddHostZoneToEpochUnbondingRecord() {
epochUnbondingRecords, _ := createNEpochUnbondingRecord(&s.App.RecordsKeeper, s.Ctx, 3)

epochNumber := 0
initialEpochUnbondingRecord := epochUnbondingRecords[epochNumber]
epochNumber := uint64(0)
initialEpochUnbondingRecord := epochUnbondingRecords[int(epochNumber)]

// Update host zone unbonding for host-C
updatedHostZoneUnbonding := newHostZoneUnbonding("host-C", types.HostZoneUnbonding_UNBONDING_IN_PROGRESS)

// Add new host zone to initial epoch unbonding records
newHostZone := types.HostZoneUnbonding{
HostZoneId: "host-D",
Status: types.HostZoneUnbonding_UNBONDING_QUEUE,
}
expectedEpochUnbondingRecord := initialEpochUnbondingRecord
expectedEpochUnbondingRecord.HostZoneUnbondings = append(expectedEpochUnbondingRecord.HostZoneUnbondings, &newHostZone)
expectedEpochUnbondingRecord.HostZoneUnbondings[2] = &updatedHostZoneUnbonding

updatedEpochUnbonding, err := s.App.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(s.Ctx, epochNumber, "host-C", updatedHostZoneUnbonding)
s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, updatedEpochUnbonding)
s.Require().NoError(err, "no error expected when updating host-C")
for i := 0; i < len(expectedEpochUnbondingRecord.HostZoneUnbondings); i++ {
expectedHostZoneUnbonding := *expectedEpochUnbondingRecord.HostZoneUnbondings[i]
actualHostZoneUnbonding := *updatedEpochUnbonding.HostZoneUnbondings[i]
s.Require().Equal(expectedHostZoneUnbonding, actualHostZoneUnbonding, "HZU %d after host-C update", i)
}

actualEpochUnbondingRecord, success := keeper.AddHostZoneToEpochUnbondingRecord(ctx, uint64(epochNumber), "host-D", &newHostZone)
// Add new host zone to initial epoch unbonding records
newHostZoneUnbonding := newHostZoneUnbonding("host-D", types.HostZoneUnbonding_UNBONDING_QUEUE)
expectedEpochUnbondingRecord.HostZoneUnbondings = append(expectedEpochUnbondingRecord.HostZoneUnbondings, &newHostZoneUnbonding)

require.True(t, success)
require.Equal(t,
expectedEpochUnbondingRecord,
*actualEpochUnbondingRecord,
)
updatedEpochUnbonding, err = s.App.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(s.Ctx, epochNumber, "host-D", newHostZoneUnbonding)
s.Require().NoError(err, "no error expected when adding host-D")
s.Require().Equal(expectedEpochUnbondingRecord, updatedEpochUnbonding, "EUR after host-D addition")
}

func TestSetHostZoneUnbondingStatus(t *testing.T) {
Expand Down
20 changes: 17 additions & 3 deletions x/records/types/records.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@ package types

import sdkmath "cosmossdk.io/math"

// Helper function to evaluate if a host zone unbonding record still needs to be initiated
// Helper function to evaluate if a host zone unbonding record should
// have it's unbonding initiated
// This is indicated by a record in status UNBONDING_QUEUE with a non-zero
// st token amount
func (r HostZoneUnbonding) ShouldInitiateUnbonding() bool {
notYetUnbonding := r.Status == HostZoneUnbonding_UNBONDING_QUEUE
hasAtLeastOneRecord := r.NativeTokenAmount.GT(sdkmath.ZeroInt())
return notYetUnbonding && hasAtLeastOneRecord
hasAtLeastOneRedemption := r.StTokenAmount.GT(sdkmath.ZeroInt())
return notYetUnbonding && hasAtLeastOneRedemption
}

// Helper function to evaluate if a host zone unbonding record should
// have it's unbonding retried
// This is indicated by a record in status UNBONDING_RETRY_QUEUE and
// 0 undelegations in progress
func (r HostZoneUnbonding) ShouldRetryUnbonding() bool {
hasAtLeastOneRedemption := r.StTokenAmount.GT(sdkmath.ZeroInt())
shouldRetryUnbonding := r.Status == HostZoneUnbonding_UNBONDING_RETRY_QUEUE
hasNoPendingICAs := r.UndelegationTxsInProgress == 0
return hasAtLeastOneRedemption && shouldRetryUnbonding && hasNoPendingICAs
}
Loading

0 comments on commit 74c51cb

Please sign in to comment.