diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 378deadc..01b0b60c 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -14,3 +14,4 @@ jobs: with: github_token: ${{ secrets.github_token }} reporter: github-pr-review + golangci_lint_flags: --timeout 10m diff --git a/CHANGELOG.md b/CHANGELOG.md index d632772d..767f5155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (community) [#1704] Add param to control when inflation will be disabled - (community) [#1707] Default staking rewards per second set to `744191` - (community) [#1706] Add disable inflation upgrade to begin blocker that updates x/mint and x/kavadist params. +- (community) [#1729] Consolidate community funds from `x/distribution` and `x/kavadist` to `x/community` ## [v0.24.0] @@ -293,6 +294,7 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md). - [#257](https://github.com/Kava-Labs/kava/pulls/257) Include scripts to run large-scale simulations remotely using aws-batch +[#1729]: https://github.com/Kava-Labs/kava/pull/1729 [#1745]: https://github.com/Kava-Labs/kava/pull/1745 [#1707]: https://github.com/Kava-Labs/kava/pull/1707 [#1706]: https://github.com/Kava-Labs/kava/pull/1706 diff --git a/x/community/keeper/consolidate.go b/x/community/keeper/consolidate.go new file mode 100644 index 00000000..41115785 --- /dev/null +++ b/x/community/keeper/consolidate.go @@ -0,0 +1,104 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/community/types" + + kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" + + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" +) + +// StartCommunityFundConsolidation consolidates the community funds from +// x/distribution and x/kavadist into the x/community module account +func (k Keeper) StartCommunityFundConsolidation(ctx sdk.Context) error { + logger := k.Logger(ctx) + logger.Info("community fund consolidation upgrade started") + + // Consolidate x/distribution community pool + if err := k.consolidateCommunityDistribution(ctx); err != nil { + return err + } + + // Consolidate x/kavadist account + if err := k.consolidateCommunityKavadist(ctx); err != nil { + return err + } + + // Log new x/community balance + communityCoins := k.GetModuleAccountBalance(ctx) + logger.Info(fmt.Sprintf("community funds consolidated, x/community balance is now %s", communityCoins)) + + return nil +} + +// consolidateCommunityDistribution transfers all coins from the x/distribution +// community pool to the x/community module account +func (k Keeper) consolidateCommunityDistribution(ctx sdk.Context) error { + logger := k.Logger(ctx) + + // Get community coins with leftover leftoverDust + truncatedCoins, leftoverDust := k.distrKeeper. + GetFeePoolCommunityCoins(ctx). + TruncateDecimal() + + // Transfer to x/community + err := k.bankKeeper.SendCoinsFromModuleToModule( + ctx, + distrtypes.ModuleName, // sender + types.ModuleName, // recipient + truncatedCoins, + ) + if err != nil { + return fmt.Errorf("failed to transfer x/distribution coins to x/community: %w", err) + } + + logger.Info(fmt.Sprintf("transferred %s from x/distribution to x/community", truncatedCoins)) + + // Set x/distribution community pool to remaining dust amounts + feePool := k.distrKeeper.GetFeePool(ctx) + feePool.CommunityPool = leftoverDust + k.distrKeeper.SetFeePool(ctx, feePool) + + logger.Info(fmt.Sprintf("remaining x/distribution community pool dust: %s", leftoverDust)) + + return nil +} + +// consolidateCommunityKavadist transfers all coins from the x/kavadist module +// account to the x/community module account +func (k Keeper) consolidateCommunityKavadist(ctx sdk.Context) error { + logger := k.Logger(ctx) + + kavadistAcc := k.accountKeeper.GetModuleAccount(ctx, kavadisttypes.KavaDistMacc) + transferCoins := k.bankKeeper.GetAllBalances(ctx, kavadistAcc.GetAddress()) + + // Remove ukava from transfer coins - ony transfer non-ukava coins + found, kavaCoins := transferCoins.Find("ukava") + if found { + transferCoins = transferCoins.Sub(kavaCoins) + } + + // Transfer remaining coins to x/community + err := k.bankKeeper.SendCoinsFromModuleToModule( + ctx, + kavadisttypes.ModuleName, // sender + types.ModuleName, // recipient + transferCoins, + ) + if err != nil { + return fmt.Errorf("failed to transfer x/kavadist coins to x/community: %w", err) + } + + kavadistRemainingCoins := k.bankKeeper.GetAllBalances(ctx, kavadistAcc.GetAddress()) + + logger.Info(fmt.Sprintf( + "transferred %s from x/kavadist to x/community, remaining x/kavadist balance: %s", + transferCoins, + kavadistRemainingCoins, + )) + + return nil +} diff --git a/x/community/keeper/disable_inflation.go b/x/community/keeper/disable_inflation.go index 86ceff6c..87c486b3 100644 --- a/x/community/keeper/disable_inflation.go +++ b/x/community/keeper/disable_inflation.go @@ -22,17 +22,21 @@ func (k Keeper) CheckAndDisableMintAndKavaDistInflation(ctx sdk.Context) { // run disable inflation logic k.disableInflation(ctx) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeInflationStop, + ), + ) + // reset disable inflation time to ensure next call is a no-op params.UpgradeTimeDisableInflation = time.Time{} // set staking rewards to provided intial value params.StakingRewardsPerSecond = params.UpgradeTimeSetStakingRewardsPerSecond k.SetParams(ctx, params) - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeInflationStop, - ), - ) + if err := k.StartCommunityFundConsolidation(ctx); err != nil { + panic(err) + } } // TODO: double check this is correct method for disabling inflation in kavadist without diff --git a/x/community/keeper/keeper.go b/x/community/keeper/keeper.go index e8e5a5a3..af982228 100644 --- a/x/community/keeper/keeper.go +++ b/x/community/keeper/keeper.go @@ -16,6 +16,7 @@ type Keeper struct { key storetypes.StoreKey cdc codec.Codec + accountKeeper types.AccountKeeper bankKeeper types.BankKeeper cdpKeeper types.CdpKeeper distrKeeper types.DistributionKeeper @@ -63,6 +64,7 @@ func NewKeeper( key: key, cdc: cdc, + accountKeeper: ak, bankKeeper: bk, cdpKeeper: ck, distrKeeper: dk, diff --git a/x/community/keeper/proposal_handler_test.go b/x/community/keeper/proposal_handler_test.go index 3a9c3831..ebf01e49 100644 --- a/x/community/keeper/proposal_handler_test.go +++ b/x/community/keeper/proposal_handler_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" abcitypes "github.com/tendermint/tendermint/abci/types" @@ -27,9 +28,11 @@ func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amo func ukava(amt int64) sdk.Coins { return sdk.NewCoins(c("ukava", amt)) } + func usdx(amt int64) sdk.Coins { return sdk.NewCoins(c("usdx", amt)) } + func otherdenom(amt int64) sdk.Coins { return sdk.NewCoins(c("other-denom", amt)) } @@ -67,10 +70,19 @@ func (suite *proposalTestSuite) SetupTest() { ChainID: chainID, }) + // Set UpgradeTimeDisableInflation to far future to not influence module + // account balances + params := types.Params{ + UpgradeTimeDisableInflation: time.Now().Add(100000 * time.Hour), + StakingRewardsPerSecond: sdkmath.LegacyNewDec(0), + } + communityGs := types.NewGenesisState(params, types.DefaultStakingRewardsState()) + tApp.InitializeFromGenesisStatesWithTimeAndChainID( genTime, chainID, app.GenesisState{hardtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&hardGS)}, app.GenesisState{pricefeedtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&pricefeedGS)}, + app.GenesisState{types.ModuleName: tApp.AppCodec().MustMarshalJSON(&communityGs)}, testutil.NewCDPGenState(tApp.AppCodec(), "ukava", "kava", sdk.NewDec(2)), ) diff --git a/x/community/migrations/v2/store.go b/x/community/migrations/v2/store.go index 7827b3d8..34907dfd 100644 --- a/x/community/migrations/v2/store.go +++ b/x/community/migrations/v2/store.go @@ -11,10 +11,6 @@ import ( "github.com/kava-labs/kava/x/community/types" ) -const ( - ModuleName = "mint" -) - // Migrate migrates the x/community module state from the consensus version 1 to // version 2. Specifically, sets new parameters in the module state. func Migrate( diff --git a/x/community/testutil/consolidate.go b/x/community/testutil/consolidate.go new file mode 100644 index 00000000..51ec6a5f --- /dev/null +++ b/x/community/testutil/consolidate.go @@ -0,0 +1,180 @@ +package testutil + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/kava-labs/kava/app" + types "github.com/kava-labs/kava/x/community/types" + kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" +) + +func (suite *disableInflationTestSuite) TestStartCommunityFundConsolidation() { + tests := []struct { + name string + initialFeePoolCoins sdk.DecCoins + initialKavadistBalance sdk.Coins + }{ + { + "basic test with both balances and dust", + sdk.NewDecCoins( + sdk.NewDecCoinFromDec("ukava", sdk.NewDecWithPrec(123456, 2)), + sdk.NewDecCoinFromDec("usdx", sdk.NewDecWithPrec(654321, 3)), + ), + sdk.NewCoins( + sdk.NewInt64Coin("ukava", 10_000), + sdk.NewInt64Coin("usdx", 10_000), + ), + }, + { + "empty x/distribution feepool", + sdk.DecCoins(nil), + sdk.NewCoins( + sdk.NewInt64Coin("ukava", 10_000), + sdk.NewInt64Coin("usdx", 10_000), + ), + }, + { + "empty x/kavadist balance", + sdk.NewDecCoins( + sdk.NewDecCoinFromDec("ukava", sdk.NewDecWithPrec(123456, 2)), + sdk.NewDecCoinFromDec("usdx", sdk.NewDecWithPrec(654321, 3)), + ), + sdk.Coins{}, + }, + { + "both x/distribution feepool and x/kavadist balance empty", + sdk.DecCoins(nil), + sdk.Coins{}, + }, + } + + for _, tc := range tests { + suite.Run(tc.name, func() { + suite.SetupTest() + ak := suite.App.GetAccountKeeper() + + initialFeePool := distrtypes.FeePool{ + CommunityPool: tc.initialFeePoolCoins, + } + + initialFeePoolCoins, initialFeePoolDust := initialFeePool.CommunityPool.TruncateDecimal() + + // More coins than initial feepool/communitypool + fundCoins := sdk.NewCoins( + sdk.NewInt64Coin("ukava", 10_000), + sdk.NewInt64Coin("usdx", 10_000), + ) + + // Always fund x/distribution with enough coins to cover feepool + err := suite.App.FundModuleAccount( + suite.Ctx, + distrtypes.ModuleName, + fundCoins, + ) + suite.NoError(err, "x/distribution account should be funded without error") + + err = suite.App.FundModuleAccount( + suite.Ctx, + kavadisttypes.ModuleName, + tc.initialKavadistBalance, + ) + suite.NoError(err, "x/kavadist account should be funded without error") + + suite.App.GetDistrKeeper().SetFeePool(suite.Ctx, initialFeePool) + + // Ensure the feepool was set before migration + feePoolBefore := suite.App.GetDistrKeeper().GetFeePool(suite.Ctx) + suite.Equal(initialFeePool, feePoolBefore, "initial feepool should be set") + communityBalanceBefore := suite.App.GetCommunityKeeper().GetModuleAccountBalance(suite.Ctx) + + kavadistAcc := ak.GetModuleAccount(suite.Ctx, kavadisttypes.KavaDistMacc) + kavaDistCoinsBefore := suite.App.GetBankKeeper().GetAllBalances(suite.Ctx, kavadistAcc.GetAddress()) + suite.Equal( + tc.initialKavadistBalance, + kavaDistCoinsBefore, + "x/kavadist balance should be funded", + ) + + expectedKavaDistCoins := sdk.NewCoins(sdk.NewCoin("ukava", kavaDistCoinsBefore.AmountOf("ukava"))) + + // ------------- + // Run upgrade + + params, found := suite.Keeper.GetParams(suite.Ctx) + suite.Require().True(found) + params.UpgradeTimeDisableInflation = suite.Ctx.BlockTime().Add(-time.Minute) + suite.Keeper.SetParams(suite.Ctx, params) + + err = suite.Keeper.StartCommunityFundConsolidation(suite.Ctx) + suite.NoError(err, "consolidation should not error") + + // ------------- + // Check results + suite.Run("module balances after consolidation should moved", func() { + feePoolAfter := suite.App.GetDistrKeeper().GetFeePool(suite.Ctx) + suite.Equal( + initialFeePoolDust, + feePoolAfter.CommunityPool, + "x/distribution community pool should be sent to x/community", + ) + + kavaDistCoinsAfter := suite.App.GetBankKeeper().GetAllBalances(suite.Ctx, kavadistAcc.GetAddress()) + suite.Equal( + expectedKavaDistCoins, + kavaDistCoinsAfter, + "x/kavadist balance should ony contain ukava", + ) + + totalExpectedCommunityPoolCoins := communityBalanceBefore. + Add(initialFeePoolCoins...). // x/distribution fee pool + Add(tc.initialKavadistBalance...) // x/kavadist module balance + + communityBalanceAfter := suite.App.GetCommunityKeeper().GetModuleAccountBalance(suite.Ctx) + + // Use .IsAllGTE to avoid types.Coins(nil) vs types.Coins{} mismatch + suite.Truef( + totalExpectedCommunityPoolCoins.IsAllGTE(communityBalanceAfter), + "x/community balance should be increased by the truncated x/distribution community pool, got %s, expected %s", + communityBalanceAfter, + totalExpectedCommunityPoolCoins, + ) + }) + + suite.Run("bank transfer events should be emitted", func() { + communityAcc := ak.GetModuleAccount(suite.Ctx, types.ModuleAccountName) + distributionAcc := ak.GetModuleAccount(suite.Ctx, distrtypes.ModuleName) + kavadistAcc := ak.GetModuleAccount(suite.Ctx, kavadisttypes.KavaDistMacc) + + events := suite.Ctx.EventManager().Events() + + suite.NoError( + app.EventsContains( + events, + sdk.NewEvent( + banktypes.EventTypeTransfer, + sdk.NewAttribute(banktypes.AttributeKeyRecipient, communityAcc.GetAddress().String()), + sdk.NewAttribute(banktypes.AttributeKeySender, distributionAcc.GetAddress().String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, initialFeePoolCoins.String()), + ), + ), + ) + + suite.NoError( + app.EventsContains( + events, + sdk.NewEvent( + banktypes.EventTypeTransfer, + sdk.NewAttribute(banktypes.AttributeKeyRecipient, communityAcc.GetAddress().String()), + sdk.NewAttribute(banktypes.AttributeKeySender, kavadistAcc.GetAddress().String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, kavaDistCoinsBefore.Sub(expectedKavaDistCoins...).String()), + ), + ), + ) + }) + }) + } +} diff --git a/x/community/types/expected_keepers.go b/x/community/types/expected_keepers.go index 4b88ab3a..1e6c95a7 100644 --- a/x/community/types/expected_keepers.go +++ b/x/community/types/expected_keepers.go @@ -3,6 +3,7 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" ) @@ -40,6 +41,8 @@ type DistributionKeeper interface { DistributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) error FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error GetFeePoolCommunityCoins(ctx sdk.Context) sdk.DecCoins + GetFeePool(ctx sdk.Context) distrtypes.FeePool + SetFeePool(ctx sdk.Context, feePool distrtypes.FeePool) } type MintKeeper interface { diff --git a/x/incentive/keeper/msg_server_delegator_test.go b/x/incentive/keeper/msg_server_delegator_test.go index 11729c60..f3d0331f 100644 --- a/x/incentive/keeper/msg_server_delegator_test.go +++ b/x/incentive/keeper/msg_server_delegator_test.go @@ -47,7 +47,7 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimMultiDenom() { // Claim denoms err := suite.DeliverIncentiveMsg(&msg) - suite.NoError(err) + suite.Require().NoError(err) // Check rewards were paid out expectedRewardsHard := c("hard", int64(0.2*float64(2*7*1e6))) @@ -97,7 +97,7 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimSingleDenom() { // Claim rewards err := suite.DeliverIncentiveMsg(&msg) - suite.NoError(err) + suite.Require().NoError(err) // Check rewards were paid out expectedRewards := c("swap", 2*7*1e6) diff --git a/x/incentive/keeper/msg_server_hard_test.go b/x/incentive/keeper/msg_server_hard_test.go index 317ec701..44e5ff71 100644 --- a/x/incentive/keeper/msg_server_hard_test.go +++ b/x/incentive/keeper/msg_server_hard_test.go @@ -40,7 +40,7 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimMultiDenom() { // Claim denoms err := suite.DeliverIncentiveMsg(&msg) - suite.NoError(err) + suite.Require().NoError(err) // Check rewards were paid out expectedRewardsHard := c("hard", int64(0.2*float64(2*7*1e6))) @@ -85,7 +85,7 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimSingleDenom() { // Claim rewards err := suite.DeliverIncentiveMsg(&msg) - suite.NoError(err) + suite.Require().NoError(err) // Check rewards were paid out expectedRewards := c("swap", 2*7*1e6) diff --git a/x/incentive/keeper/msg_server_swap_test.go b/x/incentive/keeper/msg_server_swap_test.go index 7a5b9a12..a66a319f 100644 --- a/x/incentive/keeper/msg_server_swap_test.go +++ b/x/incentive/keeper/msg_server_swap_test.go @@ -130,7 +130,7 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaimMultiDenom() { // Claim rewards err := suite.DeliverIncentiveMsg(&msg) - suite.NoError(err) + suite.Require().NoError(err) // Check rewards were paid out expectedRewardsHard := c("hard", int64(0.2*float64(7*1e6))) @@ -176,7 +176,7 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaimSingleDenom() { // Claim rewards err := suite.DeliverIncentiveMsg(&msg) - suite.NoError(err) + suite.Require().NoError(err) // Check rewards were paid out expectedRewards := c("swap", 7*1e6) diff --git a/x/incentive/keeper/msg_server_usdx_test.go b/x/incentive/keeper/msg_server_usdx_test.go index 55822b72..ae0e5603 100644 --- a/x/incentive/keeper/msg_server_usdx_test.go +++ b/x/incentive/keeper/msg_server_usdx_test.go @@ -32,7 +32,7 @@ func (suite *HandlerTestSuite) TestPayoutUSDXClaim() { // Claim a single denom err = suite.DeliverIncentiveMsg(&msg) - suite.NoError(err) + suite.Require().NoError(err) // Check rewards were paid out expectedRewards := cs(c(types.USDXMintingRewardDenom, 7*1e6)) diff --git a/x/incentive/keeper/rewards_borrow_test.go b/x/incentive/keeper/rewards_borrow_test.go index 767e6ef5..eb6aa81d 100644 --- a/x/incentive/keeper/rewards_borrow_test.go +++ b/x/incentive/keeper/rewards_borrow_test.go @@ -86,7 +86,7 @@ func (suite *BorrowIntegrationTests) TestSingleUserAccumulatesRewardsAfterSyncin }) // User claims all their rewards - suite.NoError(suite.DeliverIncentiveMsg(&msg)) + suite.Require().NoError(suite.DeliverIncentiveMsg(&msg)) // The users has always had 100% of borrows, so they should receive all rewards for the previous two blocks. // Total rewards for each block is block duration * rewards per second diff --git a/x/incentive/keeper/rewards_supply_test.go b/x/incentive/keeper/rewards_supply_test.go index b4775f58..ed51e423 100644 --- a/x/incentive/keeper/rewards_supply_test.go +++ b/x/incentive/keeper/rewards_supply_test.go @@ -89,7 +89,7 @@ func (suite *SupplyIntegrationTests) TestSingleUserAccumulatesRewardsAfterSyncin }) // User claims all their rewards - suite.NoError(suite.DeliverIncentiveMsg(&msg)) + suite.Require().NoError(suite.DeliverIncentiveMsg(&msg)) // The users has always had 100% of deposits, so they should receive all rewards for the previous two blocks. // Total rewards for each block is block duration * rewards per second diff --git a/x/incentive/keeper/rewards_usdx_test.go b/x/incentive/keeper/rewards_usdx_test.go index e7561f09..646feb62 100644 --- a/x/incentive/keeper/rewards_usdx_test.go +++ b/x/incentive/keeper/rewards_usdx_test.go @@ -94,7 +94,7 @@ func (suite *USDXIntegrationTests) TestSingleUserAccumulatesRewardsAfterSyncing( // User claims all their rewards msg := types.NewMsgClaimUSDXMintingReward(userA.String(), "large") - suite.NoError(suite.DeliverIncentiveMsg(&msg)) + suite.Require().NoError(suite.DeliverIncentiveMsg(&msg)) // The users has always had 100% of cdp debt, so they should receive all rewards for the previous two blocks. // Total rewards for each block is block duration * rewards per second @@ -141,7 +141,7 @@ func (suite *USDXIntegrationTests) TestSingleUserAccumulatesRewardsWithoutSyncin suite.NextBlockAfter(1e6 * time.Second) msg := types.NewMsgClaimUSDXMintingReward(user.String(), "large") - suite.NoError(suite.DeliverIncentiveMsg(&msg)) + suite.Require().NoError(suite.DeliverIncentiveMsg(&msg)) // The users has always had 100% of cdp debt, so they should receive all rewards for the previous two blocks. // Total rewards for each block is block duration * rewards per second @@ -217,7 +217,7 @@ func (suite *USDXIntegrationTests) TestReinstatingRewardParamsDoesNotTriggerOver // Claim rewards msg := types.NewMsgClaimUSDXMintingReward(userB.String(), "large") - suite.NoError(suite.DeliverIncentiveMsg(&msg)) + suite.Require().NoError(suite.DeliverIncentiveMsg(&msg)) // The cdp had half the total borrows for a 1s block. So should earn half the rewards for that block suite.BalanceInEpsilon(