From db326665d9f856d31b263d0cdb4e9eebfabf768b Mon Sep 17 00:00:00 2001 From: Yun Date: Tue, 10 Sep 2019 14:53:29 +0900 Subject: [PATCH] add goverance module and register treasury tax-rate & reward weight update gov proposal --- app/app.go | 25 +- app/export.go | 4 +- app/sim_test.go | 69 +++ cli_test/cli_test.go | 355 ++++++++++++ client/lcd/swagger-ui/swagger.yaml | 611 ++++++++++++++++++++- go.sum | 1 + x/auth/client/cli/estimate_fee.go | 3 +- x/auth/simulation/fake.go | 2 +- x/crisis/module.go | 2 +- x/distribution/internal/types/codec.go | 5 + x/gov/alias.go | 18 + x/gov/cosmos_alias.go | 160 ++++++ x/gov/internal/types/codec.go | 34 ++ x/gov/internal/types/expected_keepers.go | 19 + x/gov/module.go | 139 +++++ x/market/internal/keeper/keeper.go | 6 +- x/market/internal/types/querier.go | 2 +- x/params/internal/types/codec.go | 6 + x/staking/module.go | 2 +- x/treasury/alias.go | 36 +- x/treasury/client/cli/tx.go | 126 +++++ x/treasury/client/cli/utils.go | 59 ++ x/treasury/client/proposal_handler.go | 13 + x/treasury/client/rest/rest.go | 17 + x/treasury/client/rest/tx.go | 61 ++ x/treasury/client/rest/utils.go | 30 + x/treasury/handler.go | 53 ++ x/treasury/internal/types/codec.go | 7 + x/treasury/internal/types/proposal.go | 123 +++++ x/treasury/internal/types/proposal_test.go | 49 ++ x/treasury/proposal_handler_test.go | 48 ++ x/treasury/simulation/msgs.go | 56 ++ 32 files changed, 2111 insertions(+), 30 deletions(-) create mode 100644 x/gov/alias.go create mode 100644 x/gov/cosmos_alias.go create mode 100644 x/gov/internal/types/codec.go create mode 100644 x/gov/internal/types/expected_keepers.go create mode 100644 x/gov/module.go create mode 100644 x/treasury/client/cli/tx.go create mode 100644 x/treasury/client/cli/utils.go create mode 100644 x/treasury/client/proposal_handler.go create mode 100644 x/treasury/client/rest/tx.go create mode 100644 x/treasury/client/rest/utils.go create mode 100644 x/treasury/handler.go create mode 100644 x/treasury/internal/types/proposal.go create mode 100644 x/treasury/internal/types/proposal_test.go create mode 100644 x/treasury/proposal_handler_test.go create mode 100644 x/treasury/simulation/msgs.go diff --git a/app/app.go b/app/app.go index 216e5a7b0..41ba2d571 100644 --- a/app/app.go +++ b/app/app.go @@ -15,6 +15,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/version" + distrclient "github.com/cosmos/cosmos-sdk/x/distribution/client" + paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" + + treasuryclient "github.com/terra-project/core/x/treasury/client" "github.com/terra-project/core/x/auth" "github.com/terra-project/core/x/bank" @@ -22,6 +26,7 @@ import ( distr "github.com/terra-project/core/x/distribution" "github.com/terra-project/core/x/genaccounts" "github.com/terra-project/core/x/genutil" + "github.com/terra-project/core/x/gov" "github.com/terra-project/core/x/market" "github.com/terra-project/core/x/oracle" "github.com/terra-project/core/x/params" @@ -50,6 +55,7 @@ var ( bank.AppModuleBasic{}, staking.AppModuleBasic{}, distr.AppModuleBasic{}, + gov.NewAppModuleBasic(paramsclient.ProposalHandler, distrclient.ProposalHandler, treasuryclient.TaxRateUpdateProposalHandler, treasuryclient.RewardWeightUpdateProposalHandler), params.AppModuleBasic{}, crisis.AppModuleBasic{}, slashing.AppModuleBasic{}, @@ -68,6 +74,7 @@ var ( treasury.ModuleName: {supply.Minter}, staking.BondedPoolName: {supply.Burner, supply.Staking}, staking.NotBondedPoolName: {supply.Burner, supply.Staking}, + gov.ModuleName: {supply.Burner}, } ) @@ -100,6 +107,7 @@ type TerraApp struct { slashingKeeper slashing.Keeper oracleKeeper oracle.Keeper distrKeeper distr.Keeper + govKeeper gov.Keeper crisisKeeper crisis.Keeper paramsKeeper params.Keeper marketKeeper market.Keeper @@ -122,7 +130,7 @@ func NewTerraApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest keys := sdk.NewKVStoreKeys( bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, distr.StoreKey, slashing.StoreKey, - params.StoreKey, oracle.StoreKey, + gov.StoreKey, params.StoreKey, oracle.StoreKey, market.StoreKey, treasury.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(staking.TStoreKey, params.TStoreKey) @@ -142,6 +150,7 @@ func NewTerraApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest stakingSubspace := app.paramsKeeper.Subspace(staking.DefaultParamspace) distrSubspace := app.paramsKeeper.Subspace(distr.DefaultParamspace) slashingSubspace := app.paramsKeeper.Subspace(slashing.DefaultParamspace) + govSubspace := app.paramsKeeper.Subspace(gov.DefaultParamspace) crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace) oracleSubspace := app.paramsKeeper.Subspace(oracle.DefaultParamspace) marketSubspace := app.paramsKeeper.Subspace(market.DefaultParamspace) @@ -166,6 +175,15 @@ func NewTerraApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest app.supplyKeeper, app.marketKeeper, &stakingKeeper, app.distrKeeper, oracle.ModuleName, distr.ModuleName, treasury.DefaultCodespace) + // register the proposal types + govRouter := gov.NewRouter() + govRouter.AddRoute(gov.RouterKey, gov.ProposalHandler). + AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). + AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)). + AddRoute(treasury.RouterKey, treasury.NewTreasuryPolicyUpdateHandler(app.treasuryKeeper)) + app.govKeeper = gov.NewKeeper(app.cdc, keys[gov.StoreKey], app.paramsKeeper, govSubspace, + app.supplyKeeper, &stakingKeeper, gov.DefaultCodespace, govRouter) + // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks app.stakingKeeper = *stakingKeeper.SetHooks( @@ -179,6 +197,7 @@ func NewTerraApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest crisis.NewAppModule(&app.crisisKeeper), supply.NewAppModule(app.supplyKeeper, app.accountKeeper), distr.NewAppModule(app.distrKeeper, app.supplyKeeper), + gov.NewAppModule(app.govKeeper, app.supplyKeeper), slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper), staking.NewAppModule(app.stakingKeeper, app.distrKeeper, app.accountKeeper, app.supplyKeeper), market.NewAppModule(app.marketKeeper), @@ -192,13 +211,13 @@ func NewTerraApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest app.mm.SetOrderBeginBlockers(distr.ModuleName, slashing.ModuleName) // After slashing actions, update prev day issuance of market module - app.mm.SetOrderEndBlockers(crisis.ModuleName, oracle.ModuleName, market.ModuleName, treasury.ModuleName, staking.ModuleName) + app.mm.SetOrderEndBlockers(crisis.ModuleName, oracle.ModuleName, gov.ModuleName, market.ModuleName, treasury.ModuleName, staking.ModuleName) // genutils must occur after staking so that pools are properly // initialized with tokens from genesis accounts. app.mm.SetOrderInitGenesis(genaccounts.ModuleName, distr.ModuleName, staking.ModuleName, auth.ModuleName, bank.ModuleName, slashing.ModuleName, - oracle.ModuleName, market.ModuleName, treasury.ModuleName, + oracle.ModuleName, market.ModuleName, treasury.ModuleName, gov.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName) app.mm.RegisterInvariants(&app.crisisKeeper) diff --git a/app/export.go b/app/export.go index be2f7b994..228aaf72a 100644 --- a/app/export.go +++ b/app/export.go @@ -14,7 +14,7 @@ import ( "github.com/terra-project/core/x/staking" ) -// ExportAppStateAndValidators export the state of terra for a genesis file +// ExportAppStateAndValidators exports the state of terra for a genesis file func (app *TerraApp) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []string, ) (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { @@ -34,7 +34,7 @@ func (app *TerraApp) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteLi return appState, validators, nil } -// prepForZeroHeightGenesis prepare for fresh start at zero height +// prepForZeroHeightGenesis prepares for fresh start at zero height // NOTE zero height genesis is a temporary feature which will be deprecated // in favour of export at a block height func (app *TerraApp) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string) { diff --git a/app/sim_test.go b/app/sim_test.go index d3bd698c3..c41d37bbe 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -26,7 +26,9 @@ import ( distr "github.com/cosmos/cosmos-sdk/x/distribution" distrsim "github.com/cosmos/cosmos-sdk/x/distribution/simulation" "github.com/cosmos/cosmos-sdk/x/gov" + govsim "github.com/cosmos/cosmos-sdk/x/gov/simulation" "github.com/cosmos/cosmos-sdk/x/params" + paramsim "github.com/cosmos/cosmos-sdk/x/params/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/cosmos/cosmos-sdk/x/slashing" slashingsim "github.com/cosmos/cosmos-sdk/x/slashing/simulation" @@ -41,6 +43,7 @@ import ( "github.com/terra-project/core/x/oracle" oraclesim "github.com/terra-project/core/x/oracle/simulation" "github.com/terra-project/core/x/treasury" + treasurysim "github.com/terra-project/core/x/treasury/simulation" ) func init() { @@ -252,6 +255,72 @@ func testAndRunTxs(app *TerraApp) []simulation.WeightedOperation { }(nil), distrsim.SimulateMsgWithdrawValidatorCommission(app.accountKeeper, app.distrKeeper), }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(cdc, OpWeightSubmitVotingSlashingTextProposal, &v, nil, + func(_ *rand.Rand) { + v = 5 + }) + return v + }(nil), + govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, govsim.SimulateTextProposalContent), + }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(cdc, OpWeightSubmitVotingSlashingCommunitySpendProposal, &v, nil, + func(_ *rand.Rand) { + v = 5 + }) + return v + }(nil), + govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, distrsim.SimulateCommunityPoolSpendProposalContent(app.distrKeeper)), + }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(cdc, OpWeightSubmitVotingSlashingParamChangeProposal, &v, nil, + func(_ *rand.Rand) { + v = 5 + }) + return v + }(nil), + govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, paramsim.SimulateParamChangeProposalContent), + }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(cdc, OpWeightSubmitVotingSlashingTaxRateUpdateProposal, &v, nil, + func(_ *rand.Rand) { + v = 5 + }) + return v + }(nil), + govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, treasurysim.SimulateTaxRateUpdateProposalContent(app.treasuryKeeper)), + }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(cdc, OpWeightSubmitVotingSlashingRewardWeightUpdateProposal, &v, nil, + func(_ *rand.Rand) { + v = 5 + }) + return v + }(nil), + govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, treasurysim.SimulateRewardWeightUpdateProposalContent(app.treasuryKeeper)), + }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(cdc, OpWeightMsgDeposit, &v, nil, + func(_ *rand.Rand) { + v = 100 + }) + return v + }(nil), + govsim.SimulateMsgDeposit(app.govKeeper), + }, { func(_ *rand.Rand) int { var v int diff --git a/cli_test/cli_test.go b/cli_test/cli_test.go index 9aa0c1e0b..34a3929ae 100644 --- a/cli_test/cli_test.go +++ b/cli_test/cli_test.go @@ -29,6 +29,7 @@ import ( distr "github.com/terra-project/core/x/distribution" "github.com/terra-project/core/x/genaccounts" + "github.com/terra-project/core/x/gov" ) func init() { @@ -480,6 +481,360 @@ func TestTerraCLIQuerySupply(t *testing.T) { f.Cleanup() } +func TestTerraCLISubmitProposal(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // start terrad server + proc := f.TDStart() + defer proc.Stop(false) + + f.QueryGovParamDeposit() + f.QueryGovParamVoting() + f.QueryGovParamTallying() + + fooAddr := f.KeyAddress(keyFoo) + + fooAcc := f.QueryAccount(fooAddr) + startTokens := sdk.TokensFromConsensusPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(core.MicroLunaDenom)) + + proposalsQuery := f.QueryGovProposals() + require.Empty(t, proposalsQuery) + + // Test submit generate only for submit proposal + proposalTokens := sdk.TokensFromConsensusPower(5) + success, stdOut, stderr := f.TxGovSubmitProposal( + fooAddr.String(), "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "--generate-only", "-y") + require.True(t, success) + require.Empty(t, stderr) + msg := unmarshalStdTx(t, stdOut) + require.NotZero(t, msg.Fee.Gas) + require.Equal(t, len(msg.Msgs), 1) + require.Equal(t, 0, len(msg.GetSignatures())) + + // Test --dry-run + success, _, _ = f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "--dry-run") + require.True(t, success) + + // Create the proposal + f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "-y") + tests.WaitForNextNBlocksTM(1, f.Port) + + // Ensure transaction tags can be queried + searchResult := f.QueryTxs(1, 50, "message.action:submit_proposal", fmt.Sprintf("message.sender:%s", fooAddr)) + require.Len(t, searchResult.Txs, 1) + + // Ensure deposit was deducted + fooAcc = f.QueryAccount(fooAddr) + require.Equal(t, startTokens.Sub(proposalTokens), fooAcc.GetCoins().AmountOf(denom)) + + // Ensure propsal is directly queryable + proposal1 := f.QueryGovProposal(1) + require.Equal(t, uint64(1), proposal1.ProposalID) + require.Equal(t, gov.StatusDepositPeriod, proposal1.Status) + + // Ensure query proposals returns properly + proposalsQuery = f.QueryGovProposals() + require.Equal(t, uint64(1), proposalsQuery[0].ProposalID) + + // Query the deposits on the proposal + deposit := f.QueryGovDeposit(1, fooAddr) + require.Equal(t, proposalTokens, deposit.Amount.AmountOf(denom)) + + // Test deposit generate only + depositTokens := sdk.TokensFromConsensusPower(10) + success, stdOut, stderr = f.TxGovDeposit(1, fooAddr.String(), sdk.NewCoin(denom, depositTokens), "--generate-only") + require.True(t, success) + require.Empty(t, stderr) + msg = unmarshalStdTx(t, stdOut) + require.NotZero(t, msg.Fee.Gas) + require.Equal(t, len(msg.Msgs), 1) + require.Equal(t, 0, len(msg.GetSignatures())) + + // Run the deposit transaction + f.TxGovDeposit(1, keyFoo, sdk.NewCoin(denom, depositTokens), "-y") + tests.WaitForNextNBlocksTM(1, f.Port) + + // test query deposit + deposits := f.QueryGovDeposits(1) + require.Len(t, deposits, 1) + require.Equal(t, proposalTokens.Add(depositTokens), deposits[0].Amount.AmountOf(denom)) + + // Ensure querying the deposit returns the proper amount + deposit = f.QueryGovDeposit(1, fooAddr) + require.Equal(t, proposalTokens.Add(depositTokens), deposit.Amount.AmountOf(denom)) + + // Ensure tags are set on the transaction + searchResult = f.QueryTxs(1, 50, "message.action:deposit", fmt.Sprintf("message.sender:%s", fooAddr)) + require.Len(t, searchResult.Txs, 1) + + // Ensure account has expected amount of funds + fooAcc = f.QueryAccount(fooAddr) + require.Equal(t, startTokens.Sub(proposalTokens.Add(depositTokens)), fooAcc.GetCoins().AmountOf(denom)) + + // Fetch the proposal and ensure it is now in the voting period + proposal1 = f.QueryGovProposal(1) + require.Equal(t, uint64(1), proposal1.ProposalID) + require.Equal(t, gov.StatusVotingPeriod, proposal1.Status) + + // Test vote generate only + success, stdOut, stderr = f.TxGovVote(1, gov.OptionYes, fooAddr.String(), "--generate-only") + require.True(t, success) + require.Empty(t, stderr) + msg = unmarshalStdTx(t, stdOut) + require.NotZero(t, msg.Fee.Gas) + require.Equal(t, len(msg.Msgs), 1) + require.Equal(t, 0, len(msg.GetSignatures())) + + // Vote on the proposal + f.TxGovVote(1, gov.OptionYes, keyFoo, "-y") + tests.WaitForNextNBlocksTM(1, f.Port) + + // Query the vote + vote := f.QueryGovVote(1, fooAddr) + require.Equal(t, uint64(1), vote.ProposalID) + require.Equal(t, gov.OptionYes, vote.Option) + + // Query the votes + votes := f.QueryGovVotes(1) + require.Len(t, votes, 1) + require.Equal(t, uint64(1), votes[0].ProposalID) + require.Equal(t, gov.OptionYes, votes[0].Option) + + // Ensure tags are applied to voting transaction properly + searchResult = f.QueryTxs(1, 50, "message.action:vote", fmt.Sprintf("message.sender:%s", fooAddr)) + require.Len(t, searchResult.Txs, 1) + + // Ensure no proposals in deposit period + proposalsQuery = f.QueryGovProposals("--status=DepositPeriod") + require.Empty(t, proposalsQuery) + + // Ensure the proposal returns as in the voting period + proposalsQuery = f.QueryGovProposals("--status=VotingPeriod") + require.Equal(t, uint64(1), proposalsQuery[0].ProposalID) + + // submit a second test proposal + f.TxGovSubmitProposal(keyFoo, "Text", "Apples", "test", sdk.NewCoin(denom, proposalTokens), "-y") + tests.WaitForNextNBlocksTM(1, f.Port) + + // Test limit on proposals query + proposalsQuery = f.QueryGovProposals("--limit=1") + require.Equal(t, uint64(2), proposalsQuery[0].ProposalID) + + f.Cleanup() +} + +func TestTerraCLISubmitParamChangeProposal(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + proc := f.TDStart() + defer proc.Stop(false) + + fooAddr := f.KeyAddress(keyFoo) + fooAcc := f.QueryAccount(fooAddr) + startTokens := sdk.TokensFromConsensusPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(core.MicroLunaDenom)) + + // write proposal to file + proposalTokens := sdk.TokensFromConsensusPower(5) + proposal := fmt.Sprintf(`{ + "title": "Param Change", + "description": "Update max validators", + "changes": [ + { + "subspace": "staking", + "key": "MaxValidators", + "value": 105 + } + ], + "deposit": [ + { + "denom": "uluna", + "amount": "%s" + } + ] +} +`, proposalTokens.String()) + + proposalFile := WriteToNewTempFile(t, proposal) + + // create the param change proposal + f.TxGovSubmitParamChangeProposal(keyFoo, proposalFile.Name(), sdk.NewCoin(denom, proposalTokens), "-y") + tests.WaitForNextNBlocksTM(1, f.Port) + + // ensure transaction tags can be queried + txsPage := f.QueryTxs(1, 50, "message.action:submit_proposal", fmt.Sprintf("message.sender:%s", fooAddr)) + require.Len(t, txsPage.Txs, 1) + + // ensure deposit was deducted + fooAcc = f.QueryAccount(fooAddr) + require.Equal(t, startTokens.Sub(proposalTokens).String(), fooAcc.GetCoins().AmountOf(core.MicroLunaDenom).String()) + + // ensure proposal is directly queryable + proposal1 := f.QueryGovProposal(1) + require.Equal(t, uint64(1), proposal1.ProposalID) + require.Equal(t, gov.StatusDepositPeriod, proposal1.Status) + + // ensure correct query proposals result + proposalsQuery := f.QueryGovProposals() + require.Equal(t, uint64(1), proposalsQuery[0].ProposalID) + + // ensure the correct deposit amount on the proposal + deposit := f.QueryGovDeposit(1, fooAddr) + require.Equal(t, proposalTokens, deposit.Amount.AmountOf(denom)) + + // Cleanup testing directories + f.Cleanup() +} + +// func TestTerraCLISubmitBudgetGrantProposal(t *testing.T) { +// t.Parallel() +// f := InitFixtures(t) + +// proc := f.TDStart() +// defer proc.Stop(false) + +// fooAddr := f.KeyAddress(keyFoo) +// fooAcc := f.QueryAccount(fooAddr) +// startTokens := sdk.TokensFromConsensusPower(50) +// require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(core.MicroLunaDenom)) + +// tests.WaitForNextNBlocksTM(3, f.Port) + +// // write proposal to file +// proposalTokens := sdk.TokensFromConsensusPower(5) +// proposal := fmt.Sprintf(`{ +// "title": "Budget Grant Proposal", +// "description": "Grant coins to contributor", +// "executor": "%s", +// "grant_amount": [ +// { +// "denom": "%s", +// "amount": "1" +// } +// ], +// "deposit": [ +// { +// "denom": "%s", +// "amount": "%s" +// } +// ] +// } +// `, fooAddr, core.MicroLunaDenom, core.MicroLunaDenom, proposalTokens.String()) +// proposalFile := WriteToNewTempFile(t, proposal) + +// // create the param change proposal +// f.TxGovSubmitBudgetGrantProposal(keyFoo, proposalFile.Name(), sdk.NewCoin(denom, proposalTokens), "-y") +// tests.WaitForNextNBlocksTM(1, f.Port) + +// // ensure transaction tags can be queried +// txsPage := f.QueryTxs(1, 50, "message.action:submit_proposal", fmt.Sprintf("message.sender:%s", fooAddr)) +// require.Len(t, txsPage.Txs, 1) + +// // ensure deposit was deducted +// fooAcc = f.QueryAccount(fooAddr) +// require.Equal(t, startTokens.Sub(proposalTokens).String(), fooAcc.GetCoins().AmountOf(core.MicroLunaDenom).String()) + +// // ensure proposal is directly queryable +// proposal1 := f.QueryGovProposal(1) +// require.Equal(t, uint64(1), proposal1.ProposalID) +// require.Equal(t, gov.StatusDepositPeriod, proposal1.Status) + +// // ensure correct query proposals result +// proposalsQuery := f.QueryGovProposals() +// require.Equal(t, uint64(1), proposalsQuery[0].ProposalID) + +// // ensure the correct deposit amount on the proposal +// deposit := f.QueryGovDeposit(1, fooAddr) +// require.Equal(t, proposalTokens, deposit.Amount.AmountOf(denom)) + +// // Cleanup testing directories +// f.Cleanup() +// } + +func TestTerraCLISubmitCommunityPoolSpendProposal(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // create some inflation + cdc := app.MakeCodec() + genesisState := f.GenesisState() + var distrData distr.GenesisState + cdc.UnmarshalJSON(genesisState[distr.ModuleName], &distrData) + distrData.FeePool.CommunityPool = sdk.DecCoins{sdk.NewDecCoinFromDec(core.MicroLunaDenom, sdk.NewDec(1))} + distrDataBz, err := cdc.MarshalJSON(distrData) + require.NoError(t, err) + genesisState[distr.ModuleName] = distrDataBz + + genFile := filepath.Join(f.TerradHome, "config", "genesis.json") + genDoc, err := tmtypes.GenesisDocFromFile(genFile) + require.NoError(t, err) + genDoc.AppState, err = cdc.MarshalJSON(genesisState) + require.NoError(t, genDoc.SaveAs(genFile)) + + proc := f.TDStart() + defer proc.Stop(false) + + fooAddr := f.KeyAddress(keyFoo) + fooAcc := f.QueryAccount(fooAddr) + startTokens := sdk.TokensFromConsensusPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(core.MicroLunaDenom)) + + tests.WaitForNextNBlocksTM(3, f.Port) + + // write proposal to file + proposalTokens := sdk.TokensFromConsensusPower(5) + proposal := fmt.Sprintf(`{ + "title": "Community Pool Spend", + "description": "Spend from community pool", + "recipient": "%s", + "amount": [ + { + "denom": "%s", + "amount": "1" + } + ], + "deposit": [ + { + "denom": "%s", + "amount": "%s" + } + ] +} +`, fooAddr, core.MicroLunaDenom, core.MicroLunaDenom, proposalTokens.String()) + proposalFile := WriteToNewTempFile(t, proposal) + + // create the param change proposal + f.TxGovSubmitCommunityPoolSpendProposal(keyFoo, proposalFile.Name(), sdk.NewCoin(denom, proposalTokens), "-y") + tests.WaitForNextNBlocksTM(1, f.Port) + + // ensure transaction tags can be queried + txsPage := f.QueryTxs(1, 50, "message.action:submit_proposal", fmt.Sprintf("message.sender:%s", fooAddr)) + require.Len(t, txsPage.Txs, 1) + + // ensure deposit was deducted + fooAcc = f.QueryAccount(fooAddr) + require.Equal(t, startTokens.Sub(proposalTokens).String(), fooAcc.GetCoins().AmountOf(core.MicroLunaDenom).String()) + + // ensure proposal is directly queryable + proposal1 := f.QueryGovProposal(1) + require.Equal(t, uint64(1), proposal1.ProposalID) + require.Equal(t, gov.StatusDepositPeriod, proposal1.Status) + + // ensure correct query proposals result + proposalsQuery := f.QueryGovProposals() + require.Equal(t, uint64(1), proposalsQuery[0].ProposalID) + + // ensure the correct deposit amount on the proposal + deposit := f.QueryGovDeposit(1, fooAddr) + require.Equal(t, proposalTokens, deposit.Amount.AmountOf(denom)) + + // Cleanup testing directories + f.Cleanup() +} + func TestTerraCLIQueryTxPagination(t *testing.T) { t.Parallel() f := InitFixtures(t) diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index fe12ad054..e534bdde0 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -15,6 +15,8 @@ tags: description: Create and broadcast transactions - name: Staking description: Stake module APIs + - name: Governance + description: Governance module APIs - name: Slashing description: Slashing module APIs - name: Distribution @@ -1116,6 +1118,611 @@ paths: type: string 500: description: Internal Server Error + /gov/proposals: + post: + summary: Submit a proposal + description: Send transaction to submit a proposal + consumes: + - application/json + produces: + - application/json + tags: + - Governance + parameters: + - description: valid value of `"proposal_type"` can be `"text"`, `"parameter_change"`, `"software_upgrade"` + name: post_proposal_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + title: + type: string + description: + type: string + proposal_type: + type: string + example: "text" + proposer: + $ref: "#/definitions/Address" + initial_deposit: + type: array + items: + $ref: "#/definitions/Coin" + responses: + 200: + description: Tx was succesfully generated + schema: + $ref: "#/definitions/StdTx" + 400: + description: Invalid proposal body + 500: + description: Internal Server Error + get: + summary: Query proposals + description: Query proposals information with parameters + produces: + - application/json + tags: + - Governance + parameters: + - in: query + name: voter + description: voter address + required: false + type: string + - in: query + name: depositor + description: depositor address + required: false + type: string + - in: query + name: status + description: proposal status, valid values can be `"deposit_period"`, `"voting_period"`, `"passed"`, `"rejected"` + required: false + type: string + responses: + 200: + description: OK + schema: + type: array + items: + $ref: "#/definitions/TextProposal" + 400: + description: Invalid query parameters + 500: + description: Internal Server Error + /gov/proposals/param_change: + post: + summary: Generate a parameter change proposal transaction + description: Generate a parameter change proposal transaction + consumes: + - application/json + produces: + - application/json + tags: + - Governance + parameters: + - description: The parameter change proposal body that contains all parameter changes + name: post_proposal_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + title: + type: string + x-example: "Param Change" + description: + type: string + x-example: "Update max validators" + proposer: + $ref: "#/definitions/Address" + deposit: + type: array + items: + $ref: "#/definitions/Coin" + changes: + type: array + items: + $ref: "#/definitions/ParamChange" + responses: + 200: + description: The transaction was succesfully generated + schema: + $ref: "#/definitions/StdTx" + 400: + description: Invalid proposal body + 500: + description: Internal Server Error + /gov/proposals/community_pool_spend: + post: + summary: Grant community pool coins to contributor + description: Generate a community pool spend transaction + consumes: + - application/json + produces: + - application/json + tags: + - Governance + parameters: + - description: The community pool spend proposal body that contains receipient and reward info + name: post_proposal_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + title: + type: string + x-example: "Param Change" + description: + type: string + x-example: "Update max validators" + proposer: + $ref: "#/definitions/Address" + deposit: + type: array + items: + $ref: "#/definitions/Coin" + recipient: + type: array + items: + $ref: "#/definitions/Address" + amount: + type: array + items: + $ref: "#/definitions/Coin" + responses: + 200: + description: The transaction was succesfully generated + schema: + $ref: "#/definitions/StdTx" + 400: + description: Invalid proposal body + 500: + description: Internal Server Error + /gov/proposals/tax_rate_update: + post: + summary: Tax Rate update proposal + description: Generate a tax rate update transaction + consumes: + - application/json + produces: + - application/json + tags: + - Governance + parameters: + - description: The tax rate update body that contains new tax rate info + name: post_proposal_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + title: + type: string + x-example: "Param Change" + description: + type: string + x-example: "Update max validators" + proposer: + $ref: "#/definitions/Address" + deposit: + type: array + items: + $ref: "#/definitions/Coin" + tax_rate: + type: number + format: float + example: "0.12" + responses: + 200: + description: The transaction was succesfully generated + schema: + $ref: "#/definitions/StdTx" + 400: + description: Invalid proposal body + 500: + description: Internal Server Error + /gov/proposals/reward_weight_update: + post: + summary: Reward Weight update proposal + description: Generate a reward weight update transaction + consumes: + - application/json + produces: + - application/json + tags: + - Governance + parameters: + - description: The reward weight update body that contains new reward weight info + name: post_proposal_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + title: + type: string + x-example: "Param Change" + description: + type: string + x-example: "Update max validators" + proposer: + $ref: "#/definitions/Address" + deposit: + type: array + items: + $ref: "#/definitions/Coin" + reward_weight: + type: number + format: float + example: "0.12" + responses: + 200: + description: The transaction was succesfully generated + schema: + $ref: "#/definitions/StdTx" + 400: + description: Invalid proposal body + 500: + description: Internal Server Error + /gov/proposals/{proposalId}: + get: + summary: Query a proposal + description: Query a proposal by id + produces: + - application/json + tags: + - Governance + parameters: + - type: string + name: proposalId + required: true + in: path + x-example: "2" + responses: + 200: + description: OK + schema: + $ref: "#/definitions/TextProposal" + 400: + description: Invalid proposal id + 500: + description: Internal Server Error + /gov/proposals/{proposalId}/proposer: + get: + summary: Query proposer + description: Query for the proposer for a proposal + produces: + - application/json + tags: + - Governance + parameters: + - type: string + name: proposalId + required: true + in: path + x-example: "2" + responses: + 200: + description: OK + schema: + $ref: "#/definitions/Proposer" + 400: + description: Invalid proposal ID + 500: + description: Internal Server Error + /gov/proposals/{proposalId}/deposits: + get: + summary: Query deposits + description: Query deposits by proposalId + produces: + - application/json + tags: + - Governance + parameters: + - type: string + name: proposalId + required: true + in: path + x-example: "2" + responses: + 200: + description: OK + schema: + type: array + items: + $ref: "#/definitions/Deposit" + 400: + description: Invalid proposal id + 500: + description: Internal Server Error + post: + summary: Deposit tokens to a proposal + description: Send transaction to deposit tokens to a proposal + consumes: + - application/json + produces: + - application/json + tags: + - Governance + parameters: + - type: string + description: proposal id + name: proposalId + required: true + in: path + x-example: "2" + - description: "" + name: post_deposit_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + depositor: + $ref: "#/definitions/Address" + amount: + type: array + items: + $ref: "#/definitions/Coin" + responses: + 200: + description: OK + schema: + $ref: "#/definitions/BroadcastTxCommitResult" + 400: + description: Invalid proposal id or deposit body + 401: + description: Key password is wrong + 500: + description: Internal Server Error + /gov/proposals/{proposalId}/deposits/{depositor}: + get: + summary: Query deposit + description: Query deposit by proposalId and depositor address + produces: + - application/json + tags: + - Governance + parameters: + - type: string + description: proposal id + name: proposalId + required: true + in: path + x-example: "2" + - type: string + description: Bech32 depositor address + name: depositor + required: true + in: path + x-example: terra1wg2mlrxdmnnkkykgqg4znky86nyrtc45q336yv + responses: + 200: + description: OK + schema: + $ref: "#/definitions/Deposit" + 400: + description: Invalid proposal id or depositor address + 404: + description: Found no deposit + 500: + description: Internal Server Error + /gov/proposals/{proposalId}/votes: + get: + summary: Query voters + description: Query voters information by proposalId + produces: + - application/json + tags: + - Governance + parameters: + - type: string + description: proposal id + name: proposalId + required: true + in: path + x-example: "2" + responses: + 200: + description: OK + schema: + type: array + items: + $ref: "#/definitions/Vote" + 400: + description: Invalid proposal id + 500: + description: Internal Server Error + post: + summary: Vote a proposal + description: Send transaction to vote a proposal + consumes: + - application/json + produces: + - application/json + tags: + - Governance + parameters: + - type: string + description: proposal id + name: proposalId + required: true + in: path + x-example: "2" + - description: valid value of `"option"` field can be `"yes"`, `"no"`, `"no_with_veto"` and `"abstain"` + name: post_vote_body + in: body + required: true + schema: + type: object + properties: + base_req: + $ref: "#/definitions/BaseReq" + voter: + $ref: "#/definitions/Address" + option: + type: string + example: "yes" + responses: + 200: + description: OK + schema: + $ref: "#/definitions/BroadcastTxCommitResult" + 400: + description: Invalid proposal id or vote body + 401: + description: Key password is wrong + 500: + description: Internal Server Error + /gov/proposals/{proposalId}/votes/{voter}: + get: + summary: Query vote + description: Query vote information by proposal Id and voter address + produces: + - application/json + tags: + - Governance + parameters: + - type: string + description: proposal id + name: proposalId + required: true + in: path + x-example: "2" + - type: string + description: Bech32 voter address + name: voter + required: true + in: path + x-example: terra1wg2mlrxdmnnkkykgqg4znky86nyrtc45q336yv + responses: + 200: + description: OK + schema: + $ref: "#/definitions/Vote" + 400: + description: Invalid proposal id or voter address + 404: + description: Found no vote + 500: + description: Internal Server Error + /gov/proposals/{proposalId}/tally: + get: + summary: Get a proposal's tally result at the current time + description: Gets a proposal's tally result at the current time. If the proposal is pending deposits (i.e status 'DepositPeriod') it returns an empty tally result. + produces: + - application/json + tags: + - Governance + parameters: + - type: string + description: proposal id + name: proposalId + required: true + in: path + x-example: "2" + responses: + 200: + description: OK + schema: + $ref: "#/definitions/TallyResult" + 400: + description: Invalid proposal id + 500: + description: Internal Server Error + /gov/parameters/deposit: + get: + summary: Query governance deposit parameters + description: Query governance deposit parameters. The max_deposit_period units are in nanoseconds. + produces: + - application/json + tags: + - Governance + responses: + 200: + description: OK + schema: + type: object + properties: + min_deposit: + type: array + items: + $ref: "#/definitions/Coin" + max_deposit_period: + type: string + example: "86400000000000" + 400: + description: is not a valid query request path + 404: + description: Found no deposit parameters + 500: + description: Internal Server Error + /gov/parameters/tallying: + get: + summary: Query governance tally parameters + description: Query governance tally parameters + produces: + - application/json + tags: + - Governance + responses: + 200: + description: OK + schema: + properties: + threshold: + type: string + example: "0.5000000000" + veto: + type: string + example: "0.3340000000" + governance_penalty: + type: string + example: "0.0100000000" + 400: + description: is not a valid query request path + 404: + description: Found no tally parameters + 500: + description: Internal Server Error + /gov/parameters/voting: + get: + summary: Query governance voting parameters + description: Query governance voting parameters. The voting_period units are in nanoseconds. + produces: + - application/json + tags: + - Governance + responses: + 200: + description: OK + schema: + properties: + voting_period: + type: string + example: "86400000000000" + 400: + description: is not a valid query request path + 404: + description: Found no voting parameters + 500: + description: Internal Server Error /distribution/delegators/{delegatorAddr}/rewards: parameters: - in: path @@ -2120,7 +2727,7 @@ definitions: properties: denom: type: string - example: uluna + example: stake amount: type: string example: "50.000" @@ -2958,4 +3565,4 @@ definitions: gas: type: number format: integer - example: 10000 + example: 10000 \ No newline at end of file diff --git a/go.sum b/go.sum index 9be086de2..0f2dd3047 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,7 @@ github.com/google/go-github/v27 v27.0.6/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/ github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= diff --git a/x/auth/client/cli/estimate_fee.go b/x/auth/client/cli/estimate_fee.go index 7cb41ea98..87681731f 100644 --- a/x/auth/client/cli/estimate_fee.go +++ b/x/auth/client/cli/estimate_fee.go @@ -20,7 +20,7 @@ func GetTxFeesEstimateCommand(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "estimate-fee [file]", Args: cobra.ExactArgs(1), - Short: "Estimate the required fee (stability + gas) and gas amount", + Short: "Estimate required fee (stability + gas) and gas amount", Long: strings.TrimSpace(` Estimate fees for the given stdTx @@ -59,6 +59,7 @@ $ terracli tx estimate-fee [file] --gas-adjustment 1.4 --gas-prices 0.015uluna cmd.Flags().Float64(client.FlagGasAdjustment, client.DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored ") cmd.Flags().String(client.FlagGasPrices, "", "Gas prices to determine the transaction fee (e.g. 10uluna)") + // cmd.MarkFlagRequired(client.FlagGasAdjustment) return cmd } diff --git a/x/auth/simulation/fake.go b/x/auth/simulation/fake.go index 7c62435ab..262176e4d 100644 --- a/x/auth/simulation/fake.go +++ b/x/auth/simulation/fake.go @@ -27,7 +27,7 @@ func SimulateDeductFee(ak auth.AccountKeeper, supplyKeeper types.SupplyKeeper) s // NOTE - terra does not have Mint module, so feeCollector account will not exist here // feeCollector := ak.GetAccount(ctx, supplyKeeper.GetModuleAddress(types.FeeCollectorName)) // if feeCollector == nil { - // panic(fmt.Errorf("fee collector account hasn't been set")) + // panic(fmt.Errorf("fee collector account hasn't been set")) // } if len(initCoins) == 0 { diff --git a/x/crisis/module.go b/x/crisis/module.go index dc5ae9f31..53743585e 100644 --- a/x/crisis/module.go +++ b/x/crisis/module.go @@ -39,7 +39,7 @@ func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { // default genesis state func (AppModuleBasic) DefaultGenesis() json.RawMessage { - // customize to set default genesis state constant fee denom to luna + // customize to set default genesis state constant fee denom to uluna defaultGenesisState := DefaultGenesisState() defaultGenesisState.ConstantFee.Denom = core.MicroLunaDenom diff --git a/x/distribution/internal/types/codec.go b/x/distribution/internal/types/codec.go index e8fa2d37b..8fc5f94fe 100644 --- a/x/distribution/internal/types/codec.go +++ b/x/distribution/internal/types/codec.go @@ -3,6 +3,8 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/x/distribution" + + "github.com/terra-project/core/x/gov" ) // Register concrete types on codec codec @@ -10,6 +12,7 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(distribution.MsgWithdrawDelegatorReward{}, "distribution/MsgWithdrawDelegationReward", nil) cdc.RegisterConcrete(distribution.MsgWithdrawValidatorCommission{}, "distribution/MsgWithdrawValidatorCommission", nil) cdc.RegisterConcrete(distribution.MsgSetWithdrawAddress{}, "distribution/MsgModifyWithdrawAddress", nil) + cdc.RegisterConcrete(distribution.CommunityPoolSpendProposal{}, "distribution/CommunityPoolSpendProposal", nil) } // generic sealed codec to be used throughout module @@ -20,4 +23,6 @@ func init() { RegisterCodec(ModuleCdc) codec.RegisterCrypto(ModuleCdc) ModuleCdc.Seal() + + gov.RegisterProposalTypeCodec(distribution.CommunityPoolSpendProposal{}, "distribution/CommunityPoolSpendProposal") } diff --git a/x/gov/alias.go b/x/gov/alias.go new file mode 100644 index 000000000..05d15a87e --- /dev/null +++ b/x/gov/alias.go @@ -0,0 +1,18 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/terra-project/core/x/crisis/internal/types/ +package gov + +import ( + "github.com/terra-project/core/x/gov/internal/types" +) + +var ( + // functions aliases + RegisterCodec = types.RegisterCodec + RegisterProposalTypeCodec = types.RegisterProposalTypeCodec + + // variable aliases + ModuleCdc = types.ModuleCdc +) diff --git a/x/gov/cosmos_alias.go b/x/gov/cosmos_alias.go new file mode 100644 index 000000000..e12497e6b --- /dev/null +++ b/x/gov/cosmos_alias.go @@ -0,0 +1,160 @@ +// nolint +package gov + +import ( + "github.com/cosmos/cosmos-sdk/x/gov" +) + +const ( + MaxDescriptionLength = gov.MaxDescriptionLength + MaxTitleLength = gov.MaxTitleLength + DefaultCodespace = gov.DefaultCodespace + CodeUnknownProposal = gov.CodeUnknownProposal + CodeInactiveProposal = gov.CodeInactiveProposal + CodeAlreadyActiveProposal = gov.CodeAlreadyActiveProposal + CodeAlreadyFinishedProposal = gov.CodeAlreadyFinishedProposal + CodeAddressNotStaked = gov.CodeAddressNotStaked + CodeInvalidContent = gov.CodeInvalidContent + CodeInvalidProposalType = gov.CodeInvalidProposalType + CodeInvalidVote = gov.CodeInvalidVote + CodeInvalidGenesis = gov.CodeInvalidGenesis + CodeInvalidProposalStatus = gov.CodeInvalidProposalStatus + CodeProposalHandlerNotExists = gov.CodeProposalHandlerNotExists + ModuleName = gov.ModuleName + StoreKey = gov.StoreKey + RouterKey = gov.RouterKey + QuerierRoute = gov.QuerierRoute + DefaultParamspace = gov.DefaultParamspace + TypeMsgDeposit = gov.TypeMsgDeposit + TypeMsgVote = gov.TypeMsgVote + TypeMsgSubmitProposal = gov.TypeMsgSubmitProposal + StatusNil = gov.StatusNil + StatusDepositPeriod = gov.StatusDepositPeriod + StatusVotingPeriod = gov.StatusVotingPeriod + StatusPassed = gov.StatusPassed + StatusRejected = gov.StatusRejected + StatusFailed = gov.StatusFailed + ProposalTypeText = gov.ProposalTypeText + ProposalTypeSoftwareUpgrade = gov.ProposalTypeSoftwareUpgrade + QueryParams = gov.QueryParams + QueryProposals = gov.QueryProposals + QueryProposal = gov.QueryProposal + QueryDeposits = gov.QueryDeposits + QueryDeposit = gov.QueryDeposit + QueryVotes = gov.QueryVotes + QueryVote = gov.QueryVote + QueryTally = gov.QueryTally + ParamDeposit = gov.ParamDeposit + ParamVoting = gov.ParamVoting + ParamTallying = gov.ParamTallying + OptionEmpty = gov.OptionEmpty + OptionYes = gov.OptionYes + OptionAbstain = gov.OptionAbstain + OptionNo = gov.OptionNo + OptionNoWithVeto = gov.OptionNoWithVeto +) + +var ( + // functions aliases + ValidateAbstract = gov.ValidateAbstract + NewDeposit = gov.NewDeposit + ErrUnknownProposal = gov.ErrUnknownProposal + ErrInactiveProposal = gov.ErrInactiveProposal + ErrAlreadyActiveProposal = gov.ErrAlreadyActiveProposal + ErrAlreadyFinishedProposal = gov.ErrAlreadyFinishedProposal + ErrAddressNotStaked = gov.ErrAddressNotStaked + ErrInvalidProposalContent = gov.ErrInvalidProposalContent + ErrInvalidProposalType = gov.ErrInvalidProposalType + ErrInvalidVote = gov.ErrInvalidVote + ErrInvalidGenesis = gov.ErrInvalidGenesis + ErrNoProposalHandlerExists = gov.ErrNoProposalHandlerExists + ProposalKey = gov.ProposalKey + ActiveProposalByTimeKey = gov.ActiveProposalByTimeKey + ActiveProposalQueueKey = gov.ActiveProposalQueueKey + InactiveProposalByTimeKey = gov.InactiveProposalByTimeKey + InactiveProposalQueueKey = gov.InactiveProposalQueueKey + DefaultGenesisState = gov.DefaultGenesisState + DepositsKey = gov.DepositsKey + DepositKey = gov.DepositKey + VotesKey = gov.VotesKey + VoteKey = gov.VoteKey + SplitProposalKey = gov.SplitProposalKey + SplitActiveProposalQueueKey = gov.SplitActiveProposalQueueKey + SplitInactiveProposalQueueKey = gov.SplitInactiveProposalQueueKey + SplitKeyDeposit = gov.SplitKeyDeposit + SplitKeyVote = gov.SplitKeyVote + NewMsgSubmitProposal = gov.NewMsgSubmitProposal + NewMsgDeposit = gov.NewMsgDeposit + NewMsgVote = gov.NewMsgVote + ParamKeyTable = gov.ParamKeyTable + NewDepositParams = gov.NewDepositParams + NewTallyParams = gov.NewTallyParams + NewVotingParams = gov.NewVotingParams + NewParams = gov.NewParams + NewProposal = gov.NewProposal + ProposalStatusFromString = gov.ProposalStatusFromString + ValidProposalStatus = gov.ValidProposalStatus + NewTallyResult = gov.NewTallyResult + NewTallyResultFromMap = gov.NewTallyResultFromMap + EmptyTallyResult = gov.EmptyTallyResult + NewTextProposal = gov.NewTextProposal + NewSoftwareUpgradeProposal = gov.NewSoftwareUpgradeProposal + RegisterProposalType = gov.RegisterProposalType + ContentFromProposalType = gov.ContentFromProposalType + IsValidProposalType = gov.IsValidProposalType + ProposalHandler = gov.ProposalHandler + NewQueryProposalParams = gov.NewQueryProposalParams + NewQueryDepositParams = gov.NewQueryDepositParams + NewQueryVoteParams = gov.NewQueryVoteParams + NewQueryProposalsParams = gov.NewQueryProposalsParams + NewVote = gov.NewVote + VoteOptionFromString = gov.VoteOptionFromString + ValidVoteOption = gov.ValidVoteOption + NewKeeper = gov.NewKeeper + NewRouter = gov.NewRouter + NewCosmosAppModule = gov.NewAppModule + NewCosmosAppModuleBasic = gov.NewAppModuleBasic + + // variable aliases + CosmosModuleCdc = gov.ModuleCdc + ProposalsKeyPrefix = gov.ProposalsKeyPrefix + ActiveProposalQueuePrefix = gov.ActiveProposalQueuePrefix + InactiveProposalQueuePrefix = gov.InactiveProposalQueuePrefix + ProposalIDKey = gov.ProposalIDKey + DepositsKeyPrefix = gov.DepositsKeyPrefix + VotesKeyPrefix = gov.VotesKeyPrefix + ParamStoreKeyDepositParams = gov.ParamStoreKeyDepositParams + ParamStoreKeyVotingParams = gov.ParamStoreKeyVotingParams + ParamStoreKeyTallyParams = gov.ParamStoreKeyTallyParams +) + +type ( + Content = gov.Content + Keeper = gov.Keeper + Handler = gov.Handler + Deposit = gov.Deposit + Deposits = gov.Deposits + MsgSubmitProposal = gov.MsgSubmitProposal + MsgDeposit = gov.MsgDeposit + MsgVote = gov.MsgVote + DepositParams = gov.DepositParams + TallyParams = gov.TallyParams + VotingParams = gov.VotingParams + Params = gov.Params + Proposal = gov.Proposal + Proposals = gov.Proposals + ProposalQueue = gov.ProposalQueue + ProposalStatus = gov.ProposalStatus + TallyResult = gov.TallyResult + TextProposal = gov.TextProposal + SoftwareUpgradeProposal = gov.SoftwareUpgradeProposal + QueryProposalParams = gov.QueryProposalParams + QueryDepositParams = gov.QueryDepositParams + QueryVoteParams = gov.QueryVoteParams + QueryProposalsParams = gov.QueryProposalsParams + Vote = gov.Vote + Votes = gov.Votes + VoteOption = gov.VoteOption + CosmosAppModule = gov.AppModule + CosmosAppModuleBasic = gov.AppModuleBasic +) diff --git a/x/gov/internal/types/codec.go b/x/gov/internal/types/codec.go new file mode 100644 index 000000000..81e428244 --- /dev/null +++ b/x/gov/internal/types/codec.go @@ -0,0 +1,34 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/gov" +) + +// module codec +var ModuleCdc = codec.New() + +// RegisterCodec registers all the necessary types and interfaces for +// governance. +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*gov.Content)(nil), nil) + + cdc.RegisterConcrete(gov.MsgSubmitProposal{}, "gov/MsgSubmitProposal", nil) + cdc.RegisterConcrete(gov.MsgDeposit{}, "gov/MsgDeposit", nil) + cdc.RegisterConcrete(gov.MsgVote{}, "gov/MsgVote", nil) + + cdc.RegisterConcrete(gov.TextProposal{}, "gov/TextProposal", nil) + cdc.RegisterConcrete(gov.SoftwareUpgradeProposal{}, "gov/SoftwareUpgradeProposal", nil) +} + +// RegisterProposalTypeCodec registers an external proposal content type defined +// in another module for the internal ModuleCdc. This allows the MsgSubmitProposal +// to be correctly Amino encoded and decoded. +func RegisterProposalTypeCodec(o interface{}, name string) { + ModuleCdc.RegisterConcrete(o, name, nil) +} + +// TODO determine a good place to seal this codec +func init() { + RegisterCodec(ModuleCdc) +} diff --git a/x/gov/internal/types/expected_keepers.go b/x/gov/internal/types/expected_keepers.go new file mode 100644 index 000000000..70c3c5162 --- /dev/null +++ b/x/gov/internal/types/expected_keepers.go @@ -0,0 +1,19 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/terra-project/core/x/supply" +) + +// SupplyKeeper defines the supply Keeper for module accounts +type SupplyKeeper interface { + GetModuleAddress(name string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, name string) supply.ModuleAccountI + + // TODO remove with genesis 2-phases refactor https://github.com/cosmos/cosmos-sdk/issues/2862 + SetModuleAccount(sdk.Context, supply.ModuleAccountI) + + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error + BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error +} diff --git a/x/gov/module.go b/x/gov/module.go new file mode 100644 index 000000000..76c4845e1 --- /dev/null +++ b/x/gov/module.go @@ -0,0 +1,139 @@ +package gov + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/gov/client" + + core "github.com/terra-project/core/types" + "github.com/terra-project/core/x/gov/internal/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module basics object +type AppModuleBasic struct { + CosmosAppModuleBasic +} + +// NewAppModuleBasic creates a new AppModuleBasic object +func NewAppModuleBasic(proposalHandlers ...client.ProposalHandler) AppModuleBasic { + return AppModuleBasic{ + CosmosAppModuleBasic: NewCosmosAppModuleBasic(proposalHandlers...), + } +} + +var _ module.AppModuleBasic = AppModuleBasic{} + +// module name +func (am AppModuleBasic) Name() string { + return am.CosmosAppModuleBasic.Name() +} + +// register module codec +func (am AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) + *CosmosModuleCdc = *ModuleCdc // nolint +} + +// default genesis state +func (am AppModuleBasic) DefaultGenesis() json.RawMessage { + // customize to set default genesis state deposit denom to uluna + defaultGenesisState := DefaultGenesisState() + defaultGenesisState.DepositParams.MinDeposit[0].Denom = core.MicroLunaDenom + + return ModuleCdc.MustMarshalJSON(defaultGenesisState) +} + +// module validate genesis +func (am AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + return am.CosmosAppModuleBasic.ValidateGenesis(bz) +} + +// register rest routes +func (am AppModuleBasic) RegisterRESTRoutes(cliCtx context.CLIContext, route *mux.Router) { + am.CosmosAppModuleBasic.RegisterRESTRoutes(cliCtx, route) +} + +// get the root tx command of this module +func (am AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + return am.CosmosAppModuleBasic.GetTxCmd(cdc) +} + +// get the root query command of this module +func (am AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + return am.CosmosAppModuleBasic.GetQueryCmd(cdc) +} + +//___________________________ +// app module for gov +type AppModule struct { + AppModuleBasic + cosmosAppModule CosmosAppModule +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + cosmosAppModule: NewCosmosAppModule(keeper, supplyKeeper), + } +} + +// module name +func (am AppModule) Name() string { + return am.cosmosAppModule.Name() +} + +// register invariants +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + am.cosmosAppModule.RegisterInvariants(ir) +} + +// module querier route name +func (am AppModule) Route() string { + return am.cosmosAppModule.Route() +} + +// module handler +func (am AppModule) NewHandler() sdk.Handler { + return am.cosmosAppModule.NewHandler() +} + +// module querier route name +func (am AppModule) QuerierRoute() string { return am.cosmosAppModule.QuerierRoute() } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { return am.cosmosAppModule.NewQuerierHandler() } + +// module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + return am.cosmosAppModule.InitGenesis(ctx, data) +} + +// module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + return am.cosmosAppModule.ExportGenesis(ctx) +} + +// module begin-block +func (am AppModule) BeginBlock(ctx sdk.Context, rbb abci.RequestBeginBlock) { + am.cosmosAppModule.BeginBlock(ctx, rbb) +} + +// module end-block +func (am AppModule) EndBlock(ctx sdk.Context, rbb abci.RequestEndBlock) []abci.ValidatorUpdate { + return am.cosmosAppModule.EndBlock(ctx, rbb) +} diff --git a/x/market/internal/keeper/keeper.go b/x/market/internal/keeper/keeper.go index 1b505f7ac..db5e8fdc5 100644 --- a/x/market/internal/keeper/keeper.go +++ b/x/market/internal/keeper/keeper.go @@ -80,8 +80,8 @@ func (k Keeper) ComputeLunaDelta(ctx sdk.Context, change sdk.Int) sdk.Dec { return sdk.ZeroDec() } - lastDayLunaIssuance := k.GetPrevDayIssuance(ctx).AmountOf(core.MicroLunaDenom) - if lastDayLunaIssuance.IsZero() { + prevDayLunaIssuance := k.GetPrevDayIssuance(ctx).AmountOf(core.MicroLunaDenom) + if prevDayLunaIssuance.IsZero() { return sdk.ZeroDec() } @@ -90,7 +90,7 @@ func (k Keeper) ComputeLunaDelta(ctx sdk.Context, change sdk.Int) sdk.Dec { postSwapIssunace := lunaIssuance.Add(change) - return sdk.NewDecFromInt(postSwapIssunace.Sub(lastDayLunaIssuance)).QuoInt(lastDayLunaIssuance) + return sdk.NewDecFromInt(postSwapIssunace.Sub(prevDayLunaIssuance)).QuoInt(prevDayLunaIssuance) } // ComputeLunaSwapSpread returns a spread, which is initialiy MinSwapSpread and grows linearly to MaxSwapSpread with delta diff --git a/x/market/internal/types/querier.go b/x/market/internal/types/querier.go index 99164c801..cca9f02bb 100644 --- a/x/market/internal/types/querier.go +++ b/x/market/internal/types/querier.go @@ -7,7 +7,7 @@ import ( // query endpoints supported by the oracle Querier const ( QuerySwap = "swap" - QueryPrevDayIssuance = "lastDayIssuance" + QueryPrevDayIssuance = "prevDayIssuance" QueryParameters = "parameters" ) diff --git a/x/params/internal/types/codec.go b/x/params/internal/types/codec.go index aa1c4a598..72723877a 100644 --- a/x/params/internal/types/codec.go +++ b/x/params/internal/types/codec.go @@ -2,10 +2,14 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/params" + + "github.com/terra-project/core/x/gov" ) // RegisterCodec registers all necessary param module types with a given codec. func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(params.ParameterChangeProposal{}, "params/ParameterChangeProposal", nil) } // module codec @@ -15,4 +19,6 @@ func init() { ModuleCdc = codec.New() RegisterCodec(ModuleCdc) ModuleCdc.Seal() + + gov.RegisterProposalTypeCodec(params.ParameterChangeProposal{}, "params/ParameterChangeProposal") } diff --git a/x/staking/module.go b/x/staking/module.go index f8e7f664a..f281b45dd 100644 --- a/x/staking/module.go +++ b/x/staking/module.go @@ -44,7 +44,7 @@ func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { // default genesis state func (AppModuleBasic) DefaultGenesis() json.RawMessage { - // customize to set default genesis state bond denom to luna + // customize to set default genesis state bond denom to uluna defaultGenesisState := DefaultGenesisState() defaultGenesisState.Params.BondDenom = core.MicroLunaDenom diff --git a/x/treasury/alias.go b/x/treasury/alias.go index 2c04e7055..8cfd9bc0f 100644 --- a/x/treasury/alias.go +++ b/x/treasury/alias.go @@ -11,21 +11,23 @@ import ( ) const ( - DefaultCodespace = types.DefaultCodespace - CodeInvalidEpoch = types.CodeInvalidEpoch - ModuleName = types.ModuleName - StoreKey = types.StoreKey - RouterKey = types.RouterKey - QuerierRoute = types.QuerierRoute - DefaultParamspace = types.DefaultParamspace - QueryCurrentEpoch = types.QueryCurrentEpoch - QueryTaxRate = types.QueryTaxRate - QueryTaxCap = types.QueryTaxCap - QueryRewardWeight = types.QueryRewardWeight - QuerySeigniorageProceeds = types.QuerySeigniorageProceeds - QueryTaxProceeds = types.QueryTaxProceeds - QueryParameters = types.QueryParameters - QueryHistoricalIssuance = types.QueryHistoricalIssuance + DefaultCodespace = types.DefaultCodespace + CodeInvalidEpoch = types.CodeInvalidEpoch + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + ProposalTypeTaxRateUpdate = types.ProposalTypeTaxRateUpdate + ProposalTypeRewardWeightUpdate = types.ProposalTypeRewardWeightUpdate + QueryCurrentEpoch = types.QueryCurrentEpoch + QueryTaxRate = types.QueryTaxRate + QueryTaxCap = types.QueryTaxCap + QueryRewardWeight = types.QueryRewardWeight + QuerySeigniorageProceeds = types.QuerySeigniorageProceeds + QueryTaxProceeds = types.QueryTaxProceeds + QueryParameters = types.QueryParameters + QueryHistoricalIssuance = types.QueryHistoricalIssuance ) var ( @@ -41,6 +43,8 @@ var ( GetTaxProceedsKey = types.GetTaxProceedsKey GetHistoricalIssuanceKey = types.GetHistoricalIssuanceKey DefaultParams = types.DefaultParams + NewTaxRateUpdateProposal = types.NewTaxRateUpdateProposal + NewRewardWeightUpdateProposal = types.NewRewardWeightUpdateProposal NewQueryTaxCapParams = types.NewQueryTaxCapParams NewQueryTaxRateParams = types.NewQueryTaxRateParams NewQueryRewardWeightParams = types.NewQueryRewardWeightParams @@ -93,6 +97,8 @@ type ( DistributionKeeper = types.DistributionKeeper GenesisState = types.GenesisState Params = types.Params + TaxRateUpdateProposal = types.TaxRateUpdateProposal + RewardWeightUpdateProposal = types.RewardWeightUpdateProposal QueryTaxCapParams = types.QueryTaxCapParams QueryTaxRateParams = types.QueryTaxRateParams QueryRewardWeightParams = types.QueryRewardWeightParams diff --git a/x/treasury/client/cli/tx.go b/x/treasury/client/cli/tx.go new file mode 100644 index 000000000..39a8ed604 --- /dev/null +++ b/x/treasury/client/cli/tx.go @@ -0,0 +1,126 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + + "github.com/terra-project/core/x/treasury/internal/types" +) + +// GetCmdSubmitTaxRateUpdateProposal implements the command to submit a tax-rate-update proposal +func GetCmdSubmitTaxRateUpdateProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "tax-rate-update [proposal-file]", + Args: cobra.ExactArgs(1), + Short: "Submit a tax rate update proposal", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a tax rate update proposal along with an initial deposit. +The proposal details must be supplied via a JSON file. + +Example: +$ %s tx treasury submit-proposal tax-rate-update --from= + +Where proposal.json contains: + +{ + "title": "Update Tax Rate", + "description": "Lets update tax rate to 1.5%%", + "tax_rate": "0.015", + "deposit": [ + { + "denom": "stake", + "amount": "10000" + } + ] +} +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + proposal, err := ParseTaxRateUpdateProposalJSON(cdc, args[0]) + if err != nil { + return err + } + + from := cliCtx.GetFromAddress() + content := types.NewTaxRateUpdateProposal(proposal.Title, proposal.Description, proposal.TaxRate) + + msg := gov.NewMsgSubmitProposal(content, proposal.Deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return cmd +} + +// GetCmdSubmitRewardWeightUpdateProposal implements the command to submit a reward-weight-update proposal +func GetCmdSubmitRewardWeightUpdateProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "reward-weight-update [proposal-file]", + Args: cobra.ExactArgs(1), + Short: "Submit a reward weight update proposal", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a reward weight update proposal along with an initial deposit. +The proposal details must be supplied via a JSON file. + +Example: +$ %s tx treasury submit-proposal reward-weight-update --from= + +Where proposal.json contains: + +{ + "title": "Update Tax Rate", + "description": "Lets update reward weight to 1.5%%", + "reward_weight": "0.015", + "deposit": [ + { + "denom": "stake", + "amount": "10000" + } + ] +} +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + proposal, err := ParseRewardWeightUpdateProposalJSON(cdc, args[0]) + if err != nil { + return err + } + + from := cliCtx.GetFromAddress() + content := types.NewRewardWeightUpdateProposal(proposal.Title, proposal.Description, proposal.RewardWeight) + + msg := gov.NewMsgSubmitProposal(content, proposal.Deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return cmd +} diff --git a/x/treasury/client/cli/utils.go b/x/treasury/client/cli/utils.go new file mode 100644 index 000000000..9ee649e33 --- /dev/null +++ b/x/treasury/client/cli/utils.go @@ -0,0 +1,59 @@ +package cli + +import ( + "io/ioutil" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ( + + // TaxRateUpdateProposalJSON defines a TaxRateUpdateProposal with a deposit + TaxRateUpdateProposalJSON struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + TaxRate sdk.Dec `json:"tax_rate" yaml:"tax_rate"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + } + + // RewardWeightUpdateProposalJSON defines a RewardWeightUpdateProposal with a deposit + RewardWeightUpdateProposalJSON struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + RewardWeight sdk.Dec `json:"tax_rate" yaml:"tax_rate"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + } +) + +// ParseTaxRateUpdateProposalJSON reads and parses a TaxRateUpdateProposalJSON from a file. +func ParseTaxRateUpdateProposalJSON(cdc *codec.Codec, proposalFile string) (TaxRateUpdateProposalJSON, error) { + proposal := TaxRateUpdateProposalJSON{} + + contents, err := ioutil.ReadFile(proposalFile) + if err != nil { + return proposal, err + } + + if err := cdc.UnmarshalJSON(contents, &proposal); err != nil { + return proposal, err + } + + return proposal, nil +} + +// ParseRewardWeightUpdateProposalJSON reads and parses a RewardWeightUpdateProposalJSON from a file. +func ParseRewardWeightUpdateProposalJSON(cdc *codec.Codec, proposalFile string) (RewardWeightUpdateProposalJSON, error) { + proposal := RewardWeightUpdateProposalJSON{} + + contents, err := ioutil.ReadFile(proposalFile) + if err != nil { + return proposal, err + } + + if err := cdc.UnmarshalJSON(contents, &proposal); err != nil { + return proposal, err + } + + return proposal, nil +} diff --git a/x/treasury/client/proposal_handler.go b/x/treasury/client/proposal_handler.go new file mode 100644 index 000000000..ecd9f0c28 --- /dev/null +++ b/x/treasury/client/proposal_handler.go @@ -0,0 +1,13 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + "github.com/terra-project/core/x/treasury/client/cli" + "github.com/terra-project/core/x/treasury/client/rest" +) + +// param change proposal handler +var ( + TaxRateUpdateProposalHandler = govclient.NewProposalHandler(cli.GetCmdSubmitTaxRateUpdateProposal, rest.TaxRateUpdateProposalRESTHandler) + RewardWeightUpdateProposalHandler = govclient.NewProposalHandler(cli.GetCmdSubmitRewardWeightUpdateProposal, rest.RewardWeightUpdateProposalRESTHandler) +) diff --git a/x/treasury/client/rest/rest.go b/x/treasury/client/rest/rest.go index 5816bc86a..9dc900136 100644 --- a/x/treasury/client/rest/rest.go +++ b/x/treasury/client/rest/rest.go @@ -4,6 +4,7 @@ import ( "github.com/gorilla/mux" "github.com/cosmos/cosmos-sdk/client/context" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" ) const ( @@ -16,3 +17,19 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { // resgisterTxRoute(cliCtx, r) registerQueryRoute(cliCtx, r) } + +// TaxRateUpdateProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool spend REST handler with a given sub-route. +func TaxRateUpdateProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "tax_rate_update", + Handler: postTaxRateUpdateProposalHandlerFn(cliCtx), + } +} + +// RewardWeightUpdateProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool spend REST handler with a given sub-route. +func RewardWeightUpdateProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "reward_weight_update", + Handler: postRewardWeightUpdateProposalHandlerFn(cliCtx), + } +} diff --git a/x/treasury/client/rest/tx.go b/x/treasury/client/rest/tx.go new file mode 100644 index 000000000..bfd63f6ad --- /dev/null +++ b/x/treasury/client/rest/tx.go @@ -0,0 +1,61 @@ +package rest + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + + "github.com/terra-project/core/x/treasury/internal/types" +) + +func postTaxRateUpdateProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req TaxRateUpdateProposalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + content := types.NewTaxRateUpdateProposal(req.Title, req.Description, req.TaxRate) + + msg := gov.NewMsgSubmitProposal(content, req.Deposit, req.Proposer) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func postRewardWeightUpdateProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req RewardWeightUpdateProposalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + content := types.NewRewardWeightUpdateProposal(req.Title, req.Description, req.RewardWeight) + + msg := gov.NewMsgSubmitProposal(content, req.Deposit, req.Proposer) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/treasury/client/rest/utils.go b/x/treasury/client/rest/utils.go new file mode 100644 index 000000000..4e4dcc896 --- /dev/null +++ b/x/treasury/client/rest/utils.go @@ -0,0 +1,30 @@ +package rest + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" +) + +type ( + // TaxRateUpdateProposalReq defines a tax-rate-update proposal request body. + TaxRateUpdateProposalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + TaxRate sdk.Dec `json:"tax_rate" yaml:"tax_rate"` + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + } + + // RewardWeightUpdateProposalReq defines a tax-rate-update proposal request body. + RewardWeightUpdateProposalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + RewardWeight sdk.Dec `json:"reward_weight" yaml:"reward_weight"` + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + } +) diff --git a/x/treasury/handler.go b/x/treasury/handler.go new file mode 100644 index 000000000..ba7981eb5 --- /dev/null +++ b/x/treasury/handler.go @@ -0,0 +1,53 @@ +package treasury + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + core "github.com/terra-project/core/types" +) + +func NewTreasuryPolicyUpdateHandler(k Keeper) govtypes.Handler { + return func(ctx sdk.Context, content govtypes.Content) sdk.Error { + switch c := content.(type) { + case TaxRateUpdateProposal: + return handleTaxRateUpdateProposal(ctx, k, c) + case RewardWeightUpdateProposal: + return handleRewardWeightUpdateProposal(ctx, k, c) + + default: + errMsg := fmt.Sprintf("unrecognized distr proposal content type: %T", c) + return sdk.ErrUnknownRequest(errMsg) + } + } +} + +// handleTaxRateUpdateProposal is a handler for updating tax-rate +func handleTaxRateUpdateProposal(ctx sdk.Context, k Keeper, p TaxRateUpdateProposal) sdk.Error { + taxPolicy := k.TaxPolicy(ctx) + taxRate := k.GetTaxRate(ctx, core.GetEpoch(ctx)) + newTaxRate := taxPolicy.Clamp(taxRate, p.TaxRate) + + // Set the new tax rate to the store + k.SetTaxRate(ctx, newTaxRate) + + logger := k.Logger(ctx) + logger.Info(fmt.Sprintf("updated tax-rate to %s", newTaxRate)) + return nil +} + +// handleRewardWeightUpdateProposal is a handler for updating reward-weight +func handleRewardWeightUpdateProposal(ctx sdk.Context, k Keeper, p RewardWeightUpdateProposal) sdk.Error { + rewardPolicy := k.RewardPolicy(ctx) + rewardWeight := k.GetRewardWeight(ctx, core.GetEpoch(ctx)) + newRewardWeight := rewardPolicy.Clamp(rewardWeight, p.RewardWeight) + + // Set the new reward rate to the store + k.SetRewardWeight(ctx, newRewardWeight) + + logger := k.Logger(ctx) + logger.Info(fmt.Sprintf("updated reward-weight to %s", newRewardWeight)) + return nil +} diff --git a/x/treasury/internal/types/codec.go b/x/treasury/internal/types/codec.go index b53d07d50..9a6fdd6bc 100644 --- a/x/treasury/internal/types/codec.go +++ b/x/treasury/internal/types/codec.go @@ -2,10 +2,14 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" + + "github.com/terra-project/core/x/gov" ) // Register concrete types on codec codec func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(TaxRateUpdateProposal{}, "treasury/TaxRateUpdateProposal", nil) + cdc.RegisterConcrete(RewardWeightUpdateProposal{}, "treasury/RewardWeightUpdateProposal", nil) } // generic sealed codec to be used throughout module @@ -15,4 +19,7 @@ func init() { ModuleCdc = codec.New() RegisterCodec(ModuleCdc) ModuleCdc.Seal() + + gov.RegisterProposalTypeCodec(TaxRateUpdateProposal{}, "treasury/TaxRateUpdateProposal") + gov.RegisterProposalTypeCodec(RewardWeightUpdateProposal{}, "treasury/RewardWeightUpdateProposal") } diff --git a/x/treasury/internal/types/proposal.go b/x/treasury/internal/types/proposal.go new file mode 100644 index 000000000..c5fb3a97f --- /dev/null +++ b/x/treasury/internal/types/proposal.go @@ -0,0 +1,123 @@ +package types + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/terra-project/core/x/gov" +) + +const ( + // ProposalTypeTaxRateUpdate defines the type for a TaxRateUpdateProposal + ProposalTypeTaxRateUpdate = "TaxRateUpdate" + + // ProposalTypeRewardWeightUpdate defines the type for a RewardWeightUpdateProposal + ProposalTypeRewardWeightUpdate = "RewardWeightUpdate" +) + +// Assert TaxRateUpdateProposal implements govtypes.Content at compile-time +var _ gov.Content = TaxRateUpdateProposal{} + +func init() { + gov.RegisterProposalType(ProposalTypeTaxRateUpdate) + gov.RegisterProposalType(ProposalTypeRewardWeightUpdate) +} + +// TaxRateUpdateProposal updates treasury tax-rate +type TaxRateUpdateProposal struct { + Title string `json:"title" yaml:"title"` // Title of the Proposal + Description string `json:"description" yaml:"description"` // Description of the Proposal + TaxRate sdk.Dec `json:"tax_rate" yaml:"tax_rate"` // target TaxRate +} + +// NewTaxRateUpdateProposal creates an TaxRateUpdateProposal. +func NewTaxRateUpdateProposal(title, description string, taxRate sdk.Dec) TaxRateUpdateProposal { + return TaxRateUpdateProposal{title, description, taxRate} +} + +// GetTitle returns the title of an TaxRateUpdateProposal. +func (p TaxRateUpdateProposal) GetTitle() string { return p.Title } + +// GetDescription returns the description of an TaxRateUpdateProposal. +func (p TaxRateUpdateProposal) GetDescription() string { return p.Description } + +// ProposalRoute returns the routing key of an TaxRateUpdateProposal. +func (TaxRateUpdateProposal) ProposalRoute() string { return RouterKey } + +// ProposalType returns the type of an TaxRateUpdateProposal. +func (p TaxRateUpdateProposal) ProposalType() string { return ProposalTypeTaxRateUpdate } + +// ValidateBasic runs basic stateless validity checks +func (p TaxRateUpdateProposal) ValidateBasic() sdk.Error { + err := gov.ValidateAbstract(DefaultCodespace, p) + if err != nil { + return err + } + + if !p.TaxRate.IsPositive() || p.TaxRate.GT(sdk.OneDec()) { + return sdk.ErrInvalidCoins("Invalid tax-rate: " + p.TaxRate.String()) + } + + return nil +} + +// String implements the Stringer interface. +func (p TaxRateUpdateProposal) String() string { + var b strings.Builder + b.WriteString(fmt.Sprintf(`Community Pool Spend Proposal: + Title: %s + Description: %s + TaxRate: %s +`, p.Title, p.Description, p.TaxRate)) + return b.String() +} + +// RewardWeightUpdateProposal update treasury tax-rate +type RewardWeightUpdateProposal struct { + Title string `json:"title" yaml:"title"` // Title of the Proposal + Description string `json:"description" yaml:"description"` // Description of the Proposal + RewardWeight sdk.Dec `json:"reward_weight" yaml:"reward_weight"` // target RewardWeight +} + +// NewRewardWeightUpdateProposal creates an RewardWeightUpdateProposal. +func NewRewardWeightUpdateProposal(title, description string, taxRate sdk.Dec) RewardWeightUpdateProposal { + return RewardWeightUpdateProposal{title, description, taxRate} +} + +// GetTitle returns the title of an RewardWeightUpdateProposal. +func (p RewardWeightUpdateProposal) GetTitle() string { return p.Title } + +// GetDescription returns the description of an RewardWeightUpdateProposal. +func (p RewardWeightUpdateProposal) GetDescription() string { return p.Description } + +// ProposalRoute returns the routing key of an RewardWeightUpdateProposal. +func (RewardWeightUpdateProposal) ProposalRoute() string { return RouterKey } + +// ProposalType returns the type of an RewardWeightUpdateProposal. +func (p RewardWeightUpdateProposal) ProposalType() string { return ProposalTypeRewardWeightUpdate } + +// ValidateBasic runs basic stateless validity checks +func (p RewardWeightUpdateProposal) ValidateBasic() sdk.Error { + err := gov.ValidateAbstract(DefaultCodespace, p) + if err != nil { + return err + } + + if !p.RewardWeight.IsPositive() || p.RewardWeight.GT(sdk.OneDec()) { + return sdk.ErrInvalidCoins("Invalid reward-weight: " + p.RewardWeight.String()) + } + + return nil +} + +// String implements the Stringer interface. +func (p RewardWeightUpdateProposal) String() string { + var b strings.Builder + b.WriteString(fmt.Sprintf(`Community Pool Spend Proposal: + Title: %s + Description: %s + RewardWeight: %s +`, p.Title, p.Description, p.RewardWeight)) + return b.String() +} diff --git a/x/treasury/internal/types/proposal_test.go b/x/treasury/internal/types/proposal_test.go new file mode 100644 index 000000000..12c81df8e --- /dev/null +++ b/x/treasury/internal/types/proposal_test.go @@ -0,0 +1,49 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestTaxRateUpdateProposal(t *testing.T) { + // invalid title + proposal := NewTaxRateUpdateProposal("", "description", sdk.NewDec(1)) + require.Error(t, proposal.ValidateBasic()) + + // invalid descrription + proposal = NewTaxRateUpdateProposal("title", "", sdk.NewDec(1)) + require.Error(t, proposal.ValidateBasic()) + + // invalid tax-rate + proposal = NewTaxRateUpdateProposal("title", "description", sdk.NewDec(2)) + require.Error(t, proposal.ValidateBasic()) + + proposal = NewTaxRateUpdateProposal("title", "description", sdk.NewDec(-1)) + require.Error(t, proposal.ValidateBasic()) + + proposal = NewTaxRateUpdateProposal("title", "description", sdk.NewDecWithPrec(1, 1)) + require.NoError(t, proposal.ValidateBasic()) +} + +func TestRewardWeightUpdateProposal(t *testing.T) { + // invalid title + proposal := NewRewardWeightUpdateProposal("", "description", sdk.NewDec(1)) + require.Error(t, proposal.ValidateBasic()) + + // invalid descrription + proposal = NewRewardWeightUpdateProposal("title", "", sdk.NewDec(1)) + require.Error(t, proposal.ValidateBasic()) + + // invalid reward-weight + proposal = NewRewardWeightUpdateProposal("title", "description", sdk.NewDec(2)) + require.Error(t, proposal.ValidateBasic()) + + proposal = NewRewardWeightUpdateProposal("title", "description", sdk.NewDec(-1)) + require.Error(t, proposal.ValidateBasic()) + + proposal = NewRewardWeightUpdateProposal("title", "description", sdk.NewDecWithPrec(1, 1)) + require.NoError(t, proposal.ValidateBasic()) +} diff --git a/x/treasury/proposal_handler_test.go b/x/treasury/proposal_handler_test.go new file mode 100644 index 000000000..e4775de73 --- /dev/null +++ b/x/treasury/proposal_handler_test.go @@ -0,0 +1,48 @@ +package treasury + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + core "github.com/terra-project/core/types" + "github.com/terra-project/core/x/treasury/internal/keeper" + "github.com/terra-project/core/x/treasury/internal/types" +) + +func testTaxRateUpdateProposal(taxRate sdk.Dec) types.TaxRateUpdateProposal { + return types.NewTaxRateUpdateProposal( + "Test", + "description", + taxRate, + ) +} + +func testRewardWeightUpdateProposal(rewardWeight sdk.Dec) types.RewardWeightUpdateProposal { + return types.NewRewardWeightUpdateProposal( + "Test", + "description", + rewardWeight, + ) +} + +func TestTaxRateUpdateProposalHandler(t *testing.T) { + input := keeper.CreateTestInput(t) + + taxRate := sdk.NewDecWithPrec(123, 5) + tp := testTaxRateUpdateProposal(taxRate) + hdlr := NewTreasuryPolicyUpdateHandler(input.TreasuryKeeper) + require.NoError(t, hdlr(input.Ctx, tp)) + require.Equal(t, taxRate, input.TreasuryKeeper.GetTaxRate(input.Ctx, core.GetEpoch(input.Ctx))) +} + +func TestRewardWeightUpdateProposalHandler(t *testing.T) { + input := keeper.CreateTestInput(t) + + rewardWeight := sdk.NewDecWithPrec(55, 3) + tp := testRewardWeightUpdateProposal(rewardWeight) + hdlr := NewTreasuryPolicyUpdateHandler(input.TreasuryKeeper) + require.NoError(t, hdlr(input.Ctx, tp)) + require.Equal(t, rewardWeight, input.TreasuryKeeper.GetRewardWeight(input.Ctx, core.GetEpoch(input.Ctx))) +} diff --git a/x/treasury/simulation/msgs.go b/x/treasury/simulation/msgs.go new file mode 100644 index 000000000..087ac1c8b --- /dev/null +++ b/x/treasury/simulation/msgs.go @@ -0,0 +1,56 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govsim "github.com/cosmos/cosmos-sdk/x/gov/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/terra-project/core/x/treasury" + "github.com/terra-project/core/x/treasury/internal/types" +) + +// SimulateTaxRateUpdateProposalContent generates random tax-rate-update proposal content +func SimulateTaxRateUpdateProposalContent(k treasury.Keeper) govsim.ContentSimulator { + return func(r *rand.Rand, _ *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) gov.Content { + + targetTaxRate := sdk.NewDecWithPrec(r.Int63n(100), 2) + if targetTaxRate.GT(types.DefaultTaxPolicy.RateMax) { + targetTaxRate = types.DefaultTaxPolicy.RateMax + } + + if targetTaxRate.LT(types.DefaultTaxPolicy.RateMin) { + targetTaxRate = types.DefaultTaxPolicy.RateMin + } + + return treasury.NewTaxRateUpdateProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + targetTaxRate, + ) + } +} + +// SimulateRewardWeightUpdateProposalContent generates random tax-rate-update proposal content +func SimulateRewardWeightUpdateProposalContent(k treasury.Keeper) govsim.ContentSimulator { + return func(r *rand.Rand, _ *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) gov.Content { + + targetRewardWeight := sdk.NewDecWithPrec(r.Int63n(100), 2) + if targetRewardWeight.GT(types.DefaultRewardPolicy.RateMax) { + targetRewardWeight = types.DefaultRewardPolicy.RateMax + } + + if targetRewardWeight.LT(types.DefaultRewardPolicy.RateMin) { + targetRewardWeight = types.DefaultRewardPolicy.RateMin + } + + return treasury.NewRewardWeightUpdateProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + targetRewardWeight, + ) + } +}