Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(x/protocolpool)!: allow any coins in continuous funds #21916

Merged
merged 13 commits into from
Nov 11, 2024
266 changes: 150 additions & 116 deletions api/cosmos/protocolpool/v1/genesis.pulsar.go

Large diffs are not rendered by default.

246 changes: 182 additions & 64 deletions api/cosmos/protocolpool/v1/tx.pulsar.go

Large diffs are not rendered by default.

692 changes: 624 additions & 68 deletions api/cosmos/protocolpool/v1/types.pulsar.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ func NewSimApp(
panic(err)
}

app.PoolKeeper = poolkeeper.NewKeeper(appCodec, runtime.NewEnvironment(runtime.NewKVStoreService(keys[pooltypes.StoreKey]), logger.With(log.ModuleKey, "x/protocolpool")), app.AuthKeeper, app.BankKeeper, app.StakingKeeper, govModuleAddr)
app.PoolKeeper = poolkeeper.NewKeeper(appCodec, runtime.NewEnvironment(runtime.NewKVStoreService(keys[pooltypes.StoreKey]), logger.With(log.ModuleKey, "x/protocolpool")), app.AuthKeeper, app.BankKeeper, govModuleAddr)

app.DistrKeeper = distrkeeper.NewKeeper(appCodec, runtime.NewEnvironment(runtime.NewKVStoreService(keys[distrtypes.StoreKey]), logger.With(log.ModuleKey, "x/distribution")), app.AuthKeeper, app.BankKeeper, app.StakingKeeper, cometService, authtypes.FeeCollectorName, govModuleAddr)

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/distribution/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func initFixture(t *testing.T) *fixture {
stakingKeeper := stakingkeeper.NewKeeper(cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[stakingtypes.StoreKey]), log.NewNopLogger(), runtime.EnvWithQueryRouterService(grpcRouter), runtime.EnvWithMsgRouterService(msgRouter)), accountKeeper, bankKeeper, consensusParamsKeeper, authority.String(), addresscodec.NewBech32Codec(sdk.Bech32PrefixValAddr), addresscodec.NewBech32Codec(sdk.Bech32PrefixConsAddr), cometService)
require.NoError(t, stakingKeeper.Params.Set(newCtx, stakingtypes.DefaultParams()))

poolKeeper := poolkeeper.NewKeeper(cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[pooltypes.StoreKey]), log.NewNopLogger()), accountKeeper, bankKeeper, stakingKeeper, authority.String())
poolKeeper := poolkeeper.NewKeeper(cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[pooltypes.StoreKey]), log.NewNopLogger()), accountKeeper, bankKeeper, authority.String())

distrKeeper := distrkeeper.NewKeeper(
cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[distrtypes.StoreKey]), logger), accountKeeper, bankKeeper, stakingKeeper, cometService, distrtypes.ModuleName, authority.String(),
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/gov/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func initFixture(tb testing.TB) *fixture {

stakingKeeper := stakingkeeper.NewKeeper(cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[stakingtypes.StoreKey]), log.NewNopLogger()), accountKeeper, bankKeeper, consensusParamsKeeper, authority.String(), addresscodec.NewBech32Codec(sdk.Bech32PrefixValAddr), addresscodec.NewBech32Codec(sdk.Bech32PrefixConsAddr), runtime.NewContextAwareCometInfoService())

poolKeeper := poolkeeper.NewKeeper(cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[pooltypes.StoreKey]), log.NewNopLogger()), accountKeeper, bankKeeper, stakingKeeper, authority.String())
poolKeeper := poolkeeper.NewKeeper(cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[pooltypes.StoreKey]), log.NewNopLogger()), accountKeeper, bankKeeper, authority.String())

// set default staking params
err := stakingKeeper.Params.Set(newCtx, stakingtypes.DefaultParams())
Expand Down
3 changes: 1 addition & 2 deletions x/protocolpool/depinject.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ type ModuleInputs struct {

AccountKeeper types.AccountKeeper
BankKeeper types.BankKeeper
StakingKeeper types.StakingKeeper
}

type ModuleOutputs struct {
Expand All @@ -59,7 +58,7 @@ func ProvideModule(in ModuleInputs) ModuleOutputs {
panic(err)
}

k := keeper.NewKeeper(in.Codec, in.Environment, in.AccountKeeper, in.BankKeeper, in.StakingKeeper, authorityAddr)
k := keeper.NewKeeper(in.Codec, in.Environment, in.AccountKeeper, in.BankKeeper, authorityAddr)
m := NewAppModule(in.Codec, k, in.AccountKeeper, in.BankKeeper)

return ModuleOutputs{
Expand Down
17 changes: 9 additions & 8 deletions x/protocolpool/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"time"

"cosmossdk.io/math"
"cosmossdk.io/x/protocolpool/types"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -56,19 +55,19 @@ func (k Keeper) InitGenesis(ctx context.Context, data *types.GenesisState) error
return fmt.Errorf("failed to set last balance: %w", err)
}

totalToBeDistributed := math.ZeroInt()
totalToBeDistributed := sdk.NewCoins()
for _, distribution := range data.Distributions {
totalToBeDistributed = totalToBeDistributed.Add(distribution.Amount)
totalToBeDistributed = totalToBeDistributed.Add(distribution.Amount.Amount...)
if err := k.Distributions.Set(ctx, *distribution.Time, distribution.Amount); err != nil {
return fmt.Errorf("failed to set distribution: %w", err)
}
}

// sanity check to avoid trying to distribute more than what is available
if data.LastBalance.LT(totalToBeDistributed) {
return errors.New("total to be distributed is greater than the last balance")
}

if data.LastBalance.Amount.IsAnyGT(totalToBeDistributed) || !totalToBeDistributed.DenomsSubsetOf(data.LastBalance.Amount) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last balance should always be >= total to be distributed.

Simplest thing would be to flip the check: totalToBeDistributed.IsAnyGT(last balance)
This includes the denoms implicite so that you can drop the DenomsSubsetOf check

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right about the inverted comparison, but I still need DenomsSubsetOf, see:

// IsAnyGT returns true iff for any denom in coins, the denom is present at a
// greater amount in coinsB.
//
// e.g.
// {2A, 3B}.IsAnyGT{A} = true
// {2A, 3B}.IsAnyGT{5C} = false
// {}.IsAnyGT{5C} = false
// {2A, 3B}.IsAnyGT{} = false

This case {}.IsAnyGT{5C} = false shows that if totalToBeDistributed has 1foo but LastBalance contains 1bar, it will be false.

return errors.New("total to be distributed is greater than the last balance" + fmt.Sprint(data.LastBalance.Amount, totalToBeDistributed))
}
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the logical inconsistency in the condition and error message

There's a mismatch between the condition being checked and the error message returned. The condition data.LastBalance.Amount.IsAnyGT(totalToBeDistributed) checks if the last balance amount is greater than the total to be distributed. However, the error message states "total to be distributed is greater than the last balance," which implies the opposite.

To correct this, the condition should check if totalToBeDistributed.IsAnyGT(data.LastBalance.Amount) instead.

Apply this diff to fix the condition:

-if data.LastBalance.Amount.IsAnyGT(totalToBeDistributed) || !totalToBeDistributed.DenomsSubsetOf(data.LastBalance.Amount) {
+if totalToBeDistributed.IsAnyGT(data.LastBalance.Amount) || !totalToBeDistributed.DenomsSubsetOf(data.LastBalance.Amount) {
    return errors.New("total to be distributed is greater than the last balance" + fmt.Sprint(data.LastBalance.Amount, totalToBeDistributed))
}

This change ensures that the error is returned when the total to be distributed exceeds the last balance, aligning the condition with the error message.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if data.LastBalance.Amount.IsAnyGT(totalToBeDistributed) || !totalToBeDistributed.DenomsSubsetOf(data.LastBalance.Amount) {
return errors.New("total to be distributed is greater than the last balance" + fmt.Sprint(data.LastBalance.Amount, totalToBeDistributed))
}
if totalToBeDistributed.IsAnyGT(data.LastBalance.Amount) || !totalToBeDistributed.DenomsSubsetOf(data.LastBalance.Amount) {
return errors.New("total to be distributed is greater than the last balance" + fmt.Sprint(data.LastBalance.Amount, totalToBeDistributed))
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused a bit about the 1st condition, should return an error when totalToBeDistributed > lastBalance?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hieuvubk, you're correct. The error should be returned when totalToBeDistributed is greater than data.LastBalance.Amount. Currently, the condition:

if data.LastBalance.Amount.IsAnyGT(totalToBeDistributed) || !totalToBeDistributed.DenomsSubsetOf(data.LastBalance.Amount) {
    return errors.New("total to be distributed is greater than the last balance" + fmt.Sprint(data.LastBalance.Amount, totalToBeDistributed))
}

checks if data.LastBalance.Amount is greater than totalToBeDistributed, which is the opposite of the intended logic based on the error message.

To fix this inconsistency, the condition should be:

if totalToBeDistributed.IsAnyGT(data.LastBalance.Amount) || !totalToBeDistributed.DenomsSubsetOf(data.LastBalance.Amount) {
    return errors.New("total to be distributed is greater than the last balance" + fmt.Sprint(data.LastBalance.Amount, totalToBeDistributed))
}

This change ensures that an error is returned when totalToBeDistributed exceeds data.LastBalance.Amount, aligning the condition with the error message and the expected behavior.

/_/\
( •.• )

🚀<
A curious rabbit pondering over the code!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not simply go with fmt.Errorf instead which is typically used in these scenarios? For example:

fmt.Errorf("an amount to be distributed is greater than the last balance: %s > %s", totalToBeDistributed, data.LastBalance.Amount))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually for testing, but I think it can be useful if we ever encounter this, I'll modify it.

return nil
}

Expand Down Expand Up @@ -112,12 +111,14 @@ func (k Keeper) ExportGenesis(ctx context.Context) (*types.GenesisState, error)

genState := types.NewGenesisState(cf, budget)

genState.LastBalance, err = k.LastBalance.Get(ctx)
lastBalance, err := k.LastBalance.Get(ctx)
if err != nil {
Comment on lines +119 to 120
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Wrap error when retrieving last balance for better context

When returning errors, it's recommended to wrap them with additional context to aid in debugging and provide clarity.

Apply this diff to wrap the error:

 if err != nil {
-    return nil, err
+    return nil, fmt.Errorf("failed to get last balance: %w", err)
 }

Committable suggestion was skipped due to low confidence.

return nil, err
}

err = k.Distributions.Walk(ctx, nil, func(key time.Time, value math.Int) (stop bool, err error) {
genState.LastBalance = lastBalance

err = k.Distributions.Walk(ctx, nil, func(key time.Time, value types.DistributionAmount) (stop bool, err error) {
genState.Distributions = append(genState.Distributions, &types.Distribution{
Time: &key,
Comment on lines +126 to 128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Wrap error when walking through distributions for enhanced clarity

Wrapping the error from k.Distributions.Walk(ctx, nil, ...) with context can provide better insight during debugging and maintain consistency in error handling.

Apply this diff to wrap the error:

 if err != nil {
-    return nil, err
+    return nil, fmt.Errorf("failed to walk through distributions: %w", err)
 }

Committable suggestion was skipped due to low confidence.

Amount: value,
Expand Down
6 changes: 3 additions & 3 deletions x/protocolpool/keeper/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ func (suite *KeeperTestSuite) TestInitGenesis() {
)

gs.Distributions = append(gs.Distributions, &types.Distribution{
Amount: math.OneInt(),
Amount: types.DistributionAmount{Amount: sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100)))},
Time: &time.Time{},
})
Comment on lines +35 to 37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding test cases with multiple denominations

While the test initializes the Distribution with a single coin of denomination "stake", the goal of the PR is to allow any coins in continuous funds. To ensure comprehensive test coverage, please add test cases that include multiple denominations. This will help verify that distributions handle various coin types correctly.


err := suite.poolKeeper.InitGenesis(suite.ctx, gs)
suite.Require().ErrorContains(err, "total to be distributed is greater than the last balance")

// Set last balance
gs.LastBalance = math.NewInt(1)
gs.LastBalance = types.DistributionAmount{Amount: sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(1)))}
err = suite.poolKeeper.InitGenesis(suite.ctx, gs)
suite.Require().NoError(err)

Expand All @@ -49,5 +49,5 @@ func (suite *KeeperTestSuite) TestInitGenesis() {
suite.Require().NoError(err)
suite.Require().Equal(gs.ContinuousFund, exportedGenState.ContinuousFund)
suite.Require().Equal(gs.Budget, exportedGenState.Budget)
suite.Require().Equal(math.OneInt(), exportedGenState.LastBalance)
suite.Require().Equal(math.OneInt(), exportedGenState.LastBalance.Amount.AmountOf("stake"))
}
114 changes: 51 additions & 63 deletions x/protocolpool/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import (
type Keeper struct {
appmodule.Environment

authKeeper types.AccountKeeper
bankKeeper types.BankKeeper
stakingKeeper types.StakingKeeper
authKeeper types.AccountKeeper
bankKeeper types.BankKeeper

cdc codec.BinaryCodec

Expand All @@ -34,16 +33,16 @@ type Keeper struct {
BudgetProposal collections.Map[sdk.AccAddress, types.Budget]
ContinuousFund collections.Map[sdk.AccAddress, types.ContinuousFund]
// RecipientFundDistribution key: RecipientAddr | value: Claimable amount
RecipientFundDistribution collections.Map[sdk.AccAddress, math.Int]
Distributions collections.Map[time.Time, math.Int] // key: time.Time | value: amount
LastBalance collections.Item[math.Int]
RecipientFundDistribution collections.Map[sdk.AccAddress, types.DistributionAmount]
Distributions collections.Map[time.Time, types.DistributionAmount] // key: time.Time, denom | value: amounts
LastBalance collections.Item[types.DistributionAmount]
}

const (
errModuleAccountNotSet = "%s module account has not been set"
)

func NewKeeper(cdc codec.BinaryCodec, env appmodule.Environment, ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper, authority string,
func NewKeeper(cdc codec.BinaryCodec, env appmodule.Environment, ak types.AccountKeeper, bk types.BankKeeper, authority string,
) Keeper {
// ensure pool module account is set
if addr := ak.GetModuleAddress(types.ModuleName); addr == nil {
Expand All @@ -64,14 +63,13 @@ func NewKeeper(cdc codec.BinaryCodec, env appmodule.Environment, ak types.Accoun
Environment: env,
authKeeper: ak,
bankKeeper: bk,
stakingKeeper: sk,
cdc: cdc,
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)),
RecipientFundDistribution: collections.NewMap(sb, types.RecipientFundDistributionKey, "recipient_fund_distribution", sdk.AccAddressKey, sdk.IntValue),
Distributions: collections.NewMap(sb, types.DistributionsKey, "distributions", sdk.TimeKey, sdk.IntValue),
LastBalance: collections.NewItem(sb, types.LastBalanceKey, "last_balance", sdk.IntValue),
RecipientFundDistribution: collections.NewMap(sb, types.RecipientFundDistributionKey, "recipient_fund_distribution", sdk.AccAddressKey, codec.CollValue[types.DistributionAmount](cdc)),
Distributions: collections.NewMap(sb, types.DistributionsKey, "distributions", sdk.TimeKey, codec.CollValue[types.DistributionAmount](cdc)),
LastBalance: collections.NewItem(sb, types.LastBalanceKey, "last_balance", codec.CollValue[types.DistributionAmount](cdc)),
}

schema, err := sb.Build()
Expand Down Expand Up @@ -114,34 +112,27 @@ func (k Keeper) GetCommunityPool(ctx context.Context) (sdk.Coins, error) {
return k.bankKeeper.GetAllBalances(ctx, moduleAccount.GetAddress()), nil
}

func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipient []byte) (sdk.Coin, error) {
func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipient []byte) (sdk.Coins, error) {
// get allocated continuous fund
fundsAllocated, err := k.RecipientFundDistribution.Get(ctx, recipient)
if err != nil {
if errors.Is(err, collections.ErrNotFound) {
return sdk.Coin{}, types.ErrNoRecipientFound
return nil, types.ErrNoRecipientFound
}
return sdk.Coin{}, err
}

denom, err := k.stakingKeeper.BondDenom(ctx)
if err != nil {
return sdk.Coin{}, err
return nil, err
}

// Distribute funds to the recipient from pool module account
withdrawnAmount := sdk.NewCoin(denom, fundsAllocated)
err = k.DistributeFromStreamFunds(ctx, sdk.NewCoins(withdrawnAmount), recipient)
err = k.DistributeFromStreamFunds(ctx, fundsAllocated.Amount, recipient)
if err != nil {
return sdk.Coin{}, fmt.Errorf("error while distributing funds: %w", err)
return nil, fmt.Errorf("error while distributing funds: %w", err)
}

// reset fund distribution
err = k.RecipientFundDistribution.Set(ctx, recipient, math.ZeroInt())
err = k.RecipientFundDistribution.Set(ctx, recipient, types.DistributionAmount{Amount: sdk.NewCoins()})
if err != nil {
return sdk.Coin{}, err
return nil, err
}
return withdrawnAmount, nil
return fundsAllocated.Amount, nil
}

// SetToDistribute sets the amount to be distributed among recipients.
Expand All @@ -152,30 +143,29 @@ func (k Keeper) SetToDistribute(ctx context.Context) error {
return errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", types.ProtocolPoolDistrAccount)
}

denom, err := k.stakingKeeper.BondDenom(ctx)
if err != nil {
return err
}

currentBalance := k.bankKeeper.GetAllBalances(ctx, moduleAccount.GetAddress())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we get the usual: WhaT iF tHe modUle aCCounT Has ManY DeNOM?
Which is that case will be relatively valid. Any way we can make the claimer pay for this call?
As this is call in begin block maybe not, but what prevents in a token factory enabled chain, to send to the module accounts 1 millions worthless tokens until it slows the chain.

Unless we make ProtocolPoolDistrAccount a blocked module account, like FeeCollector is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is call in begin block maybe not, but what prevents in a token factory enabled chain, to send to the module accounts 1 millions worthless tokens until it slows the chain.

Yeah, I thought the same, realistically it shouldn't be needed but it could make things go wrong in the long run, so I guess I'll add the whitelist now :/

distributionBalance := currentBalance.AmountOf(denom)

// if the balance is zero, return early
if distributionBalance.IsZero() {
if currentBalance.IsZero() {
return nil
}

// if the balance does not have any of the allowed denoms, return early // TODO
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall this check be implemented now?


lastBalance, err := k.LastBalance.Get(ctx)
if err != nil {
if errors.Is(err, collections.ErrNotFound) {
lastBalance = math.ZeroInt()
lastBalance = types.DistributionAmount{Amount: sdk.NewCoins()}
} else {
return err
}
}

// Calculate the amount to be distributed
amountToDistribute := distributionBalance.Sub(lastBalance)
amountToDistribute, anyNegative := currentBalance.SafeSub(lastBalance.Amount...)
if anyNegative {
return errors.New("error while calculating the amount to distribute, result can't be negative")
}
Comment on lines +175 to +178
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate SafeSub operation to prevent unexpected errors

The use of SafeSub may produce unexpected results if lastBalance.Amount contains denominations not present in currentBalance. This could cause anyNegative to be true even if overall balances are positive. Consider aligning denominations before subtraction or handling missing denominations explicitly.


// Check if there are any recipients to distribute to, if not, send straight to the community pool and avoid
// setting the distributions
Expand All @@ -190,24 +180,23 @@ func (k Keeper) SetToDistribute(ctx context.Context) error {

// if there are no continuous funds, send all the funds to the community pool and reset the last balance
if !hasContinuousFunds {
poolCoins := sdk.NewCoins(sdk.NewCoin(denom, amountToDistribute))
if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ProtocolPoolDistrAccount, types.ModuleName, poolCoins); err != nil {
if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ProtocolPoolDistrAccount, types.ModuleName, amountToDistribute); err != nil {
return err
}

if !lastBalance.IsZero() { // only reset if the last balance is not zero (so we leave it at zero/nil)
return k.LastBalance.Set(ctx, math.ZeroInt())
if !lastBalance.Amount.IsZero() { // only reset if the last balance is not zero (so we leave it at zero)
return k.LastBalance.Set(ctx, types.DistributionAmount{Amount: sdk.NewCoins()})
Comment on lines +193 to +198
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve condition nesting in SetToDistribute

The nested if statements can be simplified to enhance readability:

if !hasContinuousFunds {
    if err := ...; err != nil {
        return err
    }
    if !lastBalance.Amount.IsZero() {
        return k.LastBalance.Set(ctx, types.DistributionAmount{Amount: sdk.NewCoins()})
    }
    return nil
}

Consider flattening the conditions or adding comments to clarify the flow.

}

return nil
}

if err = k.Distributions.Set(ctx, k.HeaderService.HeaderInfo(ctx).Time, amountToDistribute); err != nil {
if err = k.Distributions.Set(ctx, k.HeaderService.HeaderInfo(ctx).Time, types.DistributionAmount{Amount: amountToDistribute}); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Confirm uniqueness of timestamp keys in Distributions

Using k.HeaderService.HeaderInfo(ctx).Time as the key in Distributions.Set may lead to key collisions if multiple distributions occur within the same timestamp granularity. Consider ensuring that the timestamps used as keys are unique or incorporate additional logic to prevent accidental overwrites.

return fmt.Errorf("error while setting Distributions: %w", err)
}

// Update the last balance
return k.LastBalance.Set(ctx, distributionBalance)
return k.LastBalance.Set(ctx, types.DistributionAmount{Amount: currentBalance})
}

func (k Keeper) IterateAndUpdateFundsDistribution(ctx context.Context) error {
Expand All @@ -229,11 +218,11 @@ func (k Keeper) IterateAndUpdateFundsDistribution(ctx context.Context) error {
}

// next we iterate over the distributions, calculate each recipient's share and the remaining pool funds
toDistribute := map[string]math.Int{}
poolFunds := math.ZeroInt()
fullAmountToDistribute := math.ZeroInt()
toDistribute := map[string]sdk.Coins{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming is hard
personal preference: how about distributeToRecipient ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accepting these changes 👌

poolFunds := sdk.NewCoins()
fullAmountToDistribute := sdk.NewCoins()

if err = k.Distributions.Walk(ctx, nil, func(key time.Time, amount math.Int) (stop bool, err error) {
if err = k.Distributions.Walk(ctx, nil, func(key time.Time, amount types.DistributionAmount) (stop bool, err error) {
percentageToDistribute := math.LegacyZeroDec()
for _, f := range funds {
if f.Expiry != nil && f.Expiry.Before(key) {
Expand All @@ -244,20 +233,25 @@ func (k Keeper) IterateAndUpdateFundsDistribution(ctx context.Context) error {

_, ok := toDistribute[f.Recipient]
if !ok {
toDistribute[f.Recipient] = math.ZeroInt()
toDistribute[f.Recipient] = sdk.NewCoins()
}

for _, denom := range amount.Amount.Denoms() {
am := sdk.NewCoin(denom, f.Percentage.MulInt(amount.Amount.AmountOf(denom)).TruncateInt())
toDistribute[f.Recipient] = toDistribute[f.Recipient].Add(am)
fullAmountToDistribute = fullAmountToDistribute.Add(am)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review the use of TruncateInt() in distribution calculations

When calculating each recipient's share, TruncateInt() is used after multiplying by the percentage. This could lead to loss of fractional amounts. Consider whether this aligns with the expected financial logic, or if using RoundInt() would be more appropriate to minimize discrepancies.

}
amountToDistribute := f.Percentage.MulInt(amount).TruncateInt()
toDistribute[f.Recipient] = toDistribute[f.Recipient].Add(amountToDistribute)
fullAmountToDistribute = fullAmountToDistribute.Add(amountToDistribute)
}

// sanity check for max percentage
if percentageToDistribute.GT(math.LegacyOneDec()) {
return true, errors.New("total funds percentage cannot exceed 100")
}

remaining := math.LegacyOneDec().Sub(percentageToDistribute).MulInt(amount).RoundInt()
poolFunds = poolFunds.Add(remaining)
for _, denom := range amount.Amount.Denoms() {
remaining := sdk.NewCoin(denom, math.LegacyOneDec().Sub(percentageToDistribute).MulInt(amount.Amount.AmountOf(denom)).TruncateInt())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar code was used before but is another round down which has a rest that is not distributed.
Better remaining = amount - distributed amount

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be fixed now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential negative values in remaining funds calculation

When calculating remaining:

remaining := sdk.NewCoin(denom, math.LegacyOneDec().Sub(percentageToDistribute).MulInt(amount.Amount.AmountOf(denom)).TruncateInt())

If percentageToDistribute exceeds 1.0, the subtraction could result in a negative value, leading to incorrect coin amounts. Ensure that percentageToDistribute does not exceed 1.0 before performing this calculation.

poolFunds = poolFunds.Add(remaining)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review the use of TruncateInt() in remaining funds calculation

Similarly, when calculating the remaining pool funds, TruncateInt() is used. Ensure this approach meets the desired precision and that any loss of fractional amounts is acceptable within the application's context.


return false, nil
}); err != nil {
Expand All @@ -269,26 +263,20 @@ func (k Keeper) IterateAndUpdateFundsDistribution(ctx context.Context) error {
return err
}

if err = k.LastBalance.Set(ctx, math.ZeroInt()); err != nil {
if err = k.LastBalance.Set(ctx, types.DistributionAmount{Amount: sdk.NewCoins()}); err != nil {
return err
}

// send the funds to the stream account to be distributed later, and the remaining to the community pool
bondDenom, err := k.stakingKeeper.BondDenom(ctx)
if err != nil {
return err
}

streamAmt := sdk.NewCoins(sdk.NewCoin(bondDenom, fullAmountToDistribute))
streamAmt := fullAmountToDistribute
if !streamAmt.IsZero() {
if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ProtocolPoolDistrAccount, types.StreamAccount, streamAmt); err != nil {
return err
}
}

if !poolFunds.IsZero() {
poolCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, poolFunds))
if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ProtocolPoolDistrAccount, types.ModuleName, poolCoins); err != nil {
if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ProtocolPoolDistrAccount, types.ModuleName, poolFunds); err != nil {
return err
}
}
Expand All @@ -310,14 +298,14 @@ func (k Keeper) IterateAndUpdateFundsDistribution(ctx context.Context) error {
toClaim, err := k.RecipientFundDistribution.Get(ctx, bzAddr)
if err != nil {
if errors.Is(err, collections.ErrNotFound) {
toClaim = math.ZeroInt()
toClaim = types.DistributionAmount{Amount: sdk.NewCoins()}
} else {
return err
}
}

amount := toClaim.Add(toDistribute[recipient])
if err = k.RecipientFundDistribution.Set(ctx, bzAddr, amount); err != nil {
toClaim.Amount = toClaim.Amount.Add(toDistribute[recipient]...)
if err = k.RecipientFundDistribution.Set(ctx, bzAddr, toClaim); err != nil {
return err
}
}
Expand Down
Loading
Loading