Skip to content

Commit

Permalink
Add HaltZone function (#60)
Browse files Browse the repository at this point in the history
Co-authored-by: sampocs <sam.pochyly@gmail.com>
  • Loading branch information
asalzmann and sampocs committed Jan 25, 2024
1 parent 0d608d6 commit 43156c1
Show file tree
Hide file tree
Showing 15 changed files with 153 additions and 26 deletions.
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ func NewStrideApp(
app.AccountKeeper,
app.BankKeeper,
app.TransferKeeper,
app.RatelimitKeeper,
)
stakeTiaModule := staketia.NewAppModule(appCodec, app.StaketiaKeeper)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func (k msgServer) UpdateInnerRedemptionRateBounds(goCtx context.Context, msg *types.MsgUpdateInnerRedemptionRateBounds) (*types.MsgUpdateInnerRedemptionRateBoundsResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// Confirm host zone exists
// Note: we're intentionally not checking the zone is halted
zone, found := k.GetHostZone(ctx, msg.ChainId)
if !found {
k.Logger(ctx).Error(fmt.Sprintf("Host Zone not found: %s", msg.ChainId))
Expand Down
15 changes: 13 additions & 2 deletions x/staketia/keeper/abci.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
package keeper

import sdk "github.com/cosmos/cosmos-sdk/types"
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

func (k Keeper) BeginBlocker(ctx sdk.Context) {}
func (k Keeper) BeginBlocker(ctx sdk.Context) {
// Check invariants

// Check redemption rate is within safety bounds
if err := k.CheckRedemptionRateExceedsBounds(ctx); err != nil {
k.Logger(ctx).Error(err.Error())
// If not, halt the zone
k.HaltZone(ctx)
}
}
8 changes: 8 additions & 0 deletions x/staketia/keeper/delegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ func (s *KeeperTestSuite) TestPrepareDelegation() {
// It should not create a new record since there is nothing to delegate
delegationRecords = s.App.StaketiaKeeper.GetAllActiveDelegationRecords(s.Ctx)
s.Require().Equal(0, len(delegationRecords), "there should be no delegation records")

// Halt zone
s.App.StaketiaKeeper.HaltZone(s.Ctx)
err = s.App.StaketiaKeeper.PrepareDelegation(s.Ctx, epochNumber, epochDuration)
s.Require().ErrorContains(err, "host zone is halted")
}

// ----------------------------------------------------
Expand Down Expand Up @@ -418,6 +423,9 @@ func (s *KeeperTestSuite) VerifyDelegationRecords(verifyIdentical bool, archiveI
func (s *KeeperTestSuite) TestConfirmDelegation_Successful() {
s.SetupDelegationRecords()

// we're halting the zone to test that the tx works even when the host zone is halted
s.App.StaketiaKeeper.HaltZone(s.Ctx)

// try setting valid delegation queue
err := s.App.StaketiaKeeper.ConfirmDelegation(s.Ctx, 6, ValidTxHashNew, ValidOperator)
s.Require().NoError(err)
Expand Down
11 changes: 11 additions & 0 deletions x/staketia/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,14 @@ func EmitSuccessfulConfirmUnbondedTokenSweepEvent(ctx sdk.Context, recordId uint
),
)
}

// Emits an event indicating a zone was halted
func EmitHaltZoneEvent(ctx sdk.Context, hostZone types.HostZone) {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHostZoneHalt,
sdk.NewAttribute(types.AttributeKeyHostZone, hostZone.ChainId),
sdk.NewAttribute(types.AttributeKeyRedemptionRate, hostZone.RedemptionRate.String()),
),
)
}
30 changes: 30 additions & 0 deletions x/staketia/keeper/invariants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package keeper

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/Stride-Labs/stride/v17/utils"
)

func (k Keeper) HaltZone(ctx sdk.Context) {
// Set the halted flag on the zone
hostZone, err := k.GetHostZone(ctx)
if err != nil {
// No panic - we don't want to halt the chain! Just the zone.
// log the error
k.Logger(ctx).Error(fmt.Sprintf("Unable to get host zone: %s", err.Error()))
return
}
hostZone.Halted = true
k.SetHostZone(ctx, hostZone)

// set rate limit on stAsset
stDenom := utils.StAssetDenomFromHostZoneDenom(hostZone.NativeTokenDenom)
k.ratelimitKeeper.AddDenomToBlacklist(ctx, stDenom)

k.Logger(ctx).Error(fmt.Sprintf("[INVARIANT BROKEN!!!] %s's RR is %s.", hostZone.GetChainId(), hostZone.RedemptionRate.String()))

EmitHaltZoneEvent(ctx, hostZone)
}
24 changes: 24 additions & 0 deletions x/staketia/keeper/invariants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package keeper_test

import (
"github.com/Stride-Labs/stride/v17/x/staketia/types"
)

func (s *KeeperTestSuite) TestHaltZone() {
// Set a non-halted host zone
s.App.StaketiaKeeper.SetHostZone(s.Ctx, types.HostZone{
NativeTokenDenom: HostNativeDenom,
Halted: false,
})

// Halt the zone
s.App.StaketiaKeeper.HaltZone(s.Ctx)

// Confirm it's halted
hostZone := s.MustGetHostZone()
s.Require().True(hostZone.Halted, "host zone should be halted")

// Confirm denom is blacklisted
isBlacklisted := s.App.RatelimitKeeper.IsDenomBlacklisted(s.Ctx, StDenom)
s.Require().True(isBlacklisted, "halt zone should blacklist the stAsset denom")
}
23 changes: 13 additions & 10 deletions x/staketia/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import (
)

type Keeper struct {
cdc codec.BinaryCodec
storeKey storetypes.StoreKey
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
transferKeeper types.TransferKeeper
cdc codec.BinaryCodec
storeKey storetypes.StoreKey
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
transferKeeper types.TransferKeeper
ratelimitKeeper types.RatelimitKeeper
}

func NewKeeper(
Expand All @@ -25,13 +26,15 @@ func NewKeeper(
accountKeeper types.AccountKeeper,
bankKeeper types.BankKeeper,
transferKeeper types.TransferKeeper,
ratelimitKeeper types.RatelimitKeeper,
) *Keeper {
return &Keeper{
cdc: cdc,
storeKey: storeKey,
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
transferKeeper: transferKeeper,
cdc: cdc,
storeKey: storeKey,
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
transferKeeper: transferKeeper,
ratelimitKeeper: ratelimitKeeper,
}
}

Expand Down
9 changes: 5 additions & 4 deletions x/staketia/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func (k msgServer) AdjustDelegatedBalance(goCtx context.Context, msg *types.MsgA
}

// add offset to the delegated balance and write to host zone
// Note: we're intentionally not checking the zone is halted
hostZone, err := k.GetHostZone(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -180,7 +181,7 @@ func (k msgServer) ResumeHostZone(goCtx context.Context, msg *types.MsgResumeHos
return nil, types.ErrInvalidAdmin
}

// Get Host Zone
// Note: of course we don't want to fail this if the zone is halted!
zone, err := k.GetHostZone(ctx)
if err != nil {
return nil, err
Expand All @@ -191,9 +192,8 @@ func (k msgServer) ResumeHostZone(goCtx context.Context, msg *types.MsgResumeHos
return nil, errorsmod.Wrapf(types.ErrHostZoneNotHalted, "zone is not halted")
}

// TODO [sttia]: remove from blacklist
// stDenom := types.StAssetDenomFromHostZoneDenom(hostZone.HostDenom)
// k.RatelimitKeeper.RemoveDenomFromBlacklist(ctx, stDenom)
stDenom := utils.StAssetDenomFromHostZoneDenom(zone.NativeTokenDenom)
k.ratelimitKeeper.RemoveDenomFromBlacklist(ctx, stDenom)

// Resume zone
zone.Halted = false
Expand Down Expand Up @@ -269,6 +269,7 @@ func (k msgServer) SetOperatorAddress(goCtx context.Context, msg *types.MsgSetOp
}

// Fetch the zone
// Note: we're intentionally not checking the zone is halted
zone, err := k.GetHostZone(ctx)
if err != nil {
return nil, err
Expand Down
28 changes: 25 additions & 3 deletions x/staketia/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ func (s *KeeperTestSuite) TestConfirmUnbondingTokenSweep() {
// ----------------------------------------------

func (s *KeeperTestSuite) TestAdjustDelegatedBalance() {

safeAddress := "safe"

// Create the host zone
Expand All @@ -187,6 +188,9 @@ func (s *KeeperTestSuite) TestAdjustDelegatedBalance() {
DelegatedBalance: sdk.NewInt(0),
})

// we're halting the zone to test that the tx works even when the host zone is halted
s.App.StaketiaKeeper.HaltZone(s.Ctx)

// Call adjust for each test case and confirm the ending delegation
testCases := []struct {
address string
Expand Down Expand Up @@ -229,6 +233,7 @@ func (s *KeeperTestSuite) TestAdjustDelegatedBalance() {
s.App.StaketiaKeeper.RemoveHostZone(s.Ctx)
_, err = s.GetMsgServer().AdjustDelegatedBalance(s.Ctx, &types.MsgAdjustDelegatedBalance{})
s.Require().ErrorContains(err, "host zone not found")

}

// ----------------------------------------------
Expand All @@ -249,6 +254,8 @@ func (s *KeeperTestSuite) TestUpdateInnerRedemptionRateBounds() {
}

s.App.StaketiaKeeper.SetHostZone(s.Ctx, zone)
// we're halting the zone to test that the tx works even when the host zone is halted
s.App.StaketiaKeeper.HaltZone(s.Ctx)

initialMsg := types.MsgUpdateInnerRedemptionRateBounds{
Creator: adminAddress,
Expand Down Expand Up @@ -315,9 +322,13 @@ func (s *KeeperTestSuite) TestResumeHostZone() {
s.Require().True(ok)

zone := types.HostZone{
Halted: false,
ChainId: HostChainId,
RedemptionRate: sdk.NewDec(1),
Halted: false,
NativeTokenDenom: HostNativeDenom,
}
s.App.StaketiaKeeper.SetHostZone(s.Ctx, zone)

msg := types.MsgResumeHostZone{
Creator: adminAddress,
}
Expand All @@ -327,15 +338,22 @@ func (s *KeeperTestSuite) TestResumeHostZone() {
_, err := s.GetMsgServer().ResumeHostZone(s.Ctx, &msg)
s.Require().ErrorContains(err, "zone is not halted")

// Verify the denom is not in the blacklist
blacklist := s.App.RatelimitKeeper.GetAllBlacklistedDenoms(s.Ctx)
s.Require().NotContains(blacklist, StDenom, "denom should not be blacklisted")

// Confirm the zone is not halted
zone, err = s.App.StaketiaKeeper.GetHostZone(s.Ctx)
s.Require().NoError(err, "should not throw an error")
s.Require().False(zone.Halted, "zone should not be halted")

// TEST 2: Zone is halted
// Halt the zone
zone.Halted = true
s.App.StaketiaKeeper.SetHostZone(s.Ctx, zone)
s.App.StaketiaKeeper.HaltZone(s.Ctx)

// Verify the denom is in the blacklist
blacklist = s.App.RatelimitKeeper.GetAllBlacklistedDenoms(s.Ctx)
s.Require().Contains(blacklist, StDenom, "denom should be blacklisted")

// Try to unhalt the halted zone
_, err = s.GetMsgServer().ResumeHostZone(s.Ctx, &msg)
Expand All @@ -346,6 +364,10 @@ func (s *KeeperTestSuite) TestResumeHostZone() {
s.Require().NoError(err, "should not throw an error")
s.Require().False(zone.Halted, "zone should not be halted")

// Verify the denom is not in the blacklist
blacklist = s.App.RatelimitKeeper.GetAllBlacklistedDenoms(s.Ctx)
s.Require().NotContains(blacklist, StDenom, "denom should not be blacklisted")

// Attempt to resume with a non-admin address, it should fail
_, err = s.GetMsgServer().ResumeHostZone(s.Ctx, &types.MsgResumeHostZone{
Creator: "non-admin",
Expand Down
1 change: 1 addition & 0 deletions x/staketia/keeper/redemption_rate.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func (k Keeper) UpdateRedemptionRate(ctx sdk.Context) error {
}

// Checks whether the redemption rate has exceeded the inner or outer safety bounds
// and returns an error if so
func (k Keeper) CheckRedemptionRateExceedsBounds(ctx sdk.Context) error {
hostZone, err := k.GetHostZone(ctx)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions x/staketia/keeper/unbonding.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func (k Keeper) ConfirmUndelegation(ctx sdk.Context, recordId uint64, txHash str
return errorsmod.Wrapf(types.ErrInvalidUnbondingRecord, "unbonding record with id: %d has no tokens to unbond (or negative)", recordId)
}

// Note: we're intentionally not checking that the host zone is halted, because we still want to process this tx in that case
hostZone, err := k.GetHostZone(ctx)
if err != nil {
return err
Expand Down
8 changes: 7 additions & 1 deletion x/staketia/keeper/unbonding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,11 +493,13 @@ func (s *KeeperTestSuite) SetupTestConfirmUndelegation(amountToUndelegate sdkmat
return tc
}

// unit test ConfirmUndelegation
func (s *KeeperTestSuite) TestConfirmUndelegation_Success() {
amountToUndelegate := sdkmath.NewInt(100)
tc := s.SetupTestConfirmUndelegation(amountToUndelegate)

// we're halting the zone to test that the tx works even when the host zone is halted
s.App.StaketiaKeeper.HaltZone(s.Ctx)

// confirm the tx was successful
err := s.App.StaketiaKeeper.ConfirmUndelegation(s.Ctx, tc.unbondingRecord.Id, ValidTxHashDefault, tc.operatorAddress)
s.Require().NoError(err)
Expand Down Expand Up @@ -868,6 +870,9 @@ func (s *KeeperTestSuite) VerifyUnbondingRecordsAfterConfirmSweep(verifyUpdatedF
func (s *KeeperTestSuite) TestConfirmUnbondingTokenSweep_Successful() {
s.SetupTestConfirmUnbondingTokens(DefaultClaimFundingAmount)

// we're halting the zone to test that the tx works even when the host zone is halted
s.App.StaketiaKeeper.HaltZone(s.Ctx)

// process record 6
err := s.App.StaketiaKeeper.ConfirmUnbondedTokenSweep(s.Ctx, 6, ValidTxHashNew, ValidOperator)
s.Require().NoError(err)
Expand All @@ -889,6 +894,7 @@ func (s *KeeperTestSuite) TestConfirmUnbondingTokenSweep_Successful() {
s.Require().True(found)
s.Require().Equal(types.CLAIMABLE, loadedUnbondingRecord.Status, "unbonding record should be updated to status CLAIMABLE")
s.Require().Equal(ValidTxHashNew, loadedUnbondingRecord.UnbondedTokenSweepTxHash, "unbonding record should be updated with token sweep txHash")

}

func (s *KeeperTestSuite) TestConfirmUnbondingTokenSweep_NotFunded() {
Expand Down
2 changes: 2 additions & 0 deletions x/staketia/types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
EventTypeConfirmUndelegation = "confirm_undelegation"

AttributeKeyHostZone = "host_zone"
AttributeKeyRedemptionRate = "redemption_rate"
AttributeKeyLiquidStaker = "liquid_staker"
AttributeKeyRedeemer = "redeemer"
AttributeKeyNativeBaseDenom = "native_base_denom"
Expand All @@ -20,4 +21,5 @@ const (
AttributeUndelegationNativeAmount = "undelegation_native_amount"
AttributeTxHash = "tx_hash"
AttributeSender = "sender"
EventTypeHostZoneHalt = "host_zone_halt"
)
16 changes: 11 additions & 5 deletions x/staketia/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import (
transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
)

// Required AccountKeeper functions
type AccountKeeper interface {
NewAccount(sdk.Context, authtypes.AccountI) authtypes.AccountI
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI
SetAccount(ctx sdk.Context, acc authtypes.AccountI)
}

// Required BankKeeper functions
type BankKeeper interface {
MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
Expand All @@ -24,9 +31,8 @@ type TransferKeeper interface {
Transfer(goCtx context.Context, msg *transfertypes.MsgTransfer) (*transfertypes.MsgTransferResponse, error)
}

// Required AccountKeeper functions
type AccountKeeper interface {
NewAccount(sdk.Context, authtypes.AccountI) authtypes.AccountI
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI
SetAccount(ctx sdk.Context, acc authtypes.AccountI)
// Required RatelimitKeeper functions
type RatelimitKeeper interface {
AddDenomToBlacklist(ctx sdk.Context, denom string)
RemoveDenomFromBlacklist(ctx sdk.Context, denom string)
}

0 comments on commit 43156c1

Please sign in to comment.