From d546f7226b9290a803842284a5efff9b708fa8c9 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 13 Dec 2023 20:46:37 -0600 Subject: [PATCH 01/40] added shell for upgrade handler --- app/upgrades.go | 13 ++++++ app/upgrades/v17/upgrades.go | 75 +++++++++++++++++++++++++++++++ app/upgrades/v17/upgrades_test.go | 24 ++++++++++ 3 files changed, 112 insertions(+) create mode 100644 app/upgrades/v17/upgrades.go create mode 100644 app/upgrades/v17/upgrades_test.go diff --git a/app/upgrades.go b/app/upgrades.go index ecc9c3472c..03bad9077c 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -20,6 +20,7 @@ import ( v14 "github.com/Stride-Labs/stride/v16/app/upgrades/v14" v15 "github.com/Stride-Labs/stride/v16/app/upgrades/v15" v16 "github.com/Stride-Labs/stride/v16/app/upgrades/v16" + v17 "github.com/Stride-Labs/stride/v16/app/upgrades/v17" v2 "github.com/Stride-Labs/stride/v16/app/upgrades/v2" v3 "github.com/Stride-Labs/stride/v16/app/upgrades/v3" v4 "github.com/Stride-Labs/stride/v16/app/upgrades/v4" @@ -216,6 +217,18 @@ func (app *StrideApp) setupUpgradeHandlers(appOpts servertypes.AppOptions) { ), ) + // v17 upgrade handler + app.UpgradeKeeper.SetUpgradeHandler( + v17.UpgradeName, + v17.CreateUpgradeHandler( + app.mm, + app.configurator, + app.InterchainqueryKeeper, + app.RatelimitKeeper, + app.StakeibcKeeper, + ), + ) + upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() if err != nil { panic(fmt.Errorf("Failed to read upgrade info from disk: %w", err)) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go new file mode 100644 index 0000000000..ca4507a26d --- /dev/null +++ b/app/upgrades/v17/upgrades.go @@ -0,0 +1,75 @@ +package v17 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + + icqkeeper "github.com/Stride-Labs/stride/v16/x/interchainquery/keeper" + ratelimitkeeper "github.com/Stride-Labs/stride/v16/x/ratelimit/keeper" + stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" +) + +var ( + UpgradeName = "v17" +) + +// CreateUpgradeHandler creates an SDK upgrade handler for v15 +func CreateUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + icqKeeper icqkeeper.Keeper, + ratelimitKeeper ratelimitkeeper.Keeper, + stakeibcKeeper stakeibckeeper.Keeper, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + ctx.Logger().Info("Starting upgrade v17...") + + ctx.Logger().Info("Migrating host zones...") + + ctx.Logger().Info("Deleting all pending queries...") + + ctx.Logger().Info("Reseting slash query in progress...") + + ctx.Logger().Info("Updating community pool tax...") + + ctx.Logger().Info("Updating redemption rate bounds...") + + ctx.Logger().Info("Adding rate limits to Osmosis...") + + return mm.RunMigrations(ctx, configurator, vm) + } +} + +// Migrates the host zones to the new structure which supports community pool liquid staking +// We don't have to perform a true migration here since only new fields were added +// This will also register the relevant community pool ICA addresses +func MigrateHostZones() { + +} + +// Deletes all currently active queries (to remove ones that were stuck) +func DeleteAllPendingQueries() { + +} + +// Resets the slash query in progress flag for each validator +func ResetSlashQueryInProgress() { + +} + +// Increases the community pool tax from 2 to 5% +// This was from prop XXX which passed, but was deleted due to an ICS blacklist +func IncreaseCommunityPoolTax() { + +} + +// Updates the outer redemption rate bounds +func UpdatingRedemptionRateBounds() { + +} + +// Rate limits transfers to osmosis and deletes the injective rate limit +func UpdateRateLimits() { + +} diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go new file mode 100644 index 0000000000..2812521a34 --- /dev/null +++ b/app/upgrades/v17/upgrades_test.go @@ -0,0 +1,24 @@ +package v17_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/Stride-Labs/stride/v16/app/apptesting" +) + +type UpgradeTestSuite struct { + apptesting.AppTestHelper +} + +func (s *UpgradeTestSuite) SetupTest() { + s.Setup() +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(UpgradeTestSuite)) +} + +func (s *UpgradeTestSuite) TestUpgrade() { +} From dc607569f5b4d8bf743fb3ec348534992dd90ded Mon Sep 17 00:00:00 2001 From: sampocs Date: Thu, 14 Dec 2023 13:33:13 -0600 Subject: [PATCH 02/40] first pass at upgrade handler --- app/upgrades.go | 1 + app/upgrades/v17/upgrades.go | 227 +++++++++++++++++- .../keeper/msg_server_register_host_zone.go | 9 +- 3 files changed, 223 insertions(+), 14 deletions(-) diff --git a/app/upgrades.go b/app/upgrades.go index 03bad9077c..674b56fdc8 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -223,6 +223,7 @@ func (app *StrideApp) setupUpgradeHandlers(appOpts servertypes.AppOptions) { v17.CreateUpgradeHandler( app.mm, app.configurator, + app.DistrKeeper, app.InterchainqueryKeeper, app.RatelimitKeeper, app.StakeibcKeeper, diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index ca4507a26d..fced63664e 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -1,23 +1,56 @@ package v17 import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + "github.com/Stride-Labs/stride/v16/utils" icqkeeper "github.com/Stride-Labs/stride/v16/x/interchainquery/keeper" ratelimitkeeper "github.com/Stride-Labs/stride/v16/x/ratelimit/keeper" + ratelimittypes "github.com/Stride-Labs/stride/v16/x/ratelimit/types" stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" + stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) var ( UpgradeName = "v17" + + OsmosisTransferChannelId = "channel-5" + + CommunityPoolTax = sdk.MustNewDecFromStr("0.05") + + RedemptionRateOuterMinAdjustment = sdk.MustNewDecFromStr("0.05") + RedemptionRateInnerMinAdjustment = sdk.MustNewDecFromStr("0.03") + RedemptionRateInnerMaxAdjustment = sdk.MustNewDecFromStr("0.05") + RedemptionRateOuterMaxAdjustment = sdk.MustNewDecFromStr("0.10") + + UpdatedRateLimits = map[string]sdkmath.Int{ + "comdex-1": sdkmath.ZeroInt(), // TVL: ~130k | <1M | No rate limit + "cosmoshub-4": sdkmath.NewInt(10), // TVL: ~50M | 30M+ | 10% RL + "evmos_9001-2": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL + "injective-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL + "juno-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL + "osmosis-1": sdkmath.NewInt(25), // TVL: ~30M | 30M+ | 10% RL + "phoenix-1": sdkmath.ZeroInt(), // TVL: ~190k | <1M | No rate limit + "sommelier-3": sdkmath.ZeroInt(), // TVL: ~450k | <1M | No rate limit + "stargaze-1": sdkmath.NewInt(50), // TVL: 1.35M | 1M-15M+ | 50% RL + "umee-1": sdkmath.ZeroInt(), // TVL: ~200k | <1M | No rate limit + } ) // CreateUpgradeHandler creates an SDK upgrade handler for v15 func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, + distributionkeeper distributionkeeper.Keeper, icqKeeper icqkeeper.Keeper, ratelimitKeeper ratelimitkeeper.Keeper, stakeibcKeeper stakeibckeeper.Keeper, @@ -26,16 +59,31 @@ func CreateUpgradeHandler( ctx.Logger().Info("Starting upgrade v17...") ctx.Logger().Info("Migrating host zones...") + if err := RegisterCommunityPoolAddresses(ctx, stakeibcKeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to migrate host zones") + } ctx.Logger().Info("Deleting all pending queries...") + DeleteAllStaleQueries(ctx, icqKeeper) ctx.Logger().Info("Reseting slash query in progress...") + ResetSlashQueryInProgress(ctx, stakeibcKeeper) ctx.Logger().Info("Updating community pool tax...") + if err := IncreaseCommunityPoolTax(ctx, distributionkeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to increase community pool tax") + } ctx.Logger().Info("Updating redemption rate bounds...") + UpdateRedemptionRateBounds(ctx, stakeibcKeeper) + + ctx.Logger().Info("Update rate limits thresholds...") + UpdateRateLimitThresholds(ctx, stakeibcKeeper, ratelimitKeeper) ctx.Logger().Info("Adding rate limits to Osmosis...") + if err := RateLimitToOsmosis(ctx, ratelimitKeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to add rate limits to Osmosis") + } return mm.RunMigrations(ctx, configurator, vm) } @@ -43,33 +91,188 @@ func CreateUpgradeHandler( // Migrates the host zones to the new structure which supports community pool liquid staking // We don't have to perform a true migration here since only new fields were added +// (in other words, we can deserialize the old host zone structs into the new types) // This will also register the relevant community pool ICA addresses -func MigrateHostZones() { +func RegisterCommunityPoolAddresses(ctx sdk.Context, k stakeibckeeper.Keeper) error { + for _, hostZone := range k.GetAllHostZone(ctx) { + chainId := hostZone.ChainId -} + // Create and store a new community pool stake and redeem module address + stakeHoldingAddress := stakeibctypes.NewHostZoneModuleAddress( + chainId, + stakeibckeeper.CommunityPoolStakeHoldingAddressKey, + ) + redeemHoldingAddress := stakeibctypes.NewHostZoneModuleAddress( + chainId, + stakeibckeeper.CommunityPoolRedeemHoldingAddressKey, + ) + + if err := utils.CreateModuleAccount(ctx, k.AccountKeeper, stakeHoldingAddress); err != nil { + return errorsmod.Wrapf(err, "unable to create community pool stake account for host zone %s", chainId) + } + if err := utils.CreateModuleAccount(ctx, k.AccountKeeper, redeemHoldingAddress); err != nil { + return errorsmod.Wrapf(err, "unable to create community pool redeem account for host zone %s", chainId) + } -// Deletes all currently active queries (to remove ones that were stuck) -func DeleteAllPendingQueries() { + hostZone.CommunityPoolStakeHoldingAddress = stakeHoldingAddress.String() + hostZone.CommunityPoolRedeemHoldingAddress = redeemHoldingAddress.String() + k.SetHostZone(ctx, hostZone) + + // Register the deposit and return ICA addresses + // (these will get set in the OnChanAck callback) + // create community pool deposit account + connectionId := hostZone.ConnectionId + connectionEnd, found := k.IBCKeeper.ConnectionKeeper.GetConnection(ctx, connectionId) + if !found { + return errorsmod.Wrapf(connectiontypes.ErrConnectionNotFound, "connection %s not found", connectionId) + } + counterpartyConnectionId := connectionEnd.Counterparty.ConnectionId + + appVersion := string(icatypes.ModuleCdc.MustMarshalJSON(&icatypes.Metadata{ + Version: icatypes.Version, + ControllerConnectionId: connectionId, + HostConnectionId: counterpartyConnectionId, + Encoding: icatypes.EncodingProtobuf, + TxType: icatypes.TxTypeSDKMultiMsg, + })) + + depositAccount := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_DEPOSIT) + if err := k.ICAControllerKeeper.RegisterInterchainAccount(ctx, connectionId, depositAccount, appVersion); err != nil { + return errorsmod.Wrapf(stakeibctypes.ErrFailedToRegisterHostZone, "failed to register community pool deposit ICA") + } + + returnAccount := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_RETURN) + if err := k.ICAControllerKeeper.RegisterInterchainAccount(ctx, connectionId, returnAccount, appVersion); err != nil { + return errorsmod.Wrapf(stakeibctypes.ErrFailedToRegisterHostZone, "failed to register community pool return ICA") + } + } + + return nil } -// Resets the slash query in progress flag for each validator -func ResetSlashQueryInProgress() { +// Deletes all stale queries +func DeleteAllStaleQueries(ctx sdk.Context, k icqkeeper.Keeper) { + for _, query := range k.AllQueries(ctx) { + if query.CallbackId == stakeibckeeper.ICQCallbackID_Delegation { + k.DeleteQuery(ctx, query.Id) + } + } +} +// Resets the slash query in progress flag for each validator +func ResetSlashQueryInProgress(ctx sdk.Context, k stakeibckeeper.Keeper) { + for _, hostZone := range k.GetAllHostZone(ctx) { + for i, validator := range hostZone.Validators { + validator.SlashQueryInProgress = false + hostZone.Validators[i] = validator + } + k.SetHostZone(ctx, hostZone) + } } // Increases the community pool tax from 2 to 5% -// This was from prop XXX which passed, but was deleted due to an ICS blacklist -func IncreaseCommunityPoolTax() { - +// This was from prop 223 which passed, but was deleted due to an ICS blacklist +func IncreaseCommunityPoolTax(ctx sdk.Context, k distributionkeeper.Keeper) error { + params := k.GetParams(ctx) + params.CommunityTax = CommunityPoolTax + return k.SetParams(ctx, params) } // Updates the outer redemption rate bounds -func UpdatingRedemptionRateBounds() { +func UpdateRedemptionRateBounds(ctx sdk.Context, k stakeibckeeper.Keeper) { + for _, hostZone := range k.GetAllHostZone(ctx) { + outerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateOuterMinAdjustment) + innerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMinAdjustment) + innerMaxDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMaxAdjustment) + outerMaxDelta := hostZone.RedemptionRate.Mul(RedemptionRateOuterMaxAdjustment) + + outerMin := hostZone.RedemptionRate.Sub(outerMinDelta) + innerMin := hostZone.RedemptionRate.Sub(innerMinDelta) + innerMax := hostZone.RedemptionRate.Add(innerMaxDelta) + outerMax := hostZone.RedemptionRate.Add(outerMaxDelta) + + hostZone.MinRedemptionRate = outerMin + hostZone.MinInnerRedemptionRate = innerMin + hostZone.MaxInnerRedemptionRate = innerMax + hostZone.MaxRedemptionRate = outerMax + + k.SetHostZone(ctx, hostZone) + } +} + +// Update rate limits based on current TVL +func UpdateRateLimitThresholds(ctx sdk.Context, sk stakeibckeeper.Keeper, rk ratelimitkeeper.Keeper) { + for _, rateLimit := range rk.GetAllRateLimits(ctx) { + stDenom := rateLimit.Path.Denom + hostDenom := stDenom[2:] + // Lookup the associated host zone to get the chain ID + hostZone, err := sk.GetHostZoneFromHostDenom(ctx, stDenom) + if err != nil { + ctx.Logger().Error(fmt.Sprintf("host zone not found for denom %s", hostDenom)) + continue + } + + // Determine the expected rate limit threshold for the chain + updatedThreshold, ok := UpdatedRateLimits[hostZone.ChainId] + if !ok { + ctx.Logger().Error(fmt.Sprintf("rate limit not specified for %s", hostZone.ChainId)) + continue + } + + // If the expected threshold is 0, that means there should be no rate limit + // Remove the rate limit in this case + if updatedThreshold.IsZero() { + rk.RemoveRateLimit(ctx, rateLimit.Path.Denom, rateLimit.Path.ChannelId) + continue + } + + rateLimit.Quota.MaxPercentRecv = updatedThreshold + rateLimit.Quota.MaxPercentSend = updatedThreshold + rk.SetRateLimit(ctx, rateLimit) + } } -// Rate limits transfers to osmosis and deletes the injective rate limit -func UpdateRateLimits() { +// Rate limits transfers to osmosis across each stToken +func RateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { + for _, rateLimit := range k.GetAllRateLimits(ctx) { + denom := rateLimit.Path.Denom + + channelValue := k.GetChannelValue(ctx, denom) + if channelValue.IsZero() { + return ratelimittypes.ErrZeroChannelValue + } + + // Ignore the rate limit if it already exists (e.g. stuosmo) + _, found := k.GetRateLimit(ctx, rateLimit.Path.Denom, OsmosisTransferChannelId) + if found { + continue + } + + // Create and store the rate limit object with the same bounds as + // the original rate limit + path := ratelimittypes.Path{ + Denom: denom, + ChannelId: OsmosisTransferChannelId, + } + quota := ratelimittypes.Quota{ + MaxPercentSend: rateLimit.Quota.MaxPercentSend, + MaxPercentRecv: rateLimit.Quota.MaxPercentRecv, + DurationHours: rateLimit.Quota.DurationHours, + } + flow := ratelimittypes.Flow{ + Inflow: sdkmath.ZeroInt(), + Outflow: sdkmath.ZeroInt(), + ChannelValue: channelValue, + } + + k.SetRateLimit(ctx, ratelimittypes.RateLimit{ + Path: &path, + Quota: "a, + Flow: &flow, + }) + } + return nil } diff --git a/x/stakeibc/keeper/msg_server_register_host_zone.go b/x/stakeibc/keeper/msg_server_register_host_zone.go index 36d812156e..91a56fdd07 100644 --- a/x/stakeibc/keeper/msg_server_register_host_zone.go +++ b/x/stakeibc/keeper/msg_server_register_host_zone.go @@ -16,6 +16,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +const ( + CommunityPoolStakeHoldingAddressKey = "community-pool-stake" + CommunityPoolRedeemHoldingAddressKey = "community-pool-redeem" +) + func (k msgServer) RegisterHostZone(goCtx context.Context, msg *types.MsgRegisterHostZone) (*types.MsgRegisterHostZoneResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) @@ -76,8 +81,8 @@ func (k msgServer) RegisterHostZone(goCtx context.Context, msg *types.MsgRegiste } // Create the host zone's community pool holding accounts - communityPoolStakeAddress := types.NewHostZoneModuleAddress(chainId, "community-pool-stake") - communityPoolRedeemAddress := types.NewHostZoneModuleAddress(chainId, "community-pool-redeem") + communityPoolStakeAddress := types.NewHostZoneModuleAddress(chainId, CommunityPoolStakeHoldingAddressKey) + communityPoolRedeemAddress := types.NewHostZoneModuleAddress(chainId, CommunityPoolRedeemHoldingAddressKey) if err := utils.CreateModuleAccount(ctx, k.AccountKeeper, communityPoolStakeAddress); err != nil { return nil, errorsmod.Wrapf(err, "unable to create community pool stake account for host zone %s", chainId) } From 9fc442b45f288fb9447592acd785bdddcacb47fb Mon Sep 17 00:00:00 2001 From: sampocs Date: Thu, 14 Dec 2023 13:33:13 -0600 Subject: [PATCH 03/40] first pass at upgrade handler --- app/upgrades.go | 1 + app/upgrades/v17/upgrades.go | 231 +++++++++++++++++- .../keeper/msg_server_register_host_zone.go | 9 +- 3 files changed, 227 insertions(+), 14 deletions(-) diff --git a/app/upgrades.go b/app/upgrades.go index 03bad9077c..674b56fdc8 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -223,6 +223,7 @@ func (app *StrideApp) setupUpgradeHandlers(appOpts servertypes.AppOptions) { v17.CreateUpgradeHandler( app.mm, app.configurator, + app.DistrKeeper, app.InterchainqueryKeeper, app.RatelimitKeeper, app.StakeibcKeeper, diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index ca4507a26d..20e175361c 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -1,23 +1,60 @@ package v17 import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + "github.com/Stride-Labs/stride/v16/utils" icqkeeper "github.com/Stride-Labs/stride/v16/x/interchainquery/keeper" ratelimitkeeper "github.com/Stride-Labs/stride/v16/x/ratelimit/keeper" + ratelimittypes "github.com/Stride-Labs/stride/v16/x/ratelimit/types" stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" + stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) var ( UpgradeName = "v17" + + // Community pool tax updated from 2 -> 5% + CommunityPoolTax = sdk.MustNewDecFromStr("0.05") + + // Rate limit bounds updated to give ~3 months of slack on outer bounds + RedemptionRateOuterMinAdjustment = sdk.MustNewDecFromStr("0.05") + RedemptionRateInnerMinAdjustment = sdk.MustNewDecFromStr("0.03") + RedemptionRateInnerMaxAdjustment = sdk.MustNewDecFromStr("0.05") + RedemptionRateOuterMaxAdjustment = sdk.MustNewDecFromStr("0.10") + + // Rate limits updated according to TVL + UpdatedRateLimits = map[string]sdkmath.Int{ + "comdex-1": sdkmath.ZeroInt(), // TVL: ~130k | <1M | No rate limit + "cosmoshub-4": sdkmath.NewInt(10), // TVL: ~50M | 30M+ | 10% RL + "evmos_9001-2": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL + "injective-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL + "juno-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL + "osmosis-1": sdkmath.NewInt(25), // TVL: ~30M | 30M+ | 10% RL + "phoenix-1": sdkmath.ZeroInt(), // TVL: ~190k | <1M | No rate limit + "sommelier-3": sdkmath.ZeroInt(), // TVL: ~450k | <1M | No rate limit + "stargaze-1": sdkmath.NewInt(50), // TVL: 1.35M | 1M-15M+ | 50% RL + "umee-1": sdkmath.ZeroInt(), // TVL: ~200k | <1M | No rate limit + } + + // Osmo transfer channel is required for new rate limits + OsmosisTransferChannelId = "channel-5" ) // CreateUpgradeHandler creates an SDK upgrade handler for v15 func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, + distributionkeeper distributionkeeper.Keeper, icqKeeper icqkeeper.Keeper, ratelimitKeeper ratelimitkeeper.Keeper, stakeibcKeeper stakeibckeeper.Keeper, @@ -26,16 +63,31 @@ func CreateUpgradeHandler( ctx.Logger().Info("Starting upgrade v17...") ctx.Logger().Info("Migrating host zones...") + if err := RegisterCommunityPoolAddresses(ctx, stakeibcKeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to migrate host zones") + } ctx.Logger().Info("Deleting all pending queries...") + DeleteAllStaleQueries(ctx, icqKeeper) ctx.Logger().Info("Reseting slash query in progress...") + ResetSlashQueryInProgress(ctx, stakeibcKeeper) ctx.Logger().Info("Updating community pool tax...") + if err := IncreaseCommunityPoolTax(ctx, distributionkeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to increase community pool tax") + } ctx.Logger().Info("Updating redemption rate bounds...") + UpdateRedemptionRateBounds(ctx, stakeibcKeeper) + + ctx.Logger().Info("Update rate limits thresholds...") + UpdateRateLimitThresholds(ctx, stakeibcKeeper, ratelimitKeeper) ctx.Logger().Info("Adding rate limits to Osmosis...") + if err := RateLimitToOsmosis(ctx, ratelimitKeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to add rate limits to Osmosis") + } return mm.RunMigrations(ctx, configurator, vm) } @@ -43,33 +95,188 @@ func CreateUpgradeHandler( // Migrates the host zones to the new structure which supports community pool liquid staking // We don't have to perform a true migration here since only new fields were added +// (in other words, we can deserialize the old host zone structs into the new types) // This will also register the relevant community pool ICA addresses -func MigrateHostZones() { +func RegisterCommunityPoolAddresses(ctx sdk.Context, k stakeibckeeper.Keeper) error { + for _, hostZone := range k.GetAllHostZone(ctx) { + chainId := hostZone.ChainId -} + // Create and store a new community pool stake and redeem module address + stakeHoldingAddress := stakeibctypes.NewHostZoneModuleAddress( + chainId, + stakeibckeeper.CommunityPoolStakeHoldingAddressKey, + ) + redeemHoldingAddress := stakeibctypes.NewHostZoneModuleAddress( + chainId, + stakeibckeeper.CommunityPoolRedeemHoldingAddressKey, + ) + + if err := utils.CreateModuleAccount(ctx, k.AccountKeeper, stakeHoldingAddress); err != nil { + return errorsmod.Wrapf(err, "unable to create community pool stake account for host zone %s", chainId) + } + if err := utils.CreateModuleAccount(ctx, k.AccountKeeper, redeemHoldingAddress); err != nil { + return errorsmod.Wrapf(err, "unable to create community pool redeem account for host zone %s", chainId) + } -// Deletes all currently active queries (to remove ones that were stuck) -func DeleteAllPendingQueries() { + hostZone.CommunityPoolStakeHoldingAddress = stakeHoldingAddress.String() + hostZone.CommunityPoolRedeemHoldingAddress = redeemHoldingAddress.String() + k.SetHostZone(ctx, hostZone) + + // Register the deposit and return ICA addresses + // (these will get set in the OnChanAck callback) + // create community pool deposit account + connectionId := hostZone.ConnectionId + connectionEnd, found := k.IBCKeeper.ConnectionKeeper.GetConnection(ctx, connectionId) + if !found { + return errorsmod.Wrapf(connectiontypes.ErrConnectionNotFound, "connection %s not found", connectionId) + } + counterpartyConnectionId := connectionEnd.Counterparty.ConnectionId + + appVersion := string(icatypes.ModuleCdc.MustMarshalJSON(&icatypes.Metadata{ + Version: icatypes.Version, + ControllerConnectionId: connectionId, + HostConnectionId: counterpartyConnectionId, + Encoding: icatypes.EncodingProtobuf, + TxType: icatypes.TxTypeSDKMultiMsg, + })) + + depositAccount := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_DEPOSIT) + if err := k.ICAControllerKeeper.RegisterInterchainAccount(ctx, connectionId, depositAccount, appVersion); err != nil { + return errorsmod.Wrapf(stakeibctypes.ErrFailedToRegisterHostZone, "failed to register community pool deposit ICA") + } + + returnAccount := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_RETURN) + if err := k.ICAControllerKeeper.RegisterInterchainAccount(ctx, connectionId, returnAccount, appVersion); err != nil { + return errorsmod.Wrapf(stakeibctypes.ErrFailedToRegisterHostZone, "failed to register community pool return ICA") + } + } + + return nil } -// Resets the slash query in progress flag for each validator -func ResetSlashQueryInProgress() { +// Deletes all stale queries +func DeleteAllStaleQueries(ctx sdk.Context, k icqkeeper.Keeper) { + for _, query := range k.AllQueries(ctx) { + if query.CallbackId == stakeibckeeper.ICQCallbackID_Delegation { + k.DeleteQuery(ctx, query.Id) + } + } +} +// Resets the slash query in progress flag for each validator +func ResetSlashQueryInProgress(ctx sdk.Context, k stakeibckeeper.Keeper) { + for _, hostZone := range k.GetAllHostZone(ctx) { + for i, validator := range hostZone.Validators { + validator.SlashQueryInProgress = false + hostZone.Validators[i] = validator + } + k.SetHostZone(ctx, hostZone) + } } // Increases the community pool tax from 2 to 5% -// This was from prop XXX which passed, but was deleted due to an ICS blacklist -func IncreaseCommunityPoolTax() { - +// This was from prop 223 which passed, but was deleted due to an ICS blacklist +func IncreaseCommunityPoolTax(ctx sdk.Context, k distributionkeeper.Keeper) error { + params := k.GetParams(ctx) + params.CommunityTax = CommunityPoolTax + return k.SetParams(ctx, params) } // Updates the outer redemption rate bounds -func UpdatingRedemptionRateBounds() { +func UpdateRedemptionRateBounds(ctx sdk.Context, k stakeibckeeper.Keeper) { + for _, hostZone := range k.GetAllHostZone(ctx) { + outerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateOuterMinAdjustment) + innerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMinAdjustment) + innerMaxDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMaxAdjustment) + outerMaxDelta := hostZone.RedemptionRate.Mul(RedemptionRateOuterMaxAdjustment) + + outerMin := hostZone.RedemptionRate.Sub(outerMinDelta) + innerMin := hostZone.RedemptionRate.Sub(innerMinDelta) + innerMax := hostZone.RedemptionRate.Add(innerMaxDelta) + outerMax := hostZone.RedemptionRate.Add(outerMaxDelta) + + hostZone.MinRedemptionRate = outerMin + hostZone.MinInnerRedemptionRate = innerMin + hostZone.MaxInnerRedemptionRate = innerMax + hostZone.MaxRedemptionRate = outerMax + + k.SetHostZone(ctx, hostZone) + } +} + +// Update rate limits based on current TVL +func UpdateRateLimitThresholds(ctx sdk.Context, sk stakeibckeeper.Keeper, rk ratelimitkeeper.Keeper) { + for _, rateLimit := range rk.GetAllRateLimits(ctx) { + stDenom := rateLimit.Path.Denom + hostDenom := stDenom[2:] + // Lookup the associated host zone to get the chain ID + hostZone, err := sk.GetHostZoneFromHostDenom(ctx, stDenom) + if err != nil { + ctx.Logger().Error(fmt.Sprintf("host zone not found for denom %s", hostDenom)) + continue + } + + // Determine the expected rate limit threshold for the chain + updatedThreshold, ok := UpdatedRateLimits[hostZone.ChainId] + if !ok { + ctx.Logger().Error(fmt.Sprintf("rate limit not specified for %s", hostZone.ChainId)) + continue + } + + // If the expected threshold is 0, that means there should be no rate limit + // Remove the rate limit in this case + if updatedThreshold.IsZero() { + rk.RemoveRateLimit(ctx, rateLimit.Path.Denom, rateLimit.Path.ChannelId) + continue + } + + rateLimit.Quota.MaxPercentRecv = updatedThreshold + rateLimit.Quota.MaxPercentSend = updatedThreshold + rk.SetRateLimit(ctx, rateLimit) + } } -// Rate limits transfers to osmosis and deletes the injective rate limit -func UpdateRateLimits() { +// Rate limits transfers to osmosis across each stToken +func RateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { + for _, rateLimit := range k.GetAllRateLimits(ctx) { + denom := rateLimit.Path.Denom + + channelValue := k.GetChannelValue(ctx, denom) + if channelValue.IsZero() { + return ratelimittypes.ErrZeroChannelValue + } + + // Ignore the rate limit if it already exists (e.g. stuosmo) + _, found := k.GetRateLimit(ctx, rateLimit.Path.Denom, OsmosisTransferChannelId) + if found { + continue + } + + // Create and store the rate limit object with the same bounds as + // the original rate limit + path := ratelimittypes.Path{ + Denom: denom, + ChannelId: OsmosisTransferChannelId, + } + quota := ratelimittypes.Quota{ + MaxPercentSend: rateLimit.Quota.MaxPercentSend, + MaxPercentRecv: rateLimit.Quota.MaxPercentRecv, + DurationHours: rateLimit.Quota.DurationHours, + } + flow := ratelimittypes.Flow{ + Inflow: sdkmath.ZeroInt(), + Outflow: sdkmath.ZeroInt(), + ChannelValue: channelValue, + } + + k.SetRateLimit(ctx, ratelimittypes.RateLimit{ + Path: &path, + Quota: "a, + Flow: &flow, + }) + } + return nil } diff --git a/x/stakeibc/keeper/msg_server_register_host_zone.go b/x/stakeibc/keeper/msg_server_register_host_zone.go index 36d812156e..91a56fdd07 100644 --- a/x/stakeibc/keeper/msg_server_register_host_zone.go +++ b/x/stakeibc/keeper/msg_server_register_host_zone.go @@ -16,6 +16,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +const ( + CommunityPoolStakeHoldingAddressKey = "community-pool-stake" + CommunityPoolRedeemHoldingAddressKey = "community-pool-redeem" +) + func (k msgServer) RegisterHostZone(goCtx context.Context, msg *types.MsgRegisterHostZone) (*types.MsgRegisterHostZoneResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) @@ -76,8 +81,8 @@ func (k msgServer) RegisterHostZone(goCtx context.Context, msg *types.MsgRegiste } // Create the host zone's community pool holding accounts - communityPoolStakeAddress := types.NewHostZoneModuleAddress(chainId, "community-pool-stake") - communityPoolRedeemAddress := types.NewHostZoneModuleAddress(chainId, "community-pool-redeem") + communityPoolStakeAddress := types.NewHostZoneModuleAddress(chainId, CommunityPoolStakeHoldingAddressKey) + communityPoolRedeemAddress := types.NewHostZoneModuleAddress(chainId, CommunityPoolRedeemHoldingAddressKey) if err := utils.CreateModuleAccount(ctx, k.AccountKeeper, communityPoolStakeAddress); err != nil { return nil, errorsmod.Wrapf(err, "unable to create community pool stake account for host zone %s", chainId) } From 040d093b657fbd4d8d1aef98eeb740845a906138 Mon Sep 17 00:00:00 2001 From: sampocs Date: Mon, 18 Dec 2023 20:57:17 -0600 Subject: [PATCH 04/40] added unit tests --- app/upgrades/v17/upgrades.go | 23 +- app/upgrades/v17/upgrades_test.go | 423 ++++++++++++++++++++++++++++++ 2 files changed, 440 insertions(+), 6 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 20e175361c..807e27ead0 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -26,12 +26,17 @@ var ( // Community pool tax updated from 2 -> 5% CommunityPoolTax = sdk.MustNewDecFromStr("0.05") - // Rate limit bounds updated to give ~3 months of slack on outer bounds + // Redemption rate bounds updated to give ~3 months of slack on outer bounds RedemptionRateOuterMinAdjustment = sdk.MustNewDecFromStr("0.05") RedemptionRateInnerMinAdjustment = sdk.MustNewDecFromStr("0.03") RedemptionRateInnerMaxAdjustment = sdk.MustNewDecFromStr("0.05") RedemptionRateOuterMaxAdjustment = sdk.MustNewDecFromStr("0.10") + // Osmosis will have a slighly larger buffer with the redemption rate + // since their yield is less predictable + OsmosisChainId = "osmosis-1" + OsmosisRedemptionRateBuffer = sdk.MustNewDecFromStr("0.02") + // Rate limits updated according to TVL UpdatedRateLimits = map[string]sdkmath.Int{ "comdex-1": sdkmath.ZeroInt(), // TVL: ~130k | <1M | No rate limit @@ -39,7 +44,7 @@ var ( "evmos_9001-2": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL "injective-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL "juno-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL - "osmosis-1": sdkmath.NewInt(25), // TVL: ~30M | 30M+ | 10% RL + "osmosis-1": sdkmath.NewInt(10), // TVL: ~30M | 30M+ | 10% RL "phoenix-1": sdkmath.ZeroInt(), // TVL: ~190k | <1M | No rate limit "sommelier-3": sdkmath.ZeroInt(), // TVL: ~450k | <1M | No rate limit "stargaze-1": sdkmath.NewInt(50), // TVL: 1.35M | 1M-15M+ | 50% RL @@ -85,7 +90,7 @@ func CreateUpgradeHandler( UpdateRateLimitThresholds(ctx, stakeibcKeeper, ratelimitKeeper) ctx.Logger().Info("Adding rate limits to Osmosis...") - if err := RateLimitToOsmosis(ctx, ratelimitKeeper); err != nil { + if err := AddRateLimitToOsmosis(ctx, ratelimitKeeper); err != nil { return vm, errorsmod.Wrapf(err, "unable to add rate limits to Osmosis") } @@ -186,10 +191,16 @@ func IncreaseCommunityPoolTax(ctx sdk.Context, k distributionkeeper.Keeper) erro // Updates the outer redemption rate bounds func UpdateRedemptionRateBounds(ctx sdk.Context, k stakeibckeeper.Keeper) { for _, hostZone := range k.GetAllHostZone(ctx) { + // Give osmosis a bit more slack since OSMO stakers collect real yield + outerAdjustment := RedemptionRateOuterMaxAdjustment + if hostZone.ChainId == OsmosisChainId { + outerAdjustment = outerAdjustment.Add(OsmosisRedemptionRateBuffer) + } + outerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateOuterMinAdjustment) innerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMinAdjustment) innerMaxDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMaxAdjustment) - outerMaxDelta := hostZone.RedemptionRate.Mul(RedemptionRateOuterMaxAdjustment) + outerMaxDelta := hostZone.RedemptionRate.Mul(outerAdjustment) outerMin := hostZone.RedemptionRate.Sub(outerMinDelta) innerMin := hostZone.RedemptionRate.Sub(innerMinDelta) @@ -212,7 +223,7 @@ func UpdateRateLimitThresholds(ctx sdk.Context, sk stakeibckeeper.Keeper, rk rat hostDenom := stDenom[2:] // Lookup the associated host zone to get the chain ID - hostZone, err := sk.GetHostZoneFromHostDenom(ctx, stDenom) + hostZone, err := sk.GetHostZoneFromHostDenom(ctx, hostDenom) if err != nil { ctx.Logger().Error(fmt.Sprintf("host zone not found for denom %s", hostDenom)) continue @@ -239,7 +250,7 @@ func UpdateRateLimitThresholds(ctx sdk.Context, sk stakeibckeeper.Keeper, rk rat } // Rate limits transfers to osmosis across each stToken -func RateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { +func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { for _, rateLimit := range k.GetAllRateLimits(ctx) { denom := rateLimit.Path.Denom diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 2812521a34..41ec170b6b 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -1,13 +1,48 @@ package v17_test import ( + "fmt" "testing" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" "github.com/stretchr/testify/suite" + icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types" + ratelimittypes "github.com/Stride-Labs/stride/v16/x/ratelimit/types" + "github.com/Stride-Labs/stride/v16/app/apptesting" + v17 "github.com/Stride-Labs/stride/v16/app/upgrades/v17" + stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" + stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) +type UpdateRedemptionRateBounds struct { + ChainId string + CurrentRedemptionRate sdk.Dec + ExpectedMinOuterRedemptionRate sdk.Dec + ExpectedMinInnerRedemptionRate sdk.Dec + ExpectedMaxInnerRedemptionRate sdk.Dec + ExpectedMaxOuterRedemptionRate sdk.Dec +} + +type UpdateRateLimits struct { + ChainId string + ChannelId string + RateLimitDenom string + HostDenom string + Duration uint64 + Threshold sdkmath.Int +} + +type AddRateLimits struct { + ChannelId string + Denom string + ChannelValue sdkmath.Int +} + type UpgradeTestSuite struct { apptesting.AppTestHelper } @@ -21,4 +56,392 @@ func TestKeeperTestSuite(t *testing.T) { } func (s *UpgradeTestSuite) TestUpgrade() { + +} + +func (s *UpgradeTestSuite) TestRegisterCommunityPoolAddresses() { + // Create 3 host zones, with empty ICA addresses + chainIds := []string{} + for i := 1; i <= 3; i++ { + chainId := fmt.Sprintf("chain-%d", i) + connectionId := fmt.Sprintf("connection-%d", i) + clientId := fmt.Sprintf("07-tendermint-%d", i) + + s.App.StakeibcKeeper.SetHostZone(s.Ctx, stakeibctypes.HostZone{ + ChainId: chainId, + ConnectionId: connectionId, + }) + chainIds = append(chainIds, chainId) + + // Mock out the consensus state to test registering an interchain account + s.MockClientAndConnection(chainId, clientId, connectionId) + } + + // Register the accounts + err := v17.RegisterCommunityPoolAddresses(s.Ctx, s.App.StakeibcKeeper) + s.Require().NoError(err, "no error expected when registering ICA addresses") + + // Confirm the module accounts were created and stored on each host + for _, chainId := range chainIds { + hostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, chainId) + s.Require().True(found, "host zone should have been found") + + s.Require().NotEmpty(hostZone.CommunityPoolStakeHoldingAddress, "stake holding for %s should not be empty", chainId) + s.Require().NotEmpty(hostZone.CommunityPoolRedeemHoldingAddress, "redeem holding for %s should not be empty", chainId) + + stakeHoldingAddress := sdk.MustAccAddressFromBech32(hostZone.CommunityPoolStakeHoldingAddress) + redeemHoldingAddress := sdk.MustAccAddressFromBech32(hostZone.CommunityPoolRedeemHoldingAddress) + + stakeHoldingAccount := s.App.AccountKeeper.GetAccount(s.Ctx, stakeHoldingAddress) + redeemHoldingAccount := s.App.AccountKeeper.GetAccount(s.Ctx, redeemHoldingAddress) + + s.Require().NotNil(stakeHoldingAccount, "stake holding account should have been registered for %s", chainId) + s.Require().NotNil(redeemHoldingAccount, "redeem holding account should have been registered for %s", chainId) + } + + // Confirm the ICAs were registered + // The addresses don't get set until the callback, but we can check the events and confirm they were registered + for _, chainId := range chainIds { + depositOwner := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_DEPOSIT) + returnOwner := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_RETURN) + + expectedDepositPortId, _ := icatypes.NewControllerPortID(depositOwner) + expectedReturnPortId, _ := icatypes.NewControllerPortID(returnOwner) + + s.CheckEventValueEmitted(channeltypes.EventTypeChannelOpenInit, channeltypes.AttributeKeyPortID, expectedDepositPortId) + s.CheckEventValueEmitted(channeltypes.EventTypeChannelOpenInit, channeltypes.AttributeKeyPortID, expectedReturnPortId) + } + +} + +func (s *UpgradeTestSuite) TestDeleteAllStaleQueries() { + // Create queries - half of which are for slashed (CallbackId: Delegation) + initialQueries := 10 + for i := 1; i <= initialQueries; i++ { + queryId := fmt.Sprintf("query-%d", i) + + // Alternate slash queries vs other query + callbackId := stakeibckeeper.ICQCallbackID_Delegation + if i%2 == 0 { + callbackId = stakeibckeeper.ICQCallbackID_FeeBalance // arbitrary + } + + s.App.InterchainqueryKeeper.SetQuery(s.Ctx, icqtypes.Query{ + Id: queryId, + CallbackId: callbackId, + }) + } + + // Delete stale queries + v17.DeleteAllStaleQueries(s.Ctx, s.App.InterchainqueryKeeper) + + // Check that only half the queries are remaining and none are for slashes + remainingQueries := s.App.InterchainqueryKeeper.AllQueries(s.Ctx) + s.Require().Equal(initialQueries/2, len(remainingQueries), "half the queries should have been removed") + + for _, query := range remainingQueries { + s.Require().NotEqual(stakeibckeeper.ICQCallbackID_Delegation, query.CallbackId, + "all slash queries should have been removed") + } +} + +func (s *UpgradeTestSuite) TestResetSlashQueryInProgress() { + // Set multiple host zones, each with multiple validators that have slash query in progress set to true + for hostIndex := 1; hostIndex <= 3; hostIndex++ { + chainId := fmt.Sprintf("chain-%d", hostIndex) + + validators := []*stakeibctypes.Validator{} + for validatorIndex := 1; validatorIndex <= 6; validatorIndex++ { + address := fmt.Sprintf("val-%d", validatorIndex) + + inProgress := true + if validatorIndex%2 == 0 { + inProgress = false + } + + validators = append(validators, &stakeibctypes.Validator{ + Address: address, + SlashQueryInProgress: inProgress, + }) + } + + s.App.StakeibcKeeper.SetHostZone(s.Ctx, stakeibctypes.HostZone{ + ChainId: chainId, + Validators: validators, + }) + } + + // Reset the slash queries + v17.ResetSlashQueryInProgress(s.Ctx, s.App.StakeibcKeeper) + + // Confirm they were all reset + for _, hostZone := range s.App.StakeibcKeeper.GetAllHostZone(s.Ctx) { + for _, validator := range hostZone.Validators { + s.Require().False(validator.SlashQueryInProgress, + "%s %s - slash query in progress should have been reset", hostZone.ChainId, validator.Address) + } + } +} + +func (s *UpgradeTestSuite) TestIncreaseCommunityPoolTax() { + // Set initial community pool tax to 2% + initialTax := sdk.MustNewDecFromStr("0.02") + params := s.App.DistrKeeper.GetParams(s.Ctx) + params.CommunityTax = initialTax + err := s.App.DistrKeeper.SetParams(s.Ctx, params) + s.Require().NoError(err, "no error expected when setting params") + + // Increase the tax + err = v17.IncreaseCommunityPoolTax(s.Ctx, s.App.DistrKeeper) + s.Require().NoError(err, "no error expected when increasing community pool tax") + + // Confirm it increased + updatedParams := s.App.DistrKeeper.GetParams(s.Ctx) + s.Require().Equal(v17.CommunityPoolTax.String(), updatedParams.CommunityTax.String(), + "community pool tax should have been updated") +} + +func (s *UpgradeTestSuite) TestUpdateRedemptionRateBounds() { + // Define test cases consisting of an initial redemption rate and expected bounds + testCases := []UpdateRedemptionRateBounds{ + { + ChainId: "chain-0", + CurrentRedemptionRate: sdk.MustNewDecFromStr("1.0"), + ExpectedMinOuterRedemptionRate: sdk.MustNewDecFromStr("0.95"), // 1 - 5% = 0.95 + ExpectedMinInnerRedemptionRate: sdk.MustNewDecFromStr("0.97"), // 1 - 3% = 0.97 + ExpectedMaxInnerRedemptionRate: sdk.MustNewDecFromStr("1.05"), // 1 + 5% = 1.05 + ExpectedMaxOuterRedemptionRate: sdk.MustNewDecFromStr("1.10"), // 1 + 10% = 1.1 + }, + { + ChainId: "chain-1", + CurrentRedemptionRate: sdk.MustNewDecFromStr("1.1"), + ExpectedMinOuterRedemptionRate: sdk.MustNewDecFromStr("1.045"), // 1.1 - 5% = 1.045 + ExpectedMinInnerRedemptionRate: sdk.MustNewDecFromStr("1.067"), // 1.1 - 3% = 1.067 + ExpectedMaxInnerRedemptionRate: sdk.MustNewDecFromStr("1.155"), // 1.1 + 5% = 1.155 + ExpectedMaxOuterRedemptionRate: sdk.MustNewDecFromStr("1.210"), // 1.1 + 10% = 1.21 + }, + { + // Max outer for osmo uses 12% instead of 10% + ChainId: v17.OsmosisChainId, + CurrentRedemptionRate: sdk.MustNewDecFromStr("1.25"), + ExpectedMinOuterRedemptionRate: sdk.MustNewDecFromStr("1.1875"), // 1.25 - 5% = 1.1875 + ExpectedMinInnerRedemptionRate: sdk.MustNewDecFromStr("1.2125"), // 1.25 - 3% = 1.2125 + ExpectedMaxInnerRedemptionRate: sdk.MustNewDecFromStr("1.3125"), // 1.25 + 5% = 1.3125 + ExpectedMaxOuterRedemptionRate: sdk.MustNewDecFromStr("1.4000"), // 1.25 + 12% = 1.400 + }, + } + + // Create a host zone for each test case + for _, tc := range testCases { + hostZone := stakeibctypes.HostZone{ + ChainId: tc.ChainId, + RedemptionRate: tc.CurrentRedemptionRate, + } + s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone) + } + + // Update the redemption rate bounds + v17.UpdateRedemptionRateBounds(s.Ctx, s.App.StakeibcKeeper) + + // Confirm they were all updated + for _, tc := range testCases { + hostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, tc.ChainId) + s.Require().True(found) + + s.Require().Equal(tc.ExpectedMinOuterRedemptionRate, hostZone.MinRedemptionRate, "%s - min outer", tc.ChainId) + s.Require().Equal(tc.ExpectedMinInnerRedemptionRate, hostZone.MinInnerRedemptionRate, "%s - min inner", tc.ChainId) + s.Require().Equal(tc.ExpectedMaxInnerRedemptionRate, hostZone.MaxInnerRedemptionRate, "%s - max inner", tc.ChainId) + s.Require().Equal(tc.ExpectedMaxOuterRedemptionRate, hostZone.MaxRedemptionRate, "%s - max outer", tc.ChainId) + } +} + +func (s *UpgradeTestSuite) TestUpdateRateLimitThresholds() { + initialThreshold := sdkmath.OneInt() + + // Define test cases consisting of an initial redemption rates and expected bounds + testCases := map[string]UpdateRateLimits{ + "cosmoshub": { + // 10% threshold + ChainId: "cosmoshub-4", + ChannelId: "channel-0", + HostDenom: "uatom", + RateLimitDenom: "stuatom", + Duration: 10, + Threshold: sdkmath.NewInt(10), + }, + "osmosis": { + // 10% threshold + ChainId: "osmosis-1", + ChannelId: "channel-1", + HostDenom: "uosmo", + RateLimitDenom: "stuosmo", + Duration: 20, + Threshold: sdkmath.NewInt(10), + }, + "juno": { + // No denom on matching host + ChainId: "juno-1", + ChannelId: "channel-2", + HostDenom: "ujuno", + RateLimitDenom: "different-denom", + Duration: 30, + }, + "sommelier": { + // Rate limit should get removed + ChainId: "sommelier-3", + ChannelId: "channel-3", + HostDenom: "usomm", + RateLimitDenom: "stusomm", + Duration: 40, + }, + } + + // Set rate limits and host zones + for _, tc := range testCases { + s.App.RatelimitKeeper.SetRateLimit(s.Ctx, ratelimittypes.RateLimit{ + Path: &ratelimittypes.Path{ + Denom: tc.RateLimitDenom, + ChannelId: tc.ChannelId, + }, + Quota: &ratelimittypes.Quota{ + MaxPercentSend: initialThreshold, + MaxPercentRecv: initialThreshold, + DurationHours: tc.Duration, + }, + }) + + s.App.StakeibcKeeper.SetHostZone(s.Ctx, stakeibctypes.HostZone{ + ChainId: tc.ChainId, + HostDenom: tc.HostDenom, + }) + } + + // Update rate limits + v17.UpdateRateLimitThresholds(s.Ctx, s.App.StakeibcKeeper, s.App.RatelimitKeeper) + + // Check that the osmo and gaia rate limits were updated + for _, chainName := range []string{"cosmoshub", "osmosis"} { + testCase := testCases[chainName] + actualRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, testCase.RateLimitDenom, testCase.ChannelId) + s.Require().True(found, "rate limit should have been found") + + // Check that the thresholds were updated + s.Require().Equal(testCase.Threshold, actualRateLimit.Quota.MaxPercentSend, "%s - max percent send", chainName) + s.Require().Equal(testCase.Threshold, actualRateLimit.Quota.MaxPercentRecv, "%s - max percent recv", chainName) + s.Require().Equal(testCase.Duration, actualRateLimit.Quota.DurationHours, "%s - duration", chainName) + } + + // Check that the juno rate limit was not touched + // (since there was no matching host denom + junoTestCase := testCases["juno"] + actualRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, junoTestCase.RateLimitDenom, junoTestCase.ChannelId) + s.Require().True(found, "juno rate limit should have been found") + + s.Require().Equal(initialThreshold, actualRateLimit.Quota.MaxPercentSend, "juno max percent send") + s.Require().Equal(initialThreshold, actualRateLimit.Quota.MaxPercentRecv, "juno max percent recv") + + // Check that the somm rate limit was removed + sommTestCase := testCases["sommelier"] + _, found = s.App.RatelimitKeeper.GetRateLimit(s.Ctx, sommTestCase.RateLimitDenom, sommTestCase.ChannelId) + s.Require().False(found, "somm rate limit should have been removed") +} + +func (s *UpgradeTestSuite) TestAddRateLimitToOsmosis() { + initialThreshold := sdkmath.OneInt() + initialFlow := sdkmath.NewInt(100) + initialDuration := uint64(24) + initialChannelValue := sdk.NewInt(1000) + + // Define the test cases for adding new rate limits + testCases := map[string]AddRateLimits{ + "cosmoshub": { + // Will add a new rate limit to osmo + Denom: "stuatom", + ChannelId: "channel-0", + ChannelValue: sdkmath.NewInt(100), + }, + "osmosis": { + // Rate limit already exists, should not be touched + Denom: "stuosmo", + ChannelId: v17.OsmosisTransferChannelId, + ChannelValue: sdkmath.NewInt(300), + }, + "stargaze": { + // Will add a new rate limit to stars + Denom: "stustars", + ChannelId: "channel-1", + ChannelValue: sdkmath.NewInt(200), + }, + } + + // Setup the initial rate limits + for _, tc := range testCases { + s.App.RatelimitKeeper.SetRateLimit(s.Ctx, ratelimittypes.RateLimit{ + Path: &ratelimittypes.Path{ + Denom: tc.Denom, + ChannelId: tc.ChannelId, + }, + Quota: &ratelimittypes.Quota{ + MaxPercentSend: initialThreshold, + MaxPercentRecv: initialThreshold, + DurationHours: initialDuration, + }, + Flow: &ratelimittypes.Flow{ + Outflow: initialFlow, + Inflow: initialFlow, + ChannelValue: initialChannelValue, + }, + }) + + // mint tokens so there's a supply for the channel value + s.FundAccount(s.TestAccs[0], sdk.NewCoin(tc.Denom, tc.ChannelValue)) + } + + // Add the rate limits to osmo + err := v17.AddRateLimitToOsmosis(s.Ctx, s.App.RatelimitKeeper) + s.Require().NoError(err, "no error expected when adding rate limit to osmosis") + + // Check that we have new rate limits for gaia and stars + newRateLimits := []string{"cosmoshub", "stargaze"} + updatedRateLimits := s.App.RatelimitKeeper.GetAllRateLimits(s.Ctx) + s.Require().Equal(len(testCases)+len(newRateLimits), len(updatedRateLimits), "number of ending rate limits") + + for _, chainName := range newRateLimits { + testCase := testCases[chainName] + + // Confirm the new rate limit was created + actualRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, testCase.Denom, v17.OsmosisTransferChannelId) + s.Require().True(found, "new rate limit for %s to osmosis should have been found", chainName) + + // Confirm the thresholds remained the same + s.Require().Equal(initialThreshold, actualRateLimit.Quota.MaxPercentSend, "%s - max percent send", chainName) + s.Require().Equal(initialThreshold, actualRateLimit.Quota.MaxPercentRecv, "%s - max percent recv", chainName) + s.Require().Equal(initialDuration, actualRateLimit.Quota.DurationHours, "%s - duration", chainName) + + // Confirm the flow as reset + s.Require().Zero(actualRateLimit.Flow.Outflow.Int64(), "%s - outflow", chainName) + s.Require().Zero(actualRateLimit.Flow.Inflow.Int64(), "%s - inflow", chainName) + s.Require().Equal(testCase.ChannelValue, actualRateLimit.Flow.ChannelValue, "%s - channel value", chainName) + } + + // Confirm the osmo rate limit was not touched + osmoTestCase := testCases["osmosis"] + osmoRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, osmoTestCase.Denom, v17.OsmosisTransferChannelId) + s.Require().True(found, "rate limit for osmosis should have been found") + + s.Require().Equal(osmoRateLimit.Flow.Outflow, osmoRateLimit.Flow.Outflow, "osmos outflow") + s.Require().Equal(osmoRateLimit.Flow.Inflow, osmoRateLimit.Flow.Inflow, "osmos inflow") + s.Require().Equal(osmoRateLimit.Flow.ChannelValue, osmoRateLimit.Flow.ChannelValue, "osmos channel value") + + // Add a rate limit with zero channel value and confirm we cannot add a rate limit with that denom to osmosis + nonExistentDenom := "denom" + s.App.RatelimitKeeper.SetRateLimit(s.Ctx, ratelimittypes.RateLimit{ + Path: &ratelimittypes.Path{ + Denom: nonExistentDenom, + ChannelId: "channel-6", + }, + }) + + err = v17.AddRateLimitToOsmosis(s.Ctx, s.App.RatelimitKeeper) + s.Require().ErrorContains(err, "channel value is zero") } From d5648a8091e722f1b2e2d6f02059cc27b3114036 Mon Sep 17 00:00:00 2001 From: sampocs Date: Tue, 19 Dec 2023 13:11:45 -0600 Subject: [PATCH 05/40] add unit test for full upgrade --- app/apptesting/test_helpers.go | 6 +- app/upgrades/v17/upgrades.go | 2 +- app/upgrades/v17/upgrades_test.go | 323 +++++++++++++++++++++++++++--- x/ratelimit/keeper/hooks.go | 5 +- 4 files changed, 301 insertions(+), 35 deletions(-) diff --git a/app/apptesting/test_helpers.go b/app/apptesting/test_helpers.go index ee6432706a..3d6700fd43 100644 --- a/app/apptesting/test_helpers.go +++ b/app/apptesting/test_helpers.go @@ -519,7 +519,11 @@ func (s *AppTestHelper) MockICAChannel(connectionId, channelId, owner, address s func (s *AppTestHelper) ConfirmUpgradeSucceededs(upgradeName string, upgradeHeight int64) { s.Ctx = s.Ctx.WithBlockHeight(upgradeHeight - 1) - plan := upgradetypes.Plan{Name: upgradeName, Height: upgradeHeight} + plan := upgradetypes.Plan{ + Name: upgradeName, + Height: upgradeHeight, + } + err := s.App.UpgradeKeeper.ScheduleUpgrade(s.Ctx, plan) s.Require().NoError(err) _, exists := s.App.UpgradeKeeper.GetUpgradePlan(s.Ctx) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 807e27ead0..1bb7466e26 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -69,7 +69,7 @@ func CreateUpgradeHandler( ctx.Logger().Info("Migrating host zones...") if err := RegisterCommunityPoolAddresses(ctx, stakeibcKeeper); err != nil { - return vm, errorsmod.Wrapf(err, "unable to migrate host zones") + return vm, errorsmod.Wrapf(err, "unable to register community pool addresses on host zones") } ctx.Logger().Info("Deleting all pending queries...") diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 41ec170b6b..106f72e3bd 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -7,7 +7,6 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" - channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" "github.com/stretchr/testify/suite" icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types" @@ -15,10 +14,24 @@ import ( "github.com/Stride-Labs/stride/v16/app/apptesting" v17 "github.com/Stride-Labs/stride/v16/app/upgrades/v17" + "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) +const ( + GaiaChainId = "cosmoshub-4" + SommelierChainId = "sommelier-3" + + Atom = "uatom" + Osmo = "uosmo" + Somm = "usomm" + + StAtom = "st" + Atom + StOsmo = "st" + Osmo + StSomm = "st" + Somm +) + type UpdateRedemptionRateBounds struct { ChainId string CurrentRedemptionRate sdk.Dec @@ -56,7 +69,283 @@ func TestKeeperTestSuite(t *testing.T) { } func (s *UpgradeTestSuite) TestUpgrade() { + dummyUpgradeHeight := int64(5) + + // Setup store before upgrade + checkHostZonesAfterUpgrade := s.SetupHostZonesBeforeUpgrade() + checkRateLimitsAfterUpgrade := s.SetupRateLimitsBeforeUpgrade() + checkCommunityPoolTaxAfterUpgrade := s.SetupCommunityPoolTaxBeforeUpgrade() + checkQueriesAfterUpgrade := s.SetupQueriesBeforeUpgrade() + + // Submit upgrade and confirm handler succeeds + s.ConfirmUpgradeSucceededs("v17", dummyUpgradeHeight) + + // Check state after upgrade + checkHostZonesAfterUpgrade() + checkRateLimitsAfterUpgrade() + checkCommunityPoolTaxAfterUpgrade() + checkQueriesAfterUpgrade() +} + +// Helper function to check that the community pool stake and redeem holding +// module accounts were registered and stored on the host zone +func (s *UpgradeTestSuite) checkCommunityPoolModuleAccountsRegistered(chainId string) { + hostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, chainId) + s.Require().True(found, "host zone should have been found") + + s.Require().NotEmpty(hostZone.CommunityPoolStakeHoldingAddress, "stake holding for %s should not be empty", chainId) + s.Require().NotEmpty(hostZone.CommunityPoolRedeemHoldingAddress, "redeem holding for %s should not be empty", chainId) + + stakeHoldingAddress := sdk.MustAccAddressFromBech32(hostZone.CommunityPoolStakeHoldingAddress) + redeemHoldingAddress := sdk.MustAccAddressFromBech32(hostZone.CommunityPoolRedeemHoldingAddress) + stakeHoldingAccount := s.App.AccountKeeper.GetAccount(s.Ctx, stakeHoldingAddress) + redeemHoldingAccount := s.App.AccountKeeper.GetAccount(s.Ctx, redeemHoldingAddress) + + s.Require().NotNil(stakeHoldingAccount, "stake holding account should have been registered for %s", chainId) + s.Require().NotNil(redeemHoldingAccount, "redeem holding account should have been registered for %s", chainId) +} + +// Helper function to check that the community pool deposit and return ICA accounts were registered for the host zone +// The addresses don't get set until the callback, but we can check that the expected ICA controller port was claimed +// by the ICA controller module +func (s *UpgradeTestSuite) checkCommunityPoolICAAccountsRegistered(chainId string) { + depositOwner := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_DEPOSIT) + returnOwner := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_RETURN) + + expectedDepositPortId, _ := icatypes.NewControllerPortID(depositOwner) + expectedReturnPortId, _ := icatypes.NewControllerPortID(returnOwner) + + depositPortIdRegistered := s.App.ICAControllerKeeper.IsBound(s.Ctx, expectedDepositPortId) + returnPortIdRegistered := s.App.ICAControllerKeeper.IsBound(s.Ctx, expectedReturnPortId) + + s.Require().True(depositPortIdRegistered, "deposit port %s should have been bound", expectedDepositPortId) + s.Require().True(returnPortIdRegistered, "return port %s should have been bound", expectedReturnPortId) +} + +func (s *UpgradeTestSuite) SetupHostZonesBeforeUpgrade() func() { + hostZones := []stakeibctypes.HostZone{ + { + ChainId: GaiaChainId, + HostDenom: Atom, + ConnectionId: "connection-1", + Validators: []*stakeibctypes.Validator{ + {Address: "val1", SlashQueryInProgress: false}, + {Address: "val2", SlashQueryInProgress: true}, + }, + RedemptionRate: sdk.MustNewDecFromStr("1.1"), + }, + { + ChainId: v17.OsmosisChainId, + HostDenom: Osmo, + ConnectionId: "connection-2", + Validators: []*stakeibctypes.Validator{ + {Address: "val3", SlashQueryInProgress: true}, + {Address: "val4", SlashQueryInProgress: false}, + }, + RedemptionRate: sdk.MustNewDecFromStr("1.2"), + }, + { + // This host is just added for the rate limit test + // No need to validate accounts and redemptino rates + ChainId: SommelierChainId, + HostDenom: Somm, + ConnectionId: "connection-3", + Validators: []*stakeibctypes.Validator{ + {Address: "val5", SlashQueryInProgress: true}, + {Address: "val6", SlashQueryInProgress: false}, + }, + RedemptionRate: sdk.MustNewDecFromStr("1.0"), + }, + } + + for i, hostZone := range hostZones { + s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone) + + clientId := fmt.Sprintf("07-tendermint-%d", i) + s.MockClientAndConnection(hostZone.ChainId, clientId, hostZone.ConnectionId) + } + + // Return callback to check store after upgrade + return func() { + // Check that the module and ICA accounts were registered + s.checkCommunityPoolModuleAccountsRegistered(GaiaChainId) + s.checkCommunityPoolModuleAccountsRegistered(v17.OsmosisChainId) + + s.checkCommunityPoolICAAccountsRegistered(GaiaChainId) + s.checkCommunityPoolICAAccountsRegistered(v17.OsmosisChainId) + + // Check that the redemption rate bounds were set + gaiaHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, GaiaChainId) + s.Require().True(found) + + s.Require().Equal(sdk.MustNewDecFromStr("1.045"), gaiaHostZone.MinRedemptionRate, "gaia min outer") // 1.1 - 5% = 1.045 + s.Require().Equal(sdk.MustNewDecFromStr("1.067"), gaiaHostZone.MinInnerRedemptionRate, "gaia min inner") // 1.1 - 3% = 1.067 + s.Require().Equal(sdk.MustNewDecFromStr("1.155"), gaiaHostZone.MaxInnerRedemptionRate, "gaia max inner") // 1.1 + 5% = 1.155 + s.Require().Equal(sdk.MustNewDecFromStr("1.210"), gaiaHostZone.MaxRedemptionRate, "gaia max outer") // 1.1 + 10% = 1.21 + + osmoHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, "osmosis-1") + s.Require().True(found) + + s.Require().Equal(sdk.MustNewDecFromStr("1.140"), osmoHostZone.MinRedemptionRate, "osmo min outer") // 1.2 - 5% = 1.140 + s.Require().Equal(sdk.MustNewDecFromStr("1.164"), osmoHostZone.MinInnerRedemptionRate, "osmo min inner") // 1.2 - 3% = 1.164 + s.Require().Equal(sdk.MustNewDecFromStr("1.260"), osmoHostZone.MaxInnerRedemptionRate, "osmo max inner") // 1.2 + 5% = 1.260 + s.Require().Equal(sdk.MustNewDecFromStr("1.344"), osmoHostZone.MaxRedemptionRate, "osmo max outer") // 1.2 + 12% = 1.344 + + // Check that there are no slash queries in progress + for _, hostZone := range s.App.StakeibcKeeper.GetAllHostZone(s.Ctx) { + for _, validator := range hostZone.Validators { + s.Require().False(validator.SlashQueryInProgress, "slash query in progress should have been set to false") + } + } + } +} + +func (s *UpgradeTestSuite) SetupRateLimitsBeforeUpgrade() func() { + gaiaChannelId := "channel-0" + + initialThreshold := sdk.OneInt() + initialFlow := sdkmath.NewInt(10) + initialChannelValue := sdkmath.NewInt(100) + updatedChannelValue := sdkmath.NewInt(200) + + ratesLimits := []AddRateLimits{ + { + // Gaia rate limit + // Treshold should be updated, new gaia RL added + Denom: StAtom, + ChannelId: gaiaChannelId, + }, + { + // Osmo rate limit + // Treshold should be updated, no new RL added + Denom: StOsmo, + ChannelId: v17.OsmosisTransferChannelId, + }, + { + // Somm rate limit + // Should be removed + Denom: StSomm, + ChannelId: "channel-10", + }, + } + + // Add rate limits + // No need to register host zones since they're initialized in the setup host zone function + for _, rateLimit := range ratesLimits { + s.App.RatelimitKeeper.SetRateLimit(s.Ctx, ratelimittypes.RateLimit{ + Path: &ratelimittypes.Path{ + Denom: rateLimit.Denom, + ChannelId: rateLimit.ChannelId, + }, + Quota: &ratelimittypes.Quota{ + MaxPercentSend: initialThreshold, + MaxPercentRecv: initialThreshold, + DurationHours: 10, + }, + Flow: &ratelimittypes.Flow{ + Outflow: initialFlow, + Inflow: initialFlow, + ChannelValue: initialChannelValue, + }, + }) + + // mint the token for the channel value + s.FundAccount(s.TestAccs[0], sdk.NewCoin(rateLimit.Denom, updatedChannelValue)) + } + + // Return callback to check store after upgrade + return func() { + // Check that we have 3 RLs + // (1) stuosmo on Stride -> Osmosis + // (2) stuatom on Stride -> Gaia + // (3) stuatom on Stride -> Osmosis + acutalRateLimits := s.App.RatelimitKeeper.GetAllRateLimits(s.Ctx) + s.Require().Len(acutalRateLimits, 3, "there should be 3 rate limits at the end") + + // Check the stosmo rate limit + stOsmoRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, StOsmo, v17.OsmosisTransferChannelId) + s.Require().True(found) + + osmoThreshold := v17.UpdatedRateLimits[v17.OsmosisChainId] + s.Require().Equal(osmoThreshold, stOsmoRateLimit.Quota.MaxPercentSend, "stosmo max percent send") + s.Require().Equal(osmoThreshold, stOsmoRateLimit.Quota.MaxPercentRecv, "stosmo max percent recv") + s.Require().Equal(initialFlow, stOsmoRateLimit.Flow.Outflow, "stosmo outflow") + s.Require().Equal(initialFlow, stOsmoRateLimit.Flow.Inflow, "stosmo inflow") + s.Require().Equal(initialChannelValue, stOsmoRateLimit.Flow.ChannelValue, "stosmo channel value") + + // Check the stuatom -> Gaia rate limit + stAtomToGaiaRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, StAtom, gaiaChannelId) + s.Require().True(found) + + atomThreshold := v17.UpdatedRateLimits[GaiaChainId] + s.Require().Equal(atomThreshold, stAtomToGaiaRateLimit.Quota.MaxPercentSend, "statom -> gaia max percent send") + s.Require().Equal(atomThreshold, stAtomToGaiaRateLimit.Quota.MaxPercentRecv, "statom -> gaia max percent recv") + s.Require().Equal(initialFlow, stAtomToGaiaRateLimit.Flow.Outflow, "statom -> gaia outflow") + s.Require().Equal(initialFlow, stAtomToGaiaRateLimit.Flow.Inflow, "statom -> gaia inflow") + s.Require().Equal(initialChannelValue, stAtomToGaiaRateLimit.Flow.ChannelValue, "statom -> gaia channel value") + + // Check the stuatom -> Osmo rate limit + // The flow should be reset to 0 and the channel value should update + stAtomToOsmoRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, StAtom, v17.OsmosisTransferChannelId) + s.Require().True(found) + + s.Require().Equal(atomThreshold, stAtomToOsmoRateLimit.Quota.MaxPercentSend, "statom -> osmo max percent send") + s.Require().Equal(atomThreshold, stAtomToOsmoRateLimit.Quota.MaxPercentRecv, "statom -> osmo max percent recv") + + s.Require().Zero(stAtomToOsmoRateLimit.Flow.Outflow.Int64(), "statom -> osmo outflow") + s.Require().Zero(stAtomToOsmoRateLimit.Flow.Inflow.Int64(), "statom -> osmo inflow") + s.Require().Equal(updatedChannelValue, stAtomToOsmoRateLimit.Flow.ChannelValue, "statom -> osmo channel value") + } +} + +func (s *UpgradeTestSuite) SetupCommunityPoolTaxBeforeUpgrade() func() { + // Set initial community pool tax to 2% + initialTax := sdk.MustNewDecFromStr("0.02") + params := s.App.DistrKeeper.GetParams(s.Ctx) + params.CommunityTax = initialTax + err := s.App.DistrKeeper.SetParams(s.Ctx, params) + s.Require().NoError(err, "no error expected when setting params") + + // Return callback to check store after upgrade + return func() { + // Confirm the tax increased + updatedParams := s.App.DistrKeeper.GetParams(s.Ctx) + s.Require().Equal(v17.CommunityPoolTax.String(), updatedParams.CommunityTax.String(), + "community pool tax should have been updated") + } +} + +func (s *UpgradeTestSuite) SetupQueriesBeforeUpgrade() func() { + // Set two queries - one for a slash, and one that's not for a slash + queries := []icqtypes.Query{ + { + Id: "query-1", + CallbackId: keeper.ICQCallbackID_Delegation, + }, + { + Id: "query-2", + CallbackId: keeper.ICQCallbackID_Calibrate, + }, + } + for _, query := range queries { + s.App.InterchainqueryKeeper.SetQuery(s.Ctx, query) + } + + // Return callback to check store after upgrade + return func() { + // Confirm one query was removed + remainingQueries := s.App.InterchainqueryKeeper.AllQueries(s.Ctx) + s.Require().Len(remainingQueries, 1, "there should be only 1 remaining query") + + // Confirm the slash query was removed + _, found := s.App.InterchainqueryKeeper.GetQuery(s.Ctx, "query-1") + s.Require().False(found, "slash query should have been removed") + + // Confirm the other query is still there + _, found = s.App.InterchainqueryKeeper.GetQuery(s.Ctx, "query-2") + s.Require().True(found, "non-slash query should not have been removed") + } } func (s *UpgradeTestSuite) TestRegisterCommunityPoolAddresses() { @@ -81,37 +370,11 @@ func (s *UpgradeTestSuite) TestRegisterCommunityPoolAddresses() { err := v17.RegisterCommunityPoolAddresses(s.Ctx, s.App.StakeibcKeeper) s.Require().NoError(err, "no error expected when registering ICA addresses") - // Confirm the module accounts were created and stored on each host - for _, chainId := range chainIds { - hostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, chainId) - s.Require().True(found, "host zone should have been found") - - s.Require().NotEmpty(hostZone.CommunityPoolStakeHoldingAddress, "stake holding for %s should not be empty", chainId) - s.Require().NotEmpty(hostZone.CommunityPoolRedeemHoldingAddress, "redeem holding for %s should not be empty", chainId) - - stakeHoldingAddress := sdk.MustAccAddressFromBech32(hostZone.CommunityPoolStakeHoldingAddress) - redeemHoldingAddress := sdk.MustAccAddressFromBech32(hostZone.CommunityPoolRedeemHoldingAddress) - - stakeHoldingAccount := s.App.AccountKeeper.GetAccount(s.Ctx, stakeHoldingAddress) - redeemHoldingAccount := s.App.AccountKeeper.GetAccount(s.Ctx, redeemHoldingAddress) - - s.Require().NotNil(stakeHoldingAccount, "stake holding account should have been registered for %s", chainId) - s.Require().NotNil(redeemHoldingAccount, "redeem holding account should have been registered for %s", chainId) - } - - // Confirm the ICAs were registered - // The addresses don't get set until the callback, but we can check the events and confirm they were registered + // Confirm the module accounts and ICAs were registered for _, chainId := range chainIds { - depositOwner := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_DEPOSIT) - returnOwner := stakeibctypes.FormatHostZoneICAOwner(chainId, stakeibctypes.ICAAccountType_COMMUNITY_POOL_RETURN) - - expectedDepositPortId, _ := icatypes.NewControllerPortID(depositOwner) - expectedReturnPortId, _ := icatypes.NewControllerPortID(returnOwner) - - s.CheckEventValueEmitted(channeltypes.EventTypeChannelOpenInit, channeltypes.AttributeKeyPortID, expectedDepositPortId) - s.CheckEventValueEmitted(channeltypes.EventTypeChannelOpenInit, channeltypes.AttributeKeyPortID, expectedReturnPortId) + s.checkCommunityPoolModuleAccountsRegistered(chainId) + s.checkCommunityPoolICAAccountsRegistered(chainId) } - } func (s *UpgradeTestSuite) TestDeleteAllStaleQueries() { diff --git a/x/ratelimit/keeper/hooks.go b/x/ratelimit/keeper/hooks.go index 4a055d976e..7478e40fa3 100644 --- a/x/ratelimit/keeper/hooks.go +++ b/x/ratelimit/keeper/hooks.go @@ -9,14 +9,13 @@ import ( ) // Before each hour epoch, check if any of the rate limits have expired, -// -// and reset them if they have +// and reset them if they have func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInfo) { if epochInfo.Identifier == epochstypes.HOUR_EPOCH { epochHour := uint64(epochInfo.CurrentEpoch) for _, rateLimit := range k.GetAllRateLimits(ctx) { - if epochHour%rateLimit.Quota.DurationHours == 0 { + if rateLimit.Quota.DurationHours != 0 && epochHour%rateLimit.Quota.DurationHours == 0 { err := k.ResetRateLimit(ctx, rateLimit.Path.Denom, rateLimit.Path.ChannelId) if err != nil { k.Logger(ctx).Error(fmt.Sprintf("Unable to reset quota for Denom: %s, ChannelId: %s", rateLimit.Path.Denom, rateLimit.Path.ChannelId)) From 9ced924b8dcb30c981fd57e8b25fc660079663ec Mon Sep 17 00:00:00 2001 From: sampocs Date: Tue, 19 Dec 2023 13:21:26 -0600 Subject: [PATCH 06/40] updated rate limits --- app/upgrades/v17/upgrades.go | 27 +++++++++++++++++---------- app/upgrades/v17/upgrades_test.go | 2 +- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 1bb7466e26..17c2389a74 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -38,17 +38,24 @@ var ( OsmosisRedemptionRateBuffer = sdk.MustNewDecFromStr("0.02") // Rate limits updated according to TVL + // Framework: + // < 2.5M: No rate limit + // 2.5M - 10M: 50% + // 10M - 20M: 25% + // 20M - 40M: 20% + // 40M - 50M: 15% + // > 50M: 10% UpdatedRateLimits = map[string]sdkmath.Int{ - "comdex-1": sdkmath.ZeroInt(), // TVL: ~130k | <1M | No rate limit - "cosmoshub-4": sdkmath.NewInt(10), // TVL: ~50M | 30M+ | 10% RL - "evmos_9001-2": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL - "injective-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL - "juno-1": sdkmath.NewInt(50), // TVL: ~3M | 1M-15M+ | 50% RL - "osmosis-1": sdkmath.NewInt(10), // TVL: ~30M | 30M+ | 10% RL - "phoenix-1": sdkmath.ZeroInt(), // TVL: ~190k | <1M | No rate limit - "sommelier-3": sdkmath.ZeroInt(), // TVL: ~450k | <1M | No rate limit - "stargaze-1": sdkmath.NewInt(50), // TVL: 1.35M | 1M-15M+ | 50% RL - "umee-1": sdkmath.ZeroInt(), // TVL: ~200k | <1M | No rate limit + "comdex-1": sdkmath.ZeroInt(), // TVL: ~130k | <2.5M | No rate limit + "cosmoshub-4": sdkmath.NewInt(10), // TVL: ~50M | 50M+ | 10% RL + "evmos_9001-2": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL + "injective-1": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL + "juno-1": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL + "osmosis-1": sdkmath.NewInt(20), // TVL: ~30M | 20M-40M | 20% RL + "phoenix-1": sdkmath.ZeroInt(), // TVL: ~190k | <2.5M | No rate limit + "sommelier-3": sdkmath.ZeroInt(), // TVL: ~450k | <2.5M | No rate limit + "stargaze-1": sdkmath.ZeroInt(), // TVL: 1.35M | <2.5M | No rate limit + "umee-1": sdkmath.ZeroInt(), // TVL: ~200k | <2.5M | No rate limit } // Osmo transfer channel is required for new rate limits diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 106f72e3bd..59d994a49d 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -539,7 +539,7 @@ func (s *UpgradeTestSuite) TestUpdateRateLimitThresholds() { HostDenom: "uosmo", RateLimitDenom: "stuosmo", Duration: 20, - Threshold: sdkmath.NewInt(10), + Threshold: sdkmath.NewInt(20), }, "juno": { // No denom on matching host From e95161d33b37ddabe3b67abfe15051a2aa709220 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 20 Dec 2023 09:51:21 -0600 Subject: [PATCH 07/40] nit comments --- app/upgrades/v17/upgrades.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 17c2389a74..e24bbcc76d 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -46,16 +46,16 @@ var ( // 40M - 50M: 15% // > 50M: 10% UpdatedRateLimits = map[string]sdkmath.Int{ - "comdex-1": sdkmath.ZeroInt(), // TVL: ~130k | <2.5M | No rate limit + "comdex-1": sdkmath.ZeroInt(), // TVL: ~150k | <2.5M | No rate limit "cosmoshub-4": sdkmath.NewInt(10), // TVL: ~50M | 50M+ | 10% RL "evmos_9001-2": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL "injective-1": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL "juno-1": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL "osmosis-1": sdkmath.NewInt(20), // TVL: ~30M | 20M-40M | 20% RL - "phoenix-1": sdkmath.ZeroInt(), // TVL: ~190k | <2.5M | No rate limit - "sommelier-3": sdkmath.ZeroInt(), // TVL: ~450k | <2.5M | No rate limit - "stargaze-1": sdkmath.ZeroInt(), // TVL: 1.35M | <2.5M | No rate limit - "umee-1": sdkmath.ZeroInt(), // TVL: ~200k | <2.5M | No rate limit + "phoenix-1": sdkmath.ZeroInt(), // TVL: ~200k | <2.5M | No rate limit + "sommelier-3": sdkmath.ZeroInt(), // TVL: ~500k | <2.5M | No rate limit + "stargaze-1": sdkmath.ZeroInt(), // TVL: 1.5M | <2.5M | No rate limit + "umee-1": sdkmath.ZeroInt(), // TVL: ~150k | <2.5M | No rate limit } // Osmo transfer channel is required for new rate limits From 70ce9650b739f30dbcfc133be8dee309550c0b80 Mon Sep 17 00:00:00 2001 From: sampocs Date: Mon, 8 Jan 2024 20:22:02 -0600 Subject: [PATCH 08/40] Update app/upgrades/v17/upgrades.go Co-authored-by: riley-stride <104941670+riley-stride@users.noreply.github.com> --- app/upgrades/v17/upgrades.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index e24bbcc76d..f0a99622fa 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -79,7 +79,7 @@ func CreateUpgradeHandler( return vm, errorsmod.Wrapf(err, "unable to register community pool addresses on host zones") } - ctx.Logger().Info("Deleting all pending queries...") + ctx.Logger().Info("Deleting all pending slash queries...") DeleteAllStaleQueries(ctx, icqKeeper) ctx.Logger().Info("Reseting slash query in progress...") From 4871a9ce828c43ddaeb1ea03b8906319a5dece66 Mon Sep 17 00:00:00 2001 From: sampocs Date: Mon, 8 Jan 2024 20:25:57 -0600 Subject: [PATCH 09/40] updated rate limits --- app/upgrades/v17/upgrades.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index e24bbcc76d..5b4bf65afa 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -47,11 +47,11 @@ var ( // > 50M: 10% UpdatedRateLimits = map[string]sdkmath.Int{ "comdex-1": sdkmath.ZeroInt(), // TVL: ~150k | <2.5M | No rate limit - "cosmoshub-4": sdkmath.NewInt(10), // TVL: ~50M | 50M+ | 10% RL + "cosmoshub-4": sdkmath.NewInt(15), // TVL: ~45M | 40M-50M | 15% RL "evmos_9001-2": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL - "injective-1": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL + "injective-1": sdkmath.ZeroInt(), // TVL: ~1.5M | <2.5M | No rate limit "juno-1": sdkmath.NewInt(50), // TVL: ~3M | 2.5M-10M | 50% RL - "osmosis-1": sdkmath.NewInt(20), // TVL: ~30M | 20M-40M | 20% RL + "osmosis-1": sdkmath.NewInt(15), // TVL: ~45M | 40M-50M | 15% RL "phoenix-1": sdkmath.ZeroInt(), // TVL: ~200k | <2.5M | No rate limit "sommelier-3": sdkmath.ZeroInt(), // TVL: ~500k | <2.5M | No rate limit "stargaze-1": sdkmath.ZeroInt(), // TVL: 1.5M | <2.5M | No rate limit From 71e9917bd3b8fef4f545d28ec6bb648ad840af9d Mon Sep 17 00:00:00 2001 From: sampocs Date: Mon, 8 Jan 2024 20:28:16 -0600 Subject: [PATCH 10/40] removed inner RR updates --- app/upgrades/v17/upgrades.go | 8 -------- app/upgrades/v17/upgrades_test.go | 22 ++++------------------ 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 465add64be..5f11299a1f 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -28,8 +28,6 @@ var ( // Redemption rate bounds updated to give ~3 months of slack on outer bounds RedemptionRateOuterMinAdjustment = sdk.MustNewDecFromStr("0.05") - RedemptionRateInnerMinAdjustment = sdk.MustNewDecFromStr("0.03") - RedemptionRateInnerMaxAdjustment = sdk.MustNewDecFromStr("0.05") RedemptionRateOuterMaxAdjustment = sdk.MustNewDecFromStr("0.10") // Osmosis will have a slighly larger buffer with the redemption rate @@ -205,18 +203,12 @@ func UpdateRedemptionRateBounds(ctx sdk.Context, k stakeibckeeper.Keeper) { } outerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateOuterMinAdjustment) - innerMinDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMinAdjustment) - innerMaxDelta := hostZone.RedemptionRate.Mul(RedemptionRateInnerMaxAdjustment) outerMaxDelta := hostZone.RedemptionRate.Mul(outerAdjustment) outerMin := hostZone.RedemptionRate.Sub(outerMinDelta) - innerMin := hostZone.RedemptionRate.Sub(innerMinDelta) - innerMax := hostZone.RedemptionRate.Add(innerMaxDelta) outerMax := hostZone.RedemptionRate.Add(outerMaxDelta) hostZone.MinRedemptionRate = outerMin - hostZone.MinInnerRedemptionRate = innerMin - hostZone.MaxInnerRedemptionRate = innerMax hostZone.MaxRedemptionRate = outerMax k.SetHostZone(ctx, hostZone) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 59d994a49d..44df8d61ed 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -36,8 +36,6 @@ type UpdateRedemptionRateBounds struct { ChainId string CurrentRedemptionRate sdk.Dec ExpectedMinOuterRedemptionRate sdk.Dec - ExpectedMinInnerRedemptionRate sdk.Dec - ExpectedMaxInnerRedemptionRate sdk.Dec ExpectedMaxOuterRedemptionRate sdk.Dec } @@ -179,18 +177,14 @@ func (s *UpgradeTestSuite) SetupHostZonesBeforeUpgrade() func() { gaiaHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, GaiaChainId) s.Require().True(found) - s.Require().Equal(sdk.MustNewDecFromStr("1.045"), gaiaHostZone.MinRedemptionRate, "gaia min outer") // 1.1 - 5% = 1.045 - s.Require().Equal(sdk.MustNewDecFromStr("1.067"), gaiaHostZone.MinInnerRedemptionRate, "gaia min inner") // 1.1 - 3% = 1.067 - s.Require().Equal(sdk.MustNewDecFromStr("1.155"), gaiaHostZone.MaxInnerRedemptionRate, "gaia max inner") // 1.1 + 5% = 1.155 - s.Require().Equal(sdk.MustNewDecFromStr("1.210"), gaiaHostZone.MaxRedemptionRate, "gaia max outer") // 1.1 + 10% = 1.21 + s.Require().Equal(sdk.MustNewDecFromStr("1.045"), gaiaHostZone.MinRedemptionRate, "gaia min outer") // 1.1 - 5% = 1.045 + s.Require().Equal(sdk.MustNewDecFromStr("1.210"), gaiaHostZone.MaxRedemptionRate, "gaia max outer") // 1.1 + 10% = 1.21 osmoHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, "osmosis-1") s.Require().True(found) - s.Require().Equal(sdk.MustNewDecFromStr("1.140"), osmoHostZone.MinRedemptionRate, "osmo min outer") // 1.2 - 5% = 1.140 - s.Require().Equal(sdk.MustNewDecFromStr("1.164"), osmoHostZone.MinInnerRedemptionRate, "osmo min inner") // 1.2 - 3% = 1.164 - s.Require().Equal(sdk.MustNewDecFromStr("1.260"), osmoHostZone.MaxInnerRedemptionRate, "osmo max inner") // 1.2 + 5% = 1.260 - s.Require().Equal(sdk.MustNewDecFromStr("1.344"), osmoHostZone.MaxRedemptionRate, "osmo max outer") // 1.2 + 12% = 1.344 + s.Require().Equal(sdk.MustNewDecFromStr("1.140"), osmoHostZone.MinRedemptionRate, "osmo min outer") // 1.2 - 5% = 1.140 + s.Require().Equal(sdk.MustNewDecFromStr("1.344"), osmoHostZone.MaxRedemptionRate, "osmo max outer") // 1.2 + 12% = 1.344 // Check that there are no slash queries in progress for _, hostZone := range s.App.StakeibcKeeper.GetAllHostZone(s.Ctx) { @@ -471,16 +465,12 @@ func (s *UpgradeTestSuite) TestUpdateRedemptionRateBounds() { ChainId: "chain-0", CurrentRedemptionRate: sdk.MustNewDecFromStr("1.0"), ExpectedMinOuterRedemptionRate: sdk.MustNewDecFromStr("0.95"), // 1 - 5% = 0.95 - ExpectedMinInnerRedemptionRate: sdk.MustNewDecFromStr("0.97"), // 1 - 3% = 0.97 - ExpectedMaxInnerRedemptionRate: sdk.MustNewDecFromStr("1.05"), // 1 + 5% = 1.05 ExpectedMaxOuterRedemptionRate: sdk.MustNewDecFromStr("1.10"), // 1 + 10% = 1.1 }, { ChainId: "chain-1", CurrentRedemptionRate: sdk.MustNewDecFromStr("1.1"), ExpectedMinOuterRedemptionRate: sdk.MustNewDecFromStr("1.045"), // 1.1 - 5% = 1.045 - ExpectedMinInnerRedemptionRate: sdk.MustNewDecFromStr("1.067"), // 1.1 - 3% = 1.067 - ExpectedMaxInnerRedemptionRate: sdk.MustNewDecFromStr("1.155"), // 1.1 + 5% = 1.155 ExpectedMaxOuterRedemptionRate: sdk.MustNewDecFromStr("1.210"), // 1.1 + 10% = 1.21 }, { @@ -488,8 +478,6 @@ func (s *UpgradeTestSuite) TestUpdateRedemptionRateBounds() { ChainId: v17.OsmosisChainId, CurrentRedemptionRate: sdk.MustNewDecFromStr("1.25"), ExpectedMinOuterRedemptionRate: sdk.MustNewDecFromStr("1.1875"), // 1.25 - 5% = 1.1875 - ExpectedMinInnerRedemptionRate: sdk.MustNewDecFromStr("1.2125"), // 1.25 - 3% = 1.2125 - ExpectedMaxInnerRedemptionRate: sdk.MustNewDecFromStr("1.3125"), // 1.25 + 5% = 1.3125 ExpectedMaxOuterRedemptionRate: sdk.MustNewDecFromStr("1.4000"), // 1.25 + 12% = 1.400 }, } @@ -512,8 +500,6 @@ func (s *UpgradeTestSuite) TestUpdateRedemptionRateBounds() { s.Require().True(found) s.Require().Equal(tc.ExpectedMinOuterRedemptionRate, hostZone.MinRedemptionRate, "%s - min outer", tc.ChainId) - s.Require().Equal(tc.ExpectedMinInnerRedemptionRate, hostZone.MinInnerRedemptionRate, "%s - min inner", tc.ChainId) - s.Require().Equal(tc.ExpectedMaxInnerRedemptionRate, hostZone.MaxInnerRedemptionRate, "%s - max inner", tc.ChainId) s.Require().Equal(tc.ExpectedMaxOuterRedemptionRate, hostZone.MaxRedemptionRate, "%s - max outer", tc.ChainId) } } From 942418999d740961f4afcbd12586d28af4e5e4f5 Mon Sep 17 00:00:00 2001 From: sampocs Date: Mon, 8 Jan 2024 20:38:44 -0600 Subject: [PATCH 11/40] nit --- app/upgrades/v17/upgrades.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 5f11299a1f..8c92614e1c 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -60,7 +60,7 @@ var ( OsmosisTransferChannelId = "channel-5" ) -// CreateUpgradeHandler creates an SDK upgrade handler for v15 +// CreateUpgradeHandler creates an SDK upgrade handler for v17 func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, From d7204b88de39d82a765385899b920f90de170554 Mon Sep 17 00:00:00 2001 From: sampocs Date: Mon, 8 Jan 2024 22:12:29 -0600 Subject: [PATCH 12/40] added param migration --- app/upgrades/v17/upgrades.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 8c92614e1c..da3a879370 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -72,6 +72,9 @@ func CreateUpgradeHandler( return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { ctx.Logger().Info("Starting upgrade v17...") + ctx.Logger().Info("Migrating stakeibc params...") + MigrateStakeibcParams(ctx, stakeibcKeeper) + ctx.Logger().Info("Migrating host zones...") if err := RegisterCommunityPoolAddresses(ctx, stakeibcKeeper); err != nil { return vm, errorsmod.Wrapf(err, "unable to register community pool addresses on host zones") @@ -103,6 +106,16 @@ func CreateUpgradeHandler( } } +// Migrate the stakeibc params to add the ValidatorWeightCap parameter +// +// NOTE: If a parameter is added, the old params cannot be unmarshalled +// to the new schema. To get around this, we have to set each parameter explicitly +// Considering all mainnet stakeibc params are set to the default, we can just use that +func MigrateStakeibcParams(ctx sdk.Context, k stakeibckeeper.Keeper) { + params := stakeibctypes.DefaultParams() + k.SetParams(ctx, params) +} + // Migrates the host zones to the new structure which supports community pool liquid staking // We don't have to perform a true migration here since only new fields were added // (in other words, we can deserialize the old host zone structs into the new types) From 9de0cc10ec512778d8cd85856b456dad715090a3 Mon Sep 17 00:00:00 2001 From: vish-stride Date: Tue, 9 Jan 2024 10:41:01 -0700 Subject: [PATCH 13/40] update user redemption rates dynamically --- proto/stride/records/records.proto | 4 + x/records/types/records.pb.go | 174 ++++++++++++++++--------- x/stakeibc/keeper/hooks.go | 3 +- x/stakeibc/keeper/unbonding_records.go | 55 +++++++- 4 files changed, 171 insertions(+), 65 deletions(-) diff --git a/proto/stride/records/records.proto b/proto/stride/records/records.proto index da15acb865..a2424b665f 100644 --- a/proto/stride/records/records.proto +++ b/proto/stride/records/records.proto @@ -18,6 +18,10 @@ message UserRedemptionRecord { uint64 epoch_number = 7; bool claim_is_pending = 8; reserved 2; + string st_token_amount = 9 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int", + (gogoproto.nullable) = false + ]; } message DepositRecord { diff --git a/x/records/types/records.pb.go b/x/records/types/records.pb.go index 2cf072cd31..31906256f9 100644 --- a/x/records/types/records.pb.go +++ b/x/records/types/records.pb.go @@ -170,6 +170,7 @@ type UserRedemptionRecord struct { HostZoneId string `protobuf:"bytes,6,opt,name=host_zone_id,json=hostZoneId,proto3" json:"host_zone_id,omitempty"` EpochNumber uint64 `protobuf:"varint,7,opt,name=epoch_number,json=epochNumber,proto3" json:"epoch_number,omitempty"` ClaimIsPending bool `protobuf:"varint,8,opt,name=claim_is_pending,json=claimIsPending,proto3" json:"claim_is_pending,omitempty"` + StTokenAmount github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,9,opt,name=st_token_amount,json=stTokenAmount,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"st_token_amount"` } func (m *UserRedemptionRecord) Reset() { *m = UserRedemptionRecord{} } @@ -578,69 +579,70 @@ func init() { func init() { proto.RegisterFile("stride/records/records.proto", fileDescriptor_295ee594cc85d8ca) } var fileDescriptor_295ee594cc85d8ca = []byte{ - // 988 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0xdf, 0x6e, 0xdb, 0x54, - 0x1c, 0x8e, 0x13, 0x37, 0x7f, 0x7e, 0x5d, 0x52, 0xf7, 0x34, 0x63, 0x6e, 0x60, 0x59, 0x16, 0xb1, - 0x29, 0x12, 0x9a, 0x43, 0x8b, 0xb4, 0x0b, 0x84, 0x10, 0x4e, 0xe3, 0x76, 0xde, 0xb2, 0xa4, 0x38, - 0x09, 0x43, 0xbd, 0xc0, 0x72, 0xec, 0xa3, 0xe6, 0xa8, 0x8b, 0x4f, 0xe4, 0xe3, 0x44, 0xc0, 0x0d, - 0xaf, 0xc0, 0x05, 0x3c, 0x02, 0x8f, 0xc0, 0x3b, 0xec, 0x0a, 0xed, 0x12, 0x71, 0x31, 0xa1, 0xf6, - 0x82, 0xd7, 0x40, 0x3e, 0x76, 0x9c, 0xc4, 0xd9, 0xa8, 0x54, 0x76, 0x65, 0xfb, 0xfb, 0xfd, 0x39, - 0x3e, 0xdf, 0xf9, 0x7e, 0x9f, 0x0d, 0x1f, 0x31, 0xdf, 0x23, 0x0e, 0x6e, 0x7a, 0xd8, 0xa6, 0x9e, - 0xc3, 0x16, 0x57, 0x65, 0xea, 0x51, 0x9f, 0xa2, 0x52, 0x18, 0x55, 0x22, 0xb4, 0x52, 0xb5, 0x29, - 0x9b, 0x50, 0xd6, 0x1c, 0x59, 0x0c, 0x37, 0xe7, 0x07, 0x23, 0xec, 0x5b, 0x07, 0x4d, 0x9b, 0x12, - 0x37, 0xcc, 0xaf, 0x94, 0xcf, 0xe9, 0x39, 0xe5, 0xb7, 0xcd, 0xe0, 0x2e, 0x44, 0xeb, 0xbf, 0xa6, - 0xa1, 0x3c, 0x64, 0xd8, 0x33, 0xb0, 0x83, 0x27, 0x53, 0x9f, 0x50, 0xd7, 0xe0, 0xfd, 0x50, 0x09, - 0xd2, 0xc4, 0x91, 0x85, 0x9a, 0xd0, 0x28, 0x18, 0x69, 0xe2, 0xa0, 0x0a, 0xe4, 0x3d, 0x6c, 0x63, - 0x32, 0xc7, 0x9e, 0x9c, 0xe1, 0x68, 0xfc, 0x8c, 0x8e, 0x21, 0x6b, 0x4d, 0xe8, 0xcc, 0xf5, 0x65, - 0x31, 0x88, 0xb4, 0x94, 0x57, 0x6f, 0xee, 0xa5, 0xfe, 0x7a, 0x73, 0xef, 0xe1, 0x39, 0xf1, 0xc7, - 0xb3, 0x91, 0x62, 0xd3, 0x49, 0x33, 0x7a, 0xbb, 0xf0, 0xf2, 0x88, 0x39, 0x17, 0x4d, 0xff, 0x87, - 0x29, 0x66, 0x8a, 0xee, 0xfa, 0x46, 0x54, 0x8d, 0xca, 0xb0, 0xe5, 0x60, 0x97, 0x4e, 0xe4, 0x2d, - 0xbe, 0x40, 0xf8, 0x80, 0x6a, 0x70, 0x6b, 0x4c, 0x99, 0x6f, 0xfe, 0x48, 0x5d, 0x6c, 0x12, 0x47, - 0xce, 0xf2, 0x20, 0x04, 0xd8, 0x19, 0x75, 0xb1, 0xee, 0xa0, 0xfb, 0x70, 0x0b, 0x4f, 0xa9, 0x3d, - 0x36, 0xdd, 0xd9, 0x64, 0x84, 0x3d, 0x39, 0x57, 0x13, 0x1a, 0xa2, 0xb1, 0xcd, 0xb1, 0x2e, 0x87, - 0x50, 0x03, 0x24, 0xfb, 0xa5, 0x45, 0x26, 0x26, 0x61, 0xe6, 0x14, 0xbb, 0x0e, 0x71, 0xcf, 0xe5, - 0x7c, 0x4d, 0x68, 0xe4, 0x8d, 0x12, 0xc7, 0x75, 0x76, 0x1a, 0xa2, 0x4f, 0xc5, 0x7c, 0x5a, 0xca, - 0xd4, 0xff, 0xc9, 0x40, 0xb1, 0x8d, 0xa7, 0x94, 0x11, 0x7f, 0x83, 0x10, 0x91, 0x13, 0xb2, 0xdc, - 0x74, 0xfa, 0xfd, 0x6c, 0x3a, 0xf3, 0x5f, 0x9b, 0x16, 0x37, 0x36, 0xfd, 0x05, 0x64, 0x99, 0x6f, - 0xf9, 0x33, 0xc6, 0x09, 0x29, 0x1d, 0x7e, 0xac, 0xac, 0x0b, 0x42, 0x59, 0x7b, 0x7d, 0xa5, 0xcf, - 0x73, 0x8d, 0xa8, 0x06, 0x7d, 0x0a, 0x65, 0x27, 0x8c, 0x9b, 0x6f, 0xa1, 0x0e, 0x45, 0x31, 0x6d, - 0x85, 0xc1, 0x60, 0x3d, 0x3a, 0xf3, 0x6c, 0xcc, 0x79, 0xbb, 0x7e, 0x3d, 0x9e, 0x6b, 0x44, 0x35, - 0xf5, 0x31, 0x64, 0xc3, 0x37, 0x40, 0x08, 0x4a, 0x03, 0x43, 0xed, 0xf6, 0x8f, 0x35, 0xc3, 0xfc, - 0x7a, 0xa8, 0x0d, 0x35, 0x29, 0x85, 0x64, 0x28, 0xc7, 0x98, 0xde, 0x35, 0x4f, 0x8d, 0xde, 0x89, - 0xa1, 0xf5, 0xfb, 0x52, 0x1a, 0x95, 0x41, 0x6a, 0x6b, 0x1d, 0xed, 0x44, 0x1d, 0xe8, 0xbd, 0x6e, - 0x94, 0x2f, 0xa0, 0x0a, 0x7c, 0xb0, 0x82, 0xae, 0x56, 0x64, 0xea, 0x0d, 0xc8, 0x86, 0x6b, 0x23, - 0x80, 0x6c, 0x7f, 0x60, 0xe8, 0xed, 0x60, 0x05, 0x04, 0xa5, 0x17, 0xfa, 0xe0, 0x49, 0xdb, 0x50, - 0x5f, 0xa8, 0x1d, 0x53, 0x3f, 0x52, 0x25, 0xe1, 0xa9, 0x98, 0xdf, 0x92, 0xb2, 0xf5, 0xdf, 0x44, - 0xd8, 0x7d, 0x12, 0xd1, 0x3a, 0x74, 0x47, 0x94, 0xab, 0x00, 0x7d, 0x03, 0x3b, 0xcc, 0x37, 0x7d, - 0x7a, 0x81, 0x5d, 0x33, 0x3a, 0x66, 0xe1, 0x46, 0xc7, 0x5c, 0x64, 0xfe, 0x20, 0xe8, 0xa2, 0x86, - 0xa7, 0xfd, 0x1d, 0xec, 0xb9, 0x96, 0x4f, 0xe6, 0x78, 0xbd, 0xf7, 0xcd, 0x24, 0xb4, 0x1b, 0xb6, - 0x5a, 0xed, 0x7f, 0x53, 0x35, 0x3d, 0x80, 0xd2, 0x6c, 0xb1, 0x79, 0xd3, 0x27, 0x13, 0xcc, 0x67, - 0x50, 0x34, 0x8a, 0x31, 0x3a, 0x20, 0x13, 0x8c, 0xbe, 0x4a, 0x88, 0xae, 0x91, 0x14, 0xc1, 0x06, - 0x93, 0x49, 0xe1, 0x3d, 0x86, 0x3b, 0x33, 0x86, 0x3d, 0xd3, 0x8b, 0x0d, 0xc7, 0x8c, 0x6a, 0xe5, - 0x5c, 0x2d, 0xd3, 0x28, 0x18, 0xb7, 0x67, 0x6f, 0xb1, 0x23, 0x56, 0xff, 0x29, 0x16, 0xd0, 0x1e, - 0xec, 0x0c, 0xbb, 0xad, 0x5e, 0xb7, 0xad, 0x77, 0x4f, 0x62, 0x05, 0xed, 0xc3, 0xed, 0x25, 0xb8, - 0x26, 0x08, 0x74, 0x07, 0xf6, 0xb4, 0x6f, 0xf5, 0x81, 0x99, 0x50, 0x9d, 0x80, 0xee, 0xc2, 0xfe, - 0x7a, 0x60, 0xb5, 0x4e, 0x44, 0x45, 0x28, 0x1c, 0x75, 0x54, 0xfd, 0xb9, 0xda, 0xea, 0x68, 0x52, - 0xba, 0xfe, 0x8b, 0x00, 0x65, 0x3e, 0x0f, 0xf1, 0xd6, 0x22, 0x63, 0x48, 0xba, 0x8f, 0xb0, 0xe9, - 0x3e, 0x7d, 0x28, 0x2f, 0xf9, 0x8f, 0x19, 0x65, 0x72, 0xa6, 0x96, 0x69, 0x6c, 0x1f, 0xde, 0xbf, - 0x96, 0x44, 0x03, 0x8d, 0x93, 0x10, 0x8b, 0x8c, 0xea, 0x0f, 0x11, 0x76, 0x3a, 0xfd, 0xe7, 0x5c, - 0x03, 0xd1, 0x04, 0xa2, 0xbb, 0x00, 0x8b, 0xe1, 0x8e, 0x3d, 0xbc, 0x10, 0x21, 0xba, 0x83, 0xf6, - 0x21, 0x6f, 0x8f, 0x2d, 0xe2, 0x06, 0x41, 0x2e, 0x3c, 0x23, 0xc7, 0x9f, 0x75, 0xe7, 0x1d, 0xf2, - 0xf9, 0x10, 0x0a, 0x64, 0x64, 0x9b, 0x61, 0x24, 0xd4, 0x4e, 0x9e, 0x8c, 0xec, 0x36, 0x0f, 0x3e, - 0x80, 0x12, 0xf3, 0xad, 0x0b, 0xec, 0x99, 0x96, 0xe3, 0x78, 0x98, 0xb1, 0xc8, 0xbd, 0x8b, 0x21, - 0xaa, 0x86, 0x20, 0xfa, 0x04, 0x76, 0xe7, 0xd6, 0x4b, 0xe2, 0x58, 0x3e, 0x5d, 0x66, 0x86, 0x56, - 0x2e, 0xc5, 0x81, 0x45, 0xf2, 0xd2, 0x5b, 0x73, 0xff, 0xcb, 0x5b, 0x3f, 0x87, 0xfc, 0x62, 0x8a, - 0xb9, 0x6b, 0x6d, 0x1f, 0xee, 0x2b, 0x61, 0x81, 0x12, 0x7c, 0x26, 0x95, 0xe8, 0x33, 0xa9, 0x1c, - 0x51, 0xe2, 0xb6, 0xc4, 0x60, 0x11, 0x23, 0x17, 0xcd, 0x2b, 0xfa, 0x32, 0x96, 0x7a, 0x81, 0x4b, - 0xfd, 0x61, 0xf2, 0x94, 0x12, 0xac, 0x27, 0x84, 0x5e, 0xff, 0x5d, 0x58, 0x55, 0x6c, 0x5b, 0x3b, - 0xed, 0xf5, 0xf5, 0x81, 0x79, 0xaa, 0x71, 0x89, 0x86, 0x8e, 0xb4, 0xa1, 0xc8, 0x77, 0xfb, 0xe0, - 0x1e, 0xec, 0xc4, 0x91, 0x63, 0x55, 0xef, 0x68, 0x6d, 0x29, 0x13, 0xa4, 0xb7, 0xb5, 0x41, 0xef, - 0x99, 0xd6, 0xd5, 0xcf, 0x56, 0x0d, 0x52, 0x44, 0x55, 0xa8, 0x24, 0x22, 0xab, 0xed, 0xb6, 0x82, - 0x71, 0x49, 0xc4, 0xa3, 0xa6, 0xd9, 0xd6, 0xb3, 0x57, 0x97, 0x55, 0xe1, 0xf5, 0x65, 0x55, 0xf8, - 0xfb, 0xb2, 0x2a, 0xfc, 0x7c, 0x55, 0x4d, 0xbd, 0xbe, 0xaa, 0xa6, 0xfe, 0xbc, 0xaa, 0xa6, 0xce, - 0x0e, 0x56, 0xd8, 0xef, 0x73, 0x2e, 0x1e, 0x75, 0xac, 0x11, 0x6b, 0x46, 0xbf, 0x29, 0xf3, 0x83, - 0xc7, 0xcd, 0xef, 0xe3, 0x9f, 0x15, 0x7e, 0x18, 0xa3, 0x2c, 0xff, 0xcb, 0xf8, 0xec, 0xdf, 0x00, - 0x00, 0x00, 0xff, 0xff, 0x9e, 0x8f, 0x0f, 0x18, 0xcb, 0x08, 0x00, 0x00, + // 995 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0xcd, 0x6e, 0xdb, 0x46, + 0x10, 0x16, 0x25, 0x5a, 0x3f, 0xe3, 0x48, 0xa6, 0xd7, 0x4a, 0x43, 0xab, 0x8d, 0xa2, 0x08, 0x4d, + 0x20, 0xa0, 0x08, 0x55, 0xbb, 0x40, 0x0e, 0x45, 0x51, 0x94, 0xb2, 0x68, 0x87, 0x89, 0x22, 0xb9, + 0x94, 0xd4, 0x14, 0x3e, 0x94, 0xa0, 0xc8, 0x85, 0xb5, 0x70, 0xc4, 0x15, 0xb8, 0x94, 0xd0, 0xf6, + 0xd2, 0x57, 0xe8, 0xa1, 0xaf, 0xd0, 0x47, 0xe8, 0x3b, 0xe4, 0x54, 0xe4, 0x58, 0xf4, 0x10, 0x14, + 0xf6, 0xa1, 0xa7, 0xbe, 0x43, 0xc1, 0x25, 0x45, 0x49, 0x54, 0xdc, 0x00, 0x6e, 0x4e, 0x12, 0xbf, + 0x6f, 0x66, 0x96, 0xfb, 0xed, 0x37, 0xb3, 0x84, 0x8f, 0x98, 0xef, 0x11, 0x07, 0x37, 0x3d, 0x6c, + 0x53, 0xcf, 0x61, 0x8b, 0x5f, 0x65, 0xea, 0x51, 0x9f, 0xa2, 0x52, 0xc8, 0x2a, 0x11, 0x5a, 0xa9, + 0xda, 0x94, 0x4d, 0x28, 0x6b, 0x8e, 0x2c, 0x86, 0x9b, 0xf3, 0x83, 0x11, 0xf6, 0xad, 0x83, 0xa6, + 0x4d, 0x89, 0x1b, 0xc6, 0x57, 0xca, 0xe7, 0xf4, 0x9c, 0xf2, 0xbf, 0xcd, 0xe0, 0x5f, 0x88, 0xd6, + 0xff, 0x49, 0x43, 0x79, 0xc8, 0xb0, 0x67, 0x60, 0x07, 0x4f, 0xa6, 0x3e, 0xa1, 0xae, 0xc1, 0xeb, + 0xa1, 0x12, 0xa4, 0x89, 0x23, 0x0b, 0x35, 0xa1, 0x51, 0x30, 0xd2, 0xc4, 0x41, 0x15, 0xc8, 0x7b, + 0xd8, 0xc6, 0x64, 0x8e, 0x3d, 0x39, 0xc3, 0xd1, 0xf8, 0x19, 0x1d, 0x43, 0xd6, 0x9a, 0xd0, 0x99, + 0xeb, 0xcb, 0x62, 0xc0, 0xb4, 0x94, 0x57, 0x6f, 0xee, 0xa5, 0xfe, 0x7c, 0x73, 0xef, 0xe1, 0x39, + 0xf1, 0xc7, 0xb3, 0x91, 0x62, 0xd3, 0x49, 0x33, 0x7a, 0xbb, 0xf0, 0xe7, 0x11, 0x73, 0x2e, 0x9a, + 0xfe, 0x0f, 0x53, 0xcc, 0x14, 0xdd, 0xf5, 0x8d, 0x28, 0x1b, 0x95, 0x61, 0xcb, 0xc1, 0x2e, 0x9d, + 0xc8, 0x5b, 0x7c, 0x81, 0xf0, 0x01, 0xd5, 0xe0, 0xd6, 0x98, 0x32, 0xdf, 0xfc, 0x91, 0xba, 0xd8, + 0x24, 0x8e, 0x9c, 0xe5, 0x24, 0x04, 0xd8, 0x19, 0x75, 0xb1, 0xee, 0xa0, 0xfb, 0x70, 0x0b, 0x4f, + 0xa9, 0x3d, 0x36, 0xdd, 0xd9, 0x64, 0x84, 0x3d, 0x39, 0x57, 0x13, 0x1a, 0xa2, 0xb1, 0xcd, 0xb1, + 0x2e, 0x87, 0x50, 0x03, 0x24, 0xfb, 0xa5, 0x45, 0x26, 0x26, 0x61, 0xe6, 0x14, 0xbb, 0x0e, 0x71, + 0xcf, 0xe5, 0x7c, 0x4d, 0x68, 0xe4, 0x8d, 0x12, 0xc7, 0x75, 0x76, 0x1a, 0xa2, 0xe8, 0x1b, 0xd8, + 0x61, 0xbe, 0xe9, 0xd3, 0x0b, 0xec, 0x9a, 0xd1, 0xae, 0x0a, 0x37, 0xda, 0x55, 0x91, 0xf9, 0x83, + 0xa0, 0x8a, 0xca, 0x8b, 0x3c, 0x15, 0xf3, 0x69, 0x29, 0x53, 0xff, 0x3b, 0x03, 0xc5, 0x36, 0x9e, + 0x52, 0x46, 0xfc, 0x0d, 0xa1, 0x45, 0x2e, 0xf4, 0x52, 0xcc, 0xf4, 0xfb, 0x11, 0x33, 0xf3, 0x5f, + 0x62, 0x8a, 0x1b, 0x62, 0x7e, 0x01, 0x59, 0xe6, 0x5b, 0xfe, 0x8c, 0x71, 0xa1, 0x4b, 0x87, 0x1f, + 0x2b, 0xeb, 0x46, 0x53, 0xd6, 0x5e, 0x5f, 0xe9, 0xf3, 0x58, 0x23, 0xca, 0x41, 0x9f, 0x42, 0xd9, + 0x09, 0x79, 0xf3, 0x2d, 0x47, 0x82, 0x22, 0x4e, 0x5b, 0x39, 0x99, 0x60, 0x3d, 0x3a, 0xf3, 0x6c, + 0xcc, 0xcf, 0xe3, 0xdd, 0xeb, 0xf1, 0x58, 0x23, 0xca, 0xa9, 0x8f, 0x21, 0x1b, 0xbe, 0x01, 0x42, + 0x50, 0x1a, 0x18, 0x6a, 0xb7, 0x7f, 0xac, 0x19, 0xe6, 0xd7, 0x43, 0x6d, 0xa8, 0x49, 0x29, 0x24, + 0x43, 0x39, 0xc6, 0xf4, 0xae, 0x79, 0x6a, 0xf4, 0x4e, 0x0c, 0xad, 0xdf, 0x97, 0xd2, 0xa8, 0x0c, + 0x52, 0x5b, 0xeb, 0x68, 0x27, 0xea, 0x40, 0xef, 0x75, 0xa3, 0x78, 0x01, 0x55, 0xe0, 0x83, 0x15, + 0x74, 0x35, 0x23, 0x53, 0x6f, 0x40, 0x36, 0x5c, 0x1b, 0x01, 0x64, 0xfb, 0x03, 0x43, 0x6f, 0x07, + 0x2b, 0x20, 0x28, 0xbd, 0xd0, 0x07, 0x4f, 0xda, 0x86, 0xfa, 0x42, 0xed, 0x98, 0xfa, 0x91, 0x2a, + 0x09, 0x4f, 0xc5, 0xfc, 0x96, 0x94, 0xad, 0xff, 0x2a, 0xc2, 0xee, 0x93, 0x48, 0xd6, 0xa1, 0x3b, + 0xa2, 0xd7, 0xba, 0x4b, 0x78, 0x0f, 0xee, 0x42, 0xdf, 0xc1, 0x9e, 0x6b, 0xf9, 0x64, 0x8e, 0xd7, + 0x6b, 0xdf, 0xcc, 0x42, 0xbb, 0x61, 0xa9, 0xd5, 0xfa, 0x37, 0x75, 0xd3, 0x03, 0x28, 0xcd, 0x16, + 0x9b, 0x37, 0x7d, 0x32, 0xc1, 0xbc, 0xb7, 0x45, 0xa3, 0x18, 0xa3, 0x03, 0x32, 0xc1, 0xe8, 0xab, + 0x84, 0xe9, 0x1a, 0x49, 0x13, 0x6c, 0x28, 0x99, 0x34, 0xde, 0x63, 0xb8, 0x33, 0x63, 0xd8, 0x33, + 0xbd, 0x78, 0x90, 0x99, 0x51, 0xae, 0x9c, 0xab, 0x65, 0x1a, 0x05, 0xe3, 0xf6, 0xec, 0x2d, 0x63, + 0x8e, 0xd5, 0x7f, 0x8a, 0x0d, 0xb4, 0x07, 0x3b, 0xc3, 0x6e, 0xab, 0xd7, 0x6d, 0xeb, 0xdd, 0x93, + 0xd8, 0x41, 0xfb, 0x70, 0x7b, 0x09, 0xae, 0x19, 0x02, 0xdd, 0x81, 0x3d, 0xed, 0x5b, 0x7d, 0x60, + 0x26, 0x5c, 0x27, 0xa0, 0xbb, 0xb0, 0xbf, 0x4e, 0xac, 0xe6, 0x89, 0xa8, 0x08, 0x85, 0xa3, 0x8e, + 0xaa, 0x3f, 0x57, 0x5b, 0x1d, 0x4d, 0x4a, 0xd7, 0x7f, 0x11, 0xa0, 0xcc, 0xfb, 0x21, 0xde, 0x5a, + 0x34, 0x18, 0x92, 0x53, 0x4d, 0xd8, 0x9c, 0x6a, 0x7d, 0x28, 0x2f, 0xf5, 0x8f, 0x15, 0x65, 0x72, + 0xa6, 0x96, 0x69, 0x6c, 0x1f, 0xde, 0x7f, 0xa7, 0x88, 0x06, 0x1a, 0x27, 0x21, 0x16, 0x0d, 0xaa, + 0xdf, 0x45, 0xd8, 0xe9, 0xf4, 0x9f, 0x73, 0x0f, 0x44, 0x1d, 0x88, 0xee, 0x02, 0x2c, 0x9a, 0x3b, + 0xbe, 0x1b, 0x0a, 0x11, 0xa2, 0x3b, 0x68, 0x1f, 0xf2, 0xf6, 0xd8, 0x22, 0x6e, 0x40, 0x72, 0xe3, + 0x19, 0x39, 0xfe, 0xac, 0x3b, 0xd7, 0xd8, 0xe7, 0x43, 0x28, 0x90, 0x91, 0x6d, 0x86, 0x4c, 0xe8, + 0x9d, 0x3c, 0x19, 0xd9, 0x6d, 0x4e, 0x3e, 0x80, 0x12, 0xf3, 0xad, 0x0b, 0xec, 0x99, 0x96, 0xe3, + 0x78, 0x98, 0xb1, 0xe8, 0x56, 0x28, 0x86, 0xa8, 0x1a, 0x82, 0xe8, 0x13, 0xd8, 0x9d, 0x5b, 0x2f, + 0x89, 0x63, 0xf9, 0x74, 0x19, 0x19, 0x5e, 0x11, 0x52, 0x4c, 0x2c, 0x82, 0x97, 0xb3, 0x35, 0xf7, + 0xbf, 0x66, 0xeb, 0xe7, 0x90, 0x5f, 0x74, 0x31, 0x9f, 0x5a, 0xdb, 0x87, 0xfb, 0x4a, 0x98, 0xa0, + 0x04, 0xd7, 0xaf, 0x12, 0x5d, 0xbf, 0xca, 0x11, 0x25, 0x6e, 0x4b, 0x0c, 0x16, 0x31, 0x72, 0x51, + 0xbf, 0xa2, 0x2f, 0x63, 0xab, 0x17, 0xb8, 0xd5, 0x1f, 0x26, 0x4f, 0x29, 0xa1, 0x7a, 0xc2, 0xe8, + 0xf5, 0xdf, 0x84, 0x55, 0xc7, 0xb6, 0xb5, 0xd3, 0x5e, 0x5f, 0x1f, 0x98, 0xa7, 0x1a, 0xb7, 0x68, + 0x38, 0x91, 0x36, 0x1c, 0x79, 0xfd, 0x1c, 0xdc, 0x83, 0x9d, 0x98, 0x39, 0x56, 0xf5, 0x8e, 0xd6, + 0x96, 0x32, 0x41, 0x78, 0x5b, 0x1b, 0xf4, 0x9e, 0x69, 0x5d, 0xfd, 0x6c, 0x75, 0x40, 0x8a, 0xa8, + 0x0a, 0x95, 0x04, 0xb3, 0x5a, 0x6e, 0x2b, 0x68, 0x97, 0x04, 0x1f, 0x15, 0xcd, 0xb6, 0x9e, 0xbd, + 0xba, 0xac, 0x0a, 0xaf, 0x2f, 0xab, 0xc2, 0x5f, 0x97, 0x55, 0xe1, 0xe7, 0xab, 0x6a, 0xea, 0xf5, + 0x55, 0x35, 0xf5, 0xc7, 0x55, 0x35, 0x75, 0x76, 0xb0, 0xa2, 0x7e, 0x9f, 0x6b, 0xf1, 0xa8, 0x63, + 0x8d, 0x58, 0x33, 0xfa, 0xfc, 0x99, 0x1f, 0x3c, 0x6e, 0x7e, 0x1f, 0x7f, 0x04, 0xf1, 0xc3, 0x18, + 0x65, 0xf9, 0xd7, 0xcb, 0x67, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0x93, 0x00, 0x42, 0x87, 0x23, + 0x09, 0x00, 0x00, } func (m *UserRedemptionRecord) Marshal() (dAtA []byte, err error) { @@ -663,6 +665,16 @@ func (m *UserRedemptionRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size := m.StTokenAmount.Size() + i -= size + if _, err := m.StTokenAmount.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintRecords(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x4a if m.ClaimIsPending { i-- if m.ClaimIsPending { @@ -1035,6 +1047,8 @@ func (m *UserRedemptionRecord) Size() (n int) { if m.ClaimIsPending { n += 2 } + l = m.StTokenAmount.Size() + n += 1 + l + sovRecords(uint64(l)) return n } @@ -1396,6 +1410,40 @@ func (m *UserRedemptionRecord) Unmarshal(dAtA []byte) error { } } m.ClaimIsPending = bool(v != 0) + case 9: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StTokenAmount", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRecords + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthRecords + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthRecords + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.StTokenAmount.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipRecords(dAtA[iNdEx:]) diff --git a/x/stakeibc/keeper/hooks.go b/x/stakeibc/keeper/hooks.go index 7bef44a506..b30ae85df6 100644 --- a/x/stakeibc/keeper/hooks.go +++ b/x/stakeibc/keeper/hooks.go @@ -24,9 +24,10 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInf k.Logger(ctx).Error(fmt.Sprintf("Unable to update epoch tracker, err: %s", err.Error())) return } - // Day Epoch - Process Unbondings if epochInfo.Identifier == epochstypes.DAY_EPOCH { + // Update unbonding records in each host zone to reflect current RRs + k.UpdateRedemptionRatesForAllUnbondingRecords(ctx) // Initiate unbondings from any hostZone where it's appropriate k.InitiateAllHostZoneUnbondings(ctx, epochNumber) // Check previous epochs to see if unbondings finished, and sweep the tokens if so diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 30866cd61a..17cd4f2893 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -74,6 +74,14 @@ func (k Keeper) CreateEpochUnbondingRecord(ctx sdk.Context, epochNumber uint64) return true } +// Helper function to evaluate if a host zone unbonding record still needs to be initiated +func (k Keeper) ShouldInitiateHostZoneUnbondingRecord(ctx sdk.Context, hostZoneRecord *recordstypes.HostZoneUnbonding) bool { + if hostZoneRecord.Status == recordstypes.HostZoneUnbonding_UNBONDING_QUEUE && hostZoneRecord.NativeTokenAmount.GT(sdkmath.ZeroInt()) { + return true + } + return false +} + // Gets the total unbonded amount for a host zone by looping through the epoch unbonding records // Also returns the epoch unbonding record ids func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId string) (totalUnbonded sdkmath.Int, unbondingRecordIds []uint64) { @@ -87,7 +95,7 @@ func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId strin epochUnbonding.EpochNumber, hostZoneRecord.Status, hostZoneRecord.NativeTokenAmount)) // We'll unbond all records that have status UNBONDING_QUEUE and have an amount g.t. zero - if hostZoneRecord.Status == recordstypes.HostZoneUnbonding_UNBONDING_QUEUE && hostZoneRecord.NativeTokenAmount.GT(sdkmath.ZeroInt()) { + if k.ShouldInitiateHostZoneUnbondingRecord(k, hostZoneRecord) { totalUnbonded = totalUnbonded.Add(hostZoneRecord.NativeTokenAmount) unbondingRecordIds = append(unbondingRecordIds, epochUnbonding.EpochNumber) k.Logger(ctx).Info(utils.LogWithHostZone(chainId, " %v%s included in total unbonding", hostZoneRecord.NativeTokenAmount, hostZoneRecord.Denom)) @@ -507,6 +515,51 @@ func (k Keeper) InitiateAllHostZoneUnbondings(ctx sdk.Context, dayNumber uint64) } } +// this function iterates each host zone, and if it's the right time to +// initiate an unbonding, it attempts to unbond all outstanding records +func (k Keeper) UpdateRedemptionRatesForAllUnbondingRecords(ctx sdk.Context) { + k.Logger(ctx).Info(fmt.Sprintf("Updating all unbonding records redemption rates to reflect current values")) + + for _, hostZone := range k.GetAllActiveHostZone(ctx) { + redemptionRate := hostZone.RedemptionRate + chainId := hostZone.GetChainId() + // Loop through host zone unbonding records + for _, epochUnbonding := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { + hostZoneUnbondingRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbonding.EpochNumber, chainId) + if !found { + k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No unbonding records found for epoch %d", epochUnbonding.EpochNumber)) + continue + } + if k.ShouldInitiateHostZoneUnbondingRecord(ctx, hostZoneUnbondingRecord) { + // loop through user redemption records for the host zone unbonding record + totalNativeAmount := sdkmath.ZeroInt() + for _, userRedemptionRecordId := range hostZoneUnbondingRecord.GetUserRedemptionRecords() { + // get user redemption record + userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) + if !found { + k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No user redemption record found for id %d", userRedemptionRecordId)) + continue + } + // update the amount + nativeAmount := sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() + userRedemptionRecord.Amount = nativeAmount + totalNativeAmount = totalNativeAmount.Add(nativeAmount) + k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) + } + // update the host zone unbonding record + hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount + updatedEpochUnbondingRecord, success := k.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(ctx, epochUnbonding.EpochNumber, hostZone.ChainId, hostZoneUnbondingRecord) + if !success { + // TODO QUESTION: what should we do on failure? do we revert the userRedemptionRecord changes above? + k.Logger(ctx).Error(fmt.Sprintf("Failed to set host zone epoch unbonding record: epochNumber %d, chainId %s, hostZoneUnbonding %v", epochUnbonding.EpochNumber, hostZone.ChainId, hostZoneUnbondingRecord)) + } + k.RecordsKeeper.SetEpochUnbondingRecord(ctx, *updatedEpochUnbondingRecord) + } + } + + } +} + // Deletes any epoch unbonding records that have had all unbondings claimed func (k Keeper) CleanupEpochUnbondingRecords(ctx sdk.Context, epochNumber uint64) bool { k.Logger(ctx).Info("Cleaning Claimed Epoch Unbonding Records...") From e8f2d0a4e24725809470383ebf11b6c3e109506b Mon Sep 17 00:00:00 2001 From: shellvish <104537253+shellvish@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:47:20 -0700 Subject: [PATCH 14/40] Add Prop 225 to v17 Upgrade Handler (#1044) Co-authored-by: sampocs --- app/upgrades.go | 1 + app/upgrades/v17/upgrades.go | 22 ++++++++++++++++++++++ app/upgrades/v17/upgrades_test.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/app/upgrades.go b/app/upgrades.go index 674b56fdc8..642818edca 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -223,6 +223,7 @@ func (app *StrideApp) setupUpgradeHandlers(appOpts servertypes.AppOptions) { v17.CreateUpgradeHandler( app.mm, app.configurator, + app.BankKeeper, app.DistrKeeper, app.InterchainqueryKeeper, app.RatelimitKeeper, diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index da3a879370..de4f31ad95 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -12,6 +12,8 @@ import ( icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/Stride-Labs/stride/v16/utils" icqkeeper "github.com/Stride-Labs/stride/v16/x/interchainquery/keeper" ratelimitkeeper "github.com/Stride-Labs/stride/v16/x/ratelimit/keeper" @@ -58,12 +60,19 @@ var ( // Osmo transfer channel is required for new rate limits OsmosisTransferChannelId = "channel-5" + + // Constants for Prop 225 + CommunityPoolGrowthAddress = "stride1lj0m72d70qerts9ksrsphy9nmsd4h0s88ll9gfphmhemh8ewet5qj44jc9" + LiquidityReceiver = "stride1auhjs4zgp3ahvrpkspf088r2psz7wpyrypcnal" + Prop225TransferAmount = sdk.NewInt(31_572_300_000) + Ustrd = "ustrd" ) // CreateUpgradeHandler creates an SDK upgrade handler for v17 func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, + bankKeeper bankkeeper.Keeper, distributionkeeper distributionkeeper.Keeper, icqKeeper icqkeeper.Keeper, ratelimitKeeper ratelimitkeeper.Keeper, @@ -102,6 +111,11 @@ func CreateUpgradeHandler( return vm, errorsmod.Wrapf(err, "unable to add rate limits to Osmosis") } + ctx.Logger().Info("Executing Prop 225, SHD Liquidity") + if err := ExecuteProp225(ctx, bankKeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to execute prop 225") + } + return mm.RunMigrations(ctx, configurator, vm) } } @@ -303,3 +317,11 @@ func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { return nil } + +// Execute Prop 225, release STRD to stride1auhjs4zgp3ahvrpkspf088r2psz7wpyrypcnal +func ExecuteProp225(ctx sdk.Context, k bankkeeper.Keeper) error { + communityPoolGrowthAddress := sdk.MustAccAddressFromBech32(CommunityPoolGrowthAddress) + liquidityReceiverAddress := sdk.MustAccAddressFromBech32(LiquidityReceiver) + transferCoin := sdk.NewCoin(Ustrd, Prop225TransferAmount) + return k.SendCoins(ctx, communityPoolGrowthAddress, liquidityReceiverAddress, sdk.NewCoins(transferCoin)) +} diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 44df8d61ed..c8cf11579c 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -74,6 +74,7 @@ func (s *UpgradeTestSuite) TestUpgrade() { checkRateLimitsAfterUpgrade := s.SetupRateLimitsBeforeUpgrade() checkCommunityPoolTaxAfterUpgrade := s.SetupCommunityPoolTaxBeforeUpgrade() checkQueriesAfterUpgrade := s.SetupQueriesBeforeUpgrade() + checkProp225AfterUpgrade := s.SetupProp225BeforeUpgrade() // Submit upgrade and confirm handler succeeds s.ConfirmUpgradeSucceededs("v17", dummyUpgradeHeight) @@ -83,6 +84,7 @@ func (s *UpgradeTestSuite) TestUpgrade() { checkRateLimitsAfterUpgrade() checkCommunityPoolTaxAfterUpgrade() checkQueriesAfterUpgrade() + checkProp225AfterUpgrade() } // Helper function to check that the community pool stake and redeem holding @@ -342,6 +344,35 @@ func (s *UpgradeTestSuite) SetupQueriesBeforeUpgrade() func() { } } +func (s *UpgradeTestSuite) SetupProp225BeforeUpgrade() func() { + // Grab the community pool growth address and balance + communityPoolGrowthAddress := sdk.MustAccAddressFromBech32(v17.CommunityPoolGrowthAddress) + // Set the balance to 3x the amount that will be transferred + newCoin := sdk.NewCoin(v17.Ustrd, v17.Prop225TransferAmount.MulRaw(3)) + s.FundAccount(communityPoolGrowthAddress, newCoin) + originalCommunityGrowthBalance := s.App.BankKeeper.GetBalance(s.Ctx, communityPoolGrowthAddress, v17.Ustrd) + + // Grab the liquidity receiver address and balance + liquidityReceiverAddress := sdk.MustAccAddressFromBech32(v17.LiquidityReceiver) + originalLiquidityReceiverBalance := s.App.BankKeeper.GetBalance(s.Ctx, liquidityReceiverAddress, v17.Ustrd) + + // grab how much we want to transfer + transferAmount := v17.Prop225TransferAmount.Int64() + + // Return callback to check store after upgrade + return func() { + // verify funds left community growth + newCommunityGrowthBalance := s.App.BankKeeper.GetBalance(s.Ctx, communityPoolGrowthAddress, v17.Ustrd) + communityGrowthBalanceChange := originalCommunityGrowthBalance.Sub(newCommunityGrowthBalance) + s.Require().Equal(transferAmount, communityGrowthBalanceChange.Amount.Int64(), "community growth decreased by correct amount") + + // verify funds entered liquidity custodian + newLiquidityCustodianBalance := s.App.BankKeeper.GetBalance(s.Ctx, liquidityReceiverAddress, v17.Ustrd) + liquidityCustodianBalanceChange := newLiquidityCustodianBalance.Sub(originalLiquidityReceiverBalance).Amount.Int64() + s.Require().Equal(transferAmount, liquidityCustodianBalanceChange, "custodian balance increased by correct amount") + } +} + func (s *UpgradeTestSuite) TestRegisterCommunityPoolAddresses() { // Create 3 host zones, with empty ICA addresses chainIds := []string{} From a0d29d9c78ea8aaf5eba31c4671cb4f76af6d10f Mon Sep 17 00:00:00 2001 From: vish-stride Date: Tue, 9 Jan 2024 12:17:43 -0700 Subject: [PATCH 15/40] make NativeAmount 0 until unbonding succeeds --- x/stakeibc/keeper/hooks.go | 2 - x/stakeibc/keeper/msg_server_redeem_stake.go | 16 +-- x/stakeibc/keeper/unbonding_records.go | 130 ++++++++++++------- 3 files changed, 92 insertions(+), 56 deletions(-) diff --git a/x/stakeibc/keeper/hooks.go b/x/stakeibc/keeper/hooks.go index b30ae85df6..938fb302dd 100644 --- a/x/stakeibc/keeper/hooks.go +++ b/x/stakeibc/keeper/hooks.go @@ -26,8 +26,6 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInf } // Day Epoch - Process Unbondings if epochInfo.Identifier == epochstypes.DAY_EPOCH { - // Update unbonding records in each host zone to reflect current RRs - k.UpdateRedemptionRatesForAllUnbondingRecords(ctx) // Initiate unbondings from any hostZone where it's appropriate k.InitiateAllHostZoneUnbondings(ctx, epochNumber) // Check previous epochs to see if unbondings finished, and sweep the tokens if so diff --git a/x/stakeibc/keeper/msg_server_redeem_stake.go b/x/stakeibc/keeper/msg_server_redeem_stake.go index cc615694c4..46cad04850 100644 --- a/x/stakeibc/keeper/msg_server_redeem_stake.go +++ b/x/stakeibc/keeper/msg_server_redeem_stake.go @@ -89,16 +89,17 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) k.Logger(ctx).Info(fmt.Sprintf("UserRedemptionRecord found for %s", redemptionId)) // Add the unbonded amount to the UserRedemptionRecord // The record is set below - userRedemptionRecord.Amount = userRedemptionRecord.Amount.Add(nativeAmount) + userRedemptionRecord.StTokenAmount = userRedemptionRecord.StTokenAmount.Add(msg.Amount) } else { // First time a user is redeeming this epoch userRedemptionRecord = recordstypes.UserRedemptionRecord{ - Id: redemptionId, - Receiver: msg.Receiver, - Amount: nativeAmount, - Denom: hostZone.HostDenom, - HostZoneId: hostZone.ChainId, - EpochNumber: epochTracker.EpochNumber, + Id: redemptionId, + Receiver: msg.Receiver, + Amount: sdk.ZeroInt(), + Denom: hostZone.HostDenom, + HostZoneId: hostZone.ChainId, + EpochNumber: epochTracker.EpochNumber, + StTokenAmount: msg.Amount, // claimIsPending represents whether a redemption is currently being claimed, // contingent on the host zone unbonding having status CLAIMABLE ClaimIsPending: false, @@ -117,7 +118,6 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) if !found { return nil, errorsmod.Wrapf(types.ErrInvalidHostZone, "host zone not found in unbondings: %s", hostZone.ChainId) } - hostZoneUnbonding.NativeTokenAmount = hostZoneUnbonding.NativeTokenAmount.Add(nativeAmount) if !userHasRedeemedThisEpoch { // Only append a UserRedemptionRecord to the HZU if it wasn't previously appended hostZoneUnbonding.UserRedemptionRecords = append(hostZoneUnbonding.UserRedemptionRecords, userRedemptionRecord.Id) diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 17cd4f2893..d088e3c9a1 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -95,7 +95,14 @@ func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId strin epochUnbonding.EpochNumber, hostZoneRecord.Status, hostZoneRecord.NativeTokenAmount)) // We'll unbond all records that have status UNBONDING_QUEUE and have an amount g.t. zero - if k.ShouldInitiateHostZoneUnbondingRecord(k, hostZoneRecord) { + if k.ShouldInitiateHostZoneUnbondingRecord(ctx, hostZoneRecord) { + // Dynamically calculate the unbonding amount based on the current redemption rate + success := k.UpdateNativeTokensForHostZoneUnbondingRecord(ctx, epochUnbonding.EpochNumber, hostZoneRecord) + if !success { + k.Logger(ctx).Error(utils.LogWithHostZone(chainId, "Failed to update native tokens for host zone unbonding record in epoch %d", epochUnbonding.EpochNumber)) + continue + } + totalUnbonded = totalUnbonded.Add(hostZoneRecord.NativeTokenAmount) unbondingRecordIds = append(unbondingRecordIds, epochUnbonding.EpochNumber) k.Logger(ctx).Info(utils.LogWithHostZone(chainId, " %v%s included in total unbonding", hostZoneRecord.NativeTokenAmount, hostZoneRecord.Denom)) @@ -104,6 +111,82 @@ func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId strin return totalUnbonded, unbondingRecordIds } +// Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to reflect the current redemption rate +// If setToZero is true, then the native amounts are set to zero, ignoring the redemption rate. This should be used if the unbonding didn't succeed +func (k Keeper) UpdateNativeTokensForHostZoneUnbondingRecord(ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding, setToZero bool) bool { + totalNativeAmount := sdkmath.ZeroInt() + chainId := hostZoneUnbondingRecord.GetHostZoneId() + hostZone, found := k.GetHostZone(ctx, chainId) + if !found { + k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No host zone found for id %s, Not Updating Native Token Amounts", chainId)) + return false + } + redemptionRate := hostZone.RedemptionRate + // loop through user redemption records for the host zone unbonding record, update each one + for _, userRedemptionRecordId := range hostZoneUnbondingRecord.GetUserRedemptionRecords() { + userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) + if !found { + k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No user redemption record found for id %s", userRedemptionRecordId)) + continue + } + // update the amount, based on the current redemption rate (or set to zero if desired) + nativeAmount := sdkmath.ZeroInt() + if !setToZero { + nativeAmount = sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() + } + userRedemptionRecord.Amount = nativeAmount + totalNativeAmount = totalNativeAmount.Add(nativeAmount) + k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) + } + // update the host zone unbonding record + hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount + updatedEpochUnbondingRecord, success := k.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(ctx, epochNumber, chainId, hostZoneUnbondingRecord) + if !success { + // TODO QUESTION: what should we do on failure? do we revert the userRedemptionRecord changes above? + k.Logger(ctx).Error(fmt.Sprintf("Failed to set host zone epoch unbonding record: epochNumber %d, chainId %s, hostZoneUnbonding %v", epochNumber, hostZone.ChainId, hostZoneUnbondingRecord)) + return false + } + k.RecordsKeeper.SetEpochUnbondingRecord(ctx, *updatedEpochUnbondingRecord) + + return true +} + +// Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to reflect the current redemption rate +func (k Keeper) SetNativeTokensToZeroIn(ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding) bool { + totalNativeAmount := sdkmath.ZeroInt() + chainId := hostZoneUnbondingRecord.GetHostZoneId() + hostZone, found := k.GetHostZone(ctx, chainId) + if !found { + k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No host zone found for id %s, Not Updating Native Token Amounts", chainId)) + return false + } + redemptionRate := hostZone.RedemptionRate + // loop through user redemption records for the host zone unbonding record, update each one + for _, userRedemptionRecordId := range hostZoneUnbondingRecord.GetUserRedemptionRecords() { + userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) + if !found { + k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No user redemption record found for id %s", userRedemptionRecordId)) + continue + } + // update the amount, based on the current redemption rate + nativeAmount := sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() + userRedemptionRecord.Amount = nativeAmount + totalNativeAmount = totalNativeAmount.Add(nativeAmount) + k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) + } + // update the host zone unbonding record + hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount + updatedEpochUnbondingRecord, success := k.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(ctx, epochNumber, chainId, hostZoneUnbondingRecord) + if !success { + // TODO QUESTION: what should we do on failure? do we revert the userRedemptionRecord changes above? + k.Logger(ctx).Error(fmt.Sprintf("Failed to set host zone epoch unbonding record: epochNumber %d, chainId %s, hostZoneUnbonding %v", epochNumber, hostZone.ChainId, hostZoneUnbondingRecord)) + return false + } + k.RecordsKeeper.SetEpochUnbondingRecord(ctx, *updatedEpochUnbondingRecord) + + return true +} + // Determine the unbonding capacity that each validator has // The capacity is determined by the difference between their current delegation // and their fair portion of the total stake based on their weights @@ -515,51 +598,6 @@ func (k Keeper) InitiateAllHostZoneUnbondings(ctx sdk.Context, dayNumber uint64) } } -// this function iterates each host zone, and if it's the right time to -// initiate an unbonding, it attempts to unbond all outstanding records -func (k Keeper) UpdateRedemptionRatesForAllUnbondingRecords(ctx sdk.Context) { - k.Logger(ctx).Info(fmt.Sprintf("Updating all unbonding records redemption rates to reflect current values")) - - for _, hostZone := range k.GetAllActiveHostZone(ctx) { - redemptionRate := hostZone.RedemptionRate - chainId := hostZone.GetChainId() - // Loop through host zone unbonding records - for _, epochUnbonding := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { - hostZoneUnbondingRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbonding.EpochNumber, chainId) - if !found { - k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No unbonding records found for epoch %d", epochUnbonding.EpochNumber)) - continue - } - if k.ShouldInitiateHostZoneUnbondingRecord(ctx, hostZoneUnbondingRecord) { - // loop through user redemption records for the host zone unbonding record - totalNativeAmount := sdkmath.ZeroInt() - for _, userRedemptionRecordId := range hostZoneUnbondingRecord.GetUserRedemptionRecords() { - // get user redemption record - userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) - if !found { - k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No user redemption record found for id %d", userRedemptionRecordId)) - continue - } - // update the amount - nativeAmount := sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() - userRedemptionRecord.Amount = nativeAmount - totalNativeAmount = totalNativeAmount.Add(nativeAmount) - k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) - } - // update the host zone unbonding record - hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount - updatedEpochUnbondingRecord, success := k.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(ctx, epochUnbonding.EpochNumber, hostZone.ChainId, hostZoneUnbondingRecord) - if !success { - // TODO QUESTION: what should we do on failure? do we revert the userRedemptionRecord changes above? - k.Logger(ctx).Error(fmt.Sprintf("Failed to set host zone epoch unbonding record: epochNumber %d, chainId %s, hostZoneUnbonding %v", epochUnbonding.EpochNumber, hostZone.ChainId, hostZoneUnbondingRecord)) - } - k.RecordsKeeper.SetEpochUnbondingRecord(ctx, *updatedEpochUnbondingRecord) - } - } - - } -} - // Deletes any epoch unbonding records that have had all unbondings claimed func (k Keeper) CleanupEpochUnbondingRecords(ctx sdk.Context, epochNumber uint64) bool { k.Logger(ctx).Info("Cleaning Claimed Epoch Unbonding Records...") From 9c4555433ff91baf69d0d44544a63e15af26ca9b Mon Sep 17 00:00:00 2001 From: vish-stride Date: Tue, 9 Jan 2024 14:18:01 -0700 Subject: [PATCH 16/40] Add UpgradeHandler Logic --- app/upgrades/v17/upgrades.go | 64 ++++++++++++++++++++++++++ x/stakeibc/keeper/unbonding_records.go | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index da3a879370..c163141b89 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -16,6 +16,7 @@ import ( icqkeeper "github.com/Stride-Labs/stride/v16/x/interchainquery/keeper" ratelimitkeeper "github.com/Stride-Labs/stride/v16/x/ratelimit/keeper" ratelimittypes "github.com/Stride-Labs/stride/v16/x/ratelimit/types" + recordtypes "github.com/Stride-Labs/stride/v16/x/records/types" stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) @@ -102,6 +103,11 @@ func CreateUpgradeHandler( return vm, errorsmod.Wrapf(err, "unable to add rate limits to Osmosis") } + ctx.Logger().Info("Migrating Unbonding Records...") + if err := MigrateUnbondingRecords(ctx, stakeibcKeeper); err != nil { + return vm, errorsmod.Wrapf(err, "unable to migrate unbonding records") + } + return mm.RunMigrations(ctx, configurator, vm) } } @@ -303,3 +309,61 @@ func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { return nil } + +// Migrate the unbonding records +// UserUnbondingRecords previously only used Native Token Amounts, we now want to use StTokenAmounts +// Similarly, we should modify HostZoneUnbondingRecords to NOT use NativeTokenAmounts prior to unbonding being initiated +func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { + for _, epochUnbondingRecord := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { + for _, hostZoneUnbonding := range epochUnbondingRecord.GetHostZoneUnbondings() { + // if the tokens have unbonded, we don't want to modify the record + // this is because we won't be able to estimate a redemption rate + if hostZoneUnbonding.Status == recordtypes.HostZoneUnbonding_CLAIMABLE { + continue + } + // similarly, if there aren't any tokens to unbond, we don't want to modify the record + // as we won't be able to estimate a redemption rate + if hostZoneUnbonding.NativeTokenAmount == sdkmath.ZeroInt() { + continue + } + + // Calculate the estimated redemption rate + nativeTokenAmountDec := sdk.NewDecFromInt(hostZoneUnbonding.NativeTokenAmount) + stTokenAmountDec := sdk.NewDecFromInt(hostZoneUnbonding.StTokenAmount) + // this estimated rate is the amount of stTokens that would be received for 1 native token + // e.g. if the rate is 0.5, then 1 native token would be worth 0.5 stTokens + estimatedStTokenConversionRate := stTokenAmountDec.Quo(nativeTokenAmountDec) + + // store if the unbonding has not been initiated + unbondingNotInitiated := hostZoneUnbonding.Status == recordtypes.HostZoneUnbonding_UNBONDING_QUEUE + + // Loop through User Redemption Records and insert an estimated stTokenAmount + for _, userRedemptionRecordId := range hostZoneUnbonding.GetUserRedemptionRecords() { + userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) + if !found { + // this would happen if the user has already claimed the unbonding, but given the status check above, this should never happen + k.Logger(ctx).Error(fmt.Sprintf("user redemption record %s not found", userRedemptionRecordId)) + continue + } + + userRedemptionRecord.StTokenAmount = estimatedStTokenConversionRate.Mul(sdkmath.LegacyDec(userRedemptionRecord.Amount)).RoundInt() + + if unbondingNotInitiated { + userRedemptionRecord.Amount = sdkmath.ZeroInt() + } + + k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) + } + + // if the unbonding has not been initiated, we want to set nativeTokenAmount to 0 for the whole HostZoneUnbonding + if unbondingNotInitiated { + hostZoneUnbonding.NativeTokenAmount = sdkmath.ZeroInt() + + } + } + + k.RecordsKeeper.SetEpochUnbondingRecord(ctx, epochUnbondingRecord) + } + + return nil +} diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index d088e3c9a1..0cea008ef5 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -152,7 +152,7 @@ func (k Keeper) UpdateNativeTokensForHostZoneUnbondingRecord(ctx sdk.Context, ep } // Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to reflect the current redemption rate -func (k Keeper) SetNativeTokensToZeroIn(ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding) bool { +func (k Keeper) SetNativeTokensToZeroInUnbondingRecords(ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding) bool { totalNativeAmount := sdkmath.ZeroInt() chainId := hostZoneUnbondingRecord.GetHostZoneId() hostZone, found := k.GetHostZone(ctx, chainId) From a71b9b47e72f6454a585e88c5cee8a7b24ec6553 Mon Sep 17 00:00:00 2001 From: vish-stride Date: Tue, 9 Jan 2024 15:32:39 -0700 Subject: [PATCH 17/40] handle failure cases for dynamic RRs --- x/stakeibc/keeper/icacallbacks_undelegate.go | 5 ++ .../msg_server_restore_interchain_account.go | 7 +++ x/stakeibc/keeper/unbonding_records.go | 60 ++++++++----------- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/x/stakeibc/keeper/icacallbacks_undelegate.go b/x/stakeibc/keeper/icacallbacks_undelegate.go index a03da4c86c..73f5294542 100644 --- a/x/stakeibc/keeper/icacallbacks_undelegate.go +++ b/x/stakeibc/keeper/icacallbacks_undelegate.go @@ -66,6 +66,11 @@ func (k Keeper) UndelegateCallback(ctx sdk.Context, packet channeltypes.Packet, k.Logger(ctx).Error(utils.LogICACallbackStatusWithHostZone(chainId, ICACallbackID_Undelegate, icacallbackstypes.AckResponseStatus_FAILURE, packet)) + // Set NativeTokenAmounts on these HZUs to 0 + if err := k.SetNativeTokensToZeroInUnbondingRecords(ctx, chainId, undelegateCallback.EpochUnbondingRecordIds); err != nil { + return err + } + // Reset unbondings record status if err := k.RecordsKeeper.SetHostZoneUnbondings( ctx, diff --git a/x/stakeibc/keeper/msg_server_restore_interchain_account.go b/x/stakeibc/keeper/msg_server_restore_interchain_account.go index 77736a9e30..f81d7850fd 100644 --- a/x/stakeibc/keeper/msg_server_restore_interchain_account.go +++ b/x/stakeibc/keeper/msg_server_restore_interchain_account.go @@ -90,6 +90,13 @@ func (k msgServer) RestoreInterchainAccount(goCtx context.Context, msg *types.Ms epochNumberForPendingTransferRecords = append(epochNumberForPendingTransferRecords, epochUnbondingRecord.EpochNumber) } } + // Set UNBONDING_IN_PROGRESS records NativeTokenAmounts to 0 + err := k.SetNativeTokensToZeroInUnbondingRecords(ctx, hostZone.ChainId, epochNumberForPendingUnbondingRecords) + if err != nil { + errMsg := fmt.Sprintf("unable to set native token amounts to 0 for chainId: %s and epochUnbondingRecordIds: %v, err: %s") + k.Logger(ctx).Error(errMsg) + // TODO Question: should we return an error here, or just proceed? + } // Revert UNBONDING_IN_PROGRESS records to UNBONDING_QUEUE err := k.RecordsKeeper.SetHostZoneUnbondings(ctx, hostZone.ChainId, epochNumberForPendingUnbondingRecords, recordtypes.HostZoneUnbonding_UNBONDING_QUEUE) if err != nil { diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 0cea008ef5..a4901345ee 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -97,7 +97,7 @@ func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId strin // We'll unbond all records that have status UNBONDING_QUEUE and have an amount g.t. zero if k.ShouldInitiateHostZoneUnbondingRecord(ctx, hostZoneRecord) { // Dynamically calculate the unbonding amount based on the current redemption rate - success := k.UpdateNativeTokensForHostZoneUnbondingRecord(ctx, epochUnbonding.EpochNumber, hostZoneRecord) + success := k.UpdateNativeTokensForHostZoneUnbondingRecord(ctx, epochUnbonding.EpochNumber, hostZoneRecord, false) if !success { k.Logger(ctx).Error(utils.LogWithHostZone(chainId, "Failed to update native tokens for host zone unbonding record in epoch %d", epochUnbonding.EpochNumber)) continue @@ -112,8 +112,14 @@ func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId strin } // Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to reflect the current redemption rate -// If setToZero is true, then the native amounts are set to zero, ignoring the redemption rate. This should be used if the unbonding didn't succeed -func (k Keeper) UpdateNativeTokensForHostZoneUnbondingRecord(ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding, setToZero bool) bool { +// If setNativeAmountsToZero is true, then the native amounts are set to zero, ignoring the redemption rate. This should be used if the unbonding didn't succeed +// Otherwise, the native amounts are set based on the current redemption rate +func (k Keeper) UpdateNativeTokensForHostZoneUnbondingRecord( + ctx sdk.Context, + epochNumber uint64, + hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding, + setNativeAmountsToZero bool, +) bool { totalNativeAmount := sdkmath.ZeroInt() chainId := hostZoneUnbondingRecord.GetHostZoneId() hostZone, found := k.GetHostZone(ctx, chainId) @@ -131,7 +137,7 @@ func (k Keeper) UpdateNativeTokensForHostZoneUnbondingRecord(ctx sdk.Context, ep } // update the amount, based on the current redemption rate (or set to zero if desired) nativeAmount := sdkmath.ZeroInt() - if !setToZero { + if !setNativeAmountsToZero { nativeAmount = sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() } userRedemptionRecord.Amount = nativeAmount @@ -151,40 +157,26 @@ func (k Keeper) UpdateNativeTokensForHostZoneUnbondingRecord(ctx sdk.Context, ep return true } -// Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to reflect the current redemption rate -func (k Keeper) SetNativeTokensToZeroInUnbondingRecords(ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding) bool { - totalNativeAmount := sdkmath.ZeroInt() - chainId := hostZoneUnbondingRecord.GetHostZoneId() - hostZone, found := k.GetHostZone(ctx, chainId) - if !found { - k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No host zone found for id %s, Not Updating Native Token Amounts", chainId)) - return false - } - redemptionRate := hostZone.RedemptionRate - // loop through user redemption records for the host zone unbonding record, update each one - for _, userRedemptionRecordId := range hostZoneUnbondingRecord.GetUserRedemptionRecords() { - userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) +// Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to 0 +// to reflect that the unbonding has NOT been finalized yet +func (k Keeper) SetNativeTokensToZeroInUnbondingRecords(ctx sdk.Context, chainId string, epochUnbondingRecordIds []uint64) error { + for _, epochUnbondingRecordId := range epochUnbondingRecordIds { + k.Logger(ctx).Info(fmt.Sprintf("Updating native token amount on host zone unbondings on EpochUnbondingRecord %d to zero", epochUnbondingRecordId)) + // fetch the host zone unbonding + hostZoneUnbonding, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbondingRecordId, chainId) if !found { - k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No user redemption record found for id %s", userRedemptionRecordId)) + 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(types.ErrHostZoneNotFound, errMsg) + } + success := k.UpdateNativeTokensForHostZoneUnbondingRecord(ctx, epochUnbondingRecordId, hostZoneUnbonding, true) + if !success { + errMsg := fmt.Sprintf("Error zeroing native token amount on host zone unbonding record for epoch: %d, host zone: %s", epochUnbondingRecordId, chainId) + k.Logger(ctx).Error(errMsg) continue } - // update the amount, based on the current redemption rate - nativeAmount := sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() - userRedemptionRecord.Amount = nativeAmount - totalNativeAmount = totalNativeAmount.Add(nativeAmount) - k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) } - // update the host zone unbonding record - hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount - updatedEpochUnbondingRecord, success := k.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(ctx, epochNumber, chainId, hostZoneUnbondingRecord) - if !success { - // TODO QUESTION: what should we do on failure? do we revert the userRedemptionRecord changes above? - k.Logger(ctx).Error(fmt.Sprintf("Failed to set host zone epoch unbonding record: epochNumber %d, chainId %s, hostZoneUnbonding %v", epochNumber, hostZone.ChainId, hostZoneUnbondingRecord)) - return false - } - k.RecordsKeeper.SetEpochUnbondingRecord(ctx, *updatedEpochUnbondingRecord) - - return true + return nil } // Determine the unbonding capacity that each validator has From 3e8468750be48a0f1d80e9e9e205fa09259bb461 Mon Sep 17 00:00:00 2001 From: vish-stride Date: Tue, 9 Jan 2024 16:54:20 -0700 Subject: [PATCH 18/40] bugfix --- x/stakeibc/keeper/msg_server_restore_interchain_account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/stakeibc/keeper/msg_server_restore_interchain_account.go b/x/stakeibc/keeper/msg_server_restore_interchain_account.go index f81d7850fd..0c34b3b12f 100644 --- a/x/stakeibc/keeper/msg_server_restore_interchain_account.go +++ b/x/stakeibc/keeper/msg_server_restore_interchain_account.go @@ -98,7 +98,7 @@ func (k msgServer) RestoreInterchainAccount(goCtx context.Context, msg *types.Ms // TODO Question: should we return an error here, or just proceed? } // Revert UNBONDING_IN_PROGRESS records to UNBONDING_QUEUE - err := k.RecordsKeeper.SetHostZoneUnbondings(ctx, hostZone.ChainId, epochNumberForPendingUnbondingRecords, recordtypes.HostZoneUnbonding_UNBONDING_QUEUE) + err = k.RecordsKeeper.SetHostZoneUnbondings(ctx, hostZone.ChainId, epochNumberForPendingUnbondingRecords, recordtypes.HostZoneUnbonding_UNBONDING_QUEUE) if err != nil { errMsg := fmt.Sprintf("unable to update host zone unbonding record status to %s for chainId: %s and epochUnbondingRecordIds: %v, err: %s", recordtypes.HostZoneUnbonding_UNBONDING_QUEUE.String(), hostZone.ChainId, epochNumberForPendingUnbondingRecords, err) From 69a9b7be69dae7a6be10b525be6211656a5544db Mon Sep 17 00:00:00 2001 From: vish-stride Date: Tue, 9 Jan 2024 18:17:34 -0700 Subject: [PATCH 19/40] address Riley's PR comments --- x/stakeibc/keeper/msg_server_redeem_stake.go | 9 ++------- x/stakeibc/keeper/unbonding_records.go | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/x/stakeibc/keeper/msg_server_redeem_stake.go b/x/stakeibc/keeper/msg_server_redeem_stake.go index 46cad04850..554ad66540 100644 --- a/x/stakeibc/keeper/msg_server_redeem_stake.go +++ b/x/stakeibc/keeper/msg_server_redeem_stake.go @@ -63,11 +63,6 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) return nil, errorsmod.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg) } - coinString := nativeAmount.String() + stDenom - inCoin, err := sdk.ParseCoinNormalized(coinString) - if err != nil { - return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "could not parse inCoin: %s. err: %s", coinString, err.Error()) - } // safety checks on the coin // - Redemption amount must be positive if !nativeAmount.IsPositive() { @@ -75,8 +70,8 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) } // - Creator owns at least "amount" stAssets balance := k.bankKeeper.GetBalance(ctx, sender, stDenom) - k.Logger(ctx).Info(fmt.Sprintf("Redemption issuer IBCDenom balance: %v%s", balance.Amount, balance.Denom)) - k.Logger(ctx).Info(fmt.Sprintf("Redemption requested redemotion amount: %v%s", inCoin.Amount, inCoin.Denom)) + k.Logger(ctx).Info(fmt.Sprintf("Redemption issuer stDenom balance: %v%s", balance.Amount, balance.Denom)) + k.Logger(ctx).Info(fmt.Sprintf("Redemption requested stDenom amount: %v%s", msg.Amount, stDenom)) if balance.Amount.LT(msg.Amount) { return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "balance is lower than redemption amount. redemption amount: %v, balance %v: ", msg.Amount, balance.Amount) } diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index a4901345ee..29bf50c730 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -76,7 +76,7 @@ func (k Keeper) CreateEpochUnbondingRecord(ctx sdk.Context, epochNumber uint64) // Helper function to evaluate if a host zone unbonding record still needs to be initiated func (k Keeper) ShouldInitiateHostZoneUnbondingRecord(ctx sdk.Context, hostZoneRecord *recordstypes.HostZoneUnbonding) bool { - if hostZoneRecord.Status == recordstypes.HostZoneUnbonding_UNBONDING_QUEUE && hostZoneRecord.NativeTokenAmount.GT(sdkmath.ZeroInt()) { + if hostZoneRecord.Status == recordstypes.HostZoneUnbonding_UNBONDING_QUEUE && hostZoneRecord.StTokenAmount.GT(sdkmath.ZeroInt()) { return true } return false From 7152e6e45bef842402e5cff8b5c0dd45d0111f06 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 11:30:12 -0600 Subject: [PATCH 20/40] first pass at refactor --- x/records/keeper/epoch_unbonding_record.go | 12 ++ x/records/types/errors.go | 1 + x/records/types/records.go | 8 + x/stakeibc/keeper/unbonding_records.go | 163 +++++++++++---------- 4 files changed, 110 insertions(+), 74 deletions(-) create mode 100644 x/records/types/records.go diff --git a/x/records/keeper/epoch_unbonding_record.go b/x/records/keeper/epoch_unbonding_record.go index 90194f54bc..6ee59219fa 100644 --- a/x/records/keeper/epoch_unbonding_record.go +++ b/x/records/keeper/epoch_unbonding_record.go @@ -101,6 +101,7 @@ 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) { epochUnbondingRecord, found := k.GetEpochUnbondingRecord(ctx, epochNumber) if !found { @@ -121,7 +122,18 @@ func (k Keeper) AddHostZoneToEpochUnbondingRecord(ctx sdk.Context, epochNumber u return &epochUnbondingRecord, true } +// 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) + } + k.SetEpochUnbondingRecord(ctx, *epochUnbondingRecord) + return nil +} + // Updates the status for a given host zone across relevant epoch unbonding record IDs +// TODO [cleanup]: Rename to SetHostZoneUnbondingStatus func (k Keeper) SetHostZoneUnbondings(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())) diff --git a/x/records/types/errors.go b/x/records/types/errors.go index 8a77adbe81..2186fd6220 100644 --- a/x/records/types/errors.go +++ b/x/records/types/errors.go @@ -11,4 +11,5 @@ var ( ErrUnknownDepositRecord = errorsmod.Register(ModuleName, 1504, "unknown deposit record") ErrUnmarshalFailure = errorsmod.Register(ModuleName, 1505, "cannot unmarshal") ErrAddingHostZone = errorsmod.Register(ModuleName, 1506, "could not add hzu to epoch unbonding record") + ErrHostUnbondingRecordNotFound = errorsmod.Register(ModuleName, 1507, "host zone unbonding record not found on epoch unbonding record") ) diff --git a/x/records/types/records.go b/x/records/types/records.go new file mode 100644 index 0000000000..40a0d7e5e6 --- /dev/null +++ b/x/records/types/records.go @@ -0,0 +1,8 @@ +package types + +import sdkmath "cosmossdk.io/math" + +// Helper function to evaluate if a host zone unbonding record still needs to be initiated +func (r HostZoneUnbonding) ShouldInitiateUnbonding() bool { + return r.Status == HostZoneUnbonding_UNBONDING_QUEUE && r.StTokenAmount.GT(sdkmath.ZeroInt()) +} diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 29bf50c730..9a7c38754f 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -74,18 +74,12 @@ func (k Keeper) CreateEpochUnbondingRecord(ctx sdk.Context, epochNumber uint64) return true } -// Helper function to evaluate if a host zone unbonding record still needs to be initiated -func (k Keeper) ShouldInitiateHostZoneUnbondingRecord(ctx sdk.Context, hostZoneRecord *recordstypes.HostZoneUnbonding) bool { - if hostZoneRecord.Status == recordstypes.HostZoneUnbonding_UNBONDING_QUEUE && hostZoneRecord.StTokenAmount.GT(sdkmath.ZeroInt()) { - return true - } - return false -} - -// Gets the total unbonded amount for a host zone by looping through the epoch unbonding records -// Also returns the epoch unbonding record ids -func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId string) (totalUnbonded sdkmath.Int, unbondingRecordIds []uint64) { - totalUnbonded = sdk.ZeroInt() +// Returns the list of all epoch unbonding record IDs and host zone unbonding records that should unbond this epoch +// They're identified by status UNBONDING_QUEUE and a non-zero stToken amount +func (k Keeper) GetQueuedHostZoneUnbondingRecords( + ctx sdk.Context, + chainId string, +) (epochNumbers []uint64, hostZoneUnbondings []recordstypes.HostZoneUnbonding) { for _, epochUnbonding := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { hostZoneRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbonding.EpochNumber, chainId) if !found { @@ -94,87 +88,96 @@ func (k Keeper) GetTotalUnbondAmountAndRecordsIds(ctx sdk.Context, chainId strin k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "Epoch %d - Status: %s, Amount: %v", epochUnbonding.EpochNumber, hostZoneRecord.Status, hostZoneRecord.NativeTokenAmount)) - // We'll unbond all records that have status UNBONDING_QUEUE and have an amount g.t. zero - if k.ShouldInitiateHostZoneUnbondingRecord(ctx, hostZoneRecord) { - // Dynamically calculate the unbonding amount based on the current redemption rate - success := k.UpdateNativeTokensForHostZoneUnbondingRecord(ctx, epochUnbonding.EpochNumber, hostZoneRecord, false) - if !success { - k.Logger(ctx).Error(utils.LogWithHostZone(chainId, "Failed to update native tokens for host zone unbonding record in epoch %d", epochUnbonding.EpochNumber)) - continue - } - - totalUnbonded = totalUnbonded.Add(hostZoneRecord.NativeTokenAmount) - unbondingRecordIds = append(unbondingRecordIds, epochUnbonding.EpochNumber) - k.Logger(ctx).Info(utils.LogWithHostZone(chainId, " %v%s included in total unbonding", hostZoneRecord.NativeTokenAmount, hostZoneRecord.Denom)) + if hostZoneRecord.ShouldInitiateUnbonding() { + epochNumbers = append(epochNumbers, epochUnbonding.EpochNumber) + hostZoneUnbondings = append(hostZoneUnbondings, *hostZoneRecord) } } - return totalUnbonded, unbondingRecordIds + return epochNumbers, hostZoneUnbondings } -// Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to reflect the current redemption rate -// If setNativeAmountsToZero is true, then the native amounts are set to zero, ignoring the redemption rate. This should be used if the unbonding didn't succeed -// Otherwise, the native amounts are set based on the current redemption rate -func (k Keeper) UpdateNativeTokensForHostZoneUnbondingRecord( - ctx sdk.Context, - epochNumber uint64, - hostZoneUnbondingRecord *recordstypes.HostZoneUnbonding, - setNativeAmountsToZero bool, -) bool { - totalNativeAmount := sdkmath.ZeroInt() - chainId := hostZoneUnbondingRecord.GetHostZoneId() - hostZone, found := k.GetHostZone(ctx, chainId) - if !found { - k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No host zone found for id %s, Not Updating Native Token Amounts", chainId)) - return false +// Gets the total unbonded amount for a host zone by looping through the epoch unbonding records +// Also returns the epoch unbonding record ids +func (k Keeper) GetTotalUnbondAmount(ctx sdk.Context, hostZoneUnbondingRecords []recordstypes.HostZoneUnbonding) (totalUnbonded sdkmath.Int) { + totalUnbonded = sdk.ZeroInt() + for _, hostZoneRecord := range hostZoneUnbondingRecords { + totalUnbonded = totalUnbonded.Add(hostZoneRecord.NativeTokenAmount) } - redemptionRate := hostZone.RedemptionRate - // loop through user redemption records for the host zone unbonding record, update each one - for _, userRedemptionRecordId := range hostZoneUnbondingRecord.GetUserRedemptionRecords() { + return totalUnbonded +} + +// Given a list of redemption record IDs and a redemption rate, sets the native token amount on each record and returns the total +// The native amount is calculated using the redemption rate. If a zero redemption rate is passed, set the amount to zero +func (k Keeper) SetUserRedemptionRecordNativeAmounts( + ctx sdk.Context, + chainId string, + redemptionRecordIds []string, + redemptionRate sdk.Dec, +) (totalNativeAmount sdkmath.Int) { + // Loop and set the native amount for each record, keeping track fo the total + totalNativeAmount = sdkmath.ZeroInt() + for _, userRedemptionRecordId := range redemptionRecordIds { userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) if !found { - k.Logger(ctx).Info(utils.LogWithHostZone(chainId, "No user redemption record found for id %s", userRedemptionRecordId)) + k.Logger(ctx).Error(utils.LogWithHostZone(chainId, "No user redemption record found for id %s", userRedemptionRecordId)) continue } - // update the amount, based on the current redemption rate (or set to zero if desired) + + // A zero redemption rate is used to indicate that the native amount should be set to zero + // If the redemption rate is non-zero, calculate the number of native tokens nativeAmount := sdkmath.ZeroInt() - if !setNativeAmountsToZero { + if !redemptionRate.IsZero() { nativeAmount = sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() } - userRedemptionRecord.Amount = nativeAmount totalNativeAmount = totalNativeAmount.Add(nativeAmount) + + // Set the native amount on the record + userRedemptionRecord.Amount = nativeAmount k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) } - // update the host zone unbonding record - hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount - updatedEpochUnbondingRecord, success := k.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(ctx, epochNumber, chainId, hostZoneUnbondingRecord) - if !success { - // TODO QUESTION: what should we do on failure? do we revert the userRedemptionRecord changes above? - k.Logger(ctx).Error(fmt.Sprintf("Failed to set host zone epoch unbonding record: epochNumber %d, chainId %s, hostZoneUnbonding %v", epochNumber, hostZone.ChainId, hostZoneUnbondingRecord)) - return false + return totalNativeAmount +} + +// Sets the native token amount unbonded on the host zone unbonding record and the associated user redemption records +func (k Keeper) SetHostZoneUnbondingNativeTokenAmounts( + ctx sdk.Context, + epochNumber uint64, + hostZoneUnbondingRecord recordstypes.HostZoneUnbonding, +) error { + // Grab the redemption rate from the host zone (to use in the native token calculation) + chainId := hostZoneUnbondingRecord.HostZoneId + hostZone, found := k.GetHostZone(ctx, chainId) + if !found { + return errorsmod.Wrapf(types.ErrHostZoneNotFound, "host zone %s not found", chainId) } - k.RecordsKeeper.SetEpochUnbondingRecord(ctx, *updatedEpochUnbondingRecord) - return true + // Set all native token amount on each user redemption record + redemptionRecordIds := hostZoneUnbondingRecord.UserRedemptionRecords + totalNativeAmount := k.SetUserRedemptionRecordNativeAmounts(ctx, chainId, redemptionRecordIds, hostZone.RedemptionRate) + + // Then set the total on the host zone unbonding record + hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount + return k.RecordsKeeper.SetHostZoneUnbondingRecord(ctx, epochNumber, chainId, hostZoneUnbondingRecord) } -// Update Native Amounts in the Host Zone Unbonding Record (and associated User Redemption Records) to 0 -// to reflect that the unbonding has NOT been finalized yet -func (k Keeper) SetNativeTokensToZeroInUnbondingRecords(ctx sdk.Context, chainId string, epochUnbondingRecordIds []uint64) error { - for _, epochUnbondingRecordId := range epochUnbondingRecordIds { - k.Logger(ctx).Info(fmt.Sprintf("Updating native token amount on host zone unbondings on EpochUnbondingRecord %d to zero", epochUnbondingRecordId)) - // fetch the host zone unbonding - hostZoneUnbonding, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbondingRecordId, chainId) +// Resets the native token amounts back to zero on the host zone unbonding record and user redemption records +// This is done if the unbonding callback fails, used to indicate that it still needs to be processed +func (k Keeper) ResetUnbondingNativeTokenAmounts(ctx sdk.Context, chainId string, epochUnbondingRecordIds []uint64) error { + for _, epochNumber := range epochUnbondingRecordIds { + k.Logger(ctx).Info(fmt.Sprintf("Resetting native token amount on host zone unbondings on EpochUnbondingRecord %d to zero", epochNumber)) + + hostZoneUnbondingRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochNumber, 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(types.ErrHostZoneNotFound, errMsg) - } - success := k.UpdateNativeTokensForHostZoneUnbondingRecord(ctx, epochUnbondingRecordId, hostZoneUnbonding, true) - if !success { - errMsg := fmt.Sprintf("Error zeroing native token amount on host zone unbonding record for epoch: %d, host zone: %s", epochUnbondingRecordId, chainId) - k.Logger(ctx).Error(errMsg) - continue + return errorsmod.Wrapf(recordstypes.ErrHostUnbondingRecordNotFound, "chain %d, epoch %d", chainId, epochNumber) } + + // Reset the user redemption records back to zero + redemptionRecordIds := hostZoneUnbondingRecord.UserRedemptionRecords + k.SetUserRedemptionRecordNativeAmounts(ctx, chainId, redemptionRecordIds, sdk.ZeroDec()) + + // Reset the host zone unbonding record back to zero + hostZoneUnbondingRecord.NativeTokenAmount = sdkmath.ZeroInt() + return k.RecordsKeeper.SetHostZoneUnbondingRecord(ctx, epochNumber, chainId, *hostZoneUnbondingRecord) } return nil } @@ -448,8 +451,20 @@ func (k Keeper) UnbondFromHostZone(ctx sdk.Context, hostZone types.HostZone) err return errorsmod.Wrapf(types.ErrICAAccountNotFound, "no delegation account found for %s", hostZone.ChainId) } - // Iterate through every unbonding record and sum the total amount to unbond for the given host zone - totalUnbondAmount, epochUnbondingRecordIds := k.GetTotalUnbondAmountAndRecordsIds(ctx, hostZone.ChainId) + // Get the list of relevant records that should unbond + epochUnbondingRecordIds, hostZoneUnbondingRecords := k.GetQueuedHostZoneUnbondingRecords(ctx, hostZone.ChainId) + + // Update the native unbond amount on all relevant records + // The native amount is calculated from the stTokens + for i, epochNumber := range epochUnbondingRecordIds { + hostZoneUnbondingRecord := hostZoneUnbondingRecords[i] + if err := k.SetHostZoneUnbondingNativeTokenAmounts(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { + return errorsmod.Wrapf(err, "unable to set native token amount for epoch %d and chain %s", epochNumber, hostZone.ChainId) + } + } + + // Sum the total native unbond amount across all records + totalUnbondAmount := k.GetTotalUnbondAmount(ctx, hostZoneUnbondingRecords) k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Total unbonded amount: %v%s", totalUnbondAmount, hostZone.HostDenom)) From e845e1ad412ec7a8aeb1f0f5d7b4234d22fae1c4 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 11:41:46 -0600 Subject: [PATCH 21/40] added another function --- x/stakeibc/keeper/icacallbacks_undelegate.go | 2 +- x/stakeibc/keeper/unbonding_records.go | 27 ++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/x/stakeibc/keeper/icacallbacks_undelegate.go b/x/stakeibc/keeper/icacallbacks_undelegate.go index 73f5294542..e256629887 100644 --- a/x/stakeibc/keeper/icacallbacks_undelegate.go +++ b/x/stakeibc/keeper/icacallbacks_undelegate.go @@ -67,7 +67,7 @@ func (k Keeper) UndelegateCallback(ctx sdk.Context, packet channeltypes.Packet, icacallbackstypes.AckResponseStatus_FAILURE, packet)) // Set NativeTokenAmounts on these HZUs to 0 - if err := k.SetNativeTokensToZeroInUnbondingRecords(ctx, chainId, undelegateCallback.EpochUnbondingRecordIds); err != nil { + if err := k.ResetUnbondingNativeTokenAmounts(ctx, chainId, undelegateCallback.EpochUnbondingRecordIds); err != nil { return err } diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 9a7c38754f..eab2d37312 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -182,6 +182,26 @@ func (k Keeper) ResetUnbondingNativeTokenAmounts(ctx sdk.Context, chainId string return nil } +// Given a list of relevant epoch unbonding record IDs and host zone unbonding records, +// sets the native token amount across all epoch unbonding records, host zone unbonding records +// and user redemption records +func (k Keeper) SetUnbondingNativeTokenAmounts( + ctx sdk.Context, + epochUnbondingRecordIds []uint64, + hostZoneUnbondingRecords []recordstypes.HostZoneUnbonding, +) error { + if len(epochUnbondingRecordIds) != len(hostZoneUnbondingRecords) { + return errors.New("epoch numbers do not line up with host zone unbonding records") + } + for i, epochNumber := range epochUnbondingRecordIds { + hostZoneUnbondingRecord := hostZoneUnbondingRecords[i] + if err := k.SetHostZoneUnbondingNativeTokenAmounts(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { + return err + } + } + return nil +} + // Determine the unbonding capacity that each validator has // The capacity is determined by the difference between their current delegation // and their fair portion of the total stake based on their weights @@ -456,11 +476,8 @@ func (k Keeper) UnbondFromHostZone(ctx sdk.Context, hostZone types.HostZone) err // Update the native unbond amount on all relevant records // The native amount is calculated from the stTokens - for i, epochNumber := range epochUnbondingRecordIds { - hostZoneUnbondingRecord := hostZoneUnbondingRecords[i] - if err := k.SetHostZoneUnbondingNativeTokenAmounts(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { - return errorsmod.Wrapf(err, "unable to set native token amount for epoch %d and chain %s", epochNumber, hostZone.ChainId) - } + if err := k.SetUnbondingNativeTokenAmounts(ctx, epochUnbondingRecordIds, hostZoneUnbondingRecords); err != nil { + return err } // Sum the total native unbond amount across all records From 9f56e827142e6c3b4db3764163dc6140d47c675a Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 14:30:11 -0600 Subject: [PATCH 22/40] used map --- x/stakeibc/keeper/unbonding_records.go | 37 +++++++++++--------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index eab2d37312..d8eb16b10d 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -74,12 +74,13 @@ func (k Keeper) CreateEpochUnbondingRecord(ctx sdk.Context, epochNumber uint64) return true } -// Returns the list of all epoch unbonding record IDs and host zone unbonding records that should unbond this epoch -// They're identified by status UNBONDING_QUEUE and a non-zero stToken amount +// Returns all the host zone unbonding records that should unbond this epoch +// Records are returned as a mapping of epoch unbonding record ID to host zone unbonding record +// Records ready to be unbonded are identified by status UNBONDING_QUEUE and a non-zero native amount func (k Keeper) GetQueuedHostZoneUnbondingRecords( ctx sdk.Context, chainId string, -) (epochNumbers []uint64, hostZoneUnbondings []recordstypes.HostZoneUnbonding) { +) (epochNumbers []uint64, records map[uint64]recordstypes.HostZoneUnbonding) { for _, epochUnbonding := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { hostZoneRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbonding.EpochNumber, chainId) if !found { @@ -90,15 +91,15 @@ func (k Keeper) GetQueuedHostZoneUnbondingRecords( if hostZoneRecord.ShouldInitiateUnbonding() { epochNumbers = append(epochNumbers, epochUnbonding.EpochNumber) - hostZoneUnbondings = append(hostZoneUnbondings, *hostZoneRecord) + records[epochUnbonding.EpochNumber] = *hostZoneRecord } } - return epochNumbers, hostZoneUnbondings + return epochNumbers, records } // Gets the total unbonded amount for a host zone by looping through the epoch unbonding records // Also returns the epoch unbonding record ids -func (k Keeper) GetTotalUnbondAmount(ctx sdk.Context, hostZoneUnbondingRecords []recordstypes.HostZoneUnbonding) (totalUnbonded sdkmath.Int) { +func (k Keeper) GetTotalUnbondAmount(ctx sdk.Context, hostZoneUnbondingRecords map[uint64]recordstypes.HostZoneUnbonding) (totalUnbonded sdkmath.Int) { totalUnbonded = sdk.ZeroInt() for _, hostZoneRecord := range hostZoneUnbondingRecords { totalUnbonded = totalUnbonded.Add(hostZoneRecord.NativeTokenAmount) @@ -182,19 +183,11 @@ func (k Keeper) ResetUnbondingNativeTokenAmounts(ctx sdk.Context, chainId string return nil } -// Given a list of relevant epoch unbonding record IDs and host zone unbonding records, -// sets the native token amount across all epoch unbonding records, host zone unbonding records -// and user redemption records -func (k Keeper) SetUnbondingNativeTokenAmounts( - ctx sdk.Context, - epochUnbondingRecordIds []uint64, - hostZoneUnbondingRecords []recordstypes.HostZoneUnbonding, -) error { - if len(epochUnbondingRecordIds) != len(hostZoneUnbondingRecords) { - return errors.New("epoch numbers do not line up with host zone unbonding records") - } - for i, epochNumber := range epochUnbondingRecordIds { - hostZoneUnbondingRecord := hostZoneUnbondingRecords[i] +// Given a mapping of epoch unbonding record IDs to host zone unbonding records, +// sets the native token amount across all epoch unbonding records, host zone unbonding records, +// and user redemption records, using the most updated redemption rate +func (k Keeper) SetUnbondingNativeTokenAmounts(ctx sdk.Context, hostZoneUnbondings map[uint64]recordstypes.HostZoneUnbonding) error { + for epochNumber, hostZoneUnbondingRecord := range hostZoneUnbondings { if err := k.SetHostZoneUnbondingNativeTokenAmounts(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { return err } @@ -472,16 +465,16 @@ func (k Keeper) UnbondFromHostZone(ctx sdk.Context, hostZone types.HostZone) err } // Get the list of relevant records that should unbond - epochUnbondingRecordIds, hostZoneUnbondingRecords := k.GetQueuedHostZoneUnbondingRecords(ctx, hostZone.ChainId) + epochUnbondingRecordIds, epochNumberToHostZoneUnbondingMap := k.GetQueuedHostZoneUnbondingRecords(ctx, hostZone.ChainId) // Update the native unbond amount on all relevant records // The native amount is calculated from the stTokens - if err := k.SetUnbondingNativeTokenAmounts(ctx, epochUnbondingRecordIds, hostZoneUnbondingRecords); err != nil { + if err := k.SetUnbondingNativeTokenAmounts(ctx, epochNumberToHostZoneUnbondingMap); err != nil { return err } // Sum the total native unbond amount across all records - totalUnbondAmount := k.GetTotalUnbondAmount(ctx, hostZoneUnbondingRecords) + totalUnbondAmount := k.GetTotalUnbondAmount(ctx, epochNumberToHostZoneUnbondingMap) k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Total unbonded amount: %v%s", totalUnbondAmount, hostZone.HostDenom)) From 8ac6eaa2333ba98deafb8dc40ba6be5d482f39e1 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 14:57:08 -0600 Subject: [PATCH 23/40] added back in native token amount during redemption and removed from callbacks --- x/records/types/records.go | 2 +- x/stakeibc/keeper/hooks.go | 1 + x/stakeibc/keeper/icacallbacks_undelegate.go | 5 -- x/stakeibc/keeper/msg_server_redeem_stake.go | 6 +-- .../msg_server_restore_interchain_account.go | 9 +--- x/stakeibc/keeper/unbonding_records.go | 46 +++++-------------- 6 files changed, 17 insertions(+), 52 deletions(-) diff --git a/x/records/types/records.go b/x/records/types/records.go index 40a0d7e5e6..da01005aba 100644 --- a/x/records/types/records.go +++ b/x/records/types/records.go @@ -4,5 +4,5 @@ import sdkmath "cosmossdk.io/math" // Helper function to evaluate if a host zone unbonding record still needs to be initiated func (r HostZoneUnbonding) ShouldInitiateUnbonding() bool { - return r.Status == HostZoneUnbonding_UNBONDING_QUEUE && r.StTokenAmount.GT(sdkmath.ZeroInt()) + return r.Status == HostZoneUnbonding_UNBONDING_QUEUE && r.NativeTokenAmount.GT(sdkmath.ZeroInt()) } diff --git a/x/stakeibc/keeper/hooks.go b/x/stakeibc/keeper/hooks.go index 938fb302dd..7bef44a506 100644 --- a/x/stakeibc/keeper/hooks.go +++ b/x/stakeibc/keeper/hooks.go @@ -24,6 +24,7 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInf k.Logger(ctx).Error(fmt.Sprintf("Unable to update epoch tracker, err: %s", err.Error())) return } + // Day Epoch - Process Unbondings if epochInfo.Identifier == epochstypes.DAY_EPOCH { // Initiate unbondings from any hostZone where it's appropriate diff --git a/x/stakeibc/keeper/icacallbacks_undelegate.go b/x/stakeibc/keeper/icacallbacks_undelegate.go index e256629887..a03da4c86c 100644 --- a/x/stakeibc/keeper/icacallbacks_undelegate.go +++ b/x/stakeibc/keeper/icacallbacks_undelegate.go @@ -66,11 +66,6 @@ func (k Keeper) UndelegateCallback(ctx sdk.Context, packet channeltypes.Packet, k.Logger(ctx).Error(utils.LogICACallbackStatusWithHostZone(chainId, ICACallbackID_Undelegate, icacallbackstypes.AckResponseStatus_FAILURE, packet)) - // Set NativeTokenAmounts on these HZUs to 0 - if err := k.ResetUnbondingNativeTokenAmounts(ctx, chainId, undelegateCallback.EpochUnbondingRecordIds); err != nil { - return err - } - // Reset unbondings record status if err := k.RecordsKeeper.SetHostZoneUnbondings( ctx, diff --git a/x/stakeibc/keeper/msg_server_redeem_stake.go b/x/stakeibc/keeper/msg_server_redeem_stake.go index 554ad66540..28f65950e3 100644 --- a/x/stakeibc/keeper/msg_server_redeem_stake.go +++ b/x/stakeibc/keeper/msg_server_redeem_stake.go @@ -70,8 +70,6 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) } // - Creator owns at least "amount" stAssets balance := k.bankKeeper.GetBalance(ctx, sender, stDenom) - k.Logger(ctx).Info(fmt.Sprintf("Redemption issuer stDenom balance: %v%s", balance.Amount, balance.Denom)) - k.Logger(ctx).Info(fmt.Sprintf("Redemption requested stDenom amount: %v%s", msg.Amount, stDenom)) if balance.Amount.LT(msg.Amount) { return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "balance is lower than redemption amount. redemption amount: %v, balance %v: ", msg.Amount, balance.Amount) } @@ -85,12 +83,13 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) // Add the unbonded amount to the UserRedemptionRecord // The record is set below userRedemptionRecord.StTokenAmount = userRedemptionRecord.StTokenAmount.Add(msg.Amount) + userRedemptionRecord.Amount = userRedemptionRecord.Amount.Add(nativeAmount) } else { // First time a user is redeeming this epoch userRedemptionRecord = recordstypes.UserRedemptionRecord{ Id: redemptionId, Receiver: msg.Receiver, - Amount: sdk.ZeroInt(), + Amount: nativeAmount, Denom: hostZone.HostDenom, HostZoneId: hostZone.ChainId, EpochNumber: epochTracker.EpochNumber, @@ -113,6 +112,7 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) if !found { return nil, errorsmod.Wrapf(types.ErrInvalidHostZone, "host zone not found in unbondings: %s", hostZone.ChainId) } + hostZoneUnbonding.NativeTokenAmount = hostZoneUnbonding.NativeTokenAmount.Add(nativeAmount) if !userHasRedeemedThisEpoch { // Only append a UserRedemptionRecord to the HZU if it wasn't previously appended hostZoneUnbonding.UserRedemptionRecords = append(hostZoneUnbonding.UserRedemptionRecords, userRedemptionRecord.Id) diff --git a/x/stakeibc/keeper/msg_server_restore_interchain_account.go b/x/stakeibc/keeper/msg_server_restore_interchain_account.go index 0c34b3b12f..77736a9e30 100644 --- a/x/stakeibc/keeper/msg_server_restore_interchain_account.go +++ b/x/stakeibc/keeper/msg_server_restore_interchain_account.go @@ -90,15 +90,8 @@ func (k msgServer) RestoreInterchainAccount(goCtx context.Context, msg *types.Ms epochNumberForPendingTransferRecords = append(epochNumberForPendingTransferRecords, epochUnbondingRecord.EpochNumber) } } - // Set UNBONDING_IN_PROGRESS records NativeTokenAmounts to 0 - err := k.SetNativeTokensToZeroInUnbondingRecords(ctx, hostZone.ChainId, epochNumberForPendingUnbondingRecords) - if err != nil { - errMsg := fmt.Sprintf("unable to set native token amounts to 0 for chainId: %s and epochUnbondingRecordIds: %v, err: %s") - k.Logger(ctx).Error(errMsg) - // TODO Question: should we return an error here, or just proceed? - } // Revert UNBONDING_IN_PROGRESS records to UNBONDING_QUEUE - err = k.RecordsKeeper.SetHostZoneUnbondings(ctx, hostZone.ChainId, epochNumberForPendingUnbondingRecords, recordtypes.HostZoneUnbonding_UNBONDING_QUEUE) + err := k.RecordsKeeper.SetHostZoneUnbondings(ctx, hostZone.ChainId, epochNumberForPendingUnbondingRecords, recordtypes.HostZoneUnbonding_UNBONDING_QUEUE) if err != nil { errMsg := fmt.Sprintf("unable to update host zone unbonding record status to %s for chainId: %s and epochUnbondingRecordIds: %v, err: %s", recordtypes.HostZoneUnbonding_UNBONDING_QUEUE.String(), hostZone.ChainId, epochNumberForPendingUnbondingRecords, err) diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index d8eb16b10d..b1092624b9 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -80,7 +80,8 @@ func (k Keeper) CreateEpochUnbondingRecord(ctx sdk.Context, epochNumber uint64) func (k Keeper) GetQueuedHostZoneUnbondingRecords( ctx sdk.Context, chainId string, -) (epochNumbers []uint64, records map[uint64]recordstypes.HostZoneUnbonding) { +) (epochNumbers []uint64, hostZoneUnbondingMap map[uint64]recordstypes.HostZoneUnbonding) { + hostZoneUnbondingMap = map[uint64]recordstypes.HostZoneUnbonding{} for _, epochUnbonding := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { hostZoneRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbonding.EpochNumber, chainId) if !found { @@ -91,10 +92,10 @@ func (k Keeper) GetQueuedHostZoneUnbondingRecords( if hostZoneRecord.ShouldInitiateUnbonding() { epochNumbers = append(epochNumbers, epochUnbonding.EpochNumber) - records[epochUnbonding.EpochNumber] = *hostZoneRecord + hostZoneUnbondingMap[epochUnbonding.EpochNumber] = *hostZoneRecord } } - return epochNumbers, records + return epochNumbers, hostZoneUnbondingMap } // Gets the total unbonded amount for a host zone by looping through the epoch unbonding records @@ -109,7 +110,7 @@ func (k Keeper) GetTotalUnbondAmount(ctx sdk.Context, hostZoneUnbondingRecords m // Given a list of redemption record IDs and a redemption rate, sets the native token amount on each record and returns the total // The native amount is calculated using the redemption rate. If a zero redemption rate is passed, set the amount to zero -func (k Keeper) SetUserRedemptionRecordNativeAmounts( +func (k Keeper) RefreshUserRedemptionRecordNativeAmounts( ctx sdk.Context, chainId string, redemptionRecordIds []string, @@ -126,10 +127,7 @@ func (k Keeper) SetUserRedemptionRecordNativeAmounts( // A zero redemption rate is used to indicate that the native amount should be set to zero // If the redemption rate is non-zero, calculate the number of native tokens - nativeAmount := sdkmath.ZeroInt() - if !redemptionRate.IsZero() { - nativeAmount = sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() - } + nativeAmount := sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() totalNativeAmount = totalNativeAmount.Add(nativeAmount) // Set the native amount on the record @@ -140,7 +138,7 @@ func (k Keeper) SetUserRedemptionRecordNativeAmounts( } // Sets the native token amount unbonded on the host zone unbonding record and the associated user redemption records -func (k Keeper) SetHostZoneUnbondingNativeTokenAmounts( +func (k Keeper) RefreshHostZoneUnbondingNativeTokenAmounts( ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord recordstypes.HostZoneUnbonding, @@ -154,41 +152,19 @@ func (k Keeper) SetHostZoneUnbondingNativeTokenAmounts( // Set all native token amount on each user redemption record redemptionRecordIds := hostZoneUnbondingRecord.UserRedemptionRecords - totalNativeAmount := k.SetUserRedemptionRecordNativeAmounts(ctx, chainId, redemptionRecordIds, hostZone.RedemptionRate) + totalNativeAmount := k.RefreshUserRedemptionRecordNativeAmounts(ctx, chainId, redemptionRecordIds, hostZone.RedemptionRate) // Then set the total on the host zone unbonding record hostZoneUnbondingRecord.NativeTokenAmount = totalNativeAmount return k.RecordsKeeper.SetHostZoneUnbondingRecord(ctx, epochNumber, chainId, hostZoneUnbondingRecord) } -// Resets the native token amounts back to zero on the host zone unbonding record and user redemption records -// This is done if the unbonding callback fails, used to indicate that it still needs to be processed -func (k Keeper) ResetUnbondingNativeTokenAmounts(ctx sdk.Context, chainId string, epochUnbondingRecordIds []uint64) error { - for _, epochNumber := range epochUnbondingRecordIds { - k.Logger(ctx).Info(fmt.Sprintf("Resetting native token amount on host zone unbondings on EpochUnbondingRecord %d to zero", epochNumber)) - - hostZoneUnbondingRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochNumber, chainId) - if !found { - return errorsmod.Wrapf(recordstypes.ErrHostUnbondingRecordNotFound, "chain %d, epoch %d", chainId, epochNumber) - } - - // Reset the user redemption records back to zero - redemptionRecordIds := hostZoneUnbondingRecord.UserRedemptionRecords - k.SetUserRedemptionRecordNativeAmounts(ctx, chainId, redemptionRecordIds, sdk.ZeroDec()) - - // Reset the host zone unbonding record back to zero - hostZoneUnbondingRecord.NativeTokenAmount = sdkmath.ZeroInt() - return k.RecordsKeeper.SetHostZoneUnbondingRecord(ctx, epochNumber, chainId, *hostZoneUnbondingRecord) - } - return nil -} - // Given a mapping of epoch unbonding record IDs to host zone unbonding records, // sets the native token amount across all epoch unbonding records, host zone unbonding records, // and user redemption records, using the most updated redemption rate -func (k Keeper) SetUnbondingNativeTokenAmounts(ctx sdk.Context, hostZoneUnbondings map[uint64]recordstypes.HostZoneUnbonding) error { +func (k Keeper) RefreshUnbondingNativeTokenAmounts(ctx sdk.Context, hostZoneUnbondings map[uint64]recordstypes.HostZoneUnbonding) error { for epochNumber, hostZoneUnbondingRecord := range hostZoneUnbondings { - if err := k.SetHostZoneUnbondingNativeTokenAmounts(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { + if err := k.RefreshHostZoneUnbondingNativeTokenAmounts(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { return err } } @@ -469,7 +445,7 @@ func (k Keeper) UnbondFromHostZone(ctx sdk.Context, hostZone types.HostZone) err // Update the native unbond amount on all relevant records // The native amount is calculated from the stTokens - if err := k.SetUnbondingNativeTokenAmounts(ctx, epochNumberToHostZoneUnbondingMap); err != nil { + if err := k.RefreshUnbondingNativeTokenAmounts(ctx, epochNumberToHostZoneUnbondingMap); err != nil { return err } From c3ff1f81f0748ca0438010f19bb2b91ade44daf3 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 15:34:32 -0600 Subject: [PATCH 24/40] addressed vishal PR comments --- app/upgrades/v17/upgrades.go | 15 --------------- x/stakeibc/keeper/unbonding_records.go | 3 +-- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index c163141b89..461c807a45 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -312,7 +312,6 @@ func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { // Migrate the unbonding records // UserUnbondingRecords previously only used Native Token Amounts, we now want to use StTokenAmounts -// Similarly, we should modify HostZoneUnbondingRecords to NOT use NativeTokenAmounts prior to unbonding being initiated func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { for _, epochUnbondingRecord := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { for _, hostZoneUnbonding := range epochUnbondingRecord.GetHostZoneUnbondings() { @@ -334,9 +333,6 @@ func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { // e.g. if the rate is 0.5, then 1 native token would be worth 0.5 stTokens estimatedStTokenConversionRate := stTokenAmountDec.Quo(nativeTokenAmountDec) - // store if the unbonding has not been initiated - unbondingNotInitiated := hostZoneUnbonding.Status == recordtypes.HostZoneUnbonding_UNBONDING_QUEUE - // Loop through User Redemption Records and insert an estimated stTokenAmount for _, userRedemptionRecordId := range hostZoneUnbonding.GetUserRedemptionRecords() { userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) @@ -347,19 +343,8 @@ func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { } userRedemptionRecord.StTokenAmount = estimatedStTokenConversionRate.Mul(sdkmath.LegacyDec(userRedemptionRecord.Amount)).RoundInt() - - if unbondingNotInitiated { - userRedemptionRecord.Amount = sdkmath.ZeroInt() - } - k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) } - - // if the unbonding has not been initiated, we want to set nativeTokenAmount to 0 for the whole HostZoneUnbonding - if unbondingNotInitiated { - hostZoneUnbonding.NativeTokenAmount = sdkmath.ZeroInt() - - } } k.RecordsKeeper.SetEpochUnbondingRecord(ctx, epochUnbondingRecord) diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index b1092624b9..df9bd02e21 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -125,8 +125,7 @@ func (k Keeper) RefreshUserRedemptionRecordNativeAmounts( continue } - // A zero redemption rate is used to indicate that the native amount should be set to zero - // If the redemption rate is non-zero, calculate the number of native tokens + // Calculate the number of native tokens using the redemption rate nativeAmount := sdk.NewDecFromInt(userRedemptionRecord.StTokenAmount).Mul(redemptionRate).RoundInt() totalNativeAmount = totalNativeAmount.Add(nativeAmount) From c07f40dfca247de1a224ead437bcf717dc33c79e Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 15:46:03 -0600 Subject: [PATCH 25/40] fixed unit tests for GetTotalUnbondAmount --- app/upgrades/v17/upgrades_test.go | 6 +-- .../keeper/msg_server_redeem_stake_test.go | 3 +- ...ords_get_host_zone_unbondings_msgs_test.go | 44 +++++++++++++++---- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 44df8d61ed..2d672b7651 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -510,16 +510,16 @@ func (s *UpgradeTestSuite) TestUpdateRateLimitThresholds() { // Define test cases consisting of an initial redemption rates and expected bounds testCases := map[string]UpdateRateLimits{ "cosmoshub": { - // 10% threshold + // 15% threshold ChainId: "cosmoshub-4", ChannelId: "channel-0", HostDenom: "uatom", RateLimitDenom: "stuatom", Duration: 10, - Threshold: sdkmath.NewInt(10), + Threshold: sdkmath.NewInt(15), }, "osmosis": { - // 10% threshold + // 20% threshold ChainId: "osmosis-1", ChannelId: "channel-1", HostDenom: "uosmo", diff --git a/x/stakeibc/keeper/msg_server_redeem_stake_test.go b/x/stakeibc/keeper/msg_server_redeem_stake_test.go index b078dc426e..df283b3790 100644 --- a/x/stakeibc/keeper/msg_server_redeem_stake_test.go +++ b/x/stakeibc/keeper/msg_server_redeem_stake_test.go @@ -150,7 +150,8 @@ func (s *KeeperTestSuite) TestRedeemStake_Successful() { userRedemptionRecord, found := s.App.RecordsKeeper.GetUserRedemptionRecord(s.Ctx, userRedemptionRecordId) s.Require().True(found) - s.Require().Equal(tc.expectedNativeAmount, userRedemptionRecord.Amount, "redemption record amount") + s.Require().Equal(msg.Amount, userRedemptionRecord.StTokenAmount, "redemption record sttoken amount") + s.Require().Equal(tc.expectedNativeAmount, userRedemptionRecord.Amount, "redemption record native amount") s.Require().Equal(msg.Receiver, userRedemptionRecord.Receiver, "redemption record receiver") s.Require().Equal(msg.HostZone, userRedemptionRecord.HostZoneId, "redemption record host zone") s.Require().False(userRedemptionRecord.ClaimIsPending, "redemption record is not claimable") diff --git a/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go b/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go index 653976663c..7bd929bd32 100644 --- a/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go +++ b/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go @@ -484,13 +484,21 @@ func (s *KeeperTestSuite) TestGetBalanceRatio() { } } -func (s *KeeperTestSuite) TestGetTotalUnbondAmountAndRecordsIds() { +func (s *KeeperTestSuite) TestGetQueuedHostZoneUnbondingRecords() { + // This function returns a mapping of epoch unbonding record ID (i.e. epoch number) -> hostZoneUnbonding + // For the purposes of this test, the NativeTokenAmount is used in place of the host zone unbonding record + // for the purposes of validating the proper record was selected. In other words, after this function, + // we just verify that the native token amounts of the output line up with the expected map below + expectedEpochUnbondingRecordIds := []uint64{1, 2, 4} + expectedHostZoneUnbondingMap := map[uint64]int64{1: 1, 2: 3, 4: 8} // includes only the relevant records below + epochUnbondingRecords := []recordtypes.EpochUnbondingRecord{ { + // Has relevant host zone unbonding, so epoch number is included EpochNumber: uint64(1), HostZoneUnbondings: []*recordtypes.HostZoneUnbonding{ { - // Summed + // Included HostZoneId: HostChainId, NativeTokenAmount: sdkmath.NewInt(1), Status: recordtypes.HostZoneUnbonding_UNBONDING_QUEUE, @@ -504,10 +512,11 @@ func (s *KeeperTestSuite) TestGetTotalUnbondAmountAndRecordsIds() { }, }, { + // Has relevant host zone unbonding, so epoch number is included EpochNumber: uint64(2), HostZoneUnbondings: []*recordtypes.HostZoneUnbonding{ { - // Summed + // Included HostZoneId: HostChainId, NativeTokenAmount: sdkmath.NewInt(3), Status: recordtypes.HostZoneUnbonding_UNBONDING_QUEUE, @@ -521,6 +530,7 @@ func (s *KeeperTestSuite) TestGetTotalUnbondAmountAndRecordsIds() { }, }, { + // No relevant host zone unbonding, epoch number not included EpochNumber: uint64(3), HostZoneUnbondings: []*recordtypes.HostZoneUnbonding{ { @@ -538,6 +548,7 @@ func (s *KeeperTestSuite) TestGetTotalUnbondAmountAndRecordsIds() { }, }, { + // Has relevant host zone unbonding, so epoch number is included EpochNumber: uint64(4), HostZoneUnbondings: []*recordtypes.HostZoneUnbonding{ { @@ -547,7 +558,7 @@ func (s *KeeperTestSuite) TestGetTotalUnbondAmountAndRecordsIds() { Status: recordtypes.HostZoneUnbonding_CLAIMABLE, }, { - // Summed + // Included HostZoneId: HostChainId, NativeTokenAmount: sdkmath.NewInt(8), Status: recordtypes.HostZoneUnbonding_UNBONDING_QUEUE, @@ -560,12 +571,27 @@ func (s *KeeperTestSuite) TestGetTotalUnbondAmountAndRecordsIds() { s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, epochUnbondingRecord) } - expectedUnbondAmount := int64(1 + 3 + 8) - expectedRecordIds := []uint64{1, 2, 4} + actualEpochIds, actualHostZoneMap := s.App.StakeibcKeeper.GetQueuedHostZoneUnbondingRecords(s.Ctx, HostChainId) + s.Require().Equal(expectedEpochUnbondingRecordIds, actualEpochIds, "epoch unbonding record IDs") + for epochNumber, actualHostZoneUnbonding := range actualHostZoneMap { + expectedHostZoneUnbonding := expectedHostZoneUnbondingMap[epochNumber] + s.Require().Equal(expectedHostZoneUnbonding, actualHostZoneUnbonding.NativeTokenAmount.Int64(), "host zone unbonding record") + } +} + +func (s *KeeperTestSuite) TestGetTotalUnbondAmount() { + hostZoneUnbondingRecords := map[uint64]recordtypes.HostZoneUnbonding{ + 1: {NativeTokenAmount: sdkmath.NewInt(1)}, + 2: {NativeTokenAmount: sdkmath.NewInt(2)}, + 3: {NativeTokenAmount: sdkmath.NewInt(3)}, + 4: {NativeTokenAmount: sdkmath.NewInt(4)}, + } + expectedUnbondAmount := sdkmath.NewInt(1 + 2 + 3 + 4) + actualUnbondAmount := s.App.StakeibcKeeper.GetTotalUnbondAmount(s.Ctx, hostZoneUnbondingRecords) + s.Require().Equal(expectedUnbondAmount, actualUnbondAmount, "unbond amount") - actualUnbondAmount, actualRecordIds := s.App.StakeibcKeeper.GetTotalUnbondAmountAndRecordsIds(s.Ctx, HostChainId) - s.Require().Equal(expectedUnbondAmount, actualUnbondAmount.Int64(), "unbonded amount") - s.Require().Equal(expectedRecordIds, actualRecordIds, "epoch unbonding record IDs") + emptyUnbondings := map[uint64]recordtypes.HostZoneUnbonding{} + s.Require().Zero(s.App.StakeibcKeeper.GetTotalUnbondAmount(s.Ctx, emptyUnbondings).Int64()) } func (s *KeeperTestSuite) TestGetValidatorUnbondCapacity() { From 16a9d8771c5901079ce05f2b7566a2109f540852 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 16:04:28 -0600 Subject: [PATCH 26/40] fixed broken unit tests --- app/upgrades/v17/upgrades_test.go | 6 +++--- x/records/client/cli/query_user_redemption_record_test.go | 6 ++++-- x/records/keeper/user_redemption_record_test.go | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 2d672b7651..62a52a9a89 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -519,13 +519,13 @@ func (s *UpgradeTestSuite) TestUpdateRateLimitThresholds() { Threshold: sdkmath.NewInt(15), }, "osmosis": { - // 20% threshold + // 15% threshold ChainId: "osmosis-1", ChannelId: "channel-1", HostDenom: "uosmo", RateLimitDenom: "stuosmo", - Duration: 20, - Threshold: sdkmath.NewInt(20), + Duration: 15, + Threshold: sdkmath.NewInt(15), }, "juno": { // No denom on matching host diff --git a/x/records/client/cli/query_user_redemption_record_test.go b/x/records/client/cli/query_user_redemption_record_test.go index a04c6df7fa..213a0250fc 100644 --- a/x/records/client/cli/query_user_redemption_record_test.go +++ b/x/records/client/cli/query_user_redemption_record_test.go @@ -20,6 +20,7 @@ import ( "github.com/Stride-Labs/stride/v16/x/records/types" ) +// TODO [cleanup] - Migrate to new CLI testing framework func networkWithUserRedemptionRecordObjects(t *testing.T, n int) (*network.Network, []types.UserRedemptionRecord) { t.Helper() cfg := network.DefaultConfig() @@ -28,8 +29,9 @@ func networkWithUserRedemptionRecordObjects(t *testing.T, n int) (*network.Netwo for i := 0; i < n; i++ { userRedemptionRecord := types.UserRedemptionRecord{ - Id: strconv.Itoa(i), - Amount: sdkmath.NewInt(int64(i)), + Id: strconv.Itoa(i), + Amount: sdkmath.NewInt(int64(i)), + StTokenAmount: sdkmath.NewInt(int64(i)), } nullify.Fill(&userRedemptionRecord) state.UserRedemptionRecordList = append(state.UserRedemptionRecordList, userRedemptionRecord) diff --git a/x/records/keeper/user_redemption_record_test.go b/x/records/keeper/user_redemption_record_test.go index f204beb611..c0ab9967aa 100644 --- a/x/records/keeper/user_redemption_record_test.go +++ b/x/records/keeper/user_redemption_record_test.go @@ -15,11 +15,13 @@ import ( "github.com/Stride-Labs/stride/v16/x/records/types" ) +// TODO [cleanup]: Migrate to new KeeperTestSuite framework and remove use of nullify func createNUserRedemptionRecord(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.UserRedemptionRecord { items := make([]types.UserRedemptionRecord, n) for i := range items { items[i].Id = strconv.Itoa(i) items[i].Amount = sdkmath.NewInt(int64(i)) + items[i].StTokenAmount = sdkmath.NewInt(int64(i)) keeper.SetUserRedemptionRecord(ctx, items[i]) } return items From 9f5ee76f7936b484dcab7eb5760a356a4d96fb7d Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 16:21:12 -0600 Subject: [PATCH 27/40] added cache context wrapper for unbonding --- x/stakeibc/keeper/unbonding_records.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index df9bd02e21..73ad3b3c56 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -583,7 +583,10 @@ func (k Keeper) InitiateAllHostZoneUnbondings(ctx sdk.Context, dayNumber uint64) } // Get host zone unbonding message by summing up the unbonding records - if err := k.UnbondFromHostZone(ctx, hostZone); err != nil { + err := utils.ApplyFuncIfNoError(ctx, func(ctx sdk.Context) error { + return k.UnbondFromHostZone(ctx, hostZone) + }) + if err != nil { k.Logger(ctx).Error(fmt.Sprintf("Error initiating host zone unbondings for host zone %s: %s", hostZone.ChainId, err.Error())) continue } From 0afc25d0d10759f5fb884c2561870c76c7cccda3 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 16:49:46 -0600 Subject: [PATCH 28/40] added unit tests for records helpers --- .../keeper/epoch_unbonding_record_test.go | 58 +++++++++++++++++++ x/records/types/records_test.go | 49 ++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 x/records/types/records_test.go diff --git a/x/records/keeper/epoch_unbonding_record_test.go b/x/records/keeper/epoch_unbonding_record_test.go index d20c732183..84c737c250 100644 --- a/x/records/keeper/epoch_unbonding_record_test.go +++ b/x/records/keeper/epoch_unbonding_record_test.go @@ -170,3 +170,61 @@ func TestSetHostZoneUnbondings(t *testing.T) { actualEpochUnbondingRecord, ) } + +func (s *KeeperTestSuite) TestSetHostZoneUnbonding() { + initialAmount := sdkmath.NewInt(10) + updatedAmount := sdkmath.NewInt(99) + + // Create two epoch unbonding records, each with two host zone unbondings + epochUnbondingRecords := []types.EpochUnbondingRecord{ + { + EpochNumber: 1, + HostZoneUnbondings: []*types.HostZoneUnbonding{ + {HostZoneId: "chain-0", NativeTokenAmount: initialAmount}, + {HostZoneId: "chain-1", NativeTokenAmount: initialAmount}, + }, + }, + { + EpochNumber: 2, + HostZoneUnbondings: []*types.HostZoneUnbonding{ + {HostZoneId: "chain-0", NativeTokenAmount: initialAmount}, + {HostZoneId: "chain-1", NativeTokenAmount: initialAmount}, + }, + }, + } + for _, epochUnbondingRecord := range epochUnbondingRecords { + s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, epochUnbondingRecord) + } + + // Update the amount for (epoch-1, chain-0) and (epoch-2, chain-1) + updatedHostZoneUnbonding1 := types.HostZoneUnbonding{HostZoneId: "chain-0", NativeTokenAmount: updatedAmount} + err := s.App.RecordsKeeper.SetHostZoneUnbondingRecord(s.Ctx, 1, "chain-0", updatedHostZoneUnbonding1) + s.Require().NoError(err, "no error expected when setting amount for (epoch-1, chain-0)") + + updatedHostZoneUnbonding2 := types.HostZoneUnbonding{HostZoneId: "chain-1", NativeTokenAmount: updatedAmount} + err = s.App.RecordsKeeper.SetHostZoneUnbondingRecord(s.Ctx, 2, "chain-1", updatedHostZoneUnbonding2) + s.Require().NoError(err, "no error expected when setting amount for (epoch-2, chain-1)") + + // Create the mapping of expected native amounts + expectedAmountMapping := map[uint64]map[string]sdkmath.Int{ + 1: { + "chain-0": updatedAmount, + "chain-1": initialAmount, + }, + 2: { + "chain-0": initialAmount, + "chain-1": updatedAmount, + }, + } + + // Loop the records and check that the amounts match the updates + for _, epochUnbondingRecord := range s.App.RecordsKeeper.GetAllEpochUnbondingRecord(s.Ctx) { + s.Require().Len(epochUnbondingRecord.HostZoneUnbondings, 2, "there should be two host records per epoch record") + + for _, hostZoneUnbondingRecord := range epochUnbondingRecord.HostZoneUnbondings { + expectedAmount := expectedAmountMapping[epochUnbondingRecord.EpochNumber][hostZoneUnbondingRecord.HostZoneId] + s.Require().Equal(expectedAmount.Int64(), hostZoneUnbondingRecord.NativeTokenAmount.Int64(), "updated record amount") + } + } + +} diff --git a/x/records/types/records_test.go b/x/records/types/records_test.go new file mode 100644 index 0000000000..cf98e52c64 --- /dev/null +++ b/x/records/types/records_test.go @@ -0,0 +1,49 @@ +package types_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + + "github.com/stretchr/testify/require" + + "github.com/Stride-Labs/stride/v16/x/records/types" +) + +func TestShouldInitiateUnbonding(t *testing.T) { + testCases := []struct { + name string + status types.HostZoneUnbonding_Status + amount sdkmath.Int + shouldUnbond bool + }{ + { + name: "should unbond", + status: types.HostZoneUnbonding_UNBONDING_QUEUE, + amount: sdkmath.NewInt(10), + shouldUnbond: true, + }, + { + name: "not in unbonding queue", + status: types.HostZoneUnbonding_CLAIMABLE, + amount: sdkmath.NewInt(10), + shouldUnbond: false, + }, + { + name: "zero amount", + status: types.HostZoneUnbonding_CLAIMABLE, + amount: sdkmath.ZeroInt(), + shouldUnbond: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + record := types.HostZoneUnbonding{ + Status: tc.status, + NativeTokenAmount: tc.amount, + } + require.Equal(t, tc.shouldUnbond, record.ShouldInitiateUnbonding()) + }) + } +} From e8f284a1d340ba8d0cd046c1c2c2143162c895cd Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 16:56:30 -0600 Subject: [PATCH 29/40] addressed riley PR comments --- x/records/types/records.go | 4 +++- x/stakeibc/keeper/unbonding_records.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x/records/types/records.go b/x/records/types/records.go index da01005aba..902517e16d 100644 --- a/x/records/types/records.go +++ b/x/records/types/records.go @@ -4,5 +4,7 @@ import sdkmath "cosmossdk.io/math" // Helper function to evaluate if a host zone unbonding record still needs to be initiated func (r HostZoneUnbonding) ShouldInitiateUnbonding() bool { - return r.Status == HostZoneUnbonding_UNBONDING_QUEUE && r.NativeTokenAmount.GT(sdkmath.ZeroInt()) + notYetUnbonding := r.Status == HostZoneUnbonding_UNBONDING_QUEUE + hasAtLeastOneRecord := r.NativeTokenAmount.GT(sdkmath.ZeroInt()) + return notYetUnbonding && hasAtLeastOneRecord } diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 73ad3b3c56..074cf0b6bd 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -448,7 +448,7 @@ func (k Keeper) UnbondFromHostZone(ctx sdk.Context, hostZone types.HostZone) err return err } - // Sum the total native unbond amount across all records + // Sum the total number of native tokens that from the records above that are ready to unbond totalUnbondAmount := k.GetTotalUnbondAmount(ctx, epochNumberToHostZoneUnbondingMap) k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Total unbonded amount: %v%s", totalUnbondAmount, hostZone.HostDenom)) From 8da63cc592948053ce5c903542c2e0bd2d650d35 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 17:06:00 -0600 Subject: [PATCH 30/40] addressed aidan nit comments --- x/stakeibc/keeper/unbonding_records.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 074cf0b6bd..eb126486b0 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -80,8 +80,8 @@ func (k Keeper) CreateEpochUnbondingRecord(ctx sdk.Context, epochNumber uint64) func (k Keeper) GetQueuedHostZoneUnbondingRecords( ctx sdk.Context, chainId string, -) (epochNumbers []uint64, hostZoneUnbondingMap map[uint64]recordstypes.HostZoneUnbonding) { - hostZoneUnbondingMap = map[uint64]recordstypes.HostZoneUnbonding{} +) (epochNumbers []uint64, epochToHostZoneUnbondingMap map[uint64]recordstypes.HostZoneUnbonding) { + epochToHostZoneUnbondingMap = map[uint64]recordstypes.HostZoneUnbonding{} for _, epochUnbonding := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { hostZoneRecord, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbonding.EpochNumber, chainId) if !found { @@ -92,10 +92,10 @@ func (k Keeper) GetQueuedHostZoneUnbondingRecords( if hostZoneRecord.ShouldInitiateUnbonding() { epochNumbers = append(epochNumbers, epochUnbonding.EpochNumber) - hostZoneUnbondingMap[epochUnbonding.EpochNumber] = *hostZoneRecord + epochToHostZoneUnbondingMap[epochUnbonding.EpochNumber] = *hostZoneRecord } } - return epochNumbers, hostZoneUnbondingMap + return epochNumbers, epochToHostZoneUnbondingMap } // Gets the total unbonded amount for a host zone by looping through the epoch unbonding records @@ -108,17 +108,18 @@ func (k Keeper) GetTotalUnbondAmount(ctx sdk.Context, hostZoneUnbondingRecords m return totalUnbonded } -// Given a list of redemption record IDs and a redemption rate, sets the native token amount on each record and returns the total -// The native amount is calculated using the redemption rate. If a zero redemption rate is passed, set the amount to zero +// Given a list of user redemption record IDs and a redemption rate, sets the native token +// amount on each record, calculated from the stAmount and redemption rate, and returns the +// sum of all native token amounts across all user redemption records func (k Keeper) RefreshUserRedemptionRecordNativeAmounts( ctx sdk.Context, chainId string, - redemptionRecordIds []string, + userRedemptionRecordIds []string, redemptionRate sdk.Dec, ) (totalNativeAmount sdkmath.Int) { - // Loop and set the native amount for each record, keeping track fo the total + // Loop and set the native amount for each record, keeping track of the total totalNativeAmount = sdkmath.ZeroInt() - for _, userRedemptionRecordId := range redemptionRecordIds { + for _, userRedemptionRecordId := range userRedemptionRecordIds { userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) if !found { k.Logger(ctx).Error(utils.LogWithHostZone(chainId, "No user redemption record found for id %s", userRedemptionRecordId)) @@ -137,7 +138,7 @@ func (k Keeper) RefreshUserRedemptionRecordNativeAmounts( } // Sets the native token amount unbonded on the host zone unbonding record and the associated user redemption records -func (k Keeper) RefreshHostZoneUnbondingNativeTokenAmounts( +func (k Keeper) RefreshHostZoneUnbondingNativeTokenAmount( ctx sdk.Context, epochNumber uint64, hostZoneUnbondingRecord recordstypes.HostZoneUnbonding, @@ -163,7 +164,7 @@ func (k Keeper) RefreshHostZoneUnbondingNativeTokenAmounts( // and user redemption records, using the most updated redemption rate func (k Keeper) RefreshUnbondingNativeTokenAmounts(ctx sdk.Context, hostZoneUnbondings map[uint64]recordstypes.HostZoneUnbonding) error { for epochNumber, hostZoneUnbondingRecord := range hostZoneUnbondings { - if err := k.RefreshHostZoneUnbondingNativeTokenAmounts(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { + if err := k.RefreshHostZoneUnbondingNativeTokenAmount(ctx, epochNumber, hostZoneUnbondingRecord); err != nil { return err } } From 2e36b4e6f04588e45c5299d93d099843d4ca4426 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 19:28:37 -0600 Subject: [PATCH 31/40] added unit tests for refresh functions --- ...ords_get_host_zone_unbondings_msgs_test.go | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go b/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go index 7bd929bd32..2e806d078f 100644 --- a/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go +++ b/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go @@ -594,6 +594,139 @@ func (s *KeeperTestSuite) TestGetTotalUnbondAmount() { s.Require().Zero(s.App.StakeibcKeeper.GetTotalUnbondAmount(s.Ctx, emptyUnbondings).Int64()) } +func (s *KeeperTestSuite) TestRefreshUserRedemptionRecordNativeAmounts() { + // Define the expected redemption records after the function is called + redemptionRate := sdk.MustNewDecFromStr("1.999") + expectedUserRedemptionRecords := []recordtypes.UserRedemptionRecord{ + // StTokenAmount: 1000 * 1.999 = 1999 Native + {Id: "A", StTokenAmount: sdkmath.NewInt(1000), Amount: sdkmath.NewInt(1999)}, + // StTokenAmount: 999 * 1.999 = 1997.001, Rounded down to 1997 Native + {Id: "B", StTokenAmount: sdkmath.NewInt(999), Amount: sdkmath.NewInt(1997)}, + // StTokenAmount: 100 * 1.999 = 199.9, Rounded up to 200 Native + {Id: "C", StTokenAmount: sdkmath.NewInt(100), Amount: sdkmath.NewInt(200)}, + } + expectedTotalNativeAmount := sdkmath.NewInt(1999 + 1997 + 200) + + // Create the initial records which do not have the end native amount + for _, expectedUserRedemptionRecord := range expectedUserRedemptionRecords { + initialUserRedemptionRecord := expectedUserRedemptionRecord + initialUserRedemptionRecord.Amount = sdkmath.ZeroInt() + s.App.RecordsKeeper.SetUserRedemptionRecord(s.Ctx, initialUserRedemptionRecord) + } + + // Call the refresh user redemption record function + // Note: an extra ID ("D"), is passed into this function that will be ignored + // since there's not user redemption record for "D" + redemptionRecordIds := []string{"A", "B", "C", "D"} + actualTotalNativeAmount := s.App.StakeibcKeeper.RefreshUserRedemptionRecordNativeAmounts( + s.Ctx, + HostChainId, + redemptionRecordIds, + redemptionRate, + ) + + // Confirm the summation is correct and the user redemption records were updated + s.Require().Equal(expectedTotalNativeAmount.Int64(), actualTotalNativeAmount.Int64(), "total native amount") + for _, expectedRecord := range expectedUserRedemptionRecords { + actualRecord, found := s.App.RecordsKeeper.GetUserRedemptionRecord(s.Ctx, expectedRecord.Id) + s.Require().True(found, "record %s should have been found", expectedRecord.Id) + s.Require().Equal(expectedRecord.Amount.Int64(), actualRecord.Amount.Int64(), + "record %s native amount", expectedRecord.Id) + } +} + +// Tests RefreshUnbondingNativeTokenAmounts which indirectly tests +// RefreshHostZoneUnbondingNativeTokenAmount and RefreshUserRedemptionRecordNativeAmounts +func (s *KeeperTestSuite) TestRefreshUnbondingNativeTokenAmounts() { + chainA := "chain-0" + chainB := "chain-1" + epochNumberA := uint64(1) + epochNumberB := uint64(2) + + // Create the epoch unbonding records + // It doesn't need the host zone unbonding records since they'll be added + // in the tested function + s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, recordtypes.EpochUnbondingRecord{ + EpochNumber: epochNumberA, + }) + s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, recordtypes.EpochUnbondingRecord{ + EpochNumber: epochNumberB, + }) + + // Create two host zones, with different redemption rates + s.App.StakeibcKeeper.SetHostZone(s.Ctx, types.HostZone{ + ChainId: chainA, + RedemptionRate: sdk.MustNewDecFromStr("1.5"), + }) + s.App.StakeibcKeeper.SetHostZone(s.Ctx, types.HostZone{ + ChainId: chainB, + RedemptionRate: sdk.MustNewDecFromStr("2.0"), + }) + + // Create the user redemption records + userRedemptionRecords := []recordtypes.UserRedemptionRecord{ + // chainA - Redemption Rate: 1.5 + {Id: "A", StTokenAmount: sdkmath.NewInt(1000)}, // native: 1500 + {Id: "B", StTokenAmount: sdkmath.NewInt(2000)}, // native: 3000 + // chainB - Redemption Rate: 2.0 + {Id: "C", StTokenAmount: sdkmath.NewInt(3000)}, // native: 6000 + {Id: "D", StTokenAmount: sdkmath.NewInt(4000)}, // native: 8000 + } + expectedUserNativeAmounts := map[string]sdkmath.Int{ + "A": sdkmath.NewInt(1500), + "B": sdkmath.NewInt(3000), + "C": sdkmath.NewInt(6000), + "D": sdkmath.NewInt(8000), + } + for _, redemptionRecord := range userRedemptionRecords { + s.App.RecordsKeeper.SetUserRedemptionRecord(s.Ctx, redemptionRecord) + } + + // Define the two host zone unbonding records + initialHostZoneUnbondingA := recordtypes.HostZoneUnbonding{ + HostZoneId: chainA, + UserRedemptionRecords: []string{"A", "B"}, + } + expectedHostZoneUnbondAmountA := expectedUserNativeAmounts["A"].Add(expectedUserNativeAmounts["B"]) + + initialHostZoneUnbondingB := recordtypes.HostZoneUnbonding{ + HostZoneId: chainB, + UserRedemptionRecords: []string{"C", "D"}, + } + expectedHostZoneUnbondAmountB := expectedUserNativeAmounts["C"].Add(expectedUserNativeAmounts["D"]) + + // Call refresh for both hosts + epochToHostZoneMap := map[uint64]recordtypes.HostZoneUnbonding{ + epochNumberA: initialHostZoneUnbondingA, + epochNumberB: initialHostZoneUnbondingB, + } + err := s.App.StakeibcKeeper.RefreshUnbondingNativeTokenAmounts(s.Ctx, epochToHostZoneMap) + s.Require().NoError(err, "no error expected when refreshing unbond amount") + + // Confirm the host zone unbonding records were updated + updatedHostZoneUnbondingA, found := s.App.RecordsKeeper.GetHostZoneUnbondingByChainId(s.Ctx, epochNumberA, chainA) + actualHostZoneUnbondAmountA := updatedHostZoneUnbondingA.NativeTokenAmount + s.Require().True(found, "host zone unbonding record for %s should have been found", chainA) + s.Require().Equal(expectedHostZoneUnbondAmountA, actualHostZoneUnbondAmountA, "host zone unbonding native amount A") + + updatedHostZoneUnbondingB, found := s.App.RecordsKeeper.GetHostZoneUnbondingByChainId(s.Ctx, epochNumberB, chainB) + actualHostZoneUnbondAmountB := updatedHostZoneUnbondingB.NativeTokenAmount + s.Require().True(found, "host zone unbonding record for %s should have been found", chainB) + s.Require().Equal(expectedHostZoneUnbondAmountB, actualHostZoneUnbondAmountB, "host zone unbonding native amount B") + + // Confirm all user redemption records were updated + for id, expectedNativeAmount := range expectedUserNativeAmounts { + record, found := s.App.RecordsKeeper.GetUserRedemptionRecord(s.Ctx, id) + s.Require().True(found, "user redemption record for %s should have been found", id) + s.Require().Equal(expectedNativeAmount, record.Amount, "user redemption record %s native amount", id) + } + + // Remove one of the host zones and confirm it errors + s.App.StakeibcKeeper.RemoveHostZone(s.Ctx, chainA) + err = s.App.StakeibcKeeper.RefreshUnbondingNativeTokenAmounts(s.Ctx, epochToHostZoneMap) + s.Require().ErrorContains(err, "host zone not found") +} + func (s *KeeperTestSuite) TestGetValidatorUnbondCapacity() { // Start with the expected returned list of validator capacities expectedUnbondCapacity := []keeper.ValidatorUnbondCapacity{ From 4854e7039d5aac7f30f3c2e9a572db352e8f25cc Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 19:30:09 -0600 Subject: [PATCH 32/40] nit --- app/upgrades/v17/upgrades.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 461c807a45..2d3e66f4a0 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -314,7 +314,7 @@ func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { // UserUnbondingRecords previously only used Native Token Amounts, we now want to use StTokenAmounts func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { for _, epochUnbondingRecord := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { - for _, hostZoneUnbonding := range epochUnbondingRecord.GetHostZoneUnbondings() { + for _, hostZoneUnbonding := range epochUnbondingRecord.HostZoneUnbondings { // if the tokens have unbonded, we don't want to modify the record // this is because we won't be able to estimate a redemption rate if hostZoneUnbonding.Status == recordtypes.HostZoneUnbonding_CLAIMABLE { @@ -334,7 +334,7 @@ func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { estimatedStTokenConversionRate := stTokenAmountDec.Quo(nativeTokenAmountDec) // Loop through User Redemption Records and insert an estimated stTokenAmount - for _, userRedemptionRecordId := range hostZoneUnbonding.GetUserRedemptionRecords() { + for _, userRedemptionRecordId := range hostZoneUnbonding.UserRedemptionRecords { userRedemptionRecord, found := k.RecordsKeeper.GetUserRedemptionRecord(ctx, userRedemptionRecordId) if !found { // this would happen if the user has already claimed the unbonding, but given the status check above, this should never happen From 5e4d66749fe2539139a64cc83a37cb7f2ea552bf Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 19:59:12 -0600 Subject: [PATCH 33/40] added clarifying comment --- app/upgrades/v17/upgrades.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 2d3e66f4a0..8034b82604 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -310,13 +310,19 @@ func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { return nil } -// Migrate the unbonding records +// Migrate the user redemption records to add the stToken amount, calculated by estimating +// the redemption rate from the corresponding host zone unbonding records // UserUnbondingRecords previously only used Native Token Amounts, we now want to use StTokenAmounts +// We only really need to migrate records in status UNBONDING_QUEUE or UNBONDING_IN_PROGRESS +// because the stToken amount is never used after unbonding is initiated func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { for _, epochUnbondingRecord := range k.RecordsKeeper.GetAllEpochUnbondingRecord(ctx) { for _, hostZoneUnbonding := range epochUnbondingRecord.HostZoneUnbondings { - // if the tokens have unbonded, we don't want to modify the record - // this is because we won't be able to estimate a redemption rate + // If a record is in state claimable, the native token amount can't be trusted + // since it gets decremented with each claim + // As a result, we can't accurately estimate the redemption rate for these + // user redemption records (but it also doesn't matter since the stToken + // amount on the records is not used) if hostZoneUnbonding.Status == recordtypes.HostZoneUnbonding_CLAIMABLE { continue } @@ -346,8 +352,6 @@ func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) } } - - k.RecordsKeeper.SetEpochUnbondingRecord(ctx, epochUnbondingRecord) } return nil From f65bae9b743aace16620f99425f47604811d825d Mon Sep 17 00:00:00 2001 From: ethan-stride <126913021+ethan-stride@users.noreply.github.com> Date: Wed, 10 Jan 2024 19:43:39 -0700 Subject: [PATCH 34/40] send disable tokenization tx for delegation account on hub (#1045) Co-authored-by: sampocs --- app/upgrades/v17/upgrades.go | 33 ++++ app/upgrades/v17/upgrades_test.go | 47 +++++- proto/cosmos/staking/v1beta1/lsm_tx.proto | 9 ++ x/stakeibc/types/lsm_tx.pb.go | 181 +++++++++++++++++++++- 4 files changed, 255 insertions(+), 15 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index de4f31ad95..6143380d14 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -2,6 +2,7 @@ package v17 import ( "fmt" + "time" errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" @@ -9,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/cosmos/gogoproto/proto" icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" @@ -32,6 +34,9 @@ var ( RedemptionRateOuterMinAdjustment = sdk.MustNewDecFromStr("0.05") RedemptionRateOuterMaxAdjustment = sdk.MustNewDecFromStr("0.10") + // Define the hub chainId for disabling tokenization + GaiaChainId = "cosmoshub-4" + // Osmosis will have a slighly larger buffer with the redemption rate // since their yield is less predictable OsmosisChainId = "osmosis-1" @@ -111,6 +116,9 @@ func CreateUpgradeHandler( return vm, errorsmod.Wrapf(err, "unable to add rate limits to Osmosis") } + ctx.Logger().Info("Disabling tokenization on the hub...") + DisableTokenization(ctx, stakeibcKeeper, GaiaChainId) + ctx.Logger().Info("Executing Prop 225, SHD Liquidity") if err := ExecuteProp225(ctx, bankKeeper); err != nil { return vm, errorsmod.Wrapf(err, "unable to execute prop 225") @@ -318,6 +326,31 @@ func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { return nil } +// Sends the ICA message which disables LSM style tokenization of shares from the delegation +// account for this chain as a security to prevent possibility of large/fast withdrawls +func DisableTokenization(ctx sdk.Context, k stakeibckeeper.Keeper, chainId string) error { + hostZone, found := k.GetHostZone(ctx, chainId) + if !found { + return errorsmod.Wrapf(stakeibctypes.ErrHostZoneNotFound, "Unable to find chainId %s to remove tokenization", chainId) + } + + // Build the msg for the disable tokenization ICA tx + var msgs []proto.Message + msgs = append(msgs, &stakeibctypes.MsgDisableTokenizeShares{ + DelegatorAddress: hostZone.DelegationIcaAddress, + }) + + // Send the ICA tx to disable tokenization + timeoutTimestamp := uint64(ctx.BlockTime().Add(24 * time.Hour).UnixNano()) + delegationOwner := stakeibctypes.FormatHostZoneICAOwner(hostZone.ChainId, stakeibctypes.ICAAccountType_DELEGATION) + err := k.SubmitICATxWithoutCallback(ctx, hostZone.ConnectionId, delegationOwner, msgs, timeoutTimestamp) + if err != nil { + return errorsmod.Wrapf(err, "Failed to submit ICA tx to disable tokenization, Messages: %+v", msgs) + } + + return nil +} + // Execute Prop 225, release STRD to stride1auhjs4zgp3ahvrpkspf088r2psz7wpyrypcnal func ExecuteProp225(ctx sdk.Context, k bankkeeper.Keeper) error { communityPoolGrowthAddress := sdk.MustAccAddressFromBech32(CommunityPoolGrowthAddress) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index c8cf11579c..325a79feda 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -7,6 +7,7 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + ibctesting "github.com/cosmos/ibc-go/v7/testing" "github.com/stretchr/testify/suite" icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types" @@ -20,7 +21,6 @@ import ( ) const ( - GaiaChainId = "cosmoshub-4" SommelierChainId = "sommelier-3" Atom = "uatom" @@ -69,11 +69,16 @@ func TestKeeperTestSuite(t *testing.T) { func (s *UpgradeTestSuite) TestUpgrade() { dummyUpgradeHeight := int64(5) + // Create the gaia delegation channel + owner := stakeibctypes.FormatHostZoneICAOwner(v17.GaiaChainId, stakeibctypes.ICAAccountType_DELEGATION) + channelId, portId := s.CreateICAChannel(owner) + // Setup store before upgrade checkHostZonesAfterUpgrade := s.SetupHostZonesBeforeUpgrade() checkRateLimitsAfterUpgrade := s.SetupRateLimitsBeforeUpgrade() checkCommunityPoolTaxAfterUpgrade := s.SetupCommunityPoolTaxBeforeUpgrade() checkQueriesAfterUpgrade := s.SetupQueriesBeforeUpgrade() + checkDisableTokenizationICASubmitted := s.SetupTestDisableTokenization(channelId, portId) checkProp225AfterUpgrade := s.SetupProp225BeforeUpgrade() // Submit upgrade and confirm handler succeeds @@ -84,6 +89,7 @@ func (s *UpgradeTestSuite) TestUpgrade() { checkRateLimitsAfterUpgrade() checkCommunityPoolTaxAfterUpgrade() checkQueriesAfterUpgrade() + checkDisableTokenizationICASubmitted() checkProp225AfterUpgrade() } @@ -126,9 +132,9 @@ func (s *UpgradeTestSuite) checkCommunityPoolICAAccountsRegistered(chainId strin func (s *UpgradeTestSuite) SetupHostZonesBeforeUpgrade() func() { hostZones := []stakeibctypes.HostZone{ { - ChainId: GaiaChainId, + ChainId: v17.GaiaChainId, HostDenom: Atom, - ConnectionId: "connection-1", + ConnectionId: ibctesting.FirstConnectionID, // must be connection-0 since an ICA will be submitted Validators: []*stakeibctypes.Validator{ {Address: "val1", SlashQueryInProgress: false}, {Address: "val2", SlashQueryInProgress: true}, @@ -169,14 +175,14 @@ func (s *UpgradeTestSuite) SetupHostZonesBeforeUpgrade() func() { // Return callback to check store after upgrade return func() { // Check that the module and ICA accounts were registered - s.checkCommunityPoolModuleAccountsRegistered(GaiaChainId) + s.checkCommunityPoolModuleAccountsRegistered(v17.GaiaChainId) s.checkCommunityPoolModuleAccountsRegistered(v17.OsmosisChainId) - s.checkCommunityPoolICAAccountsRegistered(GaiaChainId) + s.checkCommunityPoolICAAccountsRegistered(v17.GaiaChainId) s.checkCommunityPoolICAAccountsRegistered(v17.OsmosisChainId) // Check that the redemption rate bounds were set - gaiaHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, GaiaChainId) + gaiaHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, v17.GaiaChainId) s.Require().True(found) s.Require().Equal(sdk.MustNewDecFromStr("1.045"), gaiaHostZone.MinRedemptionRate, "gaia min outer") // 1.1 - 5% = 1.045 @@ -274,7 +280,7 @@ func (s *UpgradeTestSuite) SetupRateLimitsBeforeUpgrade() func() { stAtomToGaiaRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, StAtom, gaiaChannelId) s.Require().True(found) - atomThreshold := v17.UpdatedRateLimits[GaiaChainId] + atomThreshold := v17.UpdatedRateLimits[v17.GaiaChainId] s.Require().Equal(atomThreshold, stAtomToGaiaRateLimit.Quota.MaxPercentSend, "statom -> gaia max percent send") s.Require().Equal(atomThreshold, stAtomToGaiaRateLimit.Quota.MaxPercentRecv, "statom -> gaia max percent recv") s.Require().Equal(initialFlow, stAtomToGaiaRateLimit.Flow.Outflow, "statom -> gaia outflow") @@ -344,6 +350,17 @@ func (s *UpgradeTestSuite) SetupQueriesBeforeUpgrade() func() { } } +func (s *UpgradeTestSuite) SetupTestDisableTokenization(channelId, portId string) func() { + // Get the current sequence number (to check that it incremented) + startSequence := s.MustGetNextSequenceNumber(portId, channelId) + + // Return callback to that the ICA was submitted + return func() { + endSequence := s.MustGetNextSequenceNumber(portId, channelId) + s.Require().Equal(startSequence+1, endSequence, "sequence number should have incremented from disabling detokenization") + } +} + func (s *UpgradeTestSuite) SetupProp225BeforeUpgrade() func() { // Grab the community pool growth address and balance communityPoolGrowthAddress := sdk.MustAccAddressFromBech32(v17.CommunityPoolGrowthAddress) @@ -725,3 +742,19 @@ func (s *UpgradeTestSuite) TestAddRateLimitToOsmosis() { err = v17.AddRateLimitToOsmosis(s.Ctx, s.App.RatelimitKeeper) s.Require().ErrorContains(err, "channel value is zero") } + +func (s *UpgradeTestSuite) TestDisableTokenization() { + // Create the host zone and delegation channel + owner := stakeibctypes.FormatHostZoneICAOwner(v17.GaiaChainId, stakeibctypes.ICAAccountType_DELEGATION) + channelId, portId := s.CreateICAChannel(owner) + + s.App.StakeibcKeeper.SetHostZone(s.Ctx, stakeibctypes.HostZone{ + ChainId: v17.GaiaChainId, + ConnectionId: ibctesting.FirstConnectionID, + }) + + // Call the disable function and confirm the sequence number incremented (indicating an ICA was submitted) + s.CheckICATxSubmitted(portId, channelId, func() error { + return v17.DisableTokenization(s.Ctx, s.App.StakeibcKeeper, v17.GaiaChainId) + }) +} diff --git a/proto/cosmos/staking/v1beta1/lsm_tx.proto b/proto/cosmos/staking/v1beta1/lsm_tx.proto index 239a11ebf6..3d35408192 100644 --- a/proto/cosmos/staking/v1beta1/lsm_tx.proto +++ b/proto/cosmos/staking/v1beta1/lsm_tx.proto @@ -28,3 +28,12 @@ message MsgRedeemTokensForShares { message MsgRedeemTokensForSharesResponse { cosmos.base.v1beta1.Coin amount = 1 [ (gogoproto.nullable) = false ]; } + +// MsgDisableTokenizeShares prevents LSM tokenization of shares for address +message MsgDisableTokenizeShares { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string delegator_address = 1 + [ (gogoproto.moretags) = "yaml:\"delegator_address\"" ]; +} diff --git a/x/stakeibc/types/lsm_tx.pb.go b/x/stakeibc/types/lsm_tx.pb.go index e6c3eb3a73..f824d5172a 100644 --- a/x/stakeibc/types/lsm_tx.pb.go +++ b/x/stakeibc/types/lsm_tx.pb.go @@ -110,9 +110,48 @@ func (m *MsgRedeemTokensForSharesResponse) GetAmount() types.Coin { return types.Coin{} } +// MsgDisableTokenizeShares prevents LSM tokenization of shares for address +type MsgDisableTokenizeShares struct { + DelegatorAddress string `protobuf:"bytes,1,opt,name=delegator_address,json=delegatorAddress,proto3" json:"delegator_address,omitempty" yaml:"delegator_address"` +} + +func (m *MsgDisableTokenizeShares) Reset() { *m = MsgDisableTokenizeShares{} } +func (m *MsgDisableTokenizeShares) String() string { return proto.CompactTextString(m) } +func (*MsgDisableTokenizeShares) ProtoMessage() {} +func (*MsgDisableTokenizeShares) Descriptor() ([]byte, []int) { + return fileDescriptor_34c3b474a863e424, []int{2} +} +func (m *MsgDisableTokenizeShares) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgDisableTokenizeShares) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgDisableTokenizeShares.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgDisableTokenizeShares) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgDisableTokenizeShares.Merge(m, src) +} +func (m *MsgDisableTokenizeShares) XXX_Size() int { + return m.Size() +} +func (m *MsgDisableTokenizeShares) XXX_DiscardUnknown() { + xxx_messageInfo_MsgDisableTokenizeShares.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgDisableTokenizeShares proto.InternalMessageInfo + func init() { proto.RegisterType((*MsgRedeemTokensForShares)(nil), "cosmos.staking.v1beta1.MsgRedeemTokensForShares") proto.RegisterType((*MsgRedeemTokensForSharesResponse)(nil), "cosmos.staking.v1beta1.MsgRedeemTokensForSharesResponse") + proto.RegisterType((*MsgDisableTokenizeShares)(nil), "cosmos.staking.v1beta1.MsgDisableTokenizeShares") } func init() { @@ -120,7 +159,7 @@ func init() { } var fileDescriptor_34c3b474a863e424 = []byte{ - // 327 bytes of a gzipped FileDescriptorProto + // 349 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4e, 0xce, 0x2f, 0xce, 0xcd, 0x2f, 0xd6, 0x2f, 0x2e, 0x49, 0xcc, 0xce, 0xcc, 0x4b, 0xd7, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0xcf, 0x29, 0xce, 0x8d, 0x2f, 0xa9, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, @@ -135,13 +174,14 @@ var fileDescriptor_34c3b474a863e424 = []byte{ 0xa4, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa9, 0x07, 0xf5, 0x06, 0xc8, 0x61, 0x30, 0x3f, 0xe8, 0x39, 0xe7, 0x67, 0xe6, 0x39, 0xb1, 0x9c, 0xb8, 0x27, 0xcf, 0x10, 0x04, 0x55, 0x6e, 0xc5, 0xd1, 0xb1, 0x40, 0x9e, 0xe1, 0xc5, 0x02, 0x79, 0x06, 0xa5, 0x68, 0x2e, 0x05, 0x5c, 0x2e, 0x0d, 0x4a, 0x2d, - 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x45, 0xb2, 0x86, 0x91, 0x24, 0x6b, 0x9c, 0x7c, 0x4e, 0x3c, 0x92, - 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, - 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0xca, 0x28, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, - 0x39, 0x3f, 0x57, 0x3f, 0xb8, 0xa4, 0x28, 0x33, 0x25, 0x55, 0xd7, 0x27, 0x31, 0x09, 0x14, 0x49, - 0x20, 0xb6, 0x7e, 0x99, 0xa1, 0x99, 0x7e, 0x05, 0x38, 0xc6, 0x52, 0x33, 0x93, 0x92, 0xf5, 0x4b, - 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0x81, 0x6b, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x49, - 0x5c, 0x2e, 0x7e, 0xd1, 0x01, 0x00, 0x00, + 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x45, 0xb2, 0x86, 0x91, 0x24, 0x6b, 0x94, 0xf2, 0xc1, 0xc1, 0xe0, + 0x92, 0x59, 0x9c, 0x98, 0x94, 0x93, 0x0a, 0x36, 0x3d, 0xb3, 0x2a, 0x95, 0xea, 0xc1, 0x80, 0xf0, + 0x8d, 0x93, 0xcf, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, + 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x19, 0xa5, 0x67, + 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x07, 0x97, 0x14, 0x65, 0xa6, 0xa4, 0xea, + 0xfa, 0x24, 0x26, 0x81, 0x52, 0x05, 0x88, 0xad, 0x5f, 0x66, 0x68, 0xa6, 0x5f, 0x01, 0x4e, 0x22, + 0xa9, 0x99, 0x49, 0xc9, 0xfa, 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0xe0, 0xd8, 0x34, 0x06, + 0x04, 0x00, 0x00, 0xff, 0xff, 0x8a, 0x66, 0xa5, 0x97, 0x42, 0x02, 0x00, 0x00, } func (m *MsgRedeemTokensForShares) Marshal() (dAtA []byte, err error) { @@ -217,6 +257,36 @@ func (m *MsgRedeemTokensForSharesResponse) MarshalToSizedBuffer(dAtA []byte) (in return len(dAtA) - i, nil } +func (m *MsgDisableTokenizeShares) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgDisableTokenizeShares) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgDisableTokenizeShares) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.DelegatorAddress) > 0 { + i -= len(m.DelegatorAddress) + copy(dAtA[i:], m.DelegatorAddress) + i = encodeVarintLsmTx(dAtA, i, uint64(len(m.DelegatorAddress))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintLsmTx(dAtA []byte, offset int, v uint64) int { offset -= sovLsmTx(v) base := offset @@ -254,6 +324,19 @@ func (m *MsgRedeemTokensForSharesResponse) Size() (n int) { return n } +func (m *MsgDisableTokenizeShares) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.DelegatorAddress) + if l > 0 { + n += 1 + l + sovLsmTx(uint64(l)) + } + return n +} + func sovLsmTx(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -458,6 +541,88 @@ func (m *MsgRedeemTokensForSharesResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgDisableTokenizeShares) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLsmTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgDisableTokenizeShares: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgDisableTokenizeShares: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLsmTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLsmTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLsmTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DelegatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLsmTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthLsmTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipLsmTx(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 From 4c199dc413967082bf27f8a344473e48f54e4dfe Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 20:47:02 -0600 Subject: [PATCH 35/40] fixed unit test --- app/upgrades/v17/upgrades_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index c8cf11579c..e279cc9b1e 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -541,22 +541,22 @@ func (s *UpgradeTestSuite) TestUpdateRateLimitThresholds() { // Define test cases consisting of an initial redemption rates and expected bounds testCases := map[string]UpdateRateLimits{ "cosmoshub": { - // 10% threshold + // 15% threshold ChainId: "cosmoshub-4", ChannelId: "channel-0", HostDenom: "uatom", RateLimitDenom: "stuatom", Duration: 10, - Threshold: sdkmath.NewInt(10), + Threshold: sdkmath.NewInt(15), }, "osmosis": { - // 10% threshold + // 15% threshold ChainId: "osmosis-1", ChannelId: "channel-1", HostDenom: "uosmo", RateLimitDenom: "stuosmo", Duration: 20, - Threshold: sdkmath.NewInt(20), + Threshold: sdkmath.NewInt(15), }, "juno": { // No denom on matching host From 8fbd60ad67ec49212002799896edd3c5352332d8 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 20:48:15 -0600 Subject: [PATCH 36/40] added pfm store key --- app/upgrades.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/upgrades.go b/app/upgrades.go index 446fd9518d..01e1e3ae3e 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -10,6 +10,7 @@ import ( consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + packetforwardtypes "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v7/packetforward/types" consumertypes "github.com/cosmos/interchain-security/v3/x/ccv/consumer/types" evmosvestingtypes "github.com/evmos/vesting/x/vesting/types" @@ -271,13 +272,12 @@ func (app *StrideApp) setupUpgradeHandlers(appOpts servertypes.AppOptions) { storeUpgrades = &storetypes.StoreUpgrades{ Added: []string{evmosvestingtypes.ModuleName}, } + case "v17": + storeUpgrades = &storetypes.StoreUpgrades{ + // Add PFM store key + Added: []string{packetforwardtypes.ModuleName}, + } } - // TODO: uncomment when v17 upgrade is ready - // case "v17": - // storeUpgrades = &storetypes.StoreUpgrades{ - // // Add PFM store key - // Added: []string{packetforwardtypes.ModuleName}, - // } if storeUpgrades != nil { app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, storeUpgrades)) From 6e92ff0bcbb94fc51e0dc7b385f868234002e4fe Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 21:08:33 -0600 Subject: [PATCH 37/40] addressed aidan PR comments --- app/upgrades/v17/upgrades.go | 10 ++++++---- app/upgrades/v17/upgrades_test.go | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 6143380d14..1595aad505 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -97,11 +97,11 @@ func CreateUpgradeHandler( ctx.Logger().Info("Deleting all pending slash queries...") DeleteAllStaleQueries(ctx, icqKeeper) - ctx.Logger().Info("Reseting slash query in progress...") + ctx.Logger().Info("Resetting slash query in progress...") ResetSlashQueryInProgress(ctx, stakeibcKeeper) ctx.Logger().Info("Updating community pool tax...") - if err := IncreaseCommunityPoolTax(ctx, distributionkeeper); err != nil { + if err := ExecuteProp223(ctx, distributionkeeper); err != nil { return vm, errorsmod.Wrapf(err, "unable to increase community pool tax") } @@ -117,7 +117,9 @@ func CreateUpgradeHandler( } ctx.Logger().Info("Disabling tokenization on the hub...") - DisableTokenization(ctx, stakeibcKeeper, GaiaChainId) + if err := DisableTokenization(ctx, stakeibcKeeper, GaiaChainId); err != nil { + return vm, errorsmod.Wrapf(err, "unable to submit disable tokenization transaction") + } ctx.Logger().Info("Executing Prop 225, SHD Liquidity") if err := ExecuteProp225(ctx, bankKeeper); err != nil { @@ -222,7 +224,7 @@ func ResetSlashQueryInProgress(ctx sdk.Context, k stakeibckeeper.Keeper) { // Increases the community pool tax from 2 to 5% // This was from prop 223 which passed, but was deleted due to an ICS blacklist -func IncreaseCommunityPoolTax(ctx sdk.Context, k distributionkeeper.Keeper) error { +func ExecuteProp223(ctx sdk.Context, k distributionkeeper.Keeper) error { params := k.GetParams(ctx) params.CommunityTax = CommunityPoolTax return k.SetParams(ctx, params) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index f57a176a66..e22aaae712 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -488,7 +488,7 @@ func (s *UpgradeTestSuite) TestResetSlashQueryInProgress() { } } -func (s *UpgradeTestSuite) TestIncreaseCommunityPoolTax() { +func (s *UpgradeTestSuite) TestExecuteProp223() { // Set initial community pool tax to 2% initialTax := sdk.MustNewDecFromStr("0.02") params := s.App.DistrKeeper.GetParams(s.Ctx) @@ -497,7 +497,7 @@ func (s *UpgradeTestSuite) TestIncreaseCommunityPoolTax() { s.Require().NoError(err, "no error expected when setting params") // Increase the tax - err = v17.IncreaseCommunityPoolTax(s.Ctx, s.App.DistrKeeper) + err = v17.ExecuteProp223(s.Ctx, s.App.DistrKeeper) s.Require().NoError(err, "no error expected when increasing community pool tax") // Confirm it increased From 5f546ec9954824d492e82bca39e7597babd4e82d Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 22:55:55 -0600 Subject: [PATCH 38/40] nit --- app/upgrades/v17/upgrades_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index e22aaae712..baf6adcc46 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -211,16 +211,16 @@ func (s *UpgradeTestSuite) SetupRateLimitsBeforeUpgrade() func() { initialChannelValue := sdkmath.NewInt(100) updatedChannelValue := sdkmath.NewInt(200) - ratesLimits := []AddRateLimits{ + rateLimits := []AddRateLimits{ { // Gaia rate limit - // Treshold should be updated, new gaia RL added + // Threshold should be updated, new gaia RL added Denom: StAtom, ChannelId: gaiaChannelId, }, { // Osmo rate limit - // Treshold should be updated, no new RL added + // Threshold should be updated, no new RL added Denom: StOsmo, ChannelId: v17.OsmosisTransferChannelId, }, @@ -234,7 +234,7 @@ func (s *UpgradeTestSuite) SetupRateLimitsBeforeUpgrade() func() { // Add rate limits // No need to register host zones since they're initialized in the setup host zone function - for _, rateLimit := range ratesLimits { + for _, rateLimit := range rateLimits { s.App.RatelimitKeeper.SetRateLimit(s.Ctx, ratelimittypes.RateLimit{ Path: &ratelimittypes.Path{ Denom: rateLimit.Denom, From 37251bece0d130bb76a7d941cbd4c6b4cedba988 Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 10 Jan 2024 23:04:39 -0600 Subject: [PATCH 39/40] fixed upgrade handler unit tests --- app/apptesting/test_helpers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/apptesting/test_helpers.go b/app/apptesting/test_helpers.go index 7076ffce33..53552290c1 100644 --- a/app/apptesting/test_helpers.go +++ b/app/apptesting/test_helpers.go @@ -200,6 +200,7 @@ func (s *AppTestHelper) SetupIBCChains(hostChainID string) { // Call InitGenesis on the consumer s.StrideChain.App.(*app.StrideApp).GetConsumerKeeper().InitGenesis(s.StrideChain.GetContext(), &strideConsumerGenesis) + s.StrideChain.NextBlock() // Update coordinator s.Coordinator.Chains = map[string]*ibctesting.TestChain{ From 8c5950f5a5979f73416838ed8e9378baf6ca1176 Mon Sep 17 00:00:00 2001 From: Aidan Salzmann Date: Thu, 11 Jan 2024 08:36:49 -0500 Subject: [PATCH 40/40] unittest MigrateUnbondingRecords (#1056) Co-authored-by: sampocs --- app/upgrades/v17/upgrades.go | 5 +- app/upgrades/v17/upgrades_test.go | 130 +++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index 5b7669c40e..c2552d1dd8 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -164,7 +164,7 @@ func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { } // similarly, if there aren't any tokens to unbond, we don't want to modify the record // as we won't be able to estimate a redemption rate - if hostZoneUnbonding.NativeTokenAmount == sdkmath.ZeroInt() { + if hostZoneUnbonding.NativeTokenAmount.IsZero() { continue } @@ -173,6 +173,7 @@ func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { stTokenAmountDec := sdk.NewDecFromInt(hostZoneUnbonding.StTokenAmount) // this estimated rate is the amount of stTokens that would be received for 1 native token // e.g. if the rate is 0.5, then 1 native token would be worth 0.5 stTokens + // estimatedStTokenConversionRate is 1 / redemption rate estimatedStTokenConversionRate := stTokenAmountDec.Quo(nativeTokenAmountDec) // Loop through User Redemption Records and insert an estimated stTokenAmount @@ -184,7 +185,7 @@ func MigrateUnbondingRecords(ctx sdk.Context, k stakeibckeeper.Keeper) error { continue } - userRedemptionRecord.StTokenAmount = estimatedStTokenConversionRate.Mul(sdkmath.LegacyDec(userRedemptionRecord.Amount)).RoundInt() + userRedemptionRecord.StTokenAmount = estimatedStTokenConversionRate.Mul(sdk.NewDecFromInt(userRedemptionRecord.Amount)).RoundInt() k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord) } } diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index 151ab8bb40..1dcbdab182 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -2,6 +2,7 @@ package v17_test import ( "fmt" + "strconv" "testing" sdkmath "cosmossdk.io/math" @@ -12,6 +13,7 @@ import ( icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types" ratelimittypes "github.com/Stride-Labs/stride/v16/x/ratelimit/types" + recordtypes "github.com/Stride-Labs/stride/v16/x/records/types" "github.com/Stride-Labs/stride/v16/app/apptesting" v17 "github.com/Stride-Labs/stride/v16/app/upgrades/v17" @@ -75,6 +77,7 @@ func (s *UpgradeTestSuite) TestUpgrade() { // Setup store before upgrade checkHostZonesAfterUpgrade := s.SetupHostZonesBeforeUpgrade() + checkMigrateUnbondingRecords := s.SetupMigrateUnbondingRecords() checkRateLimitsAfterUpgrade := s.SetupRateLimitsBeforeUpgrade() checkCommunityPoolTaxAfterUpgrade := s.SetupCommunityPoolTaxBeforeUpgrade() checkQueriesAfterUpgrade := s.SetupQueriesBeforeUpgrade() @@ -86,6 +89,7 @@ func (s *UpgradeTestSuite) TestUpgrade() { // Check state after upgrade checkHostZonesAfterUpgrade() + checkMigrateUnbondingRecords() checkRateLimitsAfterUpgrade() checkCommunityPoolTaxAfterUpgrade() checkQueriesAfterUpgrade() @@ -203,6 +207,128 @@ func (s *UpgradeTestSuite) SetupHostZonesBeforeUpgrade() func() { } } +func (s *UpgradeTestSuite) SetupMigrateUnbondingRecords() func() { + // Create EURs for two host zones + // 2 HZU on each host zone will trigger URR updates + // - UNBONDING_QUEUE + // - UNBONDING_IN_PROGRESS + // - EXIT_TRANSFER_QUEUE + // - EXIT_TRANSFER_IN_PROGRESS + // 2 HZU on each host zone will not trigger URR updates + // - 1 HZU has 0 NativeTokenAmount + // - 1 HZU has status CLAIMABLE + + nativeTokenAmount := int64(2000000) + stTokenAmount := int64(1000000) + URRAmount := int64(500) + + // create mockURRIds 1 through 6 and store the URRs + for i := 1; i <= 6; i++ { + mockURRId := strconv.Itoa(i) + mockURR := recordtypes.UserRedemptionRecord{ + Id: mockURRId, + Amount: sdkmath.NewInt(URRAmount), + } + s.App.RecordsKeeper.SetUserRedemptionRecord(s.Ctx, mockURR) + } + + epochUnbondingRecord := recordtypes.EpochUnbondingRecord{ + EpochNumber: 1, + HostZoneUnbondings: []*recordtypes.HostZoneUnbonding{ + // HZUs that will trigger URR updates + { + HostZoneId: v17.GaiaChainId, + NativeTokenAmount: sdkmath.NewInt(nativeTokenAmount), + StTokenAmount: sdkmath.NewInt(stTokenAmount), + Status: recordtypes.HostZoneUnbonding_UNBONDING_QUEUE, + UserRedemptionRecords: []string{"1"}, + }, + { + HostZoneId: SommelierChainId, + NativeTokenAmount: sdkmath.NewInt(nativeTokenAmount), + StTokenAmount: sdkmath.NewInt(stTokenAmount), + Status: recordtypes.HostZoneUnbonding_EXIT_TRANSFER_QUEUE, + UserRedemptionRecords: []string{"2"}, + }, + }, + } + epochUnbondingRecord2 := recordtypes.EpochUnbondingRecord{ + EpochNumber: 2, + HostZoneUnbondings: []*recordtypes.HostZoneUnbonding{ + // HZUs that will trigger URR updates + { + HostZoneId: v17.GaiaChainId, + NativeTokenAmount: sdkmath.NewInt(nativeTokenAmount), + StTokenAmount: sdkmath.NewInt(stTokenAmount), + Status: recordtypes.HostZoneUnbonding_UNBONDING_IN_PROGRESS, + UserRedemptionRecords: []string{"3"}, + }, + { + HostZoneId: SommelierChainId, + NativeTokenAmount: sdkmath.NewInt(nativeTokenAmount), + StTokenAmount: sdkmath.NewInt(stTokenAmount), + Status: recordtypes.HostZoneUnbonding_EXIT_TRANSFER_IN_PROGRESS, + UserRedemptionRecords: []string{"4"}, + }, + }, + } + epochUnbondingRecord3 := recordtypes.EpochUnbondingRecord{ + EpochNumber: 3, + HostZoneUnbondings: []*recordtypes.HostZoneUnbonding{ + // HZUs that will not trigger URR updates + { + HostZoneId: v17.GaiaChainId, + // Will not trigger update because NativeTokenAmount is 0 + NativeTokenAmount: sdkmath.NewInt(0), + StTokenAmount: sdkmath.NewInt(stTokenAmount), + Status: recordtypes.HostZoneUnbonding_UNBONDING_QUEUE, + UserRedemptionRecords: []string{"5"}, + }, + { + HostZoneId: v17.GaiaChainId, + NativeTokenAmount: sdkmath.NewInt(nativeTokenAmount), + StTokenAmount: sdkmath.NewInt(stTokenAmount), + // Will not trigger update because status is CLAIMABLE + Status: recordtypes.HostZoneUnbonding_CLAIMABLE, + UserRedemptionRecords: []string{"6"}, + }, + }, + } + s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, epochUnbondingRecord) + s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, epochUnbondingRecord2) + s.App.RecordsKeeper.SetEpochUnbondingRecord(s.Ctx, epochUnbondingRecord3) + + return func() { + // conversionRate is stTokenAmount / nativeTokenAmount + conversionRate := sdk.NewDec(stTokenAmount).Quo(sdk.NewDec(nativeTokenAmount)) + expectedConversionRate := sdk.MustNewDecFromStr("0.5") + s.Require().Equal(expectedConversionRate, conversionRate, "expected conversion rate (1/redemption rate)") + + // stTokenAmount is conversionRate * URRAmount + stTokenAmount := conversionRate.Mul(sdk.NewDec(URRAmount)).RoundInt() + expectedStTokenAmount := sdkmath.NewInt(250) + s.Require().Equal(stTokenAmount, expectedStTokenAmount, "expected st token amount") + + // Verify URR stToken amounts are set correctly for records 1 through 4 + for i := 1; i <= 4; i++ { + mockURRId := strconv.Itoa(i) + mockURR, found := s.App.RecordsKeeper.GetUserRedemptionRecord(s.Ctx, mockURRId) + s.Require().True(found) + s.Require().Equal(expectedStTokenAmount, mockURR.StTokenAmount, "URR %s - st token amount", mockURRId) + } + + // Verify URRs with status CLAIMABLE are skipped (record 5) + // Verify HZUs with 0 NativeTokenAmount are skipped (record 6) + for i := 5; i <= 6; i++ { + mockURRId := strconv.Itoa(i) + mockURR, found := s.App.RecordsKeeper.GetUserRedemptionRecord(s.Ctx, mockURRId) + s.Require().True(found) + // verify the amount was not updated + s.Require().Equal(sdk.NewInt(0), mockURR.StTokenAmount, "URR %s - st token amount", mockURRId) + } + } +} + func (s *UpgradeTestSuite) SetupRateLimitsBeforeUpgrade() func() { gaiaChannelId := "channel-0" @@ -572,11 +698,7 @@ func (s *UpgradeTestSuite) TestUpdateRateLimitThresholds() { ChannelId: "channel-1", HostDenom: "uosmo", RateLimitDenom: "stuosmo", -<<<<<<< HEAD - Duration: 15, -======= Duration: 20, ->>>>>>> v17-upgrade-handler Threshold: sdkmath.NewInt(15), }, "juno": {