Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

staketia migration - staketia accounting #1214

Merged
merged 10 commits into from
Jun 7, 2024
41 changes: 3 additions & 38 deletions proto/stride/staketia/staketia.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,45 +35,8 @@ message HostZone {
string safe_address_on_stride = 11
[ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// Previous redemption rate
string last_redemption_rate = 12 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// Current redemption rate
string redemption_rate = 13 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// Min outer redemption rate - adjusted by governance
string min_redemption_rate = 14 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// Max outer redemption rate - adjusted by governance
string max_redemption_rate = 15 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// Min inner redemption rate - adjusted by controller
string min_inner_redemption_rate = 16 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// Max inner redemption rate - adjusted by controller
string max_inner_redemption_rate = 17 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Total delegated balance on the host zone delegation account
string delegated_balance = 18 [
string remaining_delegated_balance = 18 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
Expand All @@ -82,6 +45,8 @@ message HostZone {
uint64 unbonding_period_seconds = 19;
// Indicates whether the host zone has been halted
bool halted = 20;

reserved 13;
}

// Status fields for a delegation record
Expand Down
11 changes: 1 addition & 10 deletions x/staketia/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,4 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

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)
}
}
func (k Keeper) BeginBlocker(ctx sdk.Context) {}
2 changes: 1 addition & 1 deletion x/staketia/keeper/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (k Keeper) ConfirmDelegation(ctx sdk.Context, recordId uint64, txHash strin
k.ArchiveDelegationRecord(ctx, delegationRecord)

// increment delegation on Host Zone
hostZone.DelegatedBalance = hostZone.DelegatedBalance.Add(delegationRecord.NativeAmount)
hostZone.RemainingDelegatedBalance = hostZone.RemainingDelegatedBalance.Add(delegationRecord.NativeAmount)
k.SetHostZone(ctx, hostZone)

EmitSuccessfulConfirmDelegationEvent(ctx, recordId, delegationRecord.NativeAmount, txHash, sender)
Expand Down
6 changes: 3 additions & 3 deletions x/staketia/keeper/delegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (s *KeeperTestSuite) SetupDelegationRecords() {

// Set HostZone
hostZone := s.initializeHostZone()
hostZone.DelegatedBalance = InitialDelegation
hostZone.RemainingDelegatedBalance = InitialDelegation
s.App.StaketiaKeeper.SetHostZone(s.Ctx, hostZone)
}

Expand Down Expand Up @@ -196,7 +196,7 @@ func (s *KeeperTestSuite) VerifyDelegationRecords(verifyIdentical bool, archiveI
// if nothing should have changed, verify that host zone balance is unmodified
if verifyIdentical {
// verify hostZone delegated balance is same as initial delegation
s.Require().Equal(InitialDelegation.Int64(), hostZone.DelegatedBalance.Int64(), "hostZone delegated balance should not have changed")
s.Require().Equal(InitialDelegation.Int64(), hostZone.RemainingDelegatedBalance.Int64(), "hostZone delegated balance should not have changed")
}
}
}
Expand All @@ -220,7 +220,7 @@ func (s *KeeperTestSuite) TestConfirmDelegation_Successful() {

// verify hostZone delegated balance is same as initial delegation + 6000
hostZone := s.MustGetHostZone()
s.Require().Equal(InitialDelegation.Int64()+6000, hostZone.DelegatedBalance.Int64(), "hostZone delegated balance should have increased by 6000")
s.Require().Equal(InitialDelegation.Int64()+6000, hostZone.RemainingDelegatedBalance.Int64(), "hostZone delegated balance should have increased by 6000")
}

func (s *KeeperTestSuite) TestConfirmDelegation_DelegationZero() {
Expand Down
1 change: 0 additions & 1 deletion x/staketia/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ func EmitHaltZoneEvent(ctx sdk.Context, hostZone types.HostZone) {
sdk.NewEvent(
types.EventTypeHostZoneHalt,
sdk.NewAttribute(types.AttributeKeyHostZone, hostZone.ChainId),
sdk.NewAttribute(types.AttributeKeyRedemptionRate, hostZone.RedemptionRate.String()),
riley-stride marked this conversation as resolved.
Show resolved Hide resolved
),
)
}
14 changes: 0 additions & 14 deletions x/staketia/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,6 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInf
// Every day, refresh the redemption rate and prepare delegations
// Every 4 days, prepare undelegations
if epochInfo.Identifier == epochstypes.DAY_EPOCH {
// Update the redemption rate
// If this fails, do not proceed to the delegation or undelegation step
// Note: This must be run first because it is used when refreshing the native token
// balance in prepare undelegation
if err := k.UpdateRedemptionRate(ctx); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that the ordering of refreshing the RR and sending delegations and undelegations matters. The comments here suggest the same.

If we move the update RR step to stakeibc, but keep the delegation and undelegation txs in staketia, we just need to make sure the update RR is run at the same epochly cadence.

Just to triple check - it is, right? I.e. the cadence at which we ran it here matches the cadence at which we run it in stakeibc?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GREAT question! The cadence will be different in stakeibc (it runs every stride epoch instead of every day epoch)

i think how we have it right now, the RR will be ~6 hours stale when its used for unbondings here. But imo, since that's pretty negligible, I'd prefer to keep it as is, rather than mess with the ordering. Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see.

So just to map out the worst case: some packets may be are stuck or reinvestment would be otherwise delayed for a long time, then we'd see a large RR jump that triggers within the "stale period". In that case, we'd use the stale RR for unbonding and the users who receive those unbondings would not receive their yield from the "stuck packet" period. Is that right?

Just to explore syncing up the epochs.... what if we had staketia epochs run every 6 hours? I imagine not much would change in the 6-hourly logic until the operator advances the state. But in that case, would this edge case or the "stale period" be solved?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that worst case is righ!, But I think it's pretty unlikely that we have some multi-day bottleneck that just happens to clear in this 6 hour window. And even in that case, the impact is low - they just get the RR at the time of redemption (similar to how it used to be done).

Running the epochs every 6 hours could work, but I'd be a bit worried we have some implicit invariant about the length and not sure it's worth the effort to investigate based on the above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even in that case, the impact is low - they just get the RR at the time of redemption (similar to how it used to be done).

Yeah good point that it'd revert to how it used to be done. We did get some complaints about that, but as long as we don't have a long packet backlog we're good. And ultimately this migration period is temporary so the time window within which we could see backlogs is bounded.

Fwiw I'm mostly convinced to leave things as is, especially because of "we [may] have some implicit invariant about the length" could lead to a serious accounting issue if overlooked.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point about this being temporary!

also I assume the complaints were more in the case where the unbondings were delayed right? I’d imagine w/o a delay, getting the same RR that’s shown in the FE at time of redemption wouldnt upset anyone!

k.Logger(ctx).Error(fmt.Sprintf("Unable update redemption rate: %s", err.Error()))
return
}

// Post the redemption rate to the oracle (if it doesn't exceed the bounds)
if err := k.PostRedemptionRateToOracles(ctx); err != nil {
k.Logger(ctx).Error(fmt.Sprintf("Unable to post redemption rate to oracle: %s", err.Error()))
}

// Prepare delegations by transferring the deposited tokens to the host zone
if err := k.SafelyPrepareDelegation(ctx, epochNumber, epochInfo.Duration); err != nil {
k.Logger(ctx).Error(fmt.Sprintf("Unable to prepare delegation for epoch %d: %s", epochNumber, err.Error()))
Expand Down
31 changes: 12 additions & 19 deletions x/staketia/keeper/host_zone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,17 @@ import (
// Helper function to create the singleton HostZone with attributes
func (s *KeeperTestSuite) initializeHostZone() types.HostZone {
hostZone := types.HostZone{
ChainId: "CELESTIA",
NativeTokenDenom: "utia",
NativeTokenIbcDenom: "ibc/utia",
TransferChannelId: "channel-05",
DelegationAddress: "tia0384a",
RewardAddress: "tia144f42e9",
DepositAddress: "stride8abb3e",
RedemptionAddress: "stride3400de1",
ClaimAddress: "stride00b1a83",
LastRedemptionRate: sdk.MustNewDecFromStr("1.0"),
RedemptionRate: sdk.MustNewDecFromStr("1.0"),
MinRedemptionRate: sdk.MustNewDecFromStr("0.95"),
MaxRedemptionRate: sdk.MustNewDecFromStr("1.10"),
MinInnerRedemptionRate: sdk.MustNewDecFromStr("0.97"),
MaxInnerRedemptionRate: sdk.MustNewDecFromStr("1.07"),
DelegatedBalance: sdk.NewInt(1_000_000),
Halted: false,
ChainId: "CELESTIA",
NativeTokenDenom: "utia",
NativeTokenIbcDenom: "ibc/utia",
TransferChannelId: "channel-05",
DelegationAddress: "tia0384a",
RewardAddress: "tia144f42e9",
DepositAddress: "stride8abb3e",
RedemptionAddress: "stride3400de1",
ClaimAddress: "stride00b1a83",
RemainingDelegatedBalance: sdk.NewInt(1_000_000),
Halted: false,
}
s.App.StaketiaKeeper.SetHostZone(s.Ctx, hostZone)
return hostZone
Expand All @@ -47,8 +41,7 @@ func (s *KeeperTestSuite) TestRemoveHostZone() {
func (s *KeeperTestSuite) TestSetHostZone() {
hostZone := s.initializeHostZone()

hostZone.RedemptionRate = hostZone.RedemptionRate.Add(sdk.MustNewDecFromStr("0.1"))
hostZone.DelegatedBalance = hostZone.DelegatedBalance.Add(sdk.NewInt(100_000))
hostZone.RemainingDelegatedBalance = hostZone.RemainingDelegatedBalance.Add(sdk.NewInt(100_000))
s.App.StaketiaKeeper.SetHostZone(s.Ctx, hostZone)

loadedHostZone := s.MustGetHostZone()
Expand Down
2 changes: 1 addition & 1 deletion x/staketia/keeper/invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (k Keeper) HaltZone(ctx sdk.Context) {
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()))
k.Logger(ctx).Error(fmt.Sprintf("[INVARIANT BROKEN!!!] %s's RR is %s.", hostZone.GetChainId()))
riley-stride marked this conversation as resolved.
Show resolved Hide resolved

EmitHaltZoneEvent(ctx, hostZone)
}
72 changes: 5 additions & 67 deletions x/staketia/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import (
"context"
"errors"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/Stride-Labs/stride/v22/utils"

"github.com/Stride-Labs/stride/v22/x/staketia/types"
)

Expand Down Expand Up @@ -107,10 +104,10 @@ func (k msgServer) AdjustDelegatedBalance(goCtx context.Context, msg *types.MsgA
if err != nil {
return nil, err
}
hostZone.DelegatedBalance = hostZone.DelegatedBalance.Add(msg.DelegationOffset)
hostZone.RemainingDelegatedBalance = hostZone.RemainingDelegatedBalance.Add(msg.DelegationOffset)

// safety check that this will not cause the delegated balance to be negative
if hostZone.DelegatedBalance.IsNegative() {
if hostZone.RemainingDelegatedBalance.IsNegative() {
return nil, types.ErrNegativeNotAllowed.Wrapf("offset would cause the delegated balance to be negative")
}
k.SetHostZone(ctx, hostZone)
Expand All @@ -130,71 +127,14 @@ func (k msgServer) AdjustDelegatedBalance(goCtx context.Context, msg *types.MsgA

// Adjusts the inner redemption rate bounds on the host zone
func (k msgServer) UpdateInnerRedemptionRateBounds(goCtx context.Context, msg *types.MsgUpdateInnerRedemptionRateBounds) (*types.MsgUpdateInnerRedemptionRateBoundsResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// gate this transaction to the BOUNDS address
if err := utils.ValidateAdminAddress(msg.Creator); err != nil {
return nil, types.ErrInvalidAdmin
}

// Fetch the zone
zone, err := k.GetHostZone(ctx)
if err != nil {
return nil, err
}

// Get the outer bounds
maxOuterBound := zone.MaxRedemptionRate
minOuterBound := zone.MinRedemptionRate

// Confirm the inner bounds are within the outer bounds
maxInnerBound := msg.MaxInnerRedemptionRate
minInnerBound := msg.MinInnerRedemptionRate
if maxInnerBound.GT(maxOuterBound) {
return nil, types.ErrInvalidRedemptionRateBounds
}
if minInnerBound.LT(minOuterBound) {
return nil, types.ErrInvalidRedemptionRateBounds
}

// Set the inner bounds on the host zone
zone.MinInnerRedemptionRate = minInnerBound
zone.MaxInnerRedemptionRate = maxInnerBound

// Update the host zone
k.SetHostZone(ctx, zone)

_ = sdk.UnwrapSDKContext(goCtx)
return &types.MsgUpdateInnerRedemptionRateBoundsResponse{}, nil
}

// Unhalts the host zone if redemption rates were exceeded
// BOUNDS: verified in ValidateBasic
func (k msgServer) ResumeHostZone(goCtx context.Context, msg *types.MsgResumeHostZone) (*types.MsgResumeHostZoneResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// gate this transaction to the BOUNDS address
if err := utils.ValidateAdminAddress(msg.Creator); err != nil {
return nil, types.ErrInvalidAdmin
}

// 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
}

// Check the zone is halted
if !zone.Halted {
return nil, errorsmod.Wrapf(types.ErrHostZoneNotHalted, "zone is not halted")
}

stDenom := utils.StAssetDenomFromHostZoneDenom(zone.NativeTokenDenom)
k.ratelimitKeeper.RemoveDenomFromBlacklist(ctx, stDenom)

// Resume zone
zone.Halted = false
k.SetHostZone(ctx, zone)

_ = sdk.UnwrapSDKContext(goCtx)
return &types.MsgResumeHostZoneResponse{}, nil
}

Expand All @@ -207,9 +147,7 @@ func (k msgServer) RefreshRedemptionRate(goCtx context.Context, msgTriggerRedemp
return nil, err
}

err := k.UpdateRedemptionRate(ctx)

return &types.MsgRefreshRedemptionRateResponse{}, err
return &types.MsgRefreshRedemptionRateResponse{}, nil
}

// overwrite a delegation record
Expand Down
Loading