From 3d4ba729cfd2529730fdccdb8ee0fdfe151413bd Mon Sep 17 00:00:00 2001 From: sampocs Date: Fri, 10 Mar 2023 18:23:56 -0600 Subject: [PATCH] Code Cleanup: MsgLiquidStake (#651) Closes: #XXX ## Context and purpose of the change Cleaned up MsgLiquidStake ## Brief Changelog * Cleaned up variable naming, commenting, logging, and errors * Moved validation checks as upstream as possible so that they occur before any state changes (relevant to protocol directed liquid stakes) * Added filter to GetDepositRecordsByEpochAndChain to only grab transfer records * Added event emission after LS * Added check to error if the LS would result in 0 stTokens ## Author's Checklist I have... - [ ] Run and PASSED locally all GAIA integration tests - [ ] If the change is contentful, I either: - [ ] Added a new unit test OR - [ ] Added test cases to existing unit tests - [ ] OR this change is a trivial rework / code cleanup without any test coverage If skipped any of the tests above, explain. ## Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] manually tested (if applicable) - [ ] confirmed the author wrote unit tests for new logic - [ ] reviewed documentation exists and is accurate ## Documentation and Release Note - [ ] Does this pull request introduce a new feature or user-facing behavior changes? - [ ] Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`? - [ ] This pull request updates existing proto field values (and require a backend and frontend migration)? - [ ] Does this pull request change existing proto field names (and require a frontend migration)? How is the feature or change documented? - [ ] not applicable - [ ] jira ticket `XXX` - [ ] specification (`x//spec/`) - [ ] README.md - [ ] not documented --- dockernet/tests/integration_tests.bats | 1 - readme-docs/md/records_README.md | 2 +- x/records/README.md | 2 +- x/records/keeper/deposit_record.go | 6 +- x/stakeibc/keeper/msg_server_liquid_stake.go | 157 ++++++++---------- .../keeper/msg_server_liquid_stake_test.go | 118 ++++++++----- x/stakeibc/types/errors.go | 1 + x/stakeibc/types/{events_ibc.go => events.go} | 13 +- 8 files changed, 166 insertions(+), 134 deletions(-) rename x/stakeibc/types/{events_ibc.go => events.go} (72%) diff --git a/dockernet/tests/integration_tests.bats b/dockernet/tests/integration_tests.bats index 9ae991b66..92ce97ec5 100644 --- a/dockernet/tests/integration_tests.bats +++ b/dockernet/tests/integration_tests.bats @@ -137,7 +137,6 @@ setup_file() { $STRIDE_MAIN_CMD tx stakeibc liquid-stake $STAKE_AMOUNT $HOST_DENOM --from $STRIDE_VAL -y # sleep two block for the tx to settle on stride - WAIT_FOR_STRING $STRIDE_LOGS "\[MINT ST ASSET\] success on $HOST_CHAIN_ID" WAIT_FOR_BLOCK $STRIDE_LOGS 2 # make sure IBC_DENOM went down diff --git a/readme-docs/md/records_README.md b/readme-docs/md/records_README.md index 76ebb12aa..d5b9bffea 100644 --- a/readme-docs/md/records_README.md +++ b/readme-docs/md/records_README.md @@ -24,7 +24,7 @@ Deposit Records - `GetDepositRecord()` - `RemoveDepositRecord()` - `GetAllDepositRecord()` -- `GetDepositRecordByEpochAndChain()` +- `GetTransferDepositRecordByEpochAndChain()` Epoch Unbonding Records - `SetEpochUnbondingRecord()` diff --git a/x/records/README.md b/x/records/README.md index ef76f7f10..f3ec678ec 100644 --- a/x/records/README.md +++ b/x/records/README.md @@ -26,7 +26,7 @@ Deposit Records - `GetDepositRecord()` - `RemoveDepositRecord()` - `GetAllDepositRecord()` -- `GetDepositRecordByEpochAndChain()` +- `GetTransferDepositRecordByEpochAndChain()` Epoch Unbonding Records diff --git a/x/records/keeper/deposit_record.go b/x/records/keeper/deposit_record.go index 579d4055c..191600b5c 100644 --- a/x/records/keeper/deposit_record.go +++ b/x/records/keeper/deposit_record.go @@ -101,10 +101,12 @@ func GetDepositRecordIDBytes(id uint64) []byte { return bz } -func (k Keeper) GetDepositRecordByEpochAndChain(ctx sdk.Context, epochNumber uint64, chainId string) (val *types.DepositRecord, found bool) { +func (k Keeper) GetTransferDepositRecordByEpochAndChain(ctx sdk.Context, epochNumber uint64, chainId string) (val *types.DepositRecord, found bool) { records := k.GetAllDepositRecord(ctx) for _, depositRecord := range records { - if depositRecord.DepositEpochNumber == epochNumber && depositRecord.HostZoneId == chainId { + if depositRecord.DepositEpochNumber == epochNumber && + depositRecord.HostZoneId == chainId && + depositRecord.Status == types.DepositRecord_TRANSFER_QUEUE { return &depositRecord, true } } diff --git a/x/stakeibc/keeper/msg_server_liquid_stake.go b/x/stakeibc/keeper/msg_server_liquid_stake.go index f47fd4965..8798fbb61 100644 --- a/x/stakeibc/keeper/msg_server_liquid_stake.go +++ b/x/stakeibc/keeper/msg_server_liquid_stake.go @@ -2,9 +2,7 @@ package keeper import ( "context" - "fmt" - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" errorsmod "cosmossdk.io/errors" @@ -14,123 +12,108 @@ import ( "github.com/Stride-Labs/stride/v6/x/stakeibc/types" ) +// Exchanges a user's native tokens for stTokens using the current redemption rate +// The native tokens must live on Stride with an IBC denomination before this function is called +// The typical flow consists, first, of a transfer of native tokens from the host zone to Stride, +// and then the invocation of this LiquidStake function +// +// WARNING: This function is invoked from the begin/end blocker in a way that does not revert partial state when +// an error is thrown (i.e. the execution is non-atomic). +// As a result, it is important that the validation steps are positioned at the top of the function, +// and logic that creates state changes (e.g. bank sends, mint) appear towards the end of the function func (k msgServer) LiquidStake(goCtx context.Context, msg *types.MsgLiquidStake) (*types.MsgLiquidStakeResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // Init variables - // deposit `amount` of `denom` token to the stakeibc module - // NOTE: Should we add an additional check here? This is a pretty important line of code - // NOTE: If sender doesn't have enough inCoin, this errors (error is hard to interpret) - // check that hostZone is registered - // strided tx stakeibc liquid-stake 100 uatom + // Get the host zone from the base denom in the message (e.g. uatom) hostZone, err := k.GetHostZoneFromHostDenom(ctx, msg.HostDenom) if err != nil { - k.Logger(ctx).Error(fmt.Sprintf("Host Zone not found for denom (%s)", msg.HostDenom)) - return nil, errorsmod.Wrapf(types.ErrInvalidHostZone, "no host zone found for denom (%s)", msg.HostDenom) + return nil, errorsmod.Wrapf(types.ErrInvalidToken, "no host zone found for denom (%s)", msg.HostDenom) } + // Error immediately if the host zone is halted if hostZone.Halted { - k.Logger(ctx).Error(fmt.Sprintf("Host Zone halted for denom (%s)", msg.HostDenom)) return nil, errorsmod.Wrapf(types.ErrHaltedHostZone, "halted host zone found for denom (%s)", msg.HostDenom) } - // get the sender address - sender, _ := sdk.AccAddressFromBech32(msg.Creator) - // get the coins to send, they need to be in the format {amount}{denom} - // is safe. The converse is not true. - ibcDenom := hostZone.GetIbcDenom() - coinString := msg.Amount.String() + ibcDenom - inCoin, err := sdk.ParseCoinNormalized(coinString) + // Get user and module account addresses + liquidStakerAddress, err := sdk.AccAddressFromBech32(msg.Creator) if err != nil { - k.Logger(ctx).Error(fmt.Sprintf("failed to parse coin (%s)", coinString)) - return nil, errorsmod.Wrapf(err, "failed to parse coin (%s)", coinString) + return nil, errorsmod.Wrapf(err, "user's address is invalid") } - - // Creator owns at least "amount" of inCoin - balance := k.bankKeeper.GetBalance(ctx, sender, ibcDenom) - if balance.IsLT(inCoin) { - k.Logger(ctx).Error(fmt.Sprintf("balance is lower than staking amount. staking amount: %v, balance: %v", msg.Amount, balance.Amount)) - return nil, errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, "balance is lower than staking amount. staking amount: %v, balance: %v", msg.Amount, balance.Amount) - } - // check that the token is an IBC token - isIbcToken := types.IsIBCToken(ibcDenom) - if !isIbcToken { - k.Logger(ctx).Error("invalid token denom - denom is not an IBC token (%s)", ibcDenom) - return nil, errorsmod.Wrapf(types.ErrInvalidToken, "denom is not an IBC token (%s)", ibcDenom) + hostZoneAddress, err := sdk.AccAddressFromBech32(hostZone.Address) + if err != nil { + return nil, errorsmod.Wrapf(err, "host zone address is invalid") } - // safety check: redemption rate must be above safety threshold + // Safety check: redemption rate must be within safety bounds rateIsSafe, err := k.IsRedemptionRateWithinSafetyBounds(ctx, *hostZone) if !rateIsSafe || (err != nil) { - errMsg := fmt.Sprintf("IsRedemptionRateWithinSafetyBounds check failed. hostZone: %s, err: %s", hostZone.String(), err.Error()) - return nil, errorsmod.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg) + return nil, errorsmod.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, "HostZone: %s, err: %s", hostZone.ChainId, err.Error()) } - bech32ZoneAddress, err := sdk.AccAddressFromBech32(hostZone.Address) - if err != nil { - return nil, fmt.Errorf("could not bech32 decode address %s of zone with id: %s", hostZone.Address, hostZone.ChainId) - } - err = k.bankKeeper.SendCoins(ctx, sender, bech32ZoneAddress, sdk.NewCoins(inCoin)) - if err != nil { - k.Logger(ctx).Error("failed to send tokens from Account to Module") - return nil, errorsmod.Wrap(err, "failed to send tokens from Account to Module") - } - // mint user `amount` of the corresponding stAsset - // NOTE: We should ensure that denoms are unique - we don't want anyone spoofing denoms - err = k.MintStAssetAndTransfer(ctx, sender, msg.Amount, msg.HostDenom) - if err != nil { - k.Logger(ctx).Error("failed to send tokens from Account to Module") - return nil, errorsmod.Wrapf(err, "failed to mint %s stAssets to user", msg.HostDenom) - } - - // create a deposit record of these tokens (pending transfer) + // Grab the deposit record that will be used for record keeping strideEpochTracker, found := k.GetEpochTracker(ctx, epochtypes.STRIDE_EPOCH) if !found { - k.Logger(ctx).Error("failed to find stride epoch") return nil, errorsmod.Wrapf(sdkerrors.ErrNotFound, "no epoch number for epoch (%s)", epochtypes.STRIDE_EPOCH) } - // Does this use too much gas? - depositRecord, found := k.RecordsKeeper.GetDepositRecordByEpochAndChain(ctx, strideEpochTracker.EpochNumber, hostZone.ChainId) + depositRecord, found := k.RecordsKeeper.GetTransferDepositRecordByEpochAndChain(ctx, strideEpochTracker.EpochNumber, hostZone.ChainId) if !found { - k.Logger(ctx).Error("failed to find deposit record") - return nil, errorsmod.Wrapf(sdkerrors.ErrNotFound, fmt.Sprintf("no deposit record for epoch (%d)", strideEpochTracker.EpochNumber)) + return nil, errorsmod.Wrapf(sdkerrors.ErrNotFound, "no deposit record for epoch (%d)", strideEpochTracker.EpochNumber) } - depositRecord.Amount = depositRecord.Amount.Add(msg.Amount) - k.RecordsKeeper.SetDepositRecord(ctx, *depositRecord) - k.hooks.AfterLiquidStake(ctx, sender) - return &types.MsgLiquidStakeResponse{}, nil -} - -func (k msgServer) MintStAssetAndTransfer(ctx sdk.Context, sender sdk.AccAddress, amount sdkmath.Int, denom string) error { - stAssetDenom := types.StAssetDenomFromHostZoneDenom(denom) + // The tokens that are sent to the protocol are denominated in the ibc hash of the native token on stride (e.g. ibc/xxx) + nativeDenom := hostZone.IbcDenom + nativeCoin := sdk.NewCoin(nativeDenom, msg.Amount) + if !types.IsIBCToken(nativeDenom) { + return nil, errorsmod.Wrapf(types.ErrInvalidToken, "denom is not an IBC token (%s)", nativeDenom) + } - // TODO(TEST-7): Add an exchange rate here! What object should we store the exchange rate on? - // How can we ensure that the exchange rate is not manipulated? - hz, _ := k.GetHostZoneFromHostDenom(ctx, denom) - amountToMint := (sdk.NewDecFromInt(amount).Quo(hz.RedemptionRate)).TruncateInt() - coinString := amountToMint.String() + stAssetDenom - stCoins, err := sdk.ParseCoinsNormalized(coinString) - if err != nil { - k.Logger(ctx).Error("Failed to parse coins") - return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "Failed to parse coins %s", coinString) + // Confirm the user has a sufficient balance to execute the liquid stake + balance := k.bankKeeper.GetBalance(ctx, liquidStakerAddress, nativeDenom) + if balance.IsLT(nativeCoin) { + return nil, errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, "balance is lower than staking amount. staking amount: %v, balance: %v", msg.Amount, balance.Amount) } - // Mints coins to the module account, will error if the module account does not exist or is unauthorized. + // Determine the amount of stTokens to mint using the redemption rate + stAmount := (sdk.NewDecFromInt(msg.Amount).Quo(hostZone.RedemptionRate)).TruncateInt() + if stAmount.IsZero() { + return nil, errorsmod.Wrapf(types.ErrInsufficientLiquidStake, + "Liquid stake of %s%s would return 0 stTokens", msg.Amount.String(), hostZone.HostDenom) + } - err = k.bankKeeper.MintCoins(ctx, types.ModuleName, stCoins) - if err != nil { - k.Logger(ctx).Error("Failed to mint coins") - return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "Failed to mint coins") + // Transfer the native tokens from the user to module account + if err := k.bankKeeper.SendCoins(ctx, liquidStakerAddress, hostZoneAddress, sdk.NewCoins(nativeCoin)); err != nil { + return nil, errorsmod.Wrap(err, "failed to send tokens from Account to Module") } - // transfer those coins to the user - err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, stCoins) - if err != nil { - k.Logger(ctx).Error("Failed to send coins from module to account") - return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "Failed to send %s from module to account", stCoins.GetDenomByIndex(0)) + // Mint the stTokens and transfer them to the user + stDenom := types.StAssetDenomFromHostZoneDenom(msg.HostDenom) + stCoin := sdk.NewCoin(stDenom, stAmount) + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(stCoin)); err != nil { + return nil, errorsmod.Wrapf(err, "Failed to mint coins") + } + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, liquidStakerAddress, sdk.NewCoins(stCoin)); err != nil { + return nil, errorsmod.Wrapf(err, "Failed to send %s from module to account", stCoin.String()) } - k.Logger(ctx).Info(fmt.Sprintf("[MINT ST ASSET] success on %s.", hz.GetChainId())) - return nil + // Update the liquid staked amount on the deposit record + depositRecord.Amount = depositRecord.Amount.Add(msg.Amount) + k.RecordsKeeper.SetDepositRecord(ctx, *depositRecord) + + // Emit liquid stake event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeLiquidStakeRequest, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyLiquidStaker, msg.Creator), + sdk.NewAttribute(types.AttributeKeyHostZone, hostZone.ChainId), + sdk.NewAttribute(types.AttributeKeyNativeBaseDenom, msg.HostDenom), + sdk.NewAttribute(types.AttributeKeyNativeIBCDenom, hostZone.IbcDenom), + sdk.NewAttribute(types.AttributeKeyNativeAmount, msg.Amount.String()), + sdk.NewAttribute(types.AttributeKeyStTokenAmount, stAmount.String()), + ), + ) + + k.hooks.AfterLiquidStake(ctx, liquidStakerAddress) + return &types.MsgLiquidStakeResponse{}, nil } diff --git a/x/stakeibc/keeper/msg_server_liquid_stake_test.go b/x/stakeibc/keeper/msg_server_liquid_stake_test.go index 7680ca378..33181e9d3 100644 --- a/x/stakeibc/keeper/msg_server_liquid_stake_test.go +++ b/x/stakeibc/keeper/msg_server_liquid_stake_test.go @@ -68,6 +68,7 @@ func (s *KeeperTestSuite) SetupLiquidStake() LiquidStakeTestCase { DepositEpochNumber: 1, HostZoneId: "GAIA", Amount: initialDepositAmount, + Status: recordtypes.DepositRecord_TRANSFER_QUEUE, } s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone) @@ -156,19 +157,6 @@ func (s *KeeperTestSuite) TestLiquidStake_DifferentRedemptionRates() { } } -func (s *KeeperTestSuite) TestLiquidStake_RateBelowMinThreshold() { - tc := s.SetupLiquidStake() - msg := tc.validMsg - - // Update rate in host zone to below min threshold - hz := tc.initialState.hostZone - hz.RedemptionRate = sdk.NewDec(8).Quo(sdk.NewDec(10)) - s.App.StakeibcKeeper.SetHostZone(s.Ctx, hz) - - _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &msg) - s.Require().Error(err) -} - func (s *KeeperTestSuite) TestLiquidStake_HostZoneNotFound() { tc := s.SetupLiquidStake() // Update message with invalid denom @@ -176,45 +164,68 @@ func (s *KeeperTestSuite) TestLiquidStake_HostZoneNotFound() { invalidMsg.HostDenom = "ufakedenom" _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &invalidMsg) - s.Require().EqualError(err, "no host zone found for denom (ufakedenom): host zone not registered") + s.Require().EqualError(err, "no host zone found for denom (ufakedenom): invalid token denom") } -func (s *KeeperTestSuite) TestLiquidStake_IbcCoinParseError() { +func (s *KeeperTestSuite) TestLiquidStake_HostZoneHalted() { tc := s.SetupLiquidStake() - // Update hostzone with denom that can't be parsed + + // Update the host zone so that it's halted badHostZone := tc.initialState.hostZone - badHostZone.IbcDenom = "ibc,0atom" + badHostZone.Halted = true s.App.StakeibcKeeper.SetHostZone(s.Ctx, badHostZone) + _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &tc.validMsg) + s.Require().EqualError(err, "halted host zone found for denom (uatom): Halted host zone found") +} - badCoin := fmt.Sprintf("%v%s", tc.validMsg.Amount, badHostZone.IbcDenom) - s.Require().EqualError(err, fmt.Sprintf("failed to parse coin (%s): invalid decimal coin expression: %s", badCoin, badCoin)) +func (s *KeeperTestSuite) TestLiquidStake_InvalidUserAddress() { + tc := s.SetupLiquidStake() + + // Update hostzone with invalid address + invalidMsg := tc.validMsg + invalidMsg.Creator = "cosmosXXX" + + _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &invalidMsg) + s.Require().EqualError(err, "user's address is invalid: decoding bech32 failed: string not all lowercase or all uppercase") } -func (s *KeeperTestSuite) TestLiquidStake_NotIbcDenom() { +func (s *KeeperTestSuite) TestLiquidStake_InvalidHostAddress() { tc := s.SetupLiquidStake() - // Update hostzone with non-ibc denom - badDenom := "i/uatom" + + // Update hostzone with invalid address badHostZone := tc.initialState.hostZone - badHostZone.IbcDenom = badDenom + badHostZone.Address = "cosmosXXX" s.App.StakeibcKeeper.SetHostZone(s.Ctx, badHostZone) - // Fund the user with the non-ibc denom - s.FundAccount(tc.user.acc, sdk.NewInt64Coin(badDenom, 1000000000)) + _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &tc.validMsg) + s.Require().EqualError(err, "host zone address is invalid: decoding bech32 failed: string not all lowercase or all uppercase") +} - s.Require().EqualError(err, fmt.Sprintf("denom is not an IBC token (%s): invalid token denom", badHostZone.IbcDenom)) +func (s *KeeperTestSuite) TestLiquidStake_RateBelowMinThreshold() { + tc := s.SetupLiquidStake() + msg := tc.validMsg + + // Update rate in host zone to below min threshold + hz := tc.initialState.hostZone + hz.RedemptionRate = sdk.MustNewDecFromStr("0.8") + s.App.StakeibcKeeper.SetHostZone(s.Ctx, hz) + + _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &msg) + s.Require().Error(err) } -func (s *KeeperTestSuite) TestLiquidStake_InsufficientBalance() { +func (s *KeeperTestSuite) TestLiquidStake_RateAboveMaxThreshold() { tc := s.SetupLiquidStake() - // Set liquid stake amount to value greater than account balance - invalidMsg := tc.validMsg - balance := tc.user.atomBalance.Amount - invalidMsg.Amount = balance.Add(sdkmath.NewInt(1000)) - _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &invalidMsg) + msg := tc.validMsg - expectedErr := fmt.Sprintf("balance is lower than staking amount. staking amount: %v, balance: %v: insufficient funds", balance.Add(sdkmath.NewInt(1000)), balance) - s.Require().EqualError(err, expectedErr) + // Update rate in host zone to below min threshold + hz := tc.initialState.hostZone + hz.RedemptionRate = sdk.NewDec(2) + s.App.StakeibcKeeper.SetHostZone(s.Ctx, hz) + + _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &msg) + s.Require().Error(err) } func (s *KeeperTestSuite) TestLiquidStake_NoEpochTracker() { @@ -235,16 +246,45 @@ func (s *KeeperTestSuite) TestLiquidStake_NoDepositRecord() { s.Require().EqualError(err, fmt.Sprintf("no deposit record for epoch (%d): not found", 1)) } -func (s *KeeperTestSuite) TestLiquidStake_InvalidHostAddress() { +func (s *KeeperTestSuite) TestLiquidStake_NotIbcDenom() { tc := s.SetupLiquidStake() - - // Update hostzone with invalid address + // Update hostzone with non-ibc denom + badDenom := "i/uatom" badHostZone := tc.initialState.hostZone - badHostZone.Address = "cosmosXXX" + badHostZone.IbcDenom = badDenom s.App.StakeibcKeeper.SetHostZone(s.Ctx, badHostZone) + // Fund the user with the non-ibc denom + s.FundAccount(tc.user.acc, sdk.NewInt64Coin(badDenom, 1000000000)) + _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &tc.validMsg) + + s.Require().EqualError(err, fmt.Sprintf("denom is not an IBC token (%s): invalid token denom", badHostZone.IbcDenom)) +} +func (s *KeeperTestSuite) TestLiquidStake_ZeroStTokens() { + tc := s.SetupLiquidStake() + + // Adjust redemption rate and liquid stake amount so that the number of stTokens would be zero + // stTokens = 1(amount) / 1.1(RR) = rounds down to 0 + hostZone := tc.initialState.hostZone + hostZone.RedemptionRate = sdk.NewDecWithPrec(11, 1) + s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone) + tc.validMsg.Amount = sdkmath.NewInt(1) + + // The liquid stake should fail _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &tc.validMsg) - s.Require().EqualError(err, "could not bech32 decode address cosmosXXX of zone with id: GAIA") + s.Require().EqualError(err, "Liquid stake of 1uatom would return 0 stTokens: Liquid staked amount is too small") +} + +func (s *KeeperTestSuite) TestLiquidStake_InsufficientBalance() { + tc := s.SetupLiquidStake() + // Set liquid stake amount to value greater than account balance + invalidMsg := tc.validMsg + balance := tc.user.atomBalance.Amount + invalidMsg.Amount = balance.Add(sdkmath.NewInt(1000)) + _, err := s.GetMsgServer().LiquidStake(sdk.WrapSDKContext(s.Ctx), &invalidMsg) + + expectedErr := fmt.Sprintf("balance is lower than staking amount. staking amount: %v, balance: %v: insufficient funds", balance.Add(sdkmath.NewInt(1000)), balance) + s.Require().EqualError(err, expectedErr) } func (s *KeeperTestSuite) TestLiquidStake_HaltedZone() { diff --git a/x/stakeibc/types/errors.go b/x/stakeibc/types/errors.go index 881f2c498..e2cc057bb 100644 --- a/x/stakeibc/types/errors.go +++ b/x/stakeibc/types/errors.go @@ -47,4 +47,5 @@ var ( ErrMaxNumValidators = errorsmod.Register(ModuleName, 1539, "max number of validators reached") ErrUndelegationAmount = errorsmod.Register(ModuleName, 1540, "Undelegation amount is greater than stakedBal") ErrHaltedHostZone = errorsmod.Register(ModuleName, 1541, "Halted host zone found") + ErrInsufficientLiquidStake = errorsmod.Register(ModuleName, 1542, "Liquid staked amount is too small") ) diff --git a/x/stakeibc/types/events_ibc.go b/x/stakeibc/types/events.go similarity index 72% rename from x/stakeibc/types/events_ibc.go rename to x/stakeibc/types/events.go index 552c18cfc..264a6b305 100644 --- a/x/stakeibc/types/events_ibc.go +++ b/x/stakeibc/types/events.go @@ -1,6 +1,6 @@ package types -// IBC events +// Events const ( EventTypeTimeout = "timeout" // this line is used by starport scaffolding # ibc/packet/event @@ -16,14 +16,21 @@ const ( EventTypeLiquidStakeRequest = "liquid_stake" EventTypeHostZoneHalt = "halt_zone" + AttributeKeyHostZone = "host_zone" AttributeKeyConnectionId = "connection_id" AttributeKeyRecipientChain = "chain_id" AttributeKeyRecipientAddress = "recipient" AttributeKeyBurnAmount = "burn_amount" AttributeKeyRedeemAmount = "redeem_amount" AttributeKeySourceAddress = "source" - AttributeKeyHostZone = "host_zone" - AttributeKeyRedemptionRate = "redemption_rate" + + AttributeKeyRedemptionRate = "redemption_rate" + + AttributeKeyLiquidStaker = "liquid_staker" + AttributeKeyNativeBaseDenom = "native_base_denom" + AttributeKeyNativeIBCDenom = "native_ibc_denom" + AttributeKeyNativeAmount = "native_amount" + AttributeKeyStTokenAmount = "sttoken_amount" AttributeValueCategory = ModuleName )