cosmos-sdk/tests/systemtests/protocolpool_test.go

474 lines
14 KiB
Go

//go:build system_test
package systemtests
import (
"fmt"
"os"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"cosmossdk.io/math"
"cosmossdk.io/systemtests"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/bech32"
)
const (
stakingModule = "staking"
distributionModule = "distribution"
protocolPoolModule = "protocolpool"
govModule = "gov"
authModule = "auth"
genesisAmount = 1000000000000
stakeAmount = 10000000000
feeAmount = 1
depositAmount = 50000000
poolAmount = 100
)
func modifyGovParams(t *testing.T) {
t.Helper()
// set up params so that we should just auto pass
systemtests.Sut.ModifyGenesisJSON(t,
func(genesis []byte) []byte {
state, err := sjson.Set(string(genesis), "app_state.gov.params.max_deposit_period", (1 * time.Second).String())
require.NoError(t, err)
return []byte(state)
},
func(genesis []byte) []byte {
state, err := sjson.Set(string(genesis), "app_state.gov.params.voting_period", (11 * time.Second).String())
require.NoError(t, err)
return []byte(state)
},
func(genesis []byte) []byte {
state, err := sjson.Set(string(genesis), "app_state.gov.params.veto_threshold", "0.000001")
require.NoError(t, err)
return []byte(state)
},
func(genesis []byte) []byte {
state, err := sjson.Set(string(genesis), "app_state.gov.params.threshold", "0.0000001")
require.NoError(t, err)
return []byte(state)
},
func(genesis []byte) []byte {
state, err := sjson.Set(string(genesis), "app_state.gov.params.quorum", "0.0000001")
require.NoError(t, err)
return []byte(state)
},
)
}
func submitGovProposal(t *testing.T, validatorAddress string, propFile *os.File) {
t.Helper()
sut := systemtests.Sut
cli := systemtests.NewCLIWrapper(t, sut, systemtests.Verbose)
args := []string{
"tx", govModule, "submit-proposal",
propFile.Name(),
fmt.Sprintf("--%s=%s", flags.FlagFrom, validatorAddress),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(feeAmount))).String()),
}
rsp := cli.Run(args...)
txResult, found := cli.AwaitTxCommitted(rsp)
require.True(t, found)
systemtests.RequireTxSuccess(t, txResult)
}
func voteAndEnsureProposalPassed(t *testing.T, validatorAddress string, propID int) {
t.Helper()
sut := systemtests.Sut
cli := systemtests.NewCLIWrapper(t, sut, systemtests.Verbose)
// check the proposal
proposalsResp := cli.CustomQuery("q", govModule, "proposals")
proposals := gjson.Get(proposalsResp, "proposals.#.id").Array()
require.NotEmpty(t, proposals)
rsp := cli.CustomQuery("q", govModule, "proposal", fmt.Sprintf("%d", propID))
status := gjson.Get(rsp, "proposal.status")
require.Equal(t, "PROPOSAL_STATUS_VOTING_PERIOD", status.String())
// vote on the proposal
args := []string{
"tx", govModule, "vote", fmt.Sprintf("%d", propID), "yes",
fmt.Sprintf("--%s=%s", flags.FlagFrom, validatorAddress),
}
rsp = cli.Run(args...)
txResult, found := cli.AwaitTxCommitted(rsp)
require.True(t, found)
systemtests.RequireTxSuccess(t, txResult)
time.Sleep(11 * time.Second)
systemtests.Sut.AwaitNextBlock(t)
// ensure that vote has passed
rsp = cli.CustomQuery("q", "gov", "proposal", fmt.Sprintf("%d", propID))
status = gjson.Get(rsp, "proposal.status")
require.Equal(t, "PROPOSAL_STATUS_PASSED", status.String())
}
func getGovAddress(t *testing.T) string {
t.Helper()
sut := systemtests.Sut
cli := systemtests.NewCLIWrapper(t, sut, systemtests.Verbose)
// get gov module address
resp := cli.CustomQuery("q", authModule, "module-account", "gov")
govAddress := gjson.Get(resp, "account.value.address").String()
_, bz, err := bech32.DecodeAndConvert(govAddress)
assert.NoError(t, err)
govAddress, err = bech32.ConvertAndEncode(sdk.Bech32MainPrefix, bz)
assert.NoError(t, err)
return govAddress
}
func TestQueryProtocolPool(t *testing.T) {
// Scenario:
// delegate tokens to validator
// check distribution
sut := systemtests.Sut
sut.ResetChain(t)
// set up gov params so we can pass props quickly
modifyGovParams(t)
cli := systemtests.NewCLIWrapper(t, sut, systemtests.Verbose)
// add genesis account with some tokens
account1Addr := cli.AddKey("account1")
sut.ModifyGenesisCLI(t,
[]string{"genesis", "add-genesis-account", account1Addr, fmt.Sprintf("%d%s", genesisAmount, sdk.DefaultBondDenom)},
)
sut.StartChain(t)
// query validator address to delegate tokens
rsp := cli.CustomQuery("q", stakingModule, "validators")
valAddr := gjson.Get(rsp, "validators.#.operator_address").Array()[0].String()
// get validator address
valSigner := gjson.Get(cli.Keys("keys", "list"), "0.address").String()
require.NotEmpty(t, valSigner)
// stake tokens
rsp = cli.Run(
"tx",
stakingModule,
"delegate",
valAddr,
fmt.Sprintf("%d%s", stakeAmount, sdk.DefaultBondDenom),
"--from="+account1Addr,
fmt.Sprintf("--fees=%d%s", feeAmount, sdk.DefaultBondDenom),
)
systemtests.RequireTxSuccess(t, rsp)
beforeBalance := cli.QueryBalance(account1Addr, sdk.DefaultBondDenom)
assert.Equal(t, int64(genesisAmount-stakeAmount-feeAmount), beforeBalance)
rsp = cli.CustomQuery("q", stakingModule, "delegation", account1Addr, valAddr)
assert.Equal(t, int64(stakeAmount), gjson.Get(rsp, "delegation_response.balance.amount").Int(), rsp)
assert.Equal(t, sdk.DefaultBondDenom, gjson.Get(rsp, "delegation_response.balance.denom").String(), rsp)
t.Run("check x/distribution query does not work when using x/protocolpool", func(t *testing.T) {
failingCli := cli.WithRunErrorMatcher(func(t assert.TestingT, err error, msgAndArgs ...interface{}) (ok bool) {
assert.Error(t, err)
return false
})
// query the community pool - should fail for x/distribution
_ = failingCli.CustomQuery("q", distributionModule, "community-pool")
})
t.Run("check x/protocolpool community pool query", func(t *testing.T) {
// query will work for x/protocolpool
rsp = cli.CustomQuery("q", protocolPoolModule, "community-pool")
poolAmount := gjson.Get(rsp, "pool.0.amount").Int()
assert.True(t, poolAmount > 0, rsp)
assert.Equal(t, sdk.DefaultBondDenom, gjson.Get(rsp, "pool.0.denom").String(), rsp)
t.Log("block height", sut.CurrentHeight(), "\n")
sut.AwaitNBlocks(t, 1)
t.Log("block height", sut.CurrentHeight(), "\n")
rsp = cli.CustomQuery("q", protocolPoolModule, "community-pool")
newPoolAmount := gjson.Get(rsp, "pool.0.amount").Int()
assert.Equal(t, sdk.DefaultBondDenom, gjson.Get(rsp, "pool.0.denom").String(), rsp)
// check that staking is continually rewarded
assert.True(t, newPoolAmount > poolAmount, rsp)
})
govAddress := getGovAddress(t)
t.Run("valid proposal", func(t *testing.T) {
// Create a valid new proposal JSON.
validProp := fmt.Sprintf(`
{
"messages": [
{
"@type": "/cosmos.protocolpool.v1.MsgCommunityPoolSpend",
"authority": "%s",
"recipient": "%s",
"amount": [{
"denom": "stake",
"amount": "%d"
}]
}
],
"title": "My awesome title",
"summary": "My awesome description",
"deposit": "%s"
}`,
govAddress,
account1Addr,
poolAmount,
sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(depositAmount)),
)
validPropFile := systemtests.StoreTempFile(t, []byte(validProp))
defer validPropFile.Close()
submitGovProposal(t, valSigner, validPropFile)
})
balanceBefore := cli.QueryBalance(account1Addr, sdk.DefaultBondDenom)
voteAndEnsureProposalPassed(t, valSigner, 1)
// ensure that vote has passed
t.Run("ensure that the vote has passed", func(t *testing.T) {
// check that the funds were distributed
// should be previous balance plus amount from the pool (100) plus the deposit amount (50000000)
balanceAfter := cli.QueryBalance(account1Addr, sdk.DefaultBondDenom)
require.Equal(t, balanceBefore+poolAmount+depositAmount-feeAmount, balanceAfter)
})
}
// Create a continuous fund
// - submit prop and vote until passed
// Check that funds are distributed and continuous fund is cleaned up once expired
func TestContinuousFunds(t *testing.T) {
systemtests.Sut.ResetChain(t)
cli := systemtests.NewCLIWrapper(t, systemtests.Sut, systemtests.Verbose)
// set up gov params so we can pass props quickly
modifyGovParams(t)
// get validator address
valAddr := gjson.Get(cli.Keys("keys", "list"), "0.address").String()
require.NotEmpty(t, valAddr)
// add genesis account with some tokens
account1Addr := cli.AddKey("account1")
systemtests.Sut.ModifyGenesisCLI(t,
[]string{"genesis", "add-genesis-account", account1Addr, fmt.Sprintf("%d%s", genesisAmount, sdk.DefaultBondDenom)},
)
systemtests.Sut.StartChain(t)
govAddress := getGovAddress(t)
duration := 30 * time.Second
// wait long enough that it will be expired
buffer := 11 * time.Second
expiry := time.Now().Add(duration).UTC()
var balanceBefore int64
wg := new(sync.WaitGroup)
wg.Add(2)
time.AfterFunc(duration+buffer, func() {
wg.Done()
})
go func() {
defer wg.Done()
t.Run("valid proposal", func(t *testing.T) {
// Create a valid new proposal JSON.
validProp := fmt.Sprintf(`
{
"messages": [
{
"@type": "/cosmos.protocolpool.v1.MsgCreateContinuousFund",
"authority": "%s",
"recipient": "%s",
"percentage": "0.5",
"expiry": "%s"
}
],
"title": "My awesome title",
"summary": "My awesome description",
"deposit": "%s"
}`,
govAddress,
account1Addr,
expiry.Format(time.RFC3339),
sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(depositAmount)),
)
validPropFile := systemtests.StoreTempFile(t, []byte(validProp))
defer validPropFile.Close()
submitGovProposal(t, valAddr, validPropFile)
})
// get balance before any distribution
balanceBefore = cli.QueryBalance(account1Addr, sdk.DefaultBondDenom)
voteAndEnsureProposalPassed(t, valAddr, 1)
// ensure that vote has passed
t.Run("ensure that the vote has passed", func(t *testing.T) {
// check that the fund exists
rsp := cli.CustomQuery("q", protocolPoolModule, "continuous-fund", account1Addr)
gotExpiry := gjson.Get(rsp, "continuous_fund.expiry").Time()
require.Equal(t, expiry.Truncate(time.Second), gotExpiry.Truncate(time.Second))
recipient := gjson.Get(rsp, "continuous_fund.recipient").String()
require.Equal(t, account1Addr, recipient)
})
}()
wg.Wait()
systemtests.Sut.AwaitNextBlock(t)
t.Run("check balance and that the fund is expired", func(t *testing.T) {
failingCli := cli.WithRunErrorMatcher(func(t assert.TestingT, err error, msgAndArgs ...interface{}) (ok bool) {
assert.Error(t, err)
return false
})
// query the continuous fund - should be expired
_ = failingCli.CustomQuery("q", protocolPoolModule, "continuous-fund", account1Addr)
// check that there is nothing in the store
rsp := cli.CustomQuery("q", protocolPoolModule, "continuous-funds")
require.Equal(t, "{}", rsp)
balanceAfter := cli.QueryBalance(account1Addr, sdk.DefaultBondDenom)
// check that our balance has increased due to fund accrual
require.True(t, balanceBefore < balanceAfter)
})
}
// Create a continuous fund
// - submit prop and vote until passed (no expiry)
// Create a cancellation prop
// - submit prop and vote until passed
//
// Check that some funds have been distributed and that the fund is canceled.
func TestCancelContinuousFunds(t *testing.T) {
systemtests.Sut.ResetChain(t)
cli := systemtests.NewCLIWrapper(t, systemtests.Sut, systemtests.Verbose)
// set up gov params so we can pass props quickly
modifyGovParams(t)
// get validator address
valAddr := gjson.Get(cli.Keys("keys", "list"), "0.address").String()
require.NotEmpty(t, valAddr)
// add genesis account with some tokens
account1Addr := cli.AddKey("account1")
systemtests.Sut.ModifyGenesisCLI(t,
[]string{"genesis", "add-genesis-account", account1Addr, fmt.Sprintf("%d%s", genesisAmount, sdk.DefaultBondDenom)},
)
systemtests.Sut.StartChain(t)
govAddress := getGovAddress(t)
t.Run("valid proposal - create", func(t *testing.T) {
// Create a valid new proposal JSON.
validProp := fmt.Sprintf(`
{
"messages": [
{
"@type": "/cosmos.protocolpool.v1.MsgCreateContinuousFund",
"authority": "%s",
"recipient": "%s",
"percentage": "0.5"
}
],
"title": "My awesome title",
"summary": "My awesome description",
"deposit": "%s"
}`,
govAddress,
account1Addr,
sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(depositAmount)),
)
validPropFile := systemtests.StoreTempFile(t, []byte(validProp))
defer validPropFile.Close()
submitGovProposal(t, valAddr, validPropFile)
})
// get balance before any distribution
balanceBefore := cli.QueryBalance(account1Addr, sdk.DefaultBondDenom)
voteAndEnsureProposalPassed(t, valAddr, 1)
// ensure that vote has passed
t.Run("ensure that the vote has passed - create", func(t *testing.T) {
// check that the fund exists
rsp := cli.CustomQuery("q", protocolPoolModule, "continuous-fund", account1Addr)
recipient := gjson.Get(rsp, "continuous_fund.recipient").String()
require.Equal(t, account1Addr, recipient)
})
t.Run("valid proposal - cancel", func(t *testing.T) {
// Create a valid new proposal JSON.
validProp := fmt.Sprintf(`
{
"messages": [
{
"@type": "/cosmos.protocolpool.v1.MsgCancelContinuousFund",
"authority": "%s",
"recipient": "%s"
}
],
"title": "My awesome title",
"summary": "My awesome description",
"deposit": "%s"
}`,
govAddress,
account1Addr,
sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(depositAmount)),
)
validPropFile := systemtests.StoreTempFile(t, []byte(validProp))
defer validPropFile.Close()
submitGovProposal(t, valAddr, validPropFile)
})
voteAndEnsureProposalPassed(t, valAddr, 2)
// ensure that vote has passed
t.Run("ensure that the vote has passed - cancel", func(t *testing.T) {
// check that the fund does not exist
failingCli := cli.WithRunErrorMatcher(func(t assert.TestingT, err error, msgAndArgs ...interface{}) (ok bool) {
assert.Error(t, err)
return false
})
// query the continuous fund - should be expired
_ = failingCli.CustomQuery("q", protocolPoolModule, "continuous-funds", account1Addr)
// check that there is nothing in the store
rsp := cli.CustomQuery("q", protocolPoolModule, "continuous-funds")
require.Equal(t, "{}", rsp)
balanceAfter := cli.QueryBalance(account1Addr, sdk.DefaultBondDenom)
// balance should be balance greater than initial balance
require.True(t, balanceBefore < balanceAfter)
})
}