Skip to content

Commit

Permalink
add rounding calibration function (#936)
Browse files Browse the repository at this point in the history
Closes: #XXX

## Context and purpose of the change




## Brief Changelog




## Author's Checklist

I have...

- [ ] Run and PASSED locally all GAIA integration tests
- [ ] If the change is contentful, I either:
    - [ ] Added a new unit test OR 
    - [ ] Added test cases to existing unit tests
- [ ] OR this change is a trivial rework / code cleanup without any test coverage

If skipped any of the tests above, explain.


## Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] manually tested (if applicable)
- [ ] confirmed the author wrote unit tests for new logic
- [ ] reviewed documentation exists and is accurate


## Documentation and Release Note

  - [ ] Does this pull request introduce a new feature or user-facing behavior changes? 
  - [ ] Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`?
  - [ ] This pull request updates existing proto field values (and require a backend and frontend migration)? 
  - [ ] Does this pull request change existing proto field names (and require a frontend migration)?
  How is the feature or change documented? 
      - [ ] not applicable
      - [ ] jira ticket `XXX` 
      - [ ] specification (`x/<module>/spec/`) 
      - [ ] README.md 
      - [ ] not documented
  • Loading branch information
sampocs authored Sep 18, 2023
1 parent 2988cae commit 60c18e7
Show file tree
Hide file tree
Showing 11 changed files with 837 additions and 91 deletions.
9 changes: 9 additions & 0 deletions proto/stride/stakeibc/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ service Msg {
returns (MsgRestoreInterchainAccountResponse);
rpc UpdateValidatorSharesExchRate(MsgUpdateValidatorSharesExchRate)
returns (MsgUpdateValidatorSharesExchRateResponse);
rpc CalibrateDelegation(MsgCalibrateDelegation)
returns (MsgCalibrateDelegationResponse);
rpc ClearBalance(MsgClearBalance) returns (MsgClearBalanceResponse);
rpc UpdateInnerRedemptionRateBounds(MsgUpdateInnerRedemptionRateBounds)
returns (MsgUpdateInnerRedemptionRateBoundsResponse);
Expand Down Expand Up @@ -170,3 +172,10 @@ message MsgUpdateValidatorSharesExchRate {
string valoper = 3;
}
message MsgUpdateValidatorSharesExchRateResponse {}

message MsgCalibrateDelegation {
string creator = 1;
string chain_id = 2;
string valoper = 3;
}
message MsgCalibrateDelegationResponse {}
1 change: 1 addition & 0 deletions x/stakeibc/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func GetTxCmd() *cobra.Command {
cmd.AddCommand(CmdDeleteValidator())
cmd.AddCommand(CmdRestoreInterchainAccount())
cmd.AddCommand(CmdUpdateValidatorSharesExchRate())
cmd.AddCommand(CmdCalibrateDelegation())
cmd.AddCommand(CmdClearBalance())
cmd.AddCommand(CmdUpdateInnerRedemptionRateBounds())

Expand Down
41 changes: 41 additions & 0 deletions x/stakeibc/client/cli/tx_calibrate_delegation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cli

import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/spf13/cobra"

"github.com/Stride-Labs/stride/v14/x/stakeibc/types"
)

func CmdCalibrateDelegation() *cobra.Command {
cmd := &cobra.Command{
Use: "calibrate-delegation [chainid] [valoper]",
Short: "Broadcast message calibrate-delegation",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) {
argChainId := args[0]
argValoper := args[1]

clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

msg := types.NewMsgCalibrateDelegation(
clientCtx.GetFromAddress().String(),
argChainId,
argValoper,
)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
3 changes: 3 additions & 0 deletions x/stakeibc/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ func NewMessageHandler(k keeper.Keeper) sdk.Handler {
case *types.MsgUpdateValidatorSharesExchRate:
res, err := msgServer.UpdateValidatorSharesExchRate(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgCalibrateDelegation:
res, err := msgServer.CalibrateDelegation(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgUpdateInnerRedemptionRateBounds:
res, err := msgServer.UpdateInnerRedemptionRateBounds(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
Expand Down
4 changes: 3 additions & 1 deletion x/stakeibc/keeper/icqcallbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
ICQCallbackID_FeeBalance = "feebalance"
ICQCallbackID_Delegation = "delegation"
ICQCallbackID_Validator = "validator"
ICQCallbackID_Calibrate = "calibrate"
)

// ICQCallbacks wrapper struct for stakeibc keeper
Expand Down Expand Up @@ -46,5 +47,6 @@ func (c ICQCallbacks) RegisterICQCallbacks() icqtypes.QueryCallbacks {
AddICQCallback(ICQCallbackID_WithdrawalBalance, ICQCallback(WithdrawalBalanceCallback)).
AddICQCallback(ICQCallbackID_FeeBalance, ICQCallback(FeeBalanceCallback)).
AddICQCallback(ICQCallbackID_Delegation, ICQCallback(DelegatorSharesCallback)).
AddICQCallback(ICQCallbackID_Validator, ICQCallback(ValidatorSharesToTokensRateCallback))
AddICQCallback(ICQCallbackID_Validator, ICQCallback(ValidatorSharesToTokensRateCallback)).
AddICQCallback(ICQCallbackID_Calibrate, ICQCallback(CalibrateDelegationCallback))
}
110 changes: 110 additions & 0 deletions x/stakeibc/keeper/icqcallbacks_callibrate_delegation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package keeper

import (
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/cosmos/gogoproto/proto"

"github.com/Stride-Labs/stride/v14/utils"
icqtypes "github.com/Stride-Labs/stride/v14/x/interchainquery/types"
"github.com/Stride-Labs/stride/v14/x/stakeibc/types"
)

// DelegatorSharesCallback is a callback handler for UpdateValidatorSharesExchRate queries.
//
// In an attempt to get the ICA's delegation amount on a given validator, we have to query:
// 1. the validator's internal shares to tokens rate
// 2. the Delegation ICA's delegated shares
// And apply the following equation:
// numTokens = numShares * sharesToTokensRate
//
// This is the callback from query #2
//
// Note: for now, to get proofs in your ICQs, you need to query the entire store on the host zone! e.g. "store/bank/key"
func CalibrateDelegationCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error {
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, ICQCallbackID_Calibrate,
"Starting delegator shares callback, QueryId: %vs, QueryType: %s, Connection: %s", query.Id, query.QueryType, query.ConnectionId))

// Confirm host exists
chainId := query.ChainId
hostZone, found := k.GetHostZone(ctx, chainId)
if !found {
return errorsmod.Wrapf(types.ErrHostZoneNotFound, "no registered zone for queried chain ID (%s)", chainId)
}

// Unmarshal the query response which returns a delegation object for the delegator/validator pair
queriedDelegation := stakingtypes.Delegation{}
err := k.cdc.Unmarshal(args, &queriedDelegation)
if err != nil {
return errorsmod.Wrapf(err, "unable to unmarshal delegator shares query response into Delegation type")
}
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Calibrate, "Query response - Delegator: %s, Validator: %s, Shares: %v",
queriedDelegation.DelegatorAddress, queriedDelegation.ValidatorAddress, queriedDelegation.Shares))

// Unmarshal the callback data containing the previous delegation to the validator (from the time the query was submitted)
var callbackData types.DelegatorSharesQueryCallback
if err := proto.Unmarshal(query.CallbackData, &callbackData); err != nil {
return errorsmod.Wrapf(err, "unable to unmarshal delegator shares callback data")
}

// Grab the validator object from the hostZone using the address returned from the query
validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, queriedDelegation.ValidatorAddress)
if !found {
return errorsmod.Wrapf(types.ErrValidatorNotFound, "no registered validator for address (%s)", queriedDelegation.ValidatorAddress)
}

// Check if the ICQ overlapped a delegation, undelegation, or detokenization ICA
// that would have modfied the number of delegated tokens
prevInternalDelegation := callbackData.InitialValidatorDelegation
currInternalDelegation := validator.Delegation
icaOverlappedIcq, err := k.CheckDelegationChangedDuringQuery(ctx, validator, prevInternalDelegation, currInternalDelegation)
if err != nil {
return err
}

// If the ICA/ICQ overlapped, submit a new query
if icaOverlappedIcq {
// Store the updated validator delegation amount
callbackDataBz, err := proto.Marshal(&types.DelegatorSharesQueryCallback{
InitialValidatorDelegation: currInternalDelegation,
})
if err != nil {
return errorsmod.Wrapf(err, "unable to marshal delegator shares callback data")
}
query.CallbackData = callbackDataBz

if err := k.InterchainQueryKeeper.RetryICQRequest(ctx, query); err != nil {
return errorsmod.Wrapf(err, "unable to resubmit delegator shares query")
}
return nil
}

// If there was no ICA/ICQ overlap, update the validator to indicate that the query
// is no longer in progress (which will unblock LSM liquid stakes to that validator)
validator.SlashQueryInProgress = false

// Calculate the number of tokens delegated (using the internal sharesToTokensRate)
// note: truncateInt per https://github.com/cosmos/cosmos-sdk/blob/cb31043d35bad90c4daa923bb109f38fd092feda/x/staking/types/validator.go#L431
delegatedTokens := queriedDelegation.Shares.Mul(validator.SharesToTokensRate).TruncateInt()
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Calibrate,
"Previous Delegation: %v, Current Delegation: %v", validator.Delegation, delegatedTokens))

// Confirm the validator has actually been slashed
if delegatedTokens.Equal(validator.Delegation) {
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Calibrate, "Validator delegation is correct"))
return nil
}

delegationChange := validator.Delegation.Sub(delegatedTokens)
validator.Delegation = validator.Delegation.Sub(delegationChange)
hostZone.TotalDelegations = hostZone.TotalDelegations.Sub(delegationChange)

k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Calibrate,
"Delegation updated to: %v", validator.Delegation))

hostZone.Validators[valIndex] = &validator
k.SetHostZone(ctx, hostZone)

return nil
}
25 changes: 25 additions & 0 deletions x/stakeibc/keeper/msg_server_calibrate_delegation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package keeper

import (
"context"

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

"github.com/Stride-Labs/stride/v14/x/stakeibc/types"
)

// Submits an ICQ to get the validator's delegated shares
func (k msgServer) CalibrateDelegation(goCtx context.Context, msg *types.MsgCalibrateDelegation) (*types.MsgCalibrateDelegationResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

hostZone, found := k.GetHostZone(ctx, msg.ChainId)
if !found {
return nil, types.ErrHostZoneNotFound
}

if err := k.SubmitCalibrationICQ(ctx, hostZone, msg.Valoper); err != nil {
return nil, err
}

return &types.MsgCalibrateDelegationResponse{}, nil
}
58 changes: 58 additions & 0 deletions x/stakeibc/keeper/msg_server_submit_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,61 @@ func (k Keeper) SubmitDelegationICQ(ctx sdk.Context, hostZone types.HostZone, va

return nil
}

// Submits an ICQ to get a validator's delegations
// This is called after the validator's sharesToTokens rate is determined
// The timeoutDuration parameter represents the length of the timeout (not to be confused with an actual timestamp)
func (k Keeper) SubmitCalibrationICQ(ctx sdk.Context, hostZone types.HostZone, validatorAddress string) error {
if hostZone.DelegationIcaAddress == "" {
return errorsmod.Wrapf(types.ErrICAAccountNotFound, "no delegation address found for %s", hostZone.ChainId)
}
validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, validatorAddress)
if !found {
return errorsmod.Wrapf(types.ErrValidatorNotFound, "no registered validator for address (%s)", validatorAddress)
}

// Get the validator and delegator encoded addresses to form the query request
_, validatorAddressBz, err := bech32.DecodeAndConvert(validatorAddress)
if err != nil {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "invalid validator address, could not decode (%s)", err.Error())
}
_, delegatorAddressBz, err := bech32.DecodeAndConvert(hostZone.DelegationIcaAddress)
if err != nil {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "invalid delegator address, could not decode (%s)", err.Error())
}
queryData := stakingtypes.GetDelegationKey(delegatorAddressBz, validatorAddressBz)

// Store the current validator's delegation in the callback data so we can determine if it changed
// while the query was in flight
callbackData := types.DelegatorSharesQueryCallback{
InitialValidatorDelegation: validator.Delegation,
}
callbackDataBz, err := proto.Marshal(&callbackData)
if err != nil {
return errorsmod.Wrapf(err, "unable to marshal delegator shares callback data")
}

// Update the validator to indicate that the slash query is in progress
validator.SlashQueryInProgress = true
hostZone.Validators[valIndex] = &validator
k.SetHostZone(ctx, hostZone)

// Submit delegator shares ICQ
query := icqtypes.Query{
ChainId: hostZone.ChainId,
ConnectionId: hostZone.ConnectionId,
QueryType: icqtypes.STAKING_STORE_QUERY_WITH_PROOF,
RequestData: queryData,
CallbackModule: types.ModuleName,
CallbackId: ICQCallbackID_Calibrate,
CallbackData: callbackDataBz,
TimeoutDuration: time.Hour,
TimeoutPolicy: icqtypes.TimeoutPolicy_RETRY_QUERY_REQUEST,
}
if err := k.InterchainQueryKeeper.SubmitICQRequest(ctx, query, false); err != nil {
k.Logger(ctx).Error(fmt.Sprintf("Error submitting ICQ for delegation, error : %s", err.Error()))
return err
}

return nil
}
2 changes: 2 additions & 0 deletions x/stakeibc/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func RegisterCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&ToggleLSMProposal{}, "stakeibc/ToggleLSMProposal", nil)
cdc.RegisterConcrete(&MsgRestoreInterchainAccount{}, "stakeibc/RestoreInterchainAccount", nil)
cdc.RegisterConcrete(&MsgUpdateValidatorSharesExchRate{}, "stakeibc/UpdateValidatorSharesExchRate", nil)
cdc.RegisterConcrete(&MsgCalibrateDelegation{}, "stakeibc/CalibrateDelegation", nil)
cdc.RegisterConcrete(&MsgUpdateInnerRedemptionRateBounds{}, "stakeibc/UpdateInnerRedemptionRateBounds", nil)
// this line is used by starport scaffolding # 2
}
Expand All @@ -40,6 +41,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
&MsgDeleteValidator{},
&MsgRestoreInterchainAccount{},
&MsgUpdateValidatorSharesExchRate{},
&MsgCalibrateDelegation{},
&MsgUpdateInnerRedemptionRateBounds{},
)

Expand Down
66 changes: 66 additions & 0 deletions x/stakeibc/types/message_calibrate_delegation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package types

import (
"strings"

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

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

const TypeMsgCalibrateDelegation = "calibrate_delegation"

var _ sdk.Msg = &MsgCalibrateDelegation{}

func NewMsgCalibrateDelegation(creator string, chainid string, valoper string) *MsgCalibrateDelegation {
return &MsgCalibrateDelegation{
Creator: creator,
ChainId: chainid,
Valoper: valoper,
}
}

func (msg *MsgCalibrateDelegation) Route() string {
return RouterKey
}

func (msg *MsgCalibrateDelegation) Type() string {
return TypeMsgCalibrateDelegation
}

func (msg *MsgCalibrateDelegation) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}

func (msg *MsgCalibrateDelegation) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}

func (msg *MsgCalibrateDelegation) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}
if err := utils.ValidateAdminAddress(msg.Creator); err != nil {
return err
}

if len(msg.ChainId) == 0 {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "chainid is required")
}
if len(msg.Valoper) == 0 {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "valoper is required")
}
if !strings.Contains(msg.Valoper, "valoper") {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "validator operator address must contrain 'valoper'")
}

return nil
}
Loading

0 comments on commit 60c18e7

Please sign in to comment.