Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add inner bounds #938

Merged
merged 14 commits into from
Sep 18, 2023
10 changes: 10 additions & 0 deletions proto/stride/stakeibc/host_zone.proto
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ message HostZone {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string min_inner_redemption_rate = 28 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string max_inner_redemption_rate = 29 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
bool lsm_liquid_stake_enabled = 27;
bool halted = 19;
reserved 4, 5, 6, 7, 14, 15, 16;
Expand Down
19 changes: 19 additions & 0 deletions proto/stride/stakeibc/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,27 @@ service Msg {
rpc UpdateValidatorSharesExchRate(MsgUpdateValidatorSharesExchRate)
returns (MsgUpdateValidatorSharesExchRateResponse);
rpc ClearBalance(MsgClearBalance) returns (MsgClearBalanceResponse);
rpc UpdateInnerRedemptionRateBounds(MsgUpdateInnerRedemptionRateBounds)
returns (MsgUpdateInnerRedemptionRateBoundsResponse);
}

message MsgUpdateInnerRedemptionRateBounds {
string creator = 1;
string chain_id = 2;
string min_inner_redemption_rate = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string max_inner_redemption_rate = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

message MsgUpdateInnerRedemptionRateBoundsResponse {}

message MsgLiquidStake {
string creator = 1;
string amount = 2 [
Expand Down
1 change: 1 addition & 0 deletions x/stakeibc/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func GetTxCmd() *cobra.Command {
cmd.AddCommand(CmdRestoreInterchainAccount())
cmd.AddCommand(CmdUpdateValidatorSharesExchRate())
cmd.AddCommand(CmdClearBalance())
cmd.AddCommand(CmdUpdateInnerRedemptionRateBounds())

return cmd
}
44 changes: 44 additions & 0 deletions x/stakeibc/client/cli/tx_update_inner_redemption_rate_bounds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cli

import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/cobra"

"github.com/Stride-Labs/stride/v14/x/stakeibc/types"
)

func CmdUpdateInnerRedemptionRateBounds() *cobra.Command {
cmd := &cobra.Command{
Use: "update-tight-bounds [chainid] [min-bound] [max-bound]",
Short: "Broadcast message update-tight-bounds",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) {
argChainId := args[0]
minInnerRedemptionRate := sdk.MustNewDecFromStr(args[1])
maxInnerRedemptionRate := sdk.MustNewDecFromStr(args[2])

clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

msg := types.NewMsgUpdateInnerRedemptionRateBounds(
clientCtx.GetFromAddress().String(),
argChainId,
minInnerRedemptionRate,
maxInnerRedemptionRate,
)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
3 changes: 3 additions & 0 deletions x/stakeibc/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ func NewMessageHandler(k keeper.Keeper) sdk.Handler {
case *types.MsgUpdateValidatorSharesExchRate:
res, err := msgServer.UpdateValidatorSharesExchRate(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgUpdateInnerRedemptionRateBounds:
res, err := msgServer.UpdateInnerRedemptionRateBounds(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
default:
errMsg := fmt.Sprintf("unrecognized %s message type: %T", types.ModuleName, msg)
return nil, errorsmod.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
Expand Down
2 changes: 2 additions & 0 deletions x/stakeibc/keeper/host_zone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func createNHostZone(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.Host
items[i].LastRedemptionRate = sdk.NewDec(1)
items[i].MinRedemptionRate = sdk.NewDecWithPrec(5, 1)
items[i].MaxRedemptionRate = sdk.NewDecWithPrec(15, 1)
items[i].MinInnerRedemptionRate = sdk.NewDecWithPrec(5, 1)
items[i].MaxInnerRedemptionRate = sdk.NewDecWithPrec(15, 1)
items[i].TotalDelegations = sdkmath.ZeroInt()
keeper.SetHostZone(ctx, items[i])
}
Expand Down
46 changes: 40 additions & 6 deletions x/stakeibc/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,33 @@ func (k Keeper) GetICATimeoutNanos(ctx sdk.Context, epochType string) (uint64, e

// safety check: ensure the redemption rate is NOT below our min safety threshold && NOT above our max safety threshold on host zone
func (k Keeper) IsRedemptionRateWithinSafetyBounds(ctx sdk.Context, zone types.HostZone) (bool, error) {
// Get the wide bounds
minSafetyThreshold, maxSafetyThreshold := k.GetOuterSafetyBounds(ctx, zone)

redemptionRate := zone.RedemptionRate

if redemptionRate.LT(minSafetyThreshold) || redemptionRate.GT(maxSafetyThreshold) {
errMsg := fmt.Sprintf("IsRedemptionRateWithinSafetyBounds check failed %s is outside safety bounds [%s, %s]", redemptionRate.String(), minSafetyThreshold.String(), maxSafetyThreshold.String())
k.Logger(ctx).Error(errMsg)
return false, errorsmod.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg)
}

// Verify the redemption rate is within the inner safety bounds
// The inner safety bounds should always be within the safety bounds, but
// the redundancy above is cheap.
// There is also one scenario where the outer bounds go within the inner bounds - if they're updated as part of a param change proposal.
minInnerSafetyThreshold, maxInnerSafetyThreshold := k.GetInnerSafetyBounds(ctx, zone)
if redemptionRate.LT(minInnerSafetyThreshold) || redemptionRate.GT(maxInnerSafetyThreshold) {
errMsg := fmt.Sprintf("IsRedemptionRateWithinSafetyBounds check failed %s is outside inner safety bounds [%s, %s]", redemptionRate.String(), minInnerSafetyThreshold.String(), maxInnerSafetyThreshold.String())
k.Logger(ctx).Error(errMsg)
return false, errorsmod.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg)
}

return true, nil
}

func (k Keeper) GetOuterSafetyBounds(ctx sdk.Context, zone types.HostZone) (sdk.Dec, sdk.Dec) {
sampocs marked this conversation as resolved.
Show resolved Hide resolved
// Fetch the wide bounds
minSafetyThresholdInt := k.GetParam(ctx, types.KeyDefaultMinRedemptionRateThreshold)
minSafetyThreshold := sdk.NewDec(int64(minSafetyThresholdInt)).Quo(sdk.NewDec(100))

Expand All @@ -268,12 +295,19 @@ func (k Keeper) IsRedemptionRateWithinSafetyBounds(ctx sdk.Context, zone types.H
maxSafetyThreshold = zone.MaxRedemptionRate
}

redemptionRate := zone.RedemptionRate
return minSafetyThreshold, maxSafetyThreshold
}

if redemptionRate.LT(minSafetyThreshold) || redemptionRate.GT(maxSafetyThreshold) {
errMsg := fmt.Sprintf("IsRedemptionRateWithinSafetyBounds check failed %s is outside safety bounds [%s, %s]", redemptionRate.String(), minSafetyThreshold.String(), maxSafetyThreshold.String())
k.Logger(ctx).Error(errMsg)
return false, errorsmod.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg)
func (k Keeper) GetInnerSafetyBounds(ctx sdk.Context, zone types.HostZone) (sdk.Dec, sdk.Dec) {
// Fetch the inner bounds
minSafetyThreshold, maxSafetyThreshold := k.GetOuterSafetyBounds(ctx, zone)

if !zone.MinInnerRedemptionRate.IsNil() && zone.MinInnerRedemptionRate.IsPositive() && zone.MinInnerRedemptionRate.GT(minSafetyThreshold) {
minSafetyThreshold = zone.MinInnerRedemptionRate
}
return true, nil
if !zone.MaxInnerRedemptionRate.IsNil() && zone.MaxInnerRedemptionRate.IsPositive() && zone.MaxInnerRedemptionRate.LT(maxSafetyThreshold) {
maxSafetyThreshold = zone.MaxInnerRedemptionRate
}

return minSafetyThreshold, maxSafetyThreshold
}
17 changes: 10 additions & 7 deletions x/stakeibc/keeper/msg_server_register_host_zone.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,16 @@ func (k msgServer) RegisterHostZone(goCtx context.Context, msg *types.MsgRegiste
HostDenom: msg.HostDenom,
TransferChannelId: msg.TransferChannelId,
// Start sharesToTokens rate at 1 upon registration
RedemptionRate: sdk.NewDec(1),
LastRedemptionRate: sdk.NewDec(1),
UnbondingPeriod: msg.UnbondingPeriod,
DepositAddress: depositAddress.String(),
MinRedemptionRate: msg.MinRedemptionRate,
MaxRedemptionRate: msg.MaxRedemptionRate,
LsmLiquidStakeEnabled: msg.LsmLiquidStakeEnabled,
RedemptionRate: sdk.NewDec(1),
LastRedemptionRate: sdk.NewDec(1),
UnbondingPeriod: msg.UnbondingPeriod,
DepositAddress: depositAddress.String(),
MinRedemptionRate: msg.MinRedemptionRate,
MaxRedemptionRate: msg.MaxRedemptionRate,
// Default the inner bounds to the outer bounds
MinInnerRedemptionRate: msg.MinRedemptionRate,
MaxInnerRedemptionRate: msg.MaxRedemptionRate,
LsmLiquidStakeEnabled: msg.LsmLiquidStakeEnabled,
}
// write the zone back to the store
k.SetHostZone(ctx, zone)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package keeper

import (
"context"
"fmt"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/Stride-Labs/stride/v14/x/stakeibc/types"
)

func (k msgServer) UpdateInnerRedemptionRateBounds(goCtx context.Context, msg *types.MsgUpdateInnerRedemptionRateBounds) (*types.MsgUpdateInnerRedemptionRateBoundsResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// Confirm host zone exists
zone, found := k.GetHostZone(ctx, msg.ChainId)
if !found {
k.Logger(ctx).Error(fmt.Sprintf("Host Zone not found: %s", msg.ChainId))
return nil, types.ErrInvalidHostZone
}

// Get the wide bounds
outerMinSafetyThreshold, outerMaxSafetyThreshold := k.GetOuterSafetyBounds(ctx, zone)

innerMinSafetyThreshold := msg.MinInnerRedemptionRate
innerMaxSafetyThreshold := msg.MaxInnerRedemptionRate

// Confirm the inner bounds are within the outer bounds
if innerMinSafetyThreshold.LT(outerMinSafetyThreshold) {
errMsg := fmt.Sprintf("inner min safety threshold (%s) is less than outer min safety threshold (%s)", innerMinSafetyThreshold, outerMinSafetyThreshold)
k.Logger(ctx).Error(errMsg)
return nil, errorsmod.Wrapf(types.ErrInvalidBounds, errMsg)
}

if innerMaxSafetyThreshold.GT(outerMaxSafetyThreshold) {
errMsg := fmt.Sprintf("inner max safety threshold (%s) is greater than outer max safety threshold (%s)", innerMaxSafetyThreshold, outerMaxSafetyThreshold)
k.Logger(ctx).Error(errMsg)
return nil, errorsmod.Wrapf(types.ErrInvalidBounds, errMsg)
}

// Set the inner bounds on the host zone
zone.MinInnerRedemptionRate = innerMinSafetyThreshold
zone.MaxInnerRedemptionRate = innerMaxSafetyThreshold
k.SetHostZone(ctx, zone)

return &types.MsgUpdateInnerRedemptionRateBoundsResponse{}, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package keeper_test

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
_ "github.com/stretchr/testify/suite"

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

type UpdateInnerRedemptionRateBoundsTestCase struct {
validMsg stakeibctypes.MsgUpdateInnerRedemptionRateBounds
zone stakeibctypes.HostZone
}

func (s *KeeperTestSuite) SetupUpdateInnerRedemptionRateBounds() UpdateInnerRedemptionRateBoundsTestCase {
// Register a host zone
hostZone := stakeibctypes.HostZone{
ChainId: HostChainId,
HostDenom: Atom,
IbcDenom: IbcAtom,
RedemptionRate: sdk.NewDec(1.0),
MinRedemptionRate: sdk.NewDec(9).Quo(sdk.NewDec(10)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's a bit easier to read if you create these with MustNewDecFromStr

MaxRedemptionRate: sdk.NewDec(15).Quo(sdk.NewDec(10)),
}

s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone)

defaultMsg := stakeibctypes.MsgUpdateInnerRedemptionRateBounds{
// TODO: does this need to be the admin address?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so cause I don't think validate basic is run when you call the msg server function directly

Creator: s.TestAccs[0].String(),
ChainId: HostChainId,
MinInnerRedemptionRate: sdk.NewDec(1),
MaxInnerRedemptionRate: sdk.NewDec(11).Quo(sdk.NewDec(10)),
}

return UpdateInnerRedemptionRateBoundsTestCase{
validMsg: defaultMsg,
zone: hostZone,
}
}

// Verify that bounds can be set successfully
func (s *KeeperTestSuite) TestUpdateInnerRedemptionRateBounds_Success() {
tc := s.SetupUpdateInnerRedemptionRateBounds()

// Set the inner bounds on the host zone
_, err := s.GetMsgServer().UpdateInnerRedemptionRateBounds(s.Ctx, &tc.validMsg)
s.Require().NoError(err, "should not throw an error")

// Confirm the inner bounds were set
zone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, HostChainId)
s.Require().True(found, "host zone should be in the store")
s.Require().Equal(tc.validMsg.MinInnerRedemptionRate, zone.MinInnerRedemptionRate, "min inner redemption rate should be set")
s.Require().Equal(tc.validMsg.MaxInnerRedemptionRate, zone.MaxInnerRedemptionRate, "max inner redemption rate should be set")
}

// Setting inner bounds outside of outer bounds should throw an error
func (s *KeeperTestSuite) TestUpdateInnerRedemptionRateBounds_OutOfBounds() {
tc := s.SetupUpdateInnerRedemptionRateBounds()

// Set the min inner bound to be less than the min outer bound
tc.validMsg.MinInnerRedemptionRate = sdk.NewDec(0)

// Set the inner bounds on the host zone
_, err := s.GetMsgServer().UpdateInnerRedemptionRateBounds(s.Ctx, &tc.validMsg)
// verify it throws an error
errMsg := fmt.Sprintf("inner min safety threshold (%s) is less than outer min safety threshold (%s)", tc.validMsg.MinInnerRedemptionRate, sdk.NewDec(9).Quo(sdk.NewDec(10)))
s.Require().ErrorContains(err, errMsg)

// Set the min inner bound to be valid, but the max inner bound to be greater than the max outer bound
tc.validMsg.MinInnerRedemptionRate = sdk.NewDec(1)
tc.validMsg.MaxInnerRedemptionRate = sdk.NewDec(3)
// Set the inner bounds on the host zone
_, err = s.GetMsgServer().UpdateInnerRedemptionRateBounds(s.Ctx, &tc.validMsg)
// verify it throws an error
errMsg = fmt.Sprintf("inner max safety threshold (%s) is greater than outer max safety threshold (%s)", tc.validMsg.MaxInnerRedemptionRate, sdk.NewDec(15).Quo(sdk.NewDec(10)))
s.Require().ErrorContains(err, errMsg)
}

// Validate basic tests
func (s *KeeperTestSuite) TestUpdateInnerRedemptionRateBounds_InvalidMsg() {
tc := s.SetupUpdateInnerRedemptionRateBounds()

// Set the min inner bound to be greater than than the max inner bound
invalidMsg := tc.validMsg
invalidMsg.MinInnerRedemptionRate = sdk.NewDec(2)

err := invalidMsg.ValidateBasic()

// Verify the error
errMsg := fmt.Sprintf("Inner max safety threshold (%s) is less than inner min safety threshold (%s)", invalidMsg.MaxInnerRedemptionRate, invalidMsg.MinInnerRedemptionRate)
s.Require().ErrorContains(err, errMsg)
}

// Verify that if inner bounds end up outside of outer bounds (somehow), the outer bounds are returned
func (s *KeeperTestSuite) TestGetInnerSafetyBounds() {
tc := s.SetupUpdateInnerRedemptionRateBounds()

// Set the inner bounds outside the outer bounds on the host zone directly
tc.zone.MinInnerRedemptionRate = sdk.NewDec(0)
tc.zone.MaxInnerRedemptionRate = sdk.NewDec(3)
// Set the host zone
s.App.StakeibcKeeper.SetHostZone(s.Ctx, tc.zone)

// Get the inner bounds and verify the outer bounds are used
innerMinSafetyThreshold, innerMaxSafetyThreshold := s.App.StakeibcKeeper.GetInnerSafetyBounds(s.Ctx, tc.zone)
s.Require().Equal(tc.zone.MinRedemptionRate, innerMinSafetyThreshold, "min inner redemption rate should be set")
s.Require().Equal(tc.zone.MaxRedemptionRate, innerMaxSafetyThreshold, "max inner redemption rate should be set")
}
2 changes: 2 additions & 0 deletions x/stakeibc/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func RegisterCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&ToggleLSMProposal{}, "stakeibc/ToggleLSMProposal", nil)
cdc.RegisterConcrete(&MsgRestoreInterchainAccount{}, "stakeibc/RestoreInterchainAccount", nil)
cdc.RegisterConcrete(&MsgUpdateValidatorSharesExchRate{}, "stakeibc/UpdateValidatorSharesExchRate", nil)
cdc.RegisterConcrete(&MsgUpdateInnerRedemptionRateBounds{}, "stakeibc/UpdateInnerRedemptionRateBounds", nil)
// this line is used by starport scaffolding # 2
}

Expand All @@ -39,6 +40,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
&MsgDeleteValidator{},
&MsgRestoreInterchainAccount{},
&MsgUpdateValidatorSharesExchRate{},
&MsgUpdateInnerRedemptionRateBounds{},
)

registry.RegisterImplementations((*govtypes.Content)(nil),
Expand Down
1 change: 1 addition & 0 deletions x/stakeibc/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ var (
ErrInvalidValidatorDelegationUpdates = errorsmod.Register(ModuleName, 1548, "Invalid validator delegation updates")
ErrLSMLiquidStakeDisabledForHostZone = errorsmod.Register(ModuleName, 1549, "LSM liquid stake is disabled for host zone")
ErrUnableToRemoveValidator = errorsmod.Register(ModuleName, 1550, "Unable to remove validator")
ErrInvalidBounds = errorsmod.Register(ModuleName, 1551, "Invalid safety bounds - inner bounds must be within outer bounds")
)
Loading
Loading