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

ICQCallbacks File Org #337

Merged
merged 3 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ func NewStrideApp(
)

// Register ICQ callbacks
err := app.InterchainqueryKeeper.SetCallbackHandler(stakeibcmoduletypes.ModuleName, app.StakeibcKeeper.CallbackHandler())
err := app.InterchainqueryKeeper.SetCallbackHandler(stakeibcmoduletypes.ModuleName, app.StakeibcKeeper.ICQCallbackHandler())
if err != nil {
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions x/interchainquery/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (k *Keeper) SetCallbackHandler(module string, handler types.QueryCallbacks)
if found {
return fmt.Errorf("callback handler already set for %s", module)
}
k.callbacks[module] = handler.RegisterCallbacks()
k.callbacks[module] = handler.RegisterICQCallbacks()
return nil
}

Expand Down Expand Up @@ -94,7 +94,7 @@ func (k *Keeper) MakeRequest(ctx sdk.Context, connection_id string, chain_id str
k.Logger(ctx).Error(err.Error())
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "no callback handler registered for module")
}
if exists := k.callbacks[module].Has(callback_id); !exists {
if exists := k.callbacks[module].HasICQCallback(callback_id); !exists {
err := fmt.Errorf("no callback %s registered for module %s", callback_id, module)
k.Logger(ctx).Error(err.Error())
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "no callback handler registered for module")
Expand Down
4 changes: 2 additions & 2 deletions x/interchainquery/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ func (k Keeper) InvokeCallback(ctx sdk.Context, msg *types.MsgSubmitQueryRespons
k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] executing callback for queryId (%s), module (%s)", q.Id, moduleName))
moduleCallbackHandler := k.callbacks[moduleName]

if moduleCallbackHandler.Has(q.CallbackId) {
if moduleCallbackHandler.HasICQCallback(q.CallbackId) {
k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] callback (%s) found for module (%s)", q.CallbackId, moduleName))
// call the correct callback function
err := moduleCallbackHandler.Call(ctx, q.CallbackId, msg.Result, q)
err := moduleCallbackHandler.CallICQCallback(ctx, q.CallbackId, msg.Result, q)
if err != nil {
k.Logger(ctx).Error(fmt.Sprintf("[ICQ Resp] error in ICQ callback, error: %s, msg: %s, result: %v, type: %s, params: %v", err.Error(), msg.QueryId, msg.Result, q.QueryType, q.Request))
return err
Expand Down
8 changes: 4 additions & 4 deletions x/interchainquery/types/callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
)

type QueryCallbacks interface {
AddCallback(id string, fn interface{}) QueryCallbacks
RegisterCallbacks() QueryCallbacks
Call(ctx sdk.Context, id string, args []byte, query Query) error
Has(id string) bool
AddICQCallback(id string, fn interface{}) QueryCallbacks
RegisterICQCallbacks() QueryCallbacks
CallICQCallback(ctx sdk.Context, id string, args []byte, query Query) error
HasICQCallback(id string) bool
}
404 changes: 0 additions & 404 deletions x/stakeibc/keeper/callbacks.go

This file was deleted.

24 changes: 12 additions & 12 deletions x/stakeibc/keeper/icacallbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
)

const (
DELEGATE = "delegate"
CLAIM = "claim"
UNDELEGATE = "undelegate"
REINVEST = "reinvest"
REDEMPTION = "redemption"
REBALANCE = "rebalance"
ICACallbackID_Delegate = "delegate"
ICACallbackID_Claim = "claim"
ICACallbackID_Undelegate = "undelegate"
ICACallbackID_Reinvest = "reinvest"
ICACallbackID_Redemption = "redemption"
ICACallbackID_Rebalance = "rebalance"
)

// ICACallbacks wrapper struct for stakeibc keeper
Expand Down Expand Up @@ -46,11 +46,11 @@ func (c ICACallbacks) AddICACallback(id string, fn interface{}) icacallbackstype

func (c ICACallbacks) RegisterICACallbacks() icacallbackstypes.ICACallbackHandler {
a := c.
AddICACallback(DELEGATE, ICACallback(DelegateCallback)).
AddICACallback(CLAIM, ICACallback(ClaimCallback)).
AddICACallback(UNDELEGATE, ICACallback(UndelegateCallback)).
AddICACallback(REINVEST, ICACallback(ReinvestCallback)).
AddICACallback(REDEMPTION, ICACallback(RedemptionCallback)).
AddICACallback(REBALANCE, ICACallback(RebalanceCallback))
AddICACallback(ICACallbackID_Delegate, ICACallback(DelegateCallback)).
AddICACallback(ICACallbackID_Claim, ICACallback(ClaimCallback)).
AddICACallback(ICACallbackID_Undelegate, ICACallback(UndelegateCallback)).
AddICACallback(ICACallbackID_Reinvest, ICACallback(ReinvestCallback)).
AddICACallback(ICACallbackID_Redemption, ICACallback(RedemptionCallback)).
AddICACallback(ICACallbackID_Rebalance, ICACallback(RebalanceCallback))
return a.(ICACallbacks)
}
48 changes: 48 additions & 0 deletions x/stakeibc/keeper/icqcallbacks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package keeper

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

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

const (
ICQCallbackID_WithdrawalBalance = "withdrawalbalance"
ICQCallbackID_Delegation = "delegation"
ICQCallbackID_Validator = "validator"
)

// ICQCallbacks wrapper struct for stakeibc keeper
type ICQCallback func(Keeper, sdk.Context, []byte, icqtypes.Query) error

type ICQCallbacks struct {
k Keeper
callbacks map[string]ICQCallback
}

var _ icqtypes.QueryCallbacks = ICQCallbacks{}

func (k Keeper) ICQCallbackHandler() ICQCallbacks {
return ICQCallbacks{k, make(map[string]ICQCallback)}
}

func (c ICQCallbacks) CallICQCallback(ctx sdk.Context, id string, args []byte, query icqtypes.Query) error {
return c.callbacks[id](c.k, ctx, args, query)
}

func (c ICQCallbacks) HasICQCallback(id string) bool {
_, found := c.callbacks[id]
return found
}

func (c ICQCallbacks) AddICQCallback(id string, fn interface{}) icqtypes.QueryCallbacks {
c.callbacks[id] = fn.(ICQCallback)
return c
}

func (c ICQCallbacks) RegisterICQCallbacks() icqtypes.QueryCallbacks {
return c.
AddICQCallback(ICQCallbackID_WithdrawalBalance, ICQCallback(WithdrawalBalanceCallback)).
AddICQCallback(ICQCallbackID_Delegation, ICQCallback(DelegatorSharesCallback)).
AddICQCallback(ICQCallbackID_Validator, ICQCallback(ValidatorExchangeRateCallback))
}
144 changes: 144 additions & 0 deletions x/stakeibc/keeper/icqcallbacks_delegator_shares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package keeper

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/spf13/cast"

stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

epochtypes "github.com/Stride-Labs/stride/x/epochs/types"
icqtypes "github.com/Stride-Labs/stride/x/interchainquery/types"
"github.com/Stride-Labs/stride/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 exchange rate
// 2. the Delegation ICA's delegated shares
// And apply the following equation:
// num_tokens = exchange_rate * num_shares
//
// 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 DelegatorSharesCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error {
hostZone, found := k.GetHostZone(ctx, query.GetChainId())
if !found {
errMsg := fmt.Sprintf("no registered zone for queried chain ID (%s)", query.GetChainId())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrHostZoneNotFound, errMsg)
}

// Unmarshal the query response which returns a delegation object for the delegator/validator pair
queriedDelgation := stakingtypes.Delegation{}
err := k.cdc.Unmarshal(args, &queriedDelgation)
if err != nil {
errMsg := fmt.Sprintf("unable to unmarshal queried delegation info for zone %s, err: %s", hostZone.ChainId, err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrMarshalFailure, errMsg)
}
k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: HostZone: %s, Delegator: %s, Validator: %s, Shares: %v",
hostZone.ChainId, queriedDelgation.DelegatorAddress, queriedDelgation.ValidatorAddress, queriedDelgation.Shares))

// ensure ICQ can be issued now! else fail the callback
isWithinWindow, err := k.IsWithinBufferWindow(ctx)
if err != nil {
errMsg := fmt.Sprintf("unable to determine if ICQ callback is inside buffer window, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, errMsg)
} else if !isWithinWindow {
k.Logger(ctx).Error("delegator shares callback is outside ICQ window")
return nil
}

// Grab the validator object form the hostZone using the address returned from the query
validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, queriedDelgation.ValidatorAddress)
if !found {
errMsg := fmt.Sprintf("no registered validator for address (%s)", queriedDelgation.ValidatorAddress)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrValidatorNotFound, errMsg)
}

// get the validator's internal exchange rate, aborting if it hasn't been updated this epoch
strideEpochTracker, found := k.GetEpochTracker(ctx, epochtypes.STRIDE_EPOCH)
if !found {
k.Logger(ctx).Error("failed to find stride epoch")
return sdkerrors.Wrapf(sdkerrors.ErrNotFound, "no epoch number for epoch (%s)", epochtypes.STRIDE_EPOCH)
}
if validator.InternalExchangeRate.EpochNumber != strideEpochTracker.GetEpochNumber() {
errMsg := fmt.Sprintf("DelegationCallback: validator (%s) internal exchange rate has not been updated this epoch (epoch #%d)",
validator.Address, strideEpochTracker.GetEpochNumber())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg)
}

// TODO: make sure conversion math precision matches the sdk's staking module's version (we did it slightly differently)
// note: truncateInt per https://github.com/cosmos/cosmos-sdk/blob/cb31043d35bad90c4daa923bb109f38fd092feda/x/staking/types/validator.go#L431
validatorTokens := queriedDelgation.Shares.Mul(validator.InternalExchangeRate.InternalTokensToSharesRate).TruncateInt()
k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: HostZone: %s, Validator: %s, Previous NumTokens: %d, Current NumTokens: %v",
hostZone.ChainId, validator.Address, validator.DelegationAmt, validatorTokens))

// Confirm the validator has actually been slashed
if validatorTokens.Uint64() == validator.DelegationAmt {
k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: Validator (%s) was not slashed", validator.Address))
return nil
} else if validatorTokens.Uint64() > validator.DelegationAmt {
errMsg := fmt.Sprintf("DelegationCallback: Validator (%s) tokens returned from query is greater than the DelegationAmt", validator.Address)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg)
}

// TODO(TESTS-171) add some safety checks here (e.g. we could query the slashing module to confirm the decr in tokens was due to slash)
// update our records of the total stakedbal and of the validator's delegation amt
// NOTE: we assume any decrease in delegation amt that's not tracked via records is a slash

// Get slash percentage
delegationAmount, err := cast.ToInt64E(validator.DelegationAmt)
if err != nil {
errMsg := fmt.Sprintf("unable to convert validator delegation amount to int64, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrIntCast, errMsg)
}
slashAmountUInt := validator.DelegationAmt - validatorTokens.Uint64()
slashAmount, err := cast.ToInt64E(slashAmountUInt)
if err != nil {
errMsg := fmt.Sprintf("unable to convert validator slash amount to int64, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrIntCast, errMsg)
}
weight, err := cast.ToInt64E(validator.Weight)
if err != nil {
errMsg := fmt.Sprintf("unable to convert validator weight to int64, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrIntCast, errMsg)
}

slashPct := sdk.NewDec(slashAmount).Quo(sdk.NewDec(delegationAmount))
k.Logger(ctx).Info(fmt.Sprintf("ICQ'd Delegation Amoount Mismatch, HostZone: %s, Validator: %s, Delegator: %s, Records Tokens: %d, Tokens from ICQ %v, Slash Amount: %d, Slash Pct: %v!",
hostZone.ChainId, validator.Address, queriedDelgation.DelegatorAddress, validator.DelegationAmt, validatorTokens, slashAmount, slashPct))

// Abort if the slash was greater than 10%
tenPercent := sdk.NewDec(10).Quo(sdk.NewDec(100))
if slashPct.GT(tenPercent) {
errMsg := fmt.Sprintf("DelegationCallback: Validator (%s) slashed but ABORTING update, slash is greater than 0.10 (%d)", validator.Address, slashPct)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrSlashGtTenPct, errMsg)
}

// Update the host zone and validator to reflect the weight and delegation change
weightAdjustment := sdk.NewDec(validatorTokens.Int64()).Quo(sdk.NewDec(delegationAmount))
validator.Weight = sdk.NewDec(weight).Mul(weightAdjustment).TruncateInt().Uint64()
validator.DelegationAmt -= slashAmountUInt

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

k.Logger(ctx).Info(fmt.Sprintf("Validator (%s) slashed! Delegation updated to: %v", validator.Address, validator.DelegationAmt))

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

import (
"fmt"

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

stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

epochtypes "github.com/Stride-Labs/stride/x/epochs/types"
icqtypes "github.com/Stride-Labs/stride/x/interchainquery/types"
"github.com/Stride-Labs/stride/x/stakeibc/types"
)

// ValidatorCallback is a callback handler for validator queries.
//
// In an attempt to get the ICA's delegation amount on a given validator, we have to query:
// 1. the validator's internal exchange rate
// 2. the Delegation ICA's delegated shares
// And apply the following equation:
// num_tokens = exchange_rate * num_shares
//
// This is the callback from query #1
func ValidatorExchangeRateCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error {
hostZone, found := k.GetHostZone(ctx, query.GetChainId())
if !found {
errMsg := fmt.Sprintf("no registered zone for queried chain ID (%s)", query.GetChainId())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrHostZoneNotFound, errMsg)
}
queriedValidator := stakingtypes.Validator{}
err := k.cdc.Unmarshal(args, &queriedValidator)
if err != nil {
errMsg := fmt.Sprintf("unable to unmarshal queriedValidator info for zone %s, err: %s", hostZone.ChainId, err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrMarshalFailure, errMsg)
}
k.Logger(ctx).Info(fmt.Sprintf("ValidatorCallback: HostZone %s, Queried Validator %v, Jailed: %v, Tokens: %v, Shares: %v",
hostZone.ChainId, queriedValidator.OperatorAddress, queriedValidator.Jailed, queriedValidator.Tokens, queriedValidator.DelegatorShares))

// ensure ICQ can be issued now! else fail the callback
withinBufferWindow, err := k.IsWithinBufferWindow(ctx)
if err != nil {
errMsg := fmt.Sprintf("unable to determine if ICQ callback is inside buffer window, err: %s", err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, errMsg)
} else if !withinBufferWindow {
k.Logger(ctx).Error("validator exchange rate callback is outside ICQ window")
return nil
}

// get the validator from the host zone
validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, queriedValidator.OperatorAddress)
if !found {
errMsg := fmt.Sprintf("no registered validator for address (%s)", queriedValidator.OperatorAddress)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrValidatorNotFound, errMsg)
}
// get the stride epoch number
strideEpochTracker, found := k.GetEpochTracker(ctx, epochtypes.STRIDE_EPOCH)
if !found {
k.Logger(ctx).Error("failed to find stride epoch")
return sdkerrors.Wrapf(sdkerrors.ErrNotFound, "no epoch number for epoch (%s)", epochtypes.STRIDE_EPOCH)
}

// If the validator's delegation shares is 0, we'll get a division by zero error when trying to get the exchange rate
// because `validator.TokensFromShares` uses delegation shares in the denominator
if queriedValidator.DelegatorShares.IsZero() {
errMsg := fmt.Sprintf("can't calculate validator internal exchange rate because delegation amount is 0 (validator: %s)", validator.Address)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrDivisionByZero, errMsg)
}

// We want the validator's internal exchange rate which is held internally behind `validator.TokensFromShares`
// Since,
// exchange_rate = num_tokens / num_shares
// We can use `validator.TokensFromShares`, plug in 1.0 for the number of shares,
// and the returned number of tokens will be equal to the internal exchange rate
validator.InternalExchangeRate = &types.ValidatorExchangeRate{
InternalTokensToSharesRate: queriedValidator.TokensFromShares(sdk.NewDec(1.0)),
EpochNumber: strideEpochTracker.GetEpochNumber(),
}
hostZone.Validators[valIndex] = &validator
k.SetHostZone(ctx, hostZone)

k.Logger(ctx).Info(fmt.Sprintf("ValidatorCallback: HostZone %s, Validator %v, tokensFromShares %v",
hostZone.ChainId, validator.Address, validator.InternalExchangeRate.InternalTokensToSharesRate))

// armed with the exch rate, we can now query the (val,del) delegation
err = k.QueryDelegationsIcq(ctx, hostZone, queriedValidator.OperatorAddress)
if err != nil {
errMsg := fmt.Sprintf("ValidatorCallback: failed to query delegation, zone %s, err: %s", hostZone.ChainId, err.Error())
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(types.ErrICQFailed, errMsg)
}
return nil
}
Loading