diff --git a/x/liquidstake/keeper/liquidstake.go b/x/liquidstake/keeper/liquidstake.go index 1a2e66734..b40fbf3ec 100644 --- a/x/liquidstake/keeper/liquidstake.go +++ b/x/liquidstake/keeper/liquidstake.go @@ -212,6 +212,46 @@ type NativeTokenInfo struct { Denom string `json:"denom"` } +// DelegateWithCap is a wrapper to invoke stakingKeeper.Delegate but account for +// the amount of liquid staked shares and check against liquid staking cap. +func (k Keeper) DelegateWithCap( + ctx sdk.Context, + delegatorAddress sdk.AccAddress, + validator stakingtypes.Validator, + bondAmt math.Int, +) (math.LegacyDec, error) { + if err := k.stakingKeeper.SafelyIncreaseTotalLiquidStakedTokens(ctx, bondAmt, false); err != nil { + return math.LegacyZeroDec(), types.ErrDelegationFailed.Wrap(err.Error()) + } + + newShares, err := k.stakingKeeper.Delegate(ctx, delegatorAddress, bondAmt, stakingtypes.Unbonded, validator, true) + if err != nil { + return math.LegacyZeroDec(), err + } + + return newShares, nil +} + +// UnbondWithCap is a wrapper to invoke stakingKeeper.Unbond but updates +// the total liquid staked tokens. +func (k Keeper) UnbondWithCap( + ctx sdk.Context, + delegatorAddress sdk.AccAddress, + validatorAddress sdk.ValAddress, + shares sdk.Dec, +) (math.Int, error) { + returnAmount, err := k.stakingKeeper.Unbond(ctx, delegatorAddress, validatorAddress, shares) + if err != nil { + return math.ZeroInt(), types.ErrUnbondFailed.Wrap(err.Error()) + } + + if err := k.stakingKeeper.DecreaseTotalLiquidStakedTokens(ctx, returnAmount); err != nil { + return math.ZeroInt(), types.ErrUnbondFailed.Wrap(err.Error()) + } + + return returnAmount, nil +} + // LSMDelegate captures a staked amount from existing delegation using LSM, re-stakes from proxyAcc and // mints stkXPRT worth of stk coin value according to NetAmount and performs LiquidDelegate. func (k Keeper) LSMDelegate( @@ -335,6 +375,12 @@ func (k Keeper) LSMDelegate( return sdk.ZeroDec(), sdk.ZeroInt(), types.ErrTooSmallLiquidStakeAmount } + // after LSM redemption, accounted liquid shares are ignored, + // but we have to still account it because now delegation is owned by a LS protocol + if err := k.stakingKeeper.SafelyIncreaseTotalLiquidStakedTokens(ctx, stkXPRTMintAmount, false); err != nil { + return sdk.ZeroDec(), sdk.ZeroInt(), types.ErrDelegationFailed.Wrap(err.Error()) + } + // mint stkXPRT on module acc mintCoin := sdk.NewCoins(sdk.NewCoin(liquidBondDenom, stkXPRTMintAmount)) err = k.bankKeeper.MintCoins(ctx, types.ModuleName, mintCoin) @@ -372,7 +418,7 @@ func (k Keeper) LiquidDelegate(ctx sdk.Context, proxyAcc sdk.AccAddress, activeV continue } validator, _ := k.stakingKeeper.GetValidator(ctx, val.GetOperator()) - newShares, err = k.stakingKeeper.Delegate(ctx, proxyAcc, weightedAmt[i], stakingtypes.Unbonded, validator, true) + newShares, err = k.DelegateWithCap(ctx, proxyAcc, validator, weightedAmt[i]) if err != nil { return sdk.ZeroDec(), err } @@ -517,7 +563,7 @@ func (k Keeper) LiquidUnbond( } // unbond from proxy account - returnAmount, err := k.stakingKeeper.Unbond(ctx, proxyAcc, valAddr, shares) + returnAmount, err := k.UnbondWithCap(ctx, proxyAcc, valAddr, shares) if err != nil { return time.Time{}, sdk.ZeroInt(), stakingtypes.UnbondingDelegation{}, err } diff --git a/x/liquidstake/keeper/rebalancing.go b/x/liquidstake/keeper/rebalancing.go index b5e308aba..2cd1fc310 100644 --- a/x/liquidstake/keeper/rebalancing.go +++ b/x/liquidstake/keeper/rebalancing.go @@ -265,8 +265,10 @@ func (k Keeper) AutocompoundStakingRewards(ctx sdk.Context, whitelistedValsMap t if err != nil { logger := k.Logger(ctx) logger.Error("re-staking failed", "error", err) - return + + // skip errors as they might occur due to reaching global liquid cap } + writeCache() // move autocompounding fee from the balance to fee account diff --git a/x/liquidstake/types/errors.go b/x/liquidstake/types/errors.go index 16939c035..5dd79a6bd 100644 --- a/x/liquidstake/types/errors.go +++ b/x/liquidstake/types/errors.go @@ -24,4 +24,6 @@ var ( ErrWhitelistedValidatorsList = errors.Register(ModuleName, 19, "whitelisted validators list incorrect") ErrActiveLiquidValidatorsWeightQuorumNotReached = errors.Register(ModuleName, 20, "active liquid validators weight quorum not reached") ErrModulePaused = errors.Register(ModuleName, 21, "module functions have been paused") + ErrDelegationFailed = errors.Register(ModuleName, 22, "delegation failed") + ErrUnbondFailed = errors.Register(ModuleName, 23, "unbond failed") ) diff --git a/x/liquidstake/types/expected_keepers.go b/x/liquidstake/types/expected_keepers.go index 5b2ddcc13..4d70a784c 100644 --- a/x/liquidstake/types/expected_keepers.go +++ b/x/liquidstake/types/expected_keepers.go @@ -75,6 +75,8 @@ type StakingKeeper interface { BlockValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate HasMaxUnbondingDelegationEntries(ctx sdk.Context, delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress) bool + SafelyIncreaseTotalLiquidStakedTokens(ctx sdk.Context, amount math.Int, sharesAlreadyBonded bool) error + DecreaseTotalLiquidStakedTokens(ctx sdk.Context, amount math.Int) error } // DistrKeeper expected distribution keeper (noalias)