Skip to content

Commit

Permalink
Non staking denom rewards (#1355)
Browse files Browse the repository at this point in the history
* improve distribution of non-stkaing rewards

* upgrade handler

* remove previous ics reward (non staking token) distribution logic, that didnt get called when participation rewards is not enabled; also ensure that Claims are always attributed to the previous epoch, even when participation rewards is not enabled
  • Loading branch information
Joe Bowman authored Mar 27, 2024
1 parent 98584e8 commit 42d83ff
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 83 deletions.
1 change: 1 addition & 0 deletions app/upgrades/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
V010500UpgradeName = "v1.5.0"
V010501UpgradeName = "v1.5.1"
V010503UpgradeName = "v1.5.3"
V010504UpgradeName = "v1.5.4"
V010600UpgradeName = "v1.6.0"
)

Expand Down
1 change: 1 addition & 0 deletions app/upgrades/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func Upgrades() []Upgrade {
{UpgradeName: V010500UpgradeName, CreateUpgradeHandler: V010500UpgradeHandler},
{UpgradeName: V010501UpgradeName, CreateUpgradeHandler: V010501UpgradeHandler},
{UpgradeName: V010503UpgradeName, CreateUpgradeHandler: V010503UpgradeHandler},
{UpgradeName: V010504UpgradeName, CreateUpgradeHandler: V010504UpgradeHandler},
{UpgradeName: V010600UpgradeName, CreateUpgradeHandler: V010600UpgradeHandler},
}
}
Expand Down
27 changes: 27 additions & 0 deletions app/upgrades/v1_5.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"

"github.com/quicksilver-zone/quicksilver/app/keepers"
"github.com/quicksilver-zone/quicksilver/utils"
"github.com/quicksilver-zone/quicksilver/utils/addressutils"
cmtypes "github.com/quicksilver-zone/quicksilver/x/claimsmanager/types"
icstypes "github.com/quicksilver-zone/quicksilver/x/interchainstaking/types"
prkeeper "github.com/quicksilver-zone/quicksilver/x/participationrewards/keeper"
Expand Down Expand Up @@ -105,6 +107,31 @@ func V010503rc0UpgradeHandler(

// =========== PRODUCTION UPGRADE HANDLER ===========

func V010504UpgradeHandler(
mm *module.Manager,
configurator module.Configurator,
appKeepers *keepers.AppKeepers,
) upgradetypes.UpgradeHandler {
return func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
appKeepers.InterchainstakingKeeper.IterateZones(ctx, func(_ int64, zone *icstypes.Zone) (stop bool) {
// ship everything in local corresponding account to withdrawal account address to fee collector.
account, err := addressutils.AccAddressFromBech32(zone.WithdrawalAddress.Address, "")
if err != nil {
panic(err)
}

accountBalance := appKeepers.BankKeeper.GetAllBalances(ctx, account)
err = appKeepers.BankKeeper.SendCoinsFromAccountToModule(ctx, account, authtypes.FeeCollectorName, accountBalance)
if err != nil {
panic(err)
}

return false
})
return mm.RunMigrations(ctx, configurator, fromVM)
}
}

func V010503UpgradeHandler(
mm *module.Manager,
configurator module.Configurator,
Expand Down
3 changes: 0 additions & 3 deletions utils/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"

ibctmtypes "github.com/cosmos/ibc-go/v5/modules/light-clients/07-tendermint/types"

claimsmanagertypes "github.com/quicksilver-zone/quicksilver/x/claimsmanager/types"
)

type ClaimsManagerKeeper interface {
IterateLastEpochUserClaims(ctx sdk.Context, chainID, address string, fn func(index int64, data claimsmanagertypes.Claim) (stop bool))
GetSelfConsensusState(ctx sdk.Context, key string) (ibctmtypes.ConsensusState, bool)
}
48 changes: 39 additions & 9 deletions x/interchainstaking/keeper/ibc_packet_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

"github.com/golang/protobuf/proto" // nolint:staticcheck

sdkmath "cosmossdk.io/math"
"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/telemetry"
Expand All @@ -28,6 +28,7 @@ import (

"github.com/quicksilver-zone/quicksilver/utils"
"github.com/quicksilver-zone/quicksilver/utils/addressutils"
cmtypes "github.com/quicksilver-zone/quicksilver/x/claimsmanager/types"
querytypes "github.com/quicksilver-zone/quicksilver/x/interchainquery/types"
"github.com/quicksilver-zone/quicksilver/x/interchainstaking/types"
lsmstakingtypes "github.com/quicksilver-zone/quicksilver/x/lsmtypes"
Expand Down Expand Up @@ -301,7 +302,7 @@ func (k *Keeper) HandleMsgTransfer(ctx sdk.Context, msg ibctransfertypes.Fungibl
return errors.New("unexpected recipient")
}

receivedAmount, ok := sdkmath.NewIntFromString(msg.Amount)
receivedAmount, ok := math.NewIntFromString(msg.Amount)
if !ok {
return fmt.Errorf("unable to marshal amount into math.Int: %s", msg.Amount)
}
Expand All @@ -320,18 +321,47 @@ func (k *Keeper) HandleMsgTransfer(ctx sdk.Context, msg ibctransfertypes.Fungibl
return err
}
k.Logger(ctx).Info("distributing collected rewards to users", "amount", rewardCoin)
err = k.BankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, zoneAddress, sdk.NewCoins(rewardCoin))
remaining, err := k.DistributeToClaimants(ctx, zone, zoneAddress, rewardCoin)
if err != nil {
return err
}
receivedCoin = sdk.NewCoin(receivedCoin.Denom, feeAmount)
receivedCoin = sdk.NewCoin(receivedCoin.Denom, feeAmount).Add(remaining)
}

balance := sdk.NewCoins(receivedCoin)
k.Logger(ctx).Info("distributing collected fees to stakers", "amount", balance)
return k.BankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, authtypes.FeeCollectorName, balance)
}

func (k *Keeper) DistributeToClaimants(ctx sdk.Context, zone *types.Zone, zoneAddress sdk.AccAddress, rewardsCoin sdk.Coin) (sdk.Coin, error) {
var err error
toDistribute := rewardsCoin.Amount
supply := k.BankKeeper.GetSupply(ctx, zone.BaseDenom).Amount
claimTotal := math.ZeroInt()
k.ClaimsManagerKeeper.IterateLastEpochClaims(ctx, zone.ChainId, func(index int64, data cmtypes.Claim) (stop bool) {
claimTotal = claimTotal.Add(data.Amount)
return false
})

ratio := math.LegacyOneDec()
if claimTotal.GT(supply) {
ratio = math.LegacyNewDecFromInt(supply).Quo(math.LegacyNewDecFromInt(claimTotal))
}

k.ClaimsManagerKeeper.IterateLastEpochClaims(ctx, zone.ChainId, func(index int64, data cmtypes.Claim) (stop bool) {
claimAmount := math.LegacyNewDecFromInt(data.Amount).Mul(ratio).Quo(math.LegacyNewDecFromInt(supply)).Mul(rewardsCoin.Amount.ToLegacyDec()).TruncateInt()
err = k.BankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addressutils.MustAccAddressFromBech32(data.UserAddress, ""), sdk.NewCoins(sdk.NewCoin(rewardsCoin.Denom, claimAmount)))
toDistribute = toDistribute.Sub(claimAmount)
return err != nil
})

if toDistribute.IsNegative() {
return sdk.Coin{}, fmt.Errorf("unexpected negative value")
}

return sdk.NewCoin(rewardsCoin.Denom, toDistribute), err
}

func (k *Keeper) HandleCompleteSend(ctx sdk.Context, msg sdk.Msg, memo string, connectionID string) error {
k.Logger(ctx).Info("Received MsgSend acknowledgement")
// first, type assertion. we should have banktypes.MsgSend
Expand Down Expand Up @@ -474,7 +504,7 @@ func (k *Keeper) HandleWithdrawForUser(ctx sdk.Context, zone *types.Zone, msg *b

period := int64(k.GetParam(ctx, types.KeyValidatorSetInterval))
query := stakingtypes.QueryValidatorsRequest{}
return k.EmitValSetQuery(ctx, zone.ConnectionId, zone.ChainId, query, sdkmath.NewInt(period))
return k.EmitValSetQuery(ctx, zone.ConnectionId, zone.ChainId, query, math.NewInt(period))
}

func (k *Keeper) GCCompletedRedelegations(ctx sdk.Context) error {
Expand Down Expand Up @@ -875,7 +905,7 @@ func (k *Keeper) HandleFailedUndelegate(ctx sdk.Context, msg sdk.Msg, memo strin
// - save old record
// - create new record for unhandled burn amount
newDistribution := make([]*types.Distribution, 0)
relatedAmount := sdkmath.ZeroInt()
relatedAmount := math.ZeroInt()
for _, dist := range wdr.Distribution {
if dist.Valoper != ubr.Validator {
newDistribution = append(newDistribution, dist)
Expand Down Expand Up @@ -1292,7 +1322,7 @@ func (k *Keeper) UpdateDelegationRecordForAddress(

period := int64(k.GetParam(ctx, types.KeyValidatorSetInterval))
query := stakingtypes.QueryValidatorsRequest{}
err := k.EmitValSetQuery(ctx, zone.ConnectionId, zone.ChainId, query, sdkmath.NewInt(period))
err := k.EmitValSetQuery(ctx, zone.ConnectionId, zone.ChainId, query, math.NewInt(period))
if err != nil {
return err
}
Expand Down Expand Up @@ -1447,7 +1477,7 @@ func DistributeRewardsFromWithdrawAccount(k *Keeper, ctx sdk.Context, args []byt
return k.SubmitTx(ctx, msgs, zone.WithdrawalAddress, "", zone.MessagesPerTx)
}

func (*Keeper) prepareRewardsDistributionMsgs(zone types.Zone, rewards sdkmath.Int) sdk.Msg {
func (*Keeper) prepareRewardsDistributionMsgs(zone types.Zone, rewards math.Int) sdk.Msg {
return &banktypes.MsgSend{
FromAddress: zone.WithdrawalAddress.GetAddress(),
ToAddress: zone.DelegationAddress.GetAddress(),
Expand All @@ -1467,7 +1497,7 @@ func isNumericString(in string) bool {
return err == nil
}

func equalLsmCoin(valoper string, amount sdkmath.Int, lsmAmount sdk.Coin) bool {
func equalLsmCoin(valoper string, amount math.Int, lsmAmount sdk.Coin) bool {
parts := strings.Split(lsmAmount.Denom, "/")
if len(parts) == 2 && strings.HasPrefix(parts[0], valoper) && isNumericString(parts[1]) {
return lsmAmount.Amount.Equal(amount)
Expand Down
127 changes: 97 additions & 30 deletions x/interchainstaking/keeper/ibc_packet_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

icatypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/types"
Expand All @@ -28,6 +29,7 @@ import (
"github.com/quicksilver-zone/quicksilver/utils"
"github.com/quicksilver-zone/quicksilver/utils/addressutils"
"github.com/quicksilver-zone/quicksilver/utils/randomutils"
cmtypes "github.com/quicksilver-zone/quicksilver/x/claimsmanager/types"
"github.com/quicksilver-zone/quicksilver/x/interchainstaking/types"
lsmstakingtypes "github.com/quicksilver-zone/quicksilver/x/lsmtypes"
)
Expand All @@ -42,39 +44,97 @@ var TestChannel = channeltypes.Channel{
const queryAllBalancesPath = "cosmos.bank.v1beta1.Query/AllBalances"

func (suite *KeeperTestSuite) TestHandleMsgTransferGood() {
nineDec := sdk.NewDecWithPrec(9, 2)
user1 := addressutils.GenerateAddressForTestWithPrefix("quick")
user2 := addressutils.GenerateAddressForTestWithPrefix("quick")
user3 := addressutils.GenerateAddressForTestWithPrefix("quick")

tcs := []struct {
name string
amount sdk.Coin
fcAmount math.Int
withdrawalAmount math.Int
feeAmount *sdk.Dec
name string
amount sdk.Coin
claims []cmtypes.Claim
expectedFeeCollector math.Int
supply sdk.Coin
expectedBalances map[string]math.Int
feeAmount sdk.Dec
}{
{
name: "staking denom - all goes to fc",
amount: sdk.NewCoin("uatom", math.NewInt(100)),
fcAmount: math.NewInt(100),
withdrawalAmount: math.ZeroInt(),
name: "staking denom - 100% to fc",
amount: sdk.NewCoin("uatom", math.NewInt(100)),
supply: sdk.NewCoin("uatom", math.NewInt(1000)),
expectedFeeCollector: math.NewInt(100),
},
{
name: "non staking denom, no claims - 100% to fc",
amount: sdk.NewCoin("transfer/channel-569/untrn", math.NewInt(100)),
supply: sdk.NewCoin("uatom", math.NewInt(1000)),
expectedFeeCollector: math.NewInt(100),
},
{
name: "non staking denom, claims 100%, default (2.5%) to fc, remainder proportional to users",
amount: sdk.NewCoin("ujuno", math.NewInt(100)),
claims: []cmtypes.Claim{
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "osmosis-1", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "quicksilver-2", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "juno-1", Amount: math.NewInt(2000)},
},
supply: sdk.NewCoin("uatom", math.NewInt(4000)),
expectedFeeCollector: math.NewInt(3),
expectedBalances: map[string]math.Int{
user1: math.NewInt(97),
},
},
{
name: "ibc denom denom - all goes to fc",
amount: sdk.NewCoin("transfer/channel-569/untrn", math.NewInt(100)),
fcAmount: math.NewInt(2),
withdrawalAmount: math.NewInt(98),
name: "non staking denom, claims 100%, non-default (9%) to fc, remainder proportional to users",
amount: sdk.NewCoin("ujuno", math.NewInt(1000)),
claims: []cmtypes.Claim{
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "osmosis-1", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "quicksilver-2", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "juno-1", Amount: math.NewInt(2000)},
{UserAddress: user2, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "juno-1", Amount: math.NewInt(2000)},
},
supply: sdk.NewCoin("uatom", math.NewInt(6000)),
expectedFeeCollector: math.NewInt(92),
expectedBalances: map[string]math.Int{
user1: math.NewInt(605),
user2: math.NewInt(303),
},
feeAmount: sdk.NewDecWithPrec(9, 2),
},
{
name: "non staking denom - default (2.5%) to fc, remainder to withdrawal",
amount: sdk.NewCoin("ujuno", math.NewInt(100)),
fcAmount: math.NewInt(2),
withdrawalAmount: math.NewInt(98),
name: "non staking denom, claims >100% - default (2.5%) to fc, remainder proportional to users",
amount: sdk.NewCoin("ujuno", math.NewInt(1000)),
claims: []cmtypes.Claim{
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "osmosis-1", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "quicksilver-2", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "juno-1", Amount: math.NewInt(2000)},
{UserAddress: user2, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "juno-1", Amount: math.NewInt(2000)},
{UserAddress: user3, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "quicksilver-2", Amount: math.NewInt(1000)},
},
supply: sdk.NewCoin("uatom", math.NewInt(6000)),
expectedFeeCollector: math.NewInt(27),
expectedBalances: map[string]math.Int{
user1: math.NewInt(556),
user2: math.NewInt(278),
user3: math.NewInt(139),
},
},
{
name: "non staking denom - non-default (9%) to fc, remainder to withdrawal",
amount: sdk.NewCoin("uakt", math.NewInt(100)),
fcAmount: math.NewInt(9),
withdrawalAmount: math.NewInt(91),
feeAmount: &nineDec, // 0.09 = 9%
name: "non staking denom, claims <100% - default (2.5%) to fc, remainder proportional to users",
amount: sdk.NewCoin("ujuno", math.NewInt(1000)),
claims: []cmtypes.Claim{
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "osmosis-1", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "quicksilver-2", Amount: math.NewInt(1000)},
{UserAddress: user1, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "juno-1", Amount: math.NewInt(1000)},
{UserAddress: user2, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "juno-1", Amount: math.NewInt(1000)},
{UserAddress: user3, ChainId: suite.chainB.ChainID, Module: cmtypes.ClaimTypeLiquidToken, SourceChainId: "quicksilver-2", Amount: math.NewInt(1000)},
},
supply: sdk.NewCoin("uatom", math.NewInt(6000)),
expectedFeeCollector: math.NewInt(190),
expectedBalances: map[string]math.Int{
user1: math.NewInt(486),
user2: math.NewInt(162),
user3: math.NewInt(162),
},
},
}
for _, tc := range tcs {
Expand All @@ -94,12 +154,20 @@ func (suite *KeeperTestSuite) TestHandleMsgTransferGood() {
err := quicksilver.BankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(ibcDenom, tc.amount.Amount)))
suite.NoError(err)

if tc.feeAmount != nil {
if !tc.feeAmount.IsNil() {
params := quicksilver.InterchainstakingKeeper.GetParams(ctx)
params.CommissionRate.Set(*tc.feeAmount)
params.CommissionRate.Set(tc.feeAmount)
quicksilver.InterchainstakingKeeper.SetParams(ctx, params)
}

for _, claim := range tc.claims {
claim := claim // no implicit memory aliasing
quicksilver.ClaimsManagerKeeper.SetLastEpochClaim(ctx, &claim)
}

err = quicksilver.BankKeeper.MintCoins(ctx, minttypes.ModuleName, sdk.NewCoins(tc.supply))
suite.NoError(err)

zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID)
suite.True(found)

Expand All @@ -120,18 +188,17 @@ func (suite *KeeperTestSuite) TestHandleMsgTransferGood() {

txMaccBalance := quicksilver.BankKeeper.GetAllBalances(ctx, txMacc)
feeMaccBalance := quicksilver.BankKeeper.GetAllBalances(ctx, feeMacc)
zoneAddress, err := addressutils.AccAddressFromBech32(zone.WithdrawalAddress.Address, "")
suite.NoError(err)
wdAccountBalance := quicksilver.BankKeeper.GetAllBalances(ctx, zoneAddress)

// assert that ics module balance is nil
suite.Equal(sdk.Coins{}, txMaccBalance)

// assert that fee collector module balance is the expected value
suite.Equal(feeMaccBalance.AmountOf(ibcDenom), tc.fcAmount)
suite.Equal(feeMaccBalance.AmountOf(ibcDenom), tc.expectedFeeCollector)

// assert that zone withdrawal address balance (local chain) is the expected value
suite.Equal(wdAccountBalance.AmountOf(ibcDenom), tc.withdrawalAmount)
for address, balance := range tc.expectedBalances {
suite.Equal(quicksilver.BankKeeper.GetBalance(ctx, addressutils.MustAccAddressFromBech32(address, ""), ibcDenom), sdk.NewCoin(ibcDenom, balance))
}
})
}
}
Expand Down
2 changes: 2 additions & 0 deletions x/interchainstaking/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ type BankKeeper interface {
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error
SendCoins(ctx sdk.Context, senderAddr sdk.AccAddress, recipientADdr sdk.AccAddress, amt sdk.Coins) error
}

type IcsHooks interface {
AfterZoneCreated(ctx sdk.Context, zone *Zone) error
}

type ClaimsManagerKeeper interface {
IterateLastEpochClaims(ctx sdk.Context, chainID string, fn func(index int64, data claimsmanagertypes.Claim) (stop bool))
IterateLastEpochUserClaims(ctx sdk.Context, chainID, address string, fn func(index int64, data claimsmanagertypes.Claim) (stop bool))
IterateUserClaims(ctx sdk.Context, chainID, address string, fn func(index int64, data claimsmanagertypes.Claim) (stop bool))
SetClaim(ctx sdk.Context, claim *claimsmanagertypes.Claim)
Expand Down
Loading

0 comments on commit 42d83ff

Please sign in to comment.