refactor(x/protocolpool)!: Reducing complexity and removing some bugs (#20786)

This commit is contained in:
Facundo Medica 2024-06-28 17:53:26 +02:00 committed by GitHub
parent 488e74cc5d
commit d426a5db67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 570 additions and 196 deletions

View File

@ -0,0 +1,29 @@
package protocolpool
import (
_ "cosmossdk.io/x/accounts" // import as blank for app wiring
_ "cosmossdk.io/x/auth" // import as blank for app wiring
_ "cosmossdk.io/x/auth/tx/config" // import as blank for app wiring
_ "cosmossdk.io/x/bank" // import as blank for app wiring
_ "cosmossdk.io/x/consensus" // import as blank for app wiring
_ "cosmossdk.io/x/distribution" // import as blank for app wiring
_ "cosmossdk.io/x/mint" // import as blank for app wiring
_ "cosmossdk.io/x/protocolpool" // import as blank for app wiring
_ "cosmossdk.io/x/staking" // import as blank for app wiring
"github.com/cosmos/cosmos-sdk/testutil/configurator"
_ "github.com/cosmos/cosmos-sdk/x/genutil" // import as blank for app wiring
)
var AppConfig = configurator.NewAppConfig(
configurator.AccountsModule(),
configurator.AuthModule(),
configurator.BankModule(),
configurator.StakingModule(),
configurator.TxModule(),
configurator.ConsensusModule(),
configurator.GenutilModule(),
configurator.MintModule(),
configurator.DistributionModule(),
configurator.ProtocolPoolModule(),
)

View File

@ -0,0 +1,138 @@
package protocolpool
import (
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/require"
"cosmossdk.io/core/header"
"cosmossdk.io/depinject"
"cosmossdk.io/log"
"cosmossdk.io/math"
authkeeper "cosmossdk.io/x/auth/keeper"
authtypes "cosmossdk.io/x/auth/types"
bankkeeper "cosmossdk.io/x/bank/keeper"
"cosmossdk.io/x/mint/types"
protocolpoolkeeper "cosmossdk.io/x/protocolpool/keeper"
protocolpooltypes "cosmossdk.io/x/protocolpool/types"
stakingkeeper "cosmossdk.io/x/staking/keeper"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// TestWithdrawAnytime tests if withdrawing funds many times vs withdrawing funds once
// yield the same end balance.
func TestWithdrawAnytime(t *testing.T) {
var accountKeeper authkeeper.AccountKeeper
var protocolpoolKeeper protocolpoolkeeper.Keeper
var bankKeeper bankkeeper.Keeper
var stakingKeeper *stakingkeeper.Keeper
app, err := simtestutil.SetupAtGenesis(
depinject.Configs(
AppConfig,
depinject.Supply(log.NewNopLogger()),
), &accountKeeper, &protocolpoolKeeper, &bankKeeper, &stakingKeeper)
require.NoError(t, err)
ctx := app.BaseApp.NewContext(false).WithBlockHeight(1).WithHeaderInfo(header.Info{Height: 1})
acc := accountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName))
require.NotNil(t, acc)
testAddrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 5, math.NewInt(1))
testAddr0Str, err := accountKeeper.AddressCodec().BytesToString(testAddrs[0])
require.NoError(t, err)
msgServer := protocolpoolkeeper.NewMsgServerImpl(protocolpoolKeeper)
_, err = msgServer.CreateContinuousFund(
ctx,
&protocolpooltypes.MsgCreateContinuousFund{
Authority: protocolpoolKeeper.GetAuthority(),
Recipient: testAddr0Str,
Percentage: math.LegacyMustNewDecFromStr("0.5"),
},
)
require.NoError(t, err)
// increase the community pool by a bunch
for i := 0; i < 30; i++ {
ctx, err = simtestutil.NextBlock(app, ctx, time.Minute)
require.NoError(t, err)
// withdraw funds randomly, but it must always land on the same end balance
if rand.Intn(100) > 50 {
_, err = msgServer.WithdrawContinuousFund(ctx, &protocolpooltypes.MsgWithdrawContinuousFund{
RecipientAddress: testAddr0Str,
})
require.NoError(t, err)
}
}
pool, err := protocolpoolKeeper.GetCommunityPool(ctx)
require.NoError(t, err)
require.True(t, pool.IsAllGT(sdk.NewCoins(sdk.NewInt64Coin("stake", 100000))))
_, err = msgServer.WithdrawContinuousFund(ctx, &protocolpooltypes.MsgWithdrawContinuousFund{
RecipientAddress: testAddr0Str,
})
require.NoError(t, err)
endBalance := bankKeeper.GetBalance(ctx, testAddrs[0], sdk.DefaultBondDenom)
require.Equal(t, "11883031stake", endBalance.String())
}
// TestExpireInTheMiddle tests if a continuous fund that expires without anyone
// calling the withdraw function, the funds are still distributed correctly.
func TestExpireInTheMiddle(t *testing.T) {
t.Skip("This is a bug @facu found, will fix in another PR")
var accountKeeper authkeeper.AccountKeeper
var protocolpoolKeeper protocolpoolkeeper.Keeper
var bankKeeper bankkeeper.Keeper
var stakingKeeper *stakingkeeper.Keeper
app, err := simtestutil.SetupAtGenesis(
depinject.Configs(
AppConfig,
depinject.Supply(log.NewNopLogger()),
), &accountKeeper, &protocolpoolKeeper, &bankKeeper, &stakingKeeper)
require.NoError(t, err)
ctx := app.BaseApp.NewContext(false).WithBlockHeight(1).WithHeaderInfo(header.Info{Height: 1})
acc := accountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName))
require.NotNil(t, acc)
testAddrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 5, math.NewInt(1))
testAddr0Str, err := accountKeeper.AddressCodec().BytesToString(testAddrs[0])
require.NoError(t, err)
msgServer := protocolpoolkeeper.NewMsgServerImpl(protocolpoolKeeper)
expirationTime := ctx.BlockTime().Add(time.Minute * 2)
_, err = msgServer.CreateContinuousFund(
ctx,
&protocolpooltypes.MsgCreateContinuousFund{
Authority: protocolpoolKeeper.GetAuthority(),
Recipient: testAddr0Str,
Percentage: math.LegacyMustNewDecFromStr("0.1"),
Expiry: &expirationTime,
},
)
require.NoError(t, err)
// increase the community pool by a bunch
for i := 0; i < 30; i++ {
ctx, err = simtestutil.NextBlock(app, ctx, time.Minute)
require.NoError(t, err)
}
_, err = msgServer.WithdrawContinuousFund(ctx, &protocolpooltypes.MsgWithdrawContinuousFund{
RecipientAddress: testAddr0Str,
})
require.Error(t, err)
endBalance := bankKeeper.GetBalance(ctx, testAddrs[0], sdk.DefaultBondDenom)
require.Equal(t, "158441stake", endBalance.String())
}

View File

@ -0,0 +1,42 @@
package keeper_test
import (
"time"
"cosmossdk.io/math"
"cosmossdk.io/x/protocolpool/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func (suite *KeeperTestSuite) TestInitGenesis() {
hour := time.Hour
gs := types.NewGenesisState(
[]*types.ContinuousFund{
{
Recipient: "cosmos1qy3529yj3v4xw2z3vz3vz3vz3vz3vz3v3k0vyf",
Percentage: math.LegacyMustNewDecFromStr("0.1"),
Expiry: nil,
},
},
[]*types.Budget{
{
RecipientAddress: "cosmos1qy3529yj3v4xw2z3vz3vz3vz3vz3vz3v3k0vyf",
ClaimedAmount: &sdk.Coin{},
LastClaimedAt: &time.Time{},
TranchesLeft: 10,
BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)},
Period: &hour,
},
},
)
err := suite.poolKeeper.InitGenesis(suite.ctx, gs)
suite.Require().NoError(err)
// Export
exportedGenState, err := suite.poolKeeper.ExportGenesis(suite.ctx)
suite.Require().NoError(err)
suite.Require().Equal(gs.ContinuousFund, exportedGenState.ContinuousFund)
suite.Require().Equal(gs.Budget, exportedGenState.Budget)
}

View File

@ -33,11 +33,9 @@ type Keeper struct {
Schema collections.Schema
BudgetProposal collections.Map[sdk.AccAddress, types.Budget]
ContinuousFund collections.Map[sdk.AccAddress, types.ContinuousFund]
// RecipientFundPercentage key: RecipientAddr | value: Percentage in math.Int
RecipientFundPercentage collections.Map[sdk.AccAddress, math.Int]
// RecipientFundDistribution key: RecipientAddr | value: Claimable amount
RecipientFundDistribution collections.Map[sdk.AccAddress, math.Int]
// ToDistribute is to keep track of funds distributed
// ToDistribute is to keep track of funds to be distributed. It gets zeroed out in iterateAndUpdateFundsDistribution.
ToDistribute collections.Item[math.Int]
}
@ -63,7 +61,6 @@ func NewKeeper(cdc codec.BinaryCodec, env appmodule.Environment, ak types.Accoun
authority: authority,
BudgetProposal: collections.NewMap(sb, types.BudgetKey, "budget", sdk.AccAddressKey, codec.CollValue[types.Budget](cdc)),
ContinuousFund: collections.NewMap(sb, types.ContinuousFundKey, "continuous_fund", sdk.AccAddressKey, codec.CollValue[types.ContinuousFund](cdc)),
RecipientFundPercentage: collections.NewMap(sb, types.RecipientFundPercentageKey, "recipient_fund_percentage", sdk.AccAddressKey, sdk.IntValue),
RecipientFundDistribution: collections.NewMap(sb, types.RecipientFundDistributionKey, "recipient_fund_distribution", sdk.AccAddressKey, sdk.IntValue),
ToDistribute: collections.NewItem(sb, types.ToDistributeKey, "to_distribute", sdk.IntValue),
}
@ -125,20 +122,13 @@ func (k Keeper) withdrawContinuousFund(ctx context.Context, recipientAddr string
return sdk.Coin{}, fmt.Errorf("cannot withdraw continuous funds: continuous fund expired for recipient: %s", recipientAddr)
}
toDistributeAmount, err := k.ToDistribute.Get(ctx)
err = k.IterateAndUpdateFundsDistribution(ctx)
if err != nil {
return sdk.Coin{}, err
}
if !toDistributeAmount.Equal(math.ZeroInt()) {
err = k.iterateAndUpdateFundsDistribution(ctx, toDistributeAmount)
if err != nil {
return sdk.Coin{}, fmt.Errorf("error while iterating all the continuous funds: %w", err)
}
return sdk.Coin{}, fmt.Errorf("error while iterating all the continuous funds: %w", err)
}
// withdraw continuous fund
withdrawnAmount, err := k.withdrawRecipientFunds(ctx, recipientAddr)
withdrawnAmount, err := k.withdrawRecipientFunds(ctx, recipient)
if err != nil {
return sdk.Coin{}, fmt.Errorf("error while withdrawing recipient funds for recipient: %s", recipientAddr)
}
@ -146,12 +136,7 @@ func (k Keeper) withdrawContinuousFund(ctx context.Context, recipientAddr string
return withdrawnAmount, nil
}
func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipientAddr string) (sdk.Coin, error) {
recipient, err := k.authKeeper.AddressCodec().StringToBytes(recipientAddr)
if err != nil {
return sdk.Coin{}, sdkerrors.ErrInvalidAddress.Wrapf("invalid recipient address: %s", err)
}
func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipient []byte) (sdk.Coin, error) {
// get allocated continuous fund
fundsAllocated, err := k.RecipientFundDistribution.Get(ctx, recipient)
if err != nil {
@ -170,7 +155,7 @@ func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipientAddr string
withdrawnAmount := sdk.NewCoin(denom, fundsAllocated)
err = k.DistributeFromStreamFunds(ctx, sdk.NewCoins(withdrawnAmount), recipient)
if err != nil {
return sdk.Coin{}, fmt.Errorf("error while distributing funds to the recipient %s: %w", recipientAddr, err)
return sdk.Coin{}, fmt.Errorf("error while distributing funds: %w", err)
}
// reset fund distribution
@ -181,7 +166,8 @@ func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipientAddr string
return withdrawnAmount, nil
}
// SetToDistribute sets the amount to be distributed among recipients.
// SetToDistribute sets the amount to be distributed among recipients, usually called by x/distribution while allocating
// reward and fee distribution.
// This could be only set by the authority address.
func (k Keeper) SetToDistribute(ctx context.Context, amount sdk.Coins, addr string) error {
authAddr, err := k.authKeeper.AddressCodec().StringToBytes(addr)
@ -201,46 +187,49 @@ func (k Keeper) SetToDistribute(ctx context.Context, amount sdk.Coins, addr stri
return err
}
totalStreamFundsPercentage := math.ZeroInt()
err = k.RecipientFundPercentage.Walk(ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) {
totalStreamFundsPercentage = totalStreamFundsPercentage.Add(value)
totalStreamFundsPercentage := math.LegacyZeroDec()
err = k.ContinuousFund.Walk(ctx, nil, func(key sdk.AccAddress, cf types.ContinuousFund) (stop bool, err error) {
// Check if the continuous fund has expired
if cf.Expiry != nil && cf.Expiry.Before(k.HeaderService.HeaderInfo(ctx).Time) {
return false, nil
}
totalStreamFundsPercentage = totalStreamFundsPercentage.Add(cf.Percentage)
if totalStreamFundsPercentage.GT(math.LegacyOneDec()) {
return true, fmt.Errorf("total funds percentage cannot exceed 100")
}
return false, nil
})
if totalStreamFundsPercentage.GT(math.NewInt(100)) {
return fmt.Errorf("total funds percentage cannot exceed 100")
}
if err != nil {
return err
}
// if percentage is 0 then return early
if totalStreamFundsPercentage.IsZero() {
return nil
}
// send streaming funds to the stream module account
if err := k.sendFundsToStreamModule(ctx, denom, totalStreamFundsPercentage); err != nil {
return err
}
err = k.ToDistribute.Set(ctx, amount.AmountOf(denom))
if err != nil {
return fmt.Errorf("error while setting ToDistribute: %w", err)
}
return nil
}
func (k Keeper) sendFundsToStreamModule(ctx context.Context, denom string, percentage math.Int) error {
totalPoolAmt, err := k.GetCommunityPool(ctx)
if err != nil {
return err
}
poolAmt := totalPoolAmt.AmountOf(denom)
poolAmtDec := sdk.NewDecCoins(sdk.NewDecCoin(denom, poolAmt))
amt := poolAmtDec.MulDec(math.LegacyNewDecFromIntWithPrec(percentage, 2))
streamAmt := sdk.NewCoins(sdk.NewCoin(denom, amt.AmountOf(denom).TruncateInt()))
// Send streaming funds to the StreamModuleAccount
toDistributeAmt := math.LegacyNewDecFromInt(amount.AmountOf(denom)).Mul(totalStreamFundsPercentage).TruncateInt()
streamAmt := sdk.NewCoins(sdk.NewCoin(denom, toDistributeAmt))
if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.StreamAccount, streamAmt); err != nil {
return err
}
amountToDistribute, err := k.ToDistribute.Get(ctx)
if err != nil {
if errors.Is(err, collections.ErrNotFound) {
amountToDistribute = math.ZeroInt()
} else {
return err
}
}
err = k.ToDistribute.Set(ctx, amountToDistribute.Add(amount.AmountOf(denom)))
if err != nil {
return fmt.Errorf("error while setting ToDistribute: %w", err)
}
return nil
}
@ -254,79 +243,56 @@ func (k Keeper) hasPermission(addr []byte) (bool, error) {
return bytes.Equal(authAcc, addr), nil
}
type recipientFund struct {
RecipientAddr string
Percentage math.Int
}
func (k Keeper) iterateAndUpdateFundsDistribution(ctx context.Context, toDistributeAmount math.Int) error {
totalPercentageToBeDistributed := math.ZeroInt()
recipientFundList := []recipientFund{}
// Calculate totalPercentageToBeDistributed and store values
err := k.RecipientFundPercentage.Walk(ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) {
addr, err := k.authKeeper.AddressCodec().BytesToString(key)
if err != nil {
return true, err
}
cf, err := k.ContinuousFund.Get(ctx, key)
if err != nil {
return true, err
}
// Check if the continuous fund has expired
if cf.Expiry != nil && cf.Expiry.Before(k.HeaderService.HeaderInfo(ctx).Time) {
return false, nil
}
totalPercentageToBeDistributed = totalPercentageToBeDistributed.Add(value)
recipientFundList = append(recipientFundList, recipientFund{
RecipientAddr: addr,
Percentage: value,
})
return false, nil
})
func (k Keeper) IterateAndUpdateFundsDistribution(ctx context.Context) error {
toDistributeAmount, err := k.ToDistribute.Get(ctx)
if err != nil {
return err
}
if totalPercentageToBeDistributed.GT(math.NewInt(100)) {
return fmt.Errorf("total funds percentage cannot exceed 100")
// if there are no funds to distribute, return
if toDistributeAmount.IsZero() {
return nil
}
totalPercentageToBeDistributed := math.LegacyZeroDec()
denom, err := k.stakingKeeper.BondDenom(ctx)
if err != nil {
return err
}
toDistributeDec := sdk.NewDecCoins(sdk.NewDecCoin(denom, toDistributeAmount))
toDistributeDec := sdk.NewDecCoin(denom, toDistributeAmount)
// Calculate the funds to be distributed based on the total percentage to be distributed
totalAmountToBeDistributed := toDistributeDec.MulDec(math.LegacyNewDecFromIntWithPrec(totalPercentageToBeDistributed, 2))
totalDistrAmount := totalAmountToBeDistributed.AmountOf(denom)
for _, value := range recipientFundList {
// Calculate the funds to be distributed based on the percentage
decValue := math.LegacyNewDecFromIntWithPrec(value.Percentage, 2)
percentage := math.LegacyNewDecFromIntWithPrec(totalPercentageToBeDistributed, 2)
recipientAmount := totalDistrAmount.Mul(decValue).Quo(percentage)
recipientCoins := recipientAmount.TruncateInt()
key, err := k.authKeeper.AddressCodec().StringToBytes(value.RecipientAddr)
if err != nil {
return err
// Calculate totalPercentageToBeDistributed and store values
err = k.ContinuousFund.Walk(ctx, nil, func(key sdk.AccAddress, cf types.ContinuousFund) (stop bool, err error) {
// Check if the continuous fund has expired
if cf.Expiry != nil && cf.Expiry.Before(k.HeaderService.HeaderInfo(ctx).Time) {
return false, nil
}
// sanity check for max percentage
totalPercentageToBeDistributed = totalPercentageToBeDistributed.Add(cf.Percentage)
if totalPercentageToBeDistributed.GT(math.LegacyOneDec()) {
return true, fmt.Errorf("total funds percentage cannot exceed 100")
}
// Calculate the funds to be distributed based on the percentage
recipientAmount := toDistributeDec.Amount.Mul(cf.Percentage).TruncateInt()
// Set funds to be claimed
toClaim, err := k.RecipientFundDistribution.Get(ctx, key)
if err != nil {
return err
return true, err
}
amount := toClaim.Add(recipientCoins)
amount := toClaim.Add(recipientAmount)
err = k.RecipientFundDistribution.Set(ctx, key, amount)
if err != nil {
return err
return true, err
}
return false, nil
})
if err != nil {
return err
}
// Set the coins to be distributed from toDistribute to 0

View File

@ -113,3 +113,45 @@ func (s *KeeperTestSuite) mockStreamFunds() {
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}
func (s *KeeperTestSuite) TestIterateAndUpdateFundsDistribution() {
// We'll create 2 continuous funds of 30% each, and the total pool is 1000000, meaning each fund should get 300000
s.SetupTest()
s.authKeeper.EXPECT().GetModuleAccount(s.ctx, types.ModuleName).Return(poolAcc).AnyTimes()
distrBal := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1000000)))
s.bankKeeper.EXPECT().GetAllBalances(s.ctx, poolAcc.GetAddress()).Return(distrBal).AnyTimes()
s.bankKeeper.EXPECT().SendCoinsFromModuleToModule(s.ctx, poolAcc.GetName(), streamAcc.GetName(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(600000)))).AnyTimes()
_, err := s.msgServer.CreateContinuousFund(s.ctx, &types.MsgCreateContinuousFund{
Authority: s.poolKeeper.GetAuthority(),
Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2srklj6",
Percentage: math.LegacyMustNewDecFromStr("0.3"),
})
s.Require().NoError(err)
_, err = s.msgServer.CreateContinuousFund(s.ctx, &types.MsgCreateContinuousFund{
Authority: s.poolKeeper.GetAuthority(),
Recipient: "cosmos1tygms3xhhs3yv487phx3dw4a95jn7t7lpm470r",
Percentage: math.LegacyMustNewDecFromStr("0.3"),
})
s.Require().NoError(err)
_ = s.poolKeeper.SetToDistribute(s.ctx, sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(1000000))), s.poolKeeper.GetAuthority())
err = s.poolKeeper.IterateAndUpdateFundsDistribution(s.ctx)
s.Require().NoError(err)
err = s.poolKeeper.RecipientFundDistribution.Walk(s.ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) {
strAddr, err := s.authKeeper.AddressCodec().BytesToString(key)
s.Require().NoError(err)
if strAddr == "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2srklj6" {
s.Require().Equal(value, math.NewInt(300000))
} else if strAddr == "cosmos1tygms3xhhs3yv487phx3dw4a95jn7t7lpm470r" {
s.Require().Equal(value, math.NewInt(300000))
}
return false, nil
})
s.Require().NoError(err)
}

View File

@ -126,18 +126,17 @@ func (k MsgServer) CreateContinuousFund(ctx context.Context, msg *types.MsgCreat
// Check if total funds percentage exceeds 100%
// If exceeds, we should not setup continuous fund proposal.
totalStreamFundsPercentage := math.ZeroInt()
err = k.Keeper.RecipientFundPercentage.Walk(ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) {
totalStreamFundsPercentage = totalStreamFundsPercentage.Add(value)
totalStreamFundsPercentage := math.LegacyZeroDec()
err = k.Keeper.ContinuousFund.Walk(ctx, nil, func(key sdk.AccAddress, value types.ContinuousFund) (stop bool, err error) {
totalStreamFundsPercentage = totalStreamFundsPercentage.Add(value.Percentage)
return false, nil
})
if err != nil {
return nil, err
}
percentage := msg.Percentage.MulInt64(100)
totalStreamFundsPercentage = totalStreamFundsPercentage.Add(percentage.TruncateInt())
if totalStreamFundsPercentage.GT(math.NewInt(100)) {
return nil, fmt.Errorf("cannot set continuous fund proposal\ntotal funds percentage exceeds 100\ncurrent total percentage: %v", totalStreamFundsPercentage.Sub(percentage.TruncateInt()))
totalStreamFundsPercentage = totalStreamFundsPercentage.Add(msg.Percentage)
if totalStreamFundsPercentage.GT(math.LegacyOneDec()) {
return nil, fmt.Errorf("cannot set continuous fund proposal\ntotal funds percentage exceeds 100\ncurrent total percentage: %s", totalStreamFundsPercentage.Sub(msg.Percentage).MulInt64(100).TruncateInt().String())
}
// Create continuous fund proposal
@ -153,11 +152,6 @@ func (k MsgServer) CreateContinuousFund(ctx context.Context, msg *types.MsgCreat
return nil, err
}
// Set recipient fund percentage & distribution
err = k.RecipientFundPercentage.Set(ctx, recipient, percentage.TruncateInt())
if err != nil {
return nil, err
}
err = k.RecipientFundDistribution.Set(ctx, recipient, math.ZeroInt())
if err != nil {
return nil, err
@ -171,9 +165,6 @@ func (k MsgServer) WithdrawContinuousFund(ctx context.Context, msg *types.MsgWit
if err != nil {
return nil, err
}
if amount.IsNil() {
k.Logger.Info(fmt.Sprintf("no distribution amount found for recipient %s", msg.RecipientAddress))
}
return &types.MsgWithdrawContinuousFundResponse{Amount: amount}, nil
}
@ -200,7 +191,7 @@ func (k MsgServer) CancelContinuousFund(ctx context.Context, msg *types.MsgCance
}
// withdraw funds if any are allocated
withdrawnFunds, err := k.withdrawRecipientFunds(ctx, msg.RecipientAddress)
withdrawnFunds, err := k.withdrawRecipientFunds(ctx, recipient)
if err != nil && !errorspkg.Is(err, types.ErrNoRecipientFund) {
return nil, fmt.Errorf("error while withdrawing already allocated funds for recipient %s: %w", msg.RecipientAddress, err)
}
@ -209,10 +200,6 @@ func (k MsgServer) CancelContinuousFund(ctx context.Context, msg *types.MsgCance
return nil, fmt.Errorf("failed to remove continuous fund for recipient %s: %w", msg.RecipientAddress, err)
}
if err := k.RecipientFundPercentage.Remove(ctx, recipient); err != nil {
return nil, fmt.Errorf("failed to remove recipient fund percentage for recipient %s: %w", msg.RecipientAddress, err)
}
if err := k.RecipientFundDistribution.Remove(ctx, recipient); err != nil {
return nil, fmt.Errorf("failed to remove recipient fund distribution for recipient %s: %w", msg.RecipientAddress, err)
}

View File

@ -421,9 +421,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage := percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt())
suite.Require().NoError(err)
@ -439,8 +436,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage = percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient2, math.ZeroInt())
suite.Require().NoError(err)
@ -486,9 +481,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage := percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt())
suite.Require().NoError(err)
err = suite.poolKeeper.ToDistribute.Set(suite.ctx, math.ZeroInt())
@ -510,9 +502,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage := percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt())
suite.Require().NoError(err)
toDistribute := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000)))
@ -539,9 +528,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage := percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt())
suite.Require().NoError(err)
toDistribute := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000)))
@ -569,9 +555,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage := percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt())
suite.Require().NoError(err)
@ -580,16 +563,13 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
suite.Require().NoError(err)
cf = types.ContinuousFund{
Recipient: recipient2StrAddr,
Percentage: percentage,
Percentage: percentage2,
Expiry: &expiry,
}
// Set continuous fund
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage = percentage2.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient2, math.ZeroInt())
suite.Require().NoError(err)
@ -598,16 +578,13 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() {
suite.Require().NoError(err)
cf = types.ContinuousFund{
Recipient: recipient3StrAddr,
Percentage: percentage2,
Percentage: percentage3,
Expiry: &expiry,
}
// Set continuous fund
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient3, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage = percentage3.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient3, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient3, math.ZeroInt())
suite.Require().NoError(err)
@ -767,9 +744,6 @@ func (suite *KeeperTestSuite) TestCreateContinuousFund() {
}
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf)
suite.Require().NoError(err)
intPercentage := percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt())
suite.Require().NoError(err)
},
input: &types.MsgCreateContinuousFund{
Authority: suite.poolKeeper.GetAuthority(),
@ -860,9 +834,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipientAddr, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage := percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipientAddr, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipientAddr, math.ZeroInt())
suite.Require().NoError(err)
@ -878,9 +849,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() {
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf)
suite.Require().NoError(err)
// Set recipient fund percentage and recipient fund distribution
intPercentage = percentage.MulInt64(100)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient2, math.ZeroInt())
suite.Require().NoError(err)
@ -919,8 +887,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() {
suite.mockWithdrawContinuousFund()
err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient3, cf)
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient3, math.ZeroInt())
suite.Require().NoError(err)
err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient3, math.ZeroInt())
suite.Require().NoError(err)
},
@ -955,8 +921,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() {
suite.Require().NoError(err)
suite.Require().Equal(tc.withdrawnFunds, resp.WithdrawnAllocatedFund)
// All items below should return error as they are removed from the store
_, err := suite.poolKeeper.RecipientFundPercentage.Get(suite.ctx, tc.recipientAddr)
suite.Require().Contains(err.Error(), "collections: not found")
_, err = suite.poolKeeper.ContinuousFund.Get(suite.ctx, tc.recipientAddr)
suite.Require().Contains(err.Error(), "collections: not found")
_, err = suite.poolKeeper.RecipientFundDistribution.Get(suite.ctx, tc.recipientAddr)

View File

@ -9,7 +9,6 @@ import (
"google.golang.org/grpc"
"cosmossdk.io/core/appmodule"
"cosmossdk.io/core/legacy"
"cosmossdk.io/core/registry"
"cosmossdk.io/x/protocolpool/keeper"
"cosmossdk.io/x/protocolpool/types"
@ -24,7 +23,6 @@ const ConsensusVersion = 1
var (
_ module.HasName = AppModule{}
_ module.HasAminoCodec = AppModule{}
_ module.HasGRPCGateway = AppModule{}
_ module.AppModuleSimulation = AppModule{}
@ -60,9 +58,6 @@ func (AppModule) IsAppModule() {}
// Name returns the pool module's name.
func (AppModule) Name() string { return types.ModuleName }
// RegisterLegacyAminoCodec registers the pool module's types on the LegacyAmino codec.
func (AppModule) RegisterLegacyAminoCodec(cdc legacy.Amino) {}
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes
func (AppModule) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *gwruntime.ServeMux) {
if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil {
@ -104,10 +99,8 @@ func (am AppModule) InitGenesis(ctx context.Context, data json.RawMessage) error
if err := am.cdc.UnmarshalJSON(data, &genesisState); err != nil {
return err
}
if err := am.keeper.InitGenesis(ctx, &genesisState); err != nil {
return err
}
return nil
return am.keeper.InitGenesis(ctx, &genesisState)
}
// ExportGenesis returns the exported genesis state as raw bytes for the protocolpool module.

View File

@ -129,20 +129,6 @@ func (mr *MockBankKeeperMockRecorder) GetAllBalances(ctx, addr interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAllBalances), ctx, addr)
}
// MintCoins mocks base method.
func (m *MockBankKeeper) MintCoins(ctx context.Context, moduleName string, amt types.Coins) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MintCoins", ctx, moduleName, amt)
ret0, _ := ret[0].(error)
return ret0
}
// MintCoins indicates an expected call of MintCoins.
func (mr *MockBankKeeperMockRecorder) MintCoins(ctx, moduleName, amt interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MintCoins", reflect.TypeOf((*MockBankKeeper)(nil).MintCoins), ctx, moduleName, amt)
}
// SendCoinsFromAccountToModule mocks base method.
func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx context.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error {
m.ctrl.T.Helper()

View File

@ -4,5 +4,5 @@ import "cosmossdk.io/errors"
var (
ErrInvalidSigner = errors.Register(ModuleName, 2, "expected authority account as only signer for community pool spend message")
ErrNoRecipientFund = errors.Register(ModuleName, 3, "no recipient fund found for recipient")
ErrNoRecipientFund = errors.Register(ModuleName, 3, "no recipient found")
)

View File

@ -17,7 +17,6 @@ type AccountKeeper interface {
// BankKeeper defines the expected interface needed to retrieve account balances.
type BankKeeper interface {
MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error
GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins
SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins
SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error

View File

@ -6,7 +6,6 @@ import (
"cosmossdk.io/errors"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
@ -45,12 +44,11 @@ func validateBudget(bp Budget) error {
}
// Validate BudgetPerTranche
amount := sdk.NewCoins(*bp.BudgetPerTranche)
if amount.IsZero() {
if bp.BudgetPerTranche == nil || bp.BudgetPerTranche.IsZero() {
return fmt.Errorf("budget per tranche cannot be zero")
}
if err := amount.Validate(); err != nil {
return errors.Wrap(sdkerrors.ErrInvalidCoins, amount.String())
if err := bp.BudgetPerTranche.Validate(); err != nil {
return errors.Wrap(sdkerrors.ErrInvalidCoins, bp.BudgetPerTranche.String())
}
if bp.TranchesLeft == 0 {
@ -69,7 +67,7 @@ func validateContinuousFund(cf ContinuousFund) error {
}
// Validate percentage
if cf.Percentage.IsZero() || cf.Percentage.IsNil() {
if cf.Percentage.IsNil() || cf.Percentage.IsZero() {
return fmt.Errorf("percentage cannot be zero or empty")
}
if cf.Percentage.IsNegative() {

View File

@ -18,9 +18,9 @@ func NewMsgFundCommunityPool(amount sdk.Coins, depositor string) *MsgFundCommuni
}
}
// NewCommunityPoolSpend returns a new CommunityPoolSpend with authority, recipient and
// NewMsgCommunityPoolSpend returns a new CommunityPoolSpend with authority, recipient and
// a spending amount.
func NewCommunityPoolSpend(amount sdk.Coins, authority, recipient string) *MsgCommunityPoolSpend {
func NewMsgCommunityPoolSpend(amount sdk.Coins, authority, recipient string) *MsgCommunityPoolSpend {
return &MsgCommunityPoolSpend{
Authority: authority,
Recipient: recipient,

View File

@ -0,0 +1,230 @@
package types
import (
"testing"
time "time"
"github.com/stretchr/testify/require"
"cosmossdk.io/math"
codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
)
var hour = time.Hour
func TestRegisterInterfaces(t *testing.T) {
interfaceRegistry := codectestutil.NewCodecOptionsWithPrefixes("cosmos", "cosmosval").NewInterfaceRegistry()
RegisterInterfaces(interfaceRegistry)
require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgFundCommunityPool{}))
require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgCommunityPoolSpend{}))
require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgSubmitBudgetProposal{}))
require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgClaimBudget{}))
require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgCreateContinuousFund{}))
require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgCancelContinuousFund{}))
require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgWithdrawContinuousFund{}))
}
func TestNewMsgFundCommunityPool(t *testing.T) {
amount := sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100)))
depositor := "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4"
msg := NewMsgFundCommunityPool(amount, depositor)
require.Equal(t, amount, msg.Amount)
require.Equal(t, depositor, msg.Depositor)
}
func TestNewMsgCommunityPoolSpend(t *testing.T) {
amount := sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100)))
authority := "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4"
recipient := "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z5"
msg := NewMsgCommunityPoolSpend(amount, authority, recipient)
require.Equal(t, amount, msg.Amount)
require.Equal(t, authority, msg.Authority)
require.Equal(t, recipient, msg.Recipient)
}
func TestValidateGenesis(t *testing.T) {
defaultGenesis := DefaultGenesisState()
err := ValidateGenesis(defaultGenesis)
require.NoError(t, err)
gs := NewGenesisState(
[]*ContinuousFund{
{
Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
Percentage: math.LegacyMustNewDecFromStr("0.1"),
Expiry: nil,
},
},
[]*Budget{
{
RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
ClaimedAmount: &sdk.Coin{},
LastClaimedAt: &time.Time{},
TranchesLeft: 10,
BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)},
Period: &hour,
},
},
)
err = ValidateGenesis(gs)
require.NoError(t, err)
gs.Budget[0].RecipientAddress = ""
err = ValidateGenesis(gs)
require.EqualError(t, err, "recipient cannot be empty")
gs.ContinuousFund[0].Recipient = ""
err = ValidateGenesis(gs)
require.EqualError(t, err, "recipient cannot be empty")
}
func TestValidateBudget(t *testing.T) {
testCases := []struct {
name string
budget Budget
expErrMsg string
}{
{
"valid budget",
Budget{
RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
ClaimedAmount: &sdk.Coin{},
LastClaimedAt: &time.Time{},
TranchesLeft: 10,
BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)},
Period: &hour,
},
"",
},
{
"empty recipient",
Budget{
RecipientAddress: "",
},
"recipient cannot be empty",
},
{
"zero budget per tranche",
Budget{
RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(0)},
},
"budget per tranche cannot be zero",
},
{
"nil budget per tranche",
Budget{
RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
BudgetPerTranche: nil,
},
"budget per tranche cannot be zero",
},
{
"negative budget per tranche",
Budget{
RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(-100)},
},
"-100stake: invalid coins",
},
{
"zero tranches left",
Budget{
RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
TranchesLeft: 0,
BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)},
},
"invalid budget proposal: tranches must be greater than zero",
},
{
"zero period",
Budget{
RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
ClaimedAmount: &sdk.Coin{},
LastClaimedAt: &time.Time{},
TranchesLeft: 10,
BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)},
Period: nil,
},
"invalid budget proposal: period length should be greater than zero",
},
}
for _, tc := range testCases {
err := validateBudget(tc.budget)
if tc.expErrMsg == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.expErrMsg)
}
}
}
func TestValidateContinuousFund(t *testing.T) {
testCases := []struct {
name string
cf ContinuousFund
expErrMsg string
}{
{
"valid continuous fund",
ContinuousFund{
Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
Percentage: math.LegacyMustNewDecFromStr("0.1"),
Expiry: nil,
},
"",
},
{
"empty recipient",
ContinuousFund{
Recipient: "",
},
"recipient cannot be empty",
},
{
"zero percentage",
ContinuousFund{
Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
Percentage: math.LegacyZeroDec(),
},
"percentage cannot be zero or empty",
},
{
"nil percentage",
ContinuousFund{
Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
Percentage: math.LegacyDec{},
},
"percentage cannot be zero or empty",
},
{
"negative percentage",
ContinuousFund{
Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
Percentage: math.LegacyMustNewDecFromStr("-0.1"),
},
"percentage cannot be negative",
},
{
"percentage exceeds 100%",
ContinuousFund{
Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4",
Percentage: math.LegacyMustNewDecFromStr("1.1"),
},
"percentage cannot be greater than one",
},
}
for _, tc := range testCases {
err := validateContinuousFund(tc.cf)
if tc.expErrMsg == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.expErrMsg)
}
}
}