Skip to content

Commit

Permalink
IF-STRIDE-STAKEIBC-REDEEM_STAKE (#446)
Browse files Browse the repository at this point in the history
Users may not be able to redeem stake

# Involved artifacts

- [x/stakeibc/keeper/msg_server_redeem_stake.go](https://github.com/Stride-Labs/stride/blob/v2.0.3/x/stakeibc/keeper/msg_server_redeem_stake.go#L51-L53)

# Description

Users can always redeem from Stride. When they select "redeeem" on the Stride website, Stride will initiate unbonding on the host zone. Once the unbonding period elapses, the users will receive native tokens in their wallets.

However, users cannot unstake an amount greater than staked balance on the host zone.

```go
if msg.Amount > hostZone.StakedBal {
		return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, "cannot unstake an amount g.t. staked balance on host zone: %d", msg.Amount)
	}
```

The problem with the code above is in comparing two different units; `msg.Amount` represents the input **stTokens** amount, while on the other hand `hostZone.StakedBal` describes host zone's total amount of **native** tokens.


# Problem Scenarios

An `ErrInvalidAmount` error could be returned even if the user's redeeming amount doesn't exceed total host zone's staked amount. That's because stTokens are not 1-1 alligned with native tokens. There is `redemptionRate` defined as `nativeToken amount / stToken amount` which can fluctuate between 0.9 and 1.5 for current stride implementation, hard-coded in [stakeibc/types/params.go](https://github.com/Stride-Labs/stride/blob/v2.0.3/x/stakeibc/types/params.go#L25-L26).


# Recommendation

Before comparing with `hostZone.StakedBal`, `msg.Amount` should be exchanged to the native tokens:

```go
nativeAmount := sdk.NewDec(msg.Amount).Mul(hostZone.RedemptionRate).RoundInt()
```
After that, validation check for amount would be correct: 

```go
if nativeAmount > hostZone.StakedBal {
		return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, "cannot unstake an amount g.t. staked balance on host zone: %d", msg.Amount)
	}
```
  • Loading branch information
asalzmann authored Dec 3, 2022
1 parent a8a1658 commit f8990a7
Showing 1 changed file with 14 additions and 10 deletions.
24 changes: 14 additions & 10 deletions x/stakeibc/keeper/msg_server_redeem_stake.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,22 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake)
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid receiver address (%s)", err)
}

if msg.Amount > hostZone.StakedBal {
return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, "cannot unstake an amount g.t. staked balance on host zone: %d", msg.Amount)
}

// construct desired unstaking amount from host zone
amt, err := cast.ToInt64E(msg.Amount)
if err != nil {
k.Logger(ctx).Error(fmt.Sprintf("error casting RedeemStake msg.Amount to int64, err: %s", err.Error()))
return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, fmt.Sprintf("invalid amount: %s", err.Error()))
}
stDenom := types.StAssetDenomFromHostZoneDenom(hostZone.HostDenom)
nativeAmount := sdk.NewDec(amt).Mul(hostZone.RedemptionRate).RoundInt()
stakedBal, err := cast.ToInt64E(hostZone.StakedBal)
if err != nil {
k.Logger(ctx).Error(fmt.Sprintf("error casting hostZone.StakedBal to int64, err: %s", err.Error()))
return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, fmt.Sprintf("invalid amount: %s", err.Error()))
}
if nativeAmount.GT(sdk.NewInt(stakedBal)) {
return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, "cannot unstake an amount g.t. staked balance on host zone: %d", msg.Amount)
}

// safety check: redemption rate must be within safety bounds
rateIsSafe, err := k.IsRedemptionRateWithinSafetyBounds(ctx, hostZone)
Expand All @@ -65,11 +72,8 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake)
return nil, sdkerrors.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg)
}

// construct desired unstaking amount from host zone
coinDenom := types.StAssetDenomFromHostZoneDenom(hostZone.HostDenom)
nativeAmount := sdk.NewDec(amt).Mul(hostZone.RedemptionRate).RoundInt()
// TODO(TEST-112) bigint safety
coinString := nativeAmount.String() + coinDenom
coinString := nativeAmount.String() + stDenom
inCoin, err := sdk.ParseCoinNormalized(coinString)
if err != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "could not parse inCoin: %s. err: %s", coinString, err.Error())
Expand All @@ -80,7 +84,7 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake)
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "amount must be greater than 0. found: %d", msg.Amount)
}
// - Creator owns at least "amount" stAssets
balance := k.bankKeeper.GetBalance(ctx, sender, coinDenom)
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))
if balance.Amount.LT(sdk.NewInt(amt)) {
Expand Down Expand Up @@ -114,7 +118,7 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake)
hostZoneUnbonding.UserRedemptionRecords = append(hostZoneUnbonding.UserRedemptionRecords, userRedemptionRecord.Id)

// Escrow user's balance
redeemCoin := sdk.NewCoins(sdk.NewCoin(coinDenom, sdk.NewInt(amt)))
redeemCoin := sdk.NewCoins(sdk.NewCoin(stDenom, sdk.NewInt(amt)))
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)
Expand Down

0 comments on commit f8990a7

Please sign in to comment.