diff --git a/x/community/abci.go b/x/community/abci.go deleted file mode 100644 index 1a4c88c0..00000000 --- a/x/community/abci.go +++ /dev/null @@ -1,12 +0,0 @@ -package community - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/kava-labs/kava/x/community/keeper" - abci "github.com/tendermint/tendermint/abci/types" -) - -// BeginBlocker runs at the start of every block. -func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k keeper.Keeper) { - // TODO: process proposals here -} diff --git a/x/community/client/utils/utils_test.go b/x/community/client/utils/utils_test.go index 9b582d71..1088c832 100644 --- a/x/community/client/utils/utils_test.go +++ b/x/community/client/utils/utils_test.go @@ -61,3 +61,26 @@ func TestParseWithdrawProposal(t *testing.T) { require.Equal(t, "Withdraw some KAVA from community pool to Lend!", proposal.Description) require.Equal(t, expectedAmount, proposal.Amount) } + +func TestParseFileNoExists(t *testing.T) { + cdc := codec.NewAminoCodec(codec.NewLegacyAmino()) + _, err := utils.ParseCommunityPoolLendDepositProposal(cdc, "not-a-file.json") + require.ErrorContains(t, err, "no such file or directory") + _, err = utils.ParseCommunityPoolLendWithdrawProposal(cdc, "not-a-file.json") + require.ErrorContains(t, err, "no such file or directory") +} + +func TestParseFileMalformed(t *testing.T) { + cdc := codec.NewAminoCodec(codec.NewLegacyAmino()) + malformed := testutil.WriteToNewTempFile(t, ` +{ + "title": "I'm malformed b/c there's no closing quote, + "description": "A description", + "amount": [{"denom": "ukava", "amount": "100000000000"}] +} +`) + _, err := utils.ParseCommunityPoolLendDepositProposal(cdc, malformed.Name()) + require.ErrorContains(t, err, "invalid character") + _, err = utils.ParseCommunityPoolLendWithdrawProposal(cdc, malformed.Name()) + require.ErrorContains(t, err, "invalid character") +} diff --git a/x/community/keeper/grpc_query_test.go b/x/community/keeper/grpc_query_test.go index ec2041de..7daaf983 100644 --- a/x/community/keeper/grpc_query_test.go +++ b/x/community/keeper/grpc_query_test.go @@ -12,8 +12,6 @@ import ( "github.com/kava-labs/kava/x/community/types" ) -const legacyCommunityPoolAddr = "kava1jv65s3grqf6v6jl3dp4t6c9t9rk99cd8m2splc" - type grpcQueryTestSuite struct { KeeperTestSuite diff --git a/x/community/keeper/proposal_handler.go b/x/community/keeper/proposal_handler.go index 68470d56..6f6aba10 100644 --- a/x/community/keeper/proposal_handler.go +++ b/x/community/keeper/proposal_handler.go @@ -8,10 +8,31 @@ import ( // HandleCommunityPoolLendDepositProposal is a handler for executing a passed community pool lend deposit proposal. func HandleCommunityPoolLendDepositProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolLendDepositProposal) error { + // move funds from community pool to x/community so hard position is held by this module. + err := k.distrKeeper.DistributeFromFeePool(ctx, p.Amount, k.moduleAddress) + if err != nil { + return err + } + // deposit funds into hard return k.hardKeeper.Deposit(ctx, k.moduleAddress, p.Amount) } // HandleCommunityPoolLendWithdrawProposal is a handler for executing a passed community pool lend withdraw proposal. func HandleCommunityPoolLendWithdrawProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolLendWithdrawProposal) error { - return k.hardKeeper.Withdraw(ctx, k.moduleAddress, p.Amount) + // hard allows attempting to withdraw more funds than there is a deposit for. + // this means the amount that gets withdrawn will not necessarily match the amount set in the proposal. + // to calculate how much is withdrawn, compare this module's balance before & after withdraw. + balanceBefore := k.bankKeeper.GetAllBalances(ctx, k.moduleAddress) + + // withdraw funds from x/hard to this module account + err := k.hardKeeper.Withdraw(ctx, k.moduleAddress, p.Amount) + if err != nil { + return err + } + + balanceAfter := k.bankKeeper.GetAllBalances(ctx, k.moduleAddress) + totalWithdrawn := balanceAfter.Sub(balanceBefore) + + // send all withdrawn coins back to community pool + return k.distrKeeper.FundCommunityPool(ctx, totalWithdrawn, k.moduleAddress) } diff --git a/x/community/keeper/proposal_handler_test.go b/x/community/keeper/proposal_handler_test.go index b6444b22..261789b9 100644 --- a/x/community/keeper/proposal_handler_test.go +++ b/x/community/keeper/proposal_handler_test.go @@ -78,16 +78,11 @@ func (suite *proposalTestSuite) SetupTest() { // give the community pool some funds // ukava - err := suite.App.FundModuleAccount(suite.Ctx, types.ModuleAccountName, ukava(1e10)) - suite.NoError(err) - + suite.FundCommunityPool(ukava(1e10)) // usdx - err = suite.App.FundModuleAccount(suite.Ctx, types.ModuleAccountName, usdx(1e10)) - suite.NoError(err) - + suite.FundCommunityPool(usdx(1e10)) // other-denom - err = suite.App.FundModuleAccount(suite.Ctx, types.ModuleAccountName, otherdenom(1e10)) - suite.NoError(err) + suite.FundCommunityPool(otherdenom(1e10)) } func (suite *proposalTestSuite) NextBlock() { @@ -99,6 +94,27 @@ func (suite *proposalTestSuite) NextBlock() { suite.App.BeginBlocker(suite.Ctx, abcitypes.RequestBeginBlock{}) } +func (suite *proposalTestSuite) FundCommunityPool(coins sdk.Coins) { + // mint to ephemeral account + ephemeralAcc := app.RandomAddress() + suite.NoError(suite.App.FundAccount(suite.Ctx, ephemeralAcc, coins)) + // fund community pool with newly minted coins + suite.NoError(suite.App.GetDistrKeeper().FundCommunityPool(suite.Ctx, coins, ephemeralAcc)) +} + +func (suite *proposalTestSuite) GetCommunityPoolBalance() sdk.Coins { + balance, change := suite.App.GetDistrKeeper().GetFeePoolCommunityCoins(suite.Ctx).TruncateDecimal() + // expect no decimal dust + suite.True(sdk.NewDecCoins().IsEqual(change), "expected no decimal dust in community pool") + return balance +} + +func (suite *proposalTestSuite) CheckCommunityPoolBalance(expected sdk.Coins) { + actual := suite.GetCommunityPoolBalance() + // check that balance is expected + suite.True(expected.IsEqual(actual), fmt.Sprintf("unexpected balance in community pool\nexpected: %s\nactual: %s", expected, actual)) +} + func (suite *proposalTestSuite) TestCommunityLendDepositProposal() { testCases := []struct { name string @@ -148,7 +164,7 @@ func (suite *proposalTestSuite) TestCommunityLendDepositProposal() { Amount: ukava(1e11), }, }, - expectedErr: "insufficient funds", + expectedErr: "community pool does not have sufficient coins to distribute", expectedDeposits: []sdk.Coins{}, }, { @@ -280,7 +296,7 @@ func (suite *proposalTestSuite) TestCommunityLendWithdrawProposal() { suite.NoError(err, "unexpected error while seeding lend deposit") } - beforeBalance := suite.Keeper.GetModuleAccountBalance(suite.Ctx) + beforeBalance := suite.GetCommunityPoolBalance() // run the proposals for i, proposal := range tc.proposals { @@ -306,7 +322,7 @@ func (suite *proposalTestSuite) TestCommunityLendWithdrawProposal() { } // expect funds to be distributed back to community pool - suite.App.CheckBalance(suite.T(), suite.Ctx, suite.MaccAddress, beforeBalance.Add(tc.expectedWithdrawal...)) + suite.CheckCommunityPoolBalance(beforeBalance.Add(tc.expectedWithdrawal...)) }) } } diff --git a/x/community/types/expected_keepers.go b/x/community/types/expected_keepers.go index a268cc76..6ed6dfb2 100644 --- a/x/community/types/expected_keepers.go +++ b/x/community/types/expected_keepers.go @@ -27,5 +27,6 @@ type HardKeeper interface { // DistributionKeeper defines the contract needed to be fulfilled for distribution dependencies. type DistributionKeeper interface { - GetFeePoolCommunityCoins(ctx sdk.Context) sdk.DecCoins + DistributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) error + FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error }