Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

batched undelegations #1195

Merged
merged 13 commits into from
Jun 13, 2024
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
sampocs marked this conversation as resolved.
Show resolved Hide resolved
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 [
sampocs marked this conversation as resolved.
Show resolved Hide resolved
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
uint64 undelegation_txs_in_progress = 11;
sampocs marked this conversation as resolved.
Show resolved Hide resolved
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