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

updated rebate to use native denom instead of reward denom #1162

Merged
merged 10 commits into from
Mar 27, 2024
8 changes: 6 additions & 2 deletions x/stakeibc/client/cli/tx_toggle_trade_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ import (

func CmdToggleTradeController() *cobra.Command {
cmd := &cobra.Command{
Use: "toggle-trade-controller [chain-id] [grant|revoke] [address]",
Use: "toggle-trade-controller [trade-chain-id] [grant|revoke] [address]",
Short: "Submits an ICA tx to grant or revoke permissions to trade on behalf of the trade ICA",
Args: cobra.ExactArgs(3),
Long: strings.TrimSpace(`Submits an ICA tx to grant or revoke permissions to trade on behalf of the trade ICA
Ex:
>>> strided tx toggle-trade-controller osmosis-1 grant osmoXXX
`),
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) (err error) {
chainId := args[0]
permissionChangeString := args[1]
Expand Down
20 changes: 15 additions & 5 deletions x/stakeibc/keeper/icqcallbacks_withdrawal_host_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,18 @@ func WithdrawalHostBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query
}

// Split the withdrawal amount into the stride fee and reinvest portion
feeAmount, reinvestAmount, err := k.CalculateRewardsSplitAfterRebate(ctx, hostZone, withdrawalBalanceAmount)
rewardsSplit, err := k.CalculateRewardsSplit(ctx, hostZone, withdrawalBalanceAmount)
if err != nil {
return errorsmod.Wrapf(err, "unable to split reward amount into fee and reinvest amounts")
}

// Prepare MsgSends from the withdrawal account
feeCoin := sdk.NewCoin(hostZone.HostDenom, feeAmount)
reinvestCoin := sdk.NewCoin(hostZone.HostDenom, reinvestAmount)
feeCoin := sdk.NewCoin(hostZone.HostDenom, rewardsSplit.StrideFeeAmount)
reinvestCoin := sdk.NewCoin(hostZone.HostDenom, rewardsSplit.ReinvestAmount)
rebateCoin := sdk.NewCoin(hostZone.HostDenom, rewardsSplit.RebateAmount)

var msgs []proto.Message
if feeCoin.Amount.GT(sdk.ZeroInt()) {
if feeCoin.Amount.GT(sdkmath.ZeroInt()) {
sampocs marked this conversation as resolved.
Show resolved Hide resolved
msgs = append(msgs, &banktypes.MsgSend{
FromAddress: hostZone.WithdrawalIcaAddress,
ToAddress: hostZone.FeeIcaAddress,
Expand All @@ -78,7 +79,7 @@ func WithdrawalHostBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalHostBalance,
"Preparing MsgSends of %v from the withdrawal account to the fee account (for commission)", feeCoin.String()))
}
if reinvestCoin.Amount.GT(sdk.ZeroInt()) {
if reinvestCoin.Amount.GT(sdkmath.ZeroInt()) {
msgs = append(msgs, &banktypes.MsgSend{
FromAddress: hostZone.WithdrawalIcaAddress,
ToAddress: hostZone.DelegationIcaAddress,
Expand All @@ -87,6 +88,15 @@ func WithdrawalHostBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalHostBalance,
"Preparing MsgSends of %v from the withdrawal account to the delegation account (for reinvestment)", reinvestCoin.String()))
}
if rebateCoin.Amount.GT(sdkmath.ZeroInt()) {
fundMsg, err := k.BuildFundCommunityPoolMsg(ctx, hostZone, sdk.NewCoins(rebateCoin), types.ICAAccountType_WITHDRAWAL)
if err != nil {
return err
}
msgs = append(msgs, fundMsg...)
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalHostBalance,
sampocs marked this conversation as resolved.
Show resolved Hide resolved
"Preparing MsgFundCommunityPool of %v from the withdrawal account", rebateCoin.String()))
}

// add callback data before calling reinvestment ICA
reinvestCallback := types.ReinvestCallback{
Expand Down
29 changes: 3 additions & 26 deletions x/stakeibc/keeper/icqcallbacks_withdrawal_reward_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ func WithdrawalRewardBalanceCallback(k Keeper, ctx sdk.Context, args []byte, que
"Starting withdrawal reward balance callback, QueryId: %vs, QueryType: %s, Connection: %s", query.Id, query.QueryType, query.ConnectionId))

chainId := query.ChainId
hostZone, err := k.GetActiveHostZone(ctx, chainId)
sampocs marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}

// Unmarshal the query response args to determine the balance
withdrawalRewardBalanceAmount, err := icqkeeper.UnmarshalAmountFromBalanceQuery(k.cdc, args)
Expand Down Expand Up @@ -59,33 +55,14 @@ func WithdrawalRewardBalanceCallback(k Keeper, ctx sdk.Context, args []byte, que
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalRewardBalance,
"Query response - Withdrawal Reward Balance: %v %s", withdrawalRewardBalanceAmount, tradeRoute.RewardDenomOnHostZone))

// Split the withdrawal amount into a rebate, stride fee, and reinvest portion
rebateAmount, tradeAmount, err := k.CalculateRewardsSplitBeforeRebate(ctx, hostZone, withdrawalRewardBalanceAmount)
if err != nil {
return errorsmod.Wrapf(err, "unable to check for rebate amount")
}

// If there's a rebate portion, fund the community pool with that amount
if rebateAmount.GT(sdkmath.ZeroInt()) {
rebateToken := sdk.NewCoin(tradeRoute.RewardDenomOnHostZone, rebateAmount)
if err := k.FundCommunityPool(ctx, hostZone, rebateToken, types.ICAAccountType_WITHDRAWAL); err != nil {
return errorsmod.Wrapf(err, "unable to submit fund community pool ICA")
}

k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalRewardBalance,
"Sending rebate tokens %v %s to community pool",
rebateAmount, tradeRoute.RewardDenomOnRewardZone))
}

// Transfer the amount leftover after to the rebate to the trade zone so it can be swapped for the native token
// We transfer both the amount to be reinvested, and the amount for the stride fee
if err := k.TransferRewardTokensHostToTrade(ctx, tradeAmount, tradeRoute); err != nil {
// Transfer the reward amount to the trade zone so it can be swapped for the native token
riley-stride marked this conversation as resolved.
Show resolved Hide resolved
if err := k.TransferRewardTokensHostToTrade(ctx, withdrawalRewardBalanceAmount, tradeRoute); err != nil {
return errorsmod.Wrapf(err, "initiating transfer of reward tokens to trade ICA failed")
}

k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalRewardBalance,
"Sending discovered reward tokens %v %s from hostZone to tradeZone",
tradeAmount, tradeRoute.RewardDenomOnRewardZone))
withdrawalRewardBalanceAmount, tradeRoute.RewardDenomOnRewardZone))

return nil
}
39 changes: 0 additions & 39 deletions x/stakeibc/keeper/icqcallbacks_withdrawal_reward_balance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,29 +111,6 @@ func (s *KeeperTestSuite) TestWithdrawalRewardBalanceCallback_SuccessfulNoTransf
})
}

// Verify that if the amount returned by the ICQ response is less than the min_swap_amount, no transfer happens
func (s *KeeperTestSuite) TestWithdrawalRewardBalanceCallback_SuccessfulWithRebate() {
tc := s.SetupWithdrawalRewardBalanceCallbackTestCase()

// Update the host zone to have a rebate
stTokenSupply := sdkmath.NewInt(1_000_000)
hostZone := s.MustGetHostZone(HostChainId)
hostZone.TotalDelegations = sdkmath.NewInt(10_000_000)
hostZone.CommunityPoolRebate = &types.CommunityPoolRebate{
RebateRate: sdk.MustNewDecFromStr("0.5"),
LiquidStakedStTokenAmount: stTokenSupply,
}
s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone)

// Mint stTokens so that the supply matches the liquid staked amount
s.FundAccount(s.TestAccs[0], sdk.NewCoin(types.StAssetDenomFromHostZoneDenom(hostZone.HostDenom), stTokenSupply))

// ICA inside of TransferRewardTokensHostToTrade should not actually execute because of min_swap_amount
s.CheckMultipleICATxSubmitted(tc.PortID, tc.ChannelID, func() error {
return keeper.WithdrawalRewardBalanceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, tc.Response.Query)
})
}

func (s *KeeperTestSuite) TestWithdrawalRewardBalanceCallback_ZeroBalance() {
tc := s.SetupWithdrawalRewardBalanceCallbackTestCase()

Expand Down Expand Up @@ -182,22 +159,6 @@ func (s *KeeperTestSuite) TestWithdrawalRewardBalanceCallback_TradeRouteNotFound
s.Require().ErrorContains(err, "trade route not found")
}

func (s *KeeperTestSuite) TestWithdrawalRewardBalanceCallback_FailedToCheckForRebate() {
tc := s.SetupWithdrawalRewardBalanceCallbackTestCase()

// Add a rebate to the host zone and set the total delegations to 0 so the check fails
hostZone := s.MustGetHostZone(HostChainId)
hostZone.CommunityPoolRebate = &types.CommunityPoolRebate{
RebateRate: sdk.MustNewDecFromStr("0.5"),
LiquidStakedStTokenAmount: sdkmath.NewInt(1),
}
hostZone.TotalDelegations = sdkmath.ZeroInt()
s.App.StakeibcKeeper.SetHostZone(s.Ctx, hostZone)

err := keeper.WithdrawalRewardBalanceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, tc.Response.Query)
s.Require().ErrorContains(err, "unable to check for rebate amount")
}

func (s *KeeperTestSuite) TestWithdrawalRewardBalanceCallback_FailedSubmitTx() {
tc := s.SetupWithdrawalRewardBalanceCallbackTestCase()

Expand Down
133 changes: 41 additions & 92 deletions x/stakeibc/keeper/reward_converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ type ForwardMetadata struct {
Retries int64 `json:"retries"`
}

type RewardsSplit struct {
RebateAmount sdkmath.Int
StrideFeeAmount sdkmath.Int
ReinvestAmount sdkmath.Int
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// The goal of this code is to allow certain reward token types to be automatically traded into other types
// This happens before the rest of the staking, allocation, distribution etc. would continue as normal
Expand All @@ -53,130 +59,73 @@ type ForwardMetadata struct {
// and the normal staking and distribution flow will continue from there.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Breaks down the split of non-native-denom rewards into the portions intended for a rebate vs the remainder
// that's used for fees and reinvestment
// For instance, in the case of dYdX, this is used on the USDC that has not been pushed through the trade route
// yet, and has not yet been converted to DYDX
// Breaks down the split of native rewards into the portions intended for (a) a rebate, (b) stride commission,
// and (c) reinvestment
// For most host zones, the rewards here were generated from normal staking rewards, but in the case of dYdX,
// this is called on the rewards that were converted from USDC to DYDX during the trade route
//
// The rebate percentage is determined by: (% of total TVL contributed by commuity pool) * (rebate percentage)
//
// E.g. Community pool liquid staked 1M, TVL is 10M, rebate is 20%
// Total rewards this epoch are 1000, and the stride fee is 10%
// => Then the rebate is 1000 rewards * 10% stride fee * (1M / 10M) * 20% rebate = 2
func (k Keeper) CalculateRewardsSplitBeforeRebate(
// => Then the rebate is 1000 rewards * 10% stride fee * (1M / 10M) * 20% rebate = 2 tokens
// => Stride fee is 1000 rewards * 10% stride fee - 2 rebate = 98 tokens
// => Reinvestment is 1000 rewards * (100% - 10% stride fee) = 900 tokens
func (k Keeper) CalculateRewardsSplit(
ctx sdk.Context,
hostZone types.HostZone,
rewardAmount sdkmath.Int,
) (rebateAmount sdkmath.Int, remainingAmount sdkmath.Int, err error) {
// Get the rebate info from the host zone if applicable
// If there's no rebate, return 0 rebate and the full reward as the remainder
rewardsAmount sdkmath.Int,
) (rewardSplit RewardsSplit, err error) {
// Get the fee rate and total fees from params (e.g. 0.1 for 10% fee)
strideFeeParam := sdk.NewIntFromUint64(k.GetParams(ctx).StrideCommission)
totalFeeRate := sdk.NewDecFromInt(strideFeeParam).Quo(sdk.NewDec(100))

// Get the total fee amount from the fee percentage
totalFeesAmount := sdk.NewDecFromInt(rewardsAmount).Mul(totalFeeRate).TruncateInt()
reinvestAmount := rewardsAmount.Sub(totalFeesAmount)

// Check if the chain has a rebate
// If there's no rebate, return 0 rebate and send all fees as stride commission
rebateInfo, chainHasRebate := hostZone.SafelyGetCommunityPoolRebate()
if !chainHasRebate {
return sdkmath.ZeroInt(), rewardAmount, nil
rewardSplit = RewardsSplit{
RebateAmount: sdkmath.ZeroInt(),
StrideFeeAmount: totalFeesAmount,
ReinvestAmount: reinvestAmount,
}
return rewardSplit, nil
}

// Get the fee rate from params (e.g. 0.1 for a 10% fee)
strideFee := sdk.NewIntFromUint64(k.GetParams(ctx).StrideCommission)
strideFeeRate := sdk.NewDecFromInt(strideFee).Quo(sdk.NewDec(100))

// Get supply of stTokens to determine the portion of TVL that the community pool liquid stake makes up
stDenom := utils.StAssetDenomFromHostZoneDenom(hostZone.HostDenom)
stTokenSupply := k.bankKeeper.GetSupply(ctx, stDenom).Amount

// It shouldn't be possible to have 0 token supply (since there are rewards and there was a community pool stake)
// This will also prevent a division by 0 error
if stTokenSupply.IsZero() {
return sdkmath.ZeroInt(), sdkmath.ZeroInt(), errorsmod.Wrapf(types.ErrDivisionByZero,
return rewardSplit, errorsmod.Wrapf(types.ErrDivisionByZero,
sampocs marked this conversation as resolved.
Show resolved Hide resolved
"unable to calculate rebate amount for %s since total delegations are 0", hostZone.ChainId)
}

// It also shouldn't be possible for the liquid stake amount to be greater than the full TVL
if rebateInfo.LiquidStakedStTokenAmount.GT(stTokenSupply) {
return sdkmath.ZeroInt(), sdkmath.ZeroInt(), errorsmod.Wrapf(types.ErrFeeSplitInvariantFailed,
return rewardSplit, errorsmod.Wrapf(types.ErrFeeSplitInvariantFailed,
"community pool liquid staked amount greater than total delegations")
}

// The rebate amount is determined by the contribution of the community pool stake towards the total TVL,
// multiplied by the rebate fee percentage
contributionRate := sdk.NewDecFromInt(rebateInfo.LiquidStakedStTokenAmount).Quo(sdk.NewDecFromInt(stTokenSupply))
totalFeesAmount := sdk.NewDecFromInt(rewardAmount).Mul(strideFeeRate).TruncateInt()
rebateAmount = sdk.NewDecFromInt(totalFeesAmount).Mul(contributionRate).Mul(rebateInfo.RebateRate).TruncateInt()
remainingAmount = rewardAmount.Sub(rebateAmount)

return rebateAmount, remainingAmount, nil
}

// Given the native-denom reward balance from an ICQ, calculates the relevant portions earmarked as a
// stride fee vs for reinvestment
// This is called *after* a rebate has been issued (if there is one to begin with)
// The reward amount is denominated in the host zone's native denom - this is either directly from
// staking rewards, or, in the case of a host zone with a trade route, this is called with the converted tokens
// For instance, with dYdX, this is applied to the DYDX tokens that were converted from USDC
//
// If the chain doesn't have a rebate in place, the split is decided entirely from the stride commission percent
// However, if the chain does have a rebate, we need to factor that into the calculation, by scaling
// up the rewards to find the amount before the rebate
//
// For instance, if 1000 rewards were collected and 2 were sent as a rebate, then the stride fee should be based
// on the original 1000 rewards instead of the remaining 998 in the query response:
//
// Community pool liquid staked 1M, TVL is 10M, rebate is 20%, stride fee is 10%
// If 998 native tokens were queried, we have to scale that up to 1000 original reward tokens
//
// Effective Rebate Pct = 10% fees * (1M LS / 10M TVL) * 20% rebate = 0.20% (aka 0.002)
// Effective Stride Fee Pct = 10% fees - 0.20% effective rebate = 9.8%
// Original Reward Amount = 998 Queried Rewards / (1 - 0.002 effective rebate rate) = 1000 original rewards
// Then stride fees are 9.8% of that 1000 original rewards = 98
func (k Keeper) CalculateRewardsSplitAfterRebate(
ctx sdk.Context,
hostZone types.HostZone,
rewardsAmount sdkmath.Int,
) (strideFeeAmount sdkmath.Int, reinvestAmount sdkmath.Int, err error) {
// Get the fee rate and total fees from params (e.g. 0.1 for 10% fee)
strideFee := sdk.NewIntFromUint64(k.GetParams(ctx).StrideCommission)
totalFeeRate := sdk.NewDecFromInt(strideFee).Quo(sdk.NewDec(100))

// Check if the chain has a rebate
rebateInfo, chainHasRebate := hostZone.SafelyGetCommunityPoolRebate()
rebateAmount := sdk.NewDecFromInt(totalFeesAmount).Mul(contributionRate).Mul(rebateInfo.RebateRate).TruncateInt()
sampocs marked this conversation as resolved.
Show resolved Hide resolved
strideFeeAmount := totalFeesAmount.Sub(rebateAmount)

// If there's no rebate, the fee split just uses the commission
if !chainHasRebate {
strideFeeAmount = sdk.NewDecFromInt(rewardsAmount).Mul(totalFeeRate).TruncateInt()
} else {
// Get supply of stTokens to determine the portion of TVL that the community pool liquid stake makes up
stDenom := utils.StAssetDenomFromHostZoneDenom(hostZone.HostDenom)
stTokenSupply := k.bankKeeper.GetSupply(ctx, stDenom).Amount

// It shouldn't be possible to have 0 token supply (since there are rewards and there was a community pool stake)
// This will also prevent a division by 0 error
if stTokenSupply.IsZero() {
return sdkmath.ZeroInt(), sdkmath.ZeroInt(), errorsmod.Wrapf(types.ErrDivisionByZero,
"unable to calculate rebate amount for %s since total delegations are 0", hostZone.ChainId)
}

// It also shouldn't be possible for the liquid stake amount to be greater than the full TVL
if rebateInfo.LiquidStakedStTokenAmount.GT(stTokenSupply) {
riley-stride marked this conversation as resolved.
Show resolved Hide resolved
return sdkmath.ZeroInt(), sdkmath.ZeroInt(), errorsmod.Wrapf(types.ErrFeeSplitInvariantFailed,
"community pool liquid staked amount greater than total delegations")
}

// Otherwise, the rebate must be considered in the fee split
// The rebate portion is the portion of TVL contributed to by the liquid stake * the rebate percentage
// The stride fee poriton is the remaining percentage
contributionRate := sdk.NewDecFromInt(rebateInfo.LiquidStakedStTokenAmount).Quo(sdk.NewDecFromInt(stTokenSupply))
effectiveRebateRate := totalFeeRate.Mul(contributionRate).Mul(rebateInfo.RebateRate)
effectiveStrideFeeRate := totalFeeRate.Sub(effectiveRebateRate)

// Before calculating the fee, we have to scale up the rewards amount to the amount before the rebate
originalRewardScalingFactor := sdk.OneDec().Sub(effectiveRebateRate)
originalRewardsAmount := sdk.NewDecFromInt(rewardsAmount).Quo(originalRewardScalingFactor).TruncateDec()
strideFeeAmount = originalRewardsAmount.Mul(effectiveStrideFeeRate).Ceil().TruncateInt() // ceiling since rebate was truncated
rewardSplit = RewardsSplit{
RebateAmount: rebateAmount,
StrideFeeAmount: strideFeeAmount,
ReinvestAmount: reinvestAmount,
}

// Using the strideFeeAmount, back into the reinvest amount
reinvestAmount = rewardsAmount.Sub(strideFeeAmount)

return strideFeeAmount, reinvestAmount, nil
return rewardSplit, nil
}

// Builds an authz MsgGrant or MsgRevoke to grant an account trade capabilties on behalf of the trade ICA
Expand Down
Loading
Loading