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

add rounding calibration function #936

Merged
merged 15 commits into from
Sep 18, 2023
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);
}

Expand Down Expand Up @@ -151,3 +153,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())

return cmd
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)
default:
errMsg := fmt.Sprintf("unrecognized %s message type: %T", types.ModuleName, msg)
return nil, errorsmod.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
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
}
3 changes: 2 additions & 1 deletion 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)
// this line is used by starport scaffolding # 2
}

Expand All @@ -38,7 +39,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
&MsgChangeValidatorWeight{},
&MsgDeleteValidator{},
&MsgRestoreInterchainAccount{},
&MsgUpdateValidatorSharesExchRate{},
&MsgCalibrateDelegation{},
)

registry.RegisterImplementations((*govtypes.Content)(nil),
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