Skip to content

Commit

Permalink
fix: stkXPRT audit fix rebased (#766)
Browse files Browse the repository at this point in the history
* fix: noisy unit test

* fix: [oak-14] misleading attribute name emitted during unstake

* fix: [oak-12] incorrect amount of stkXPRT minted due to rounding

* fix: [oak-11] incorrect new_shares attribute emitted

* fix: [oak-9] missing CwLockedPoolAddress address validation for Params

* fix: [oak-8] updateParams allows governance to change any parameter without restrictions

* chore: format proto

* feat: whitelisted validators list upgradeable by admin

* fix: [oak-7] arbitrary validator weights and units are configurable

make it required to match 10000 cents.

* fix: [oak-6] only one active validator is needed to allow liquid staking

check for 33.33% quorum when considering active liquid validators map

* fix: [oak-5] forcing users to stake liquid amount during MsgStakeToLP

Adjust checks so msg.LiquidAmount can be left empty

* fix: [oak-4] delegation failures cause fees to be repeatedly charged

moved fee charging below delegation code, so it's calculated but not executed
unless autocompounding is successful.

* fix: [oak-3] undelegated funds will be distributed as auto-compounding fees

auto-compounding fees only on withdrawn reward value, fix unit tests.

* test: [oak-1] first depositor can initiate a share inflation

having initial deposit on the protocol before launching it

* [HEX-PRST-1] Division By Zero In Share Conversion Leads To Panic And Denial Of Service

Prevent this by checking values and erroring out. Normally it must not be reachable,
if division by zero occurs, DoS of the module is better than loss of funds.

Added c_value event to better monitor the outcome of liquid staking and spot this kind of issues earlier.

* [HEX-PRST-4] Unbonding Of Validators Does Not Give Priority To Inactive Validators

Added liquid validators prioritisation based on inactive state, need to study gas implications
and practicality of DivideByCurrentWeight on an inactive subset separately.

* [HEX-PRST-3] Whitelisted Validators Cannot Be Inactivated

The list of whitelisted validators is updated by an offchain process and always has active validators allowed to accept liquid stake, not supposed to be inactivated by 0 target weight.
All target weights must be positive and add up to 10000.

* feat: safety flag to pause the module

Admin account can pause and unpause staking/unstaking/stake-to-lp functions of the module,
as well as the logic in BeginBlocker. Allows to do emergency updates or fixes.

* [HEX-PRST-4] fix slice sort for inactive validators
  • Loading branch information
Max Kupriianov authored Feb 21, 2024
1 parent 0818a31 commit 0a0a033
Show file tree
Hide file tree
Showing 25 changed files with 1,895 additions and 375 deletions.
32 changes: 17 additions & 15 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,24 +187,26 @@ var (

// module account permissions
maccPerms = map[string][]string{
authtypes.FeeCollectorName: nil,
distrtypes.ModuleName: nil,
icatypes.ModuleName: nil,
minttypes.ModuleName: {authtypes.Minter},
stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking},
stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking},
govtypes.ModuleName: {authtypes.Burner},
ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner},
ibcfeetypes.ModuleName: nil,
liquidstakeibctypes.ModuleName: {authtypes.Minter, authtypes.Burner},
liquidstaketypes.ModuleName: {authtypes.Minter, authtypes.Burner},
liquidstakeibctypes.DepositModuleAccount: nil,
liquidstakeibctypes.UndelegationModuleAccount: {authtypes.Burner},
authtypes.FeeCollectorName: nil,
distrtypes.ModuleName: nil,
icatypes.ModuleName: nil,
minttypes.ModuleName: {authtypes.Minter},
stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking},
stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking},
govtypes.ModuleName: {authtypes.Burner},
ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner},
ibcfeetypes.ModuleName: nil,
liquidstakeibctypes.ModuleName: {authtypes.Minter, authtypes.Burner},
liquidstaketypes.ModuleName: {authtypes.Minter, authtypes.Burner},
liquidstaketypes.ModuleName + "-LiquidStakeProxyAcc": {authtypes.Staking},
liquidstakeibctypes.DepositModuleAccount: nil,
liquidstakeibctypes.UndelegationModuleAccount: {authtypes.Burner},
}

receiveAllowedMAcc = map[string]bool{
liquidstakeibctypes.DepositModuleAccount: true,
liquidstakeibctypes.UndelegationModuleAccount: true,
liquidstakeibctypes.DepositModuleAccount: true,
liquidstakeibctypes.UndelegationModuleAccount: true,
liquidstaketypes.ModuleName + "-LiquidStakeProxyAcc": true,
}
)

Expand Down
15 changes: 13 additions & 2 deletions proto/pstake/liquidstake/v1beta1/liquidstake.proto
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ message Params {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// WhitelistAdminAddress the bech32-encoded address of an admin authority
// that is allowed to update whitelisted validators or pause liquidstaking
// module entirely. The key is controlled by an offchain process that is
// selecting validators based on a criteria. Pausing of the module can be
// required during important migrations or failures.
string whitelist_admin_address = 9
[ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// ModulePaused is a safety toggle that allows to stop main module functions
// such as stake/unstake/stake-to-lp and the BeginBlocker logic.
bool module_paused = 10;
}

// ValidatorStatus enumerates the status of a liquid validator.
Expand All @@ -76,8 +88,7 @@ enum ValidatorStatus {

// WhitelistedValidator consists of the validator operator address and the
// target weight, which is a value for calculating the real weight to be derived
// according to the active status. In the case of inactive, it is calculated as
// zero.
// according to the active status.
message WhitelistedValidator {
option (gogoproto.goproto_getters) = false;

Expand Down
48 changes: 47 additions & 1 deletion proto/pstake/liquidstake/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ service Msg {

// UpdateParams defines a method to update the module params.
rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse);

// UpdateWhitelistedValidators defines a method to update the whitelisted
// validators list.
rpc UpdateWhitelistedValidators(MsgUpdateWhitelistedValidators)
returns (MsgUpdateWhitelistedValidatorsResponse);

// SetModulePaused defines a method to update the module's pause status,
// setting value of the safety flag in params.
rpc SetModulePaused(MsgSetModulePaused) returns (MsgSetModulePausedResponse);
}

// MsgLiquidStake defines a SDK message for performing a liquid stake of coins
Expand Down Expand Up @@ -94,9 +103,46 @@ message MsgUpdateParams {

// params defines the parameters to update.
//
// NOTE: All parameters must be supplied.
// NOTE: denom and whitelisted validators are not updated.
//
Params params = 2 [ (gogoproto.nullable) = false ];
}

// MsgUpdateParamsResponse defines the response structure for executing a
message MsgUpdateParamsResponse {}

message MsgUpdateWhitelistedValidators {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "authority";

// Authority is the address that is allowed to update whitelisted validators,
// defined as admin address in params (WhitelistAdminAddress).
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// WhitelistedValidators specifies the validators elected to become Active
// Liquid Validators.
repeated WhitelistedValidator whitelisted_validators = 2
[ (gogoproto.nullable) = false ];
}

// MsgUpdateWhitelistedValidatorsResponse defines the response structure for
// executing a
message MsgUpdateWhitelistedValidatorsResponse {}

message MsgSetModulePaused {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
option (cosmos.msg.v1.signer) = "authority";

// Authority is the address that is allowed to update module's paused state,
// defined as admin address in params (WhitelistAdminAddress).
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// IsPaused represents the target state of the paused flag.
bool is_paused = 2;
}

// MsgSetModulePausedResponse defines the response structure for
// executing a
message MsgSetModulePausedResponse {}
1 change: 1 addition & 0 deletions proto/pstake/ratesync/v1beta1/ratesync.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ message HostChain {
string transfer_channel_i_d = 6;
string transfer_port_i_d = 7;
}

message Feature {
// triggers on hooks
LiquidStake liquid_stake_i_b_c = 1
Expand Down
6 changes: 4 additions & 2 deletions x/liquidstake/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker)

// return value of UpdateLiquidValidatorSet is useful only in testing
_ = k.UpdateLiquidValidatorSet(ctx)
if !k.GetParams(ctx).ModulePaused {
// return value of UpdateLiquidValidatorSet is useful only in testing
_ = k.UpdateLiquidValidatorSet(ctx)
}
}
112 changes: 104 additions & 8 deletions x/liquidstake/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func GetTxCmd() *cobra.Command {
NewStakeToLPCmd(),
NewLiquidUnstakeCmd(),
NewUpdateParamsCmd(),
NewUpdateWhitelistedValidatorsCmd(),
NewSetModulePausedCmd(),
)

return liquidstakeTxCmd
Expand All @@ -47,8 +49,8 @@ func NewLiquidStakeCmd() *cobra.Command {
Args: cobra.ExactArgs(1),
Short: "Liquid-stake XPRT",
Long: strings.TrimSpace(
fmt.Sprintf(`Liquid-stake XPRT.
fmt.Sprintf(`Liquid-stake XPRT.
Example:
$ %s tx %s liquid-stake 1000uxprt --from mykey
`,
Expand Down Expand Up @@ -143,8 +145,8 @@ func NewLiquidUnstakeCmd() *cobra.Command {
Args: cobra.ExactArgs(1),
Short: "Liquid-unstake stkXPRT",
Long: strings.TrimSpace(
fmt.Sprintf(`Liquid-unstake stkXPRT.
fmt.Sprintf(`Liquid-unstake stkXPRT.
Example:
$ %s tx %s liquid-unstake 500stk/uxprt --from mykey
`,
Expand Down Expand Up @@ -175,19 +177,19 @@ $ %s tx %s liquid-unstake 500stk/uxprt --from mykey
return cmd
}

// NewUpdateParamsCmd implements the liquid unstake coin command handler.
// NewUpdateParamsCmd implements the params update command handler.
func NewUpdateParamsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "update-params [params.json]",
Args: cobra.ExactArgs(1),
Short: "Update-params of liquidstake module.",
Long: strings.TrimSpace(
fmt.Sprintf(`update-params param-file.
fmt.Sprintf(`update-params param-file.
Example:
$ %s tx %s update-params ~/params.json --from mykey
Example params.json
Example params.json
{
"liquid_bond_denom": "stk/uxprt",
"whitelisted_validators": [
Expand Down Expand Up @@ -235,3 +237,97 @@ Example params.json

return cmd
}

// NewUpdateWhitelistedValidatorsCmd implements the update of whitelisted validators command handler.
func NewUpdateWhitelistedValidatorsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "update-whitelisted-validators [validators_list.json]",
Args: cobra.ExactArgs(1),
Short: "Update whitelisted validators in params of liquidstake module.",
Long: strings.TrimSpace(
fmt.Sprintf(`update-whitelisted-validators validators_list.json
Example:
$ %s tx %s update-whitelisted-validators ~/validators_list.json --from mykey
Example validators_list.json
[
{
"validator_address": "persistencevaloper1hcqg5wj9t42zawqkqucs7la85ffyv08lmnhye9",
"target_weight": "10"
}
]
`,
version.AppName, types.ModuleName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

var validatorsList []types.WhitelistedValidator

validatorsListFile, err := os.ReadFile(args[0])
if err != nil {
return err
}

err = json.Unmarshal(validatorsListFile, &validatorsList)
if err != nil {
return err
}
authority := clientCtx.GetFromAddress()

msg := types.NewMsgUpdateWhitelistedValidators(authority, validatorsList)

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}

// NewSetModulePausedCmd implements the command handler for updating of safety toggle that disables the module.
func NewSetModulePausedCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "pause-module [flag]",
Args: cobra.ExactArgs(1),
Short: "Pause or unpause the liquidstake module for an emergency updates.",
Long: strings.TrimSpace(
fmt.Sprintf(`pause-module [true/false]
Example:
$ %s tx %s pause-module true --from mykey
`,
version.AppName, types.ModuleName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

isPaused := false
if strings.ToLower(args[0]) == "true" {
isPaused = true
} else if strings.ToLower(args[0]) != "false" {
err := fmt.Errorf("expected flag to be true or false – where 'true' means the module is paused")
return err
}

authority := clientCtx.GetFromAddress()
msg := types.NewMsgSetModulePaused(authority, isPaused)

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
36 changes: 0 additions & 36 deletions x/liquidstake/handler.go

This file was deleted.

4 changes: 2 additions & 2 deletions x/liquidstake/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func (s *KeeperTestSuite) TestImportExportGenesis() {
params := k.GetParams(ctx)

params.WhitelistedValidators = []types.WhitelistedValidator{
{ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)},
{ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)},
{ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(5000)},
{ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(5000)},
}
k.SetParams(ctx, params)
k.UpdateLiquidValidatorSet(ctx)
Expand Down
6 changes: 3 additions & 3 deletions x/liquidstake/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ func (s *KeeperTestSuite) TestGRPCQueries() {

// add active validator
params.WhitelistedValidators = []types.WhitelistedValidator{
{ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(1)},
{ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)},
{ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)},
{ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(3334)},
{ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(3333)},
{ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(3333)},
}
s.keeper.SetParams(s.ctx, params)
s.keeper.UpdateLiquidValidatorSet(s.ctx)
Expand Down
4 changes: 1 addition & 3 deletions x/liquidstake/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,14 @@ func (s *KeeperTestSuite) liquidUnstaking(
sdk.DefaultBondDenom,
).Amount

ubdTime, unbondingAmt, dels, unbondedAmt, err := s.liquidUnstakingWithResult(
ubdTime, unbondingAmt, _, unbondedAmt, err := s.liquidUnstakingWithResult(
liquidStaker,
sdk.NewCoin(params.LiquidBondDenom, ubdStkXPRTAmt),
)
if err != nil {
return err
}

testhelpers.PP(fmt.Sprintf("GOT UBDS FROM liquidUnstakingWithResult: %d", len(dels)))

if ubdComplete {
alv := s.keeper.GetActiveLiquidValidators(ctx, params.WhitelistedValsMap())
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 200).
Expand Down
Loading

0 comments on commit 0a0a033

Please sign in to comment.