Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions sei-cosmos/types/dec_coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,18 @@ func sanitizeDecCoins(decCoins []DecCoin) DecCoins {
return newDecCoins.Sort()
}

// NewDecCoinsFromCoins constructs a new coin set with decimal values
// from regular Coins.
func NewDecCoinsFromCoins(coins ...Coin) DecCoins {
decCoins := make(DecCoins, len(coins))
// NewDecCoinsFromCoins constructs DecCoins from Coins, or an error if any
// amount exceeds the decimal conversion range.
func NewDecCoinsFromCoins(coins ...Coin) (DecCoins, error) {
newCoins := NewCoins(coins...)
decCoins := make(DecCoins, len(newCoins))
for i, coin := range newCoins {
if !coin.Amount.CanConvertToDec() {
return nil, fmt.Errorf("coin %s amount exceeds decimal conversion range", coin)
}
decCoins[i] = NewDecCoinFromCoin(coin)
}

return decCoins
return decCoins, nil
}

// String implements the Stringer interface for DecCoins. It returns a
Expand Down
27 changes: 24 additions & 3 deletions sei-cosmos/types/dec_coin_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types_test

import (
"math/big"
"strings"
"testing"

Expand All @@ -18,7 +19,26 @@ import (
func TestNewDecCoinConversionRange(t *testing.T) {
require.Panics(t, func() { sdk.NewDecCoin("stake", maxInt()) }, "NewDecCoin must reject max Int")
require.Panics(t, func() { sdk.NewDecCoinFromCoin(sdk.NewCoin("stake", maxInt())) }, "NewDecCoinFromCoin must reject max Int")
require.Panics(t, func() { sdk.NewDecCoinsFromCoins(sdk.NewCoin("stake", maxInt())) }, "NewDecCoinsFromCoins must reject max Int")

require.False(t, sdk.Int{}.CanConvertToDec())
require.False(t, maxInt().CanConvertToDec())
_, err := sdk.NewDecCoinsFromCoins(sdk.NewCoin("stake", maxInt()))
require.Error(t, err)

// Largest Int whose whole-coin Dec stays within maxDecBitLen (315):
// floor((2^315 - 1) / 10^18).
ten18 := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)
maxScaled := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 315), big.NewInt(1))
maxConvertible := sdk.NewIntFromBigInt(new(big.Int).Quo(maxScaled, ten18))
require.True(t, maxConvertible.CanConvertToDec())
decCoins, err := sdk.NewDecCoinsFromCoins(sdk.NewCoin("stake", maxConvertible))
require.NoError(t, err)
require.Equal(t, maxConvertible.ToDec(), decCoins[0].Amount)

oneAbove := maxConvertible.Add(sdk.OneInt())
require.False(t, oneAbove.CanConvertToDec())
_, err = sdk.NewDecCoinsFromCoins(sdk.NewCoin("stake", oneAbove))
require.Error(t, err)
}

type decCoinTestSuite struct {
Expand Down Expand Up @@ -293,7 +313,7 @@ func (s *decCoinTestSuite) TestSubDecCoins() {
msg string
}{
{
sdk.NewDecCoinsFromCoins(sdk.NewCoin("mytoken", sdk.NewInt(10)), sdk.NewCoin("btc", sdk.NewInt(20)), sdk.NewCoin("eth", sdk.NewInt(30))),
sdk.NewDecCoins(sdk.NewDecCoin("mytoken", sdk.NewInt(10)), sdk.NewDecCoin("btc", sdk.NewInt(20)), sdk.NewDecCoin("eth", sdk.NewInt(30))),
true,
"sorted coins should have passed",
},
Expand All @@ -309,7 +329,8 @@ func (s *decCoinTestSuite) TestSubDecCoins() {
},
}

decCoins := sdk.NewDecCoinsFromCoins(sdk.NewCoin("btc", sdk.NewInt(10)), sdk.NewCoin("eth", sdk.NewInt(15)), sdk.NewCoin("mytoken", sdk.NewInt(5)))
decCoins, err := sdk.NewDecCoinsFromCoins(sdk.NewCoin("btc", sdk.NewInt(10)), sdk.NewCoin("eth", sdk.NewInt(15)), sdk.NewCoin("mytoken", sdk.NewInt(5)))
s.Require().NoError(err)

for _, tc := range tests {
tc := tc
Expand Down
9 changes: 9 additions & 0 deletions sei-cosmos/types/int.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ func (i Int) ToDec() Dec {
return NewDecFromInt(i)
}

// CanConvertToDec reports whether i fits in a whole-number Dec (i × 10^Precision).
func (i Int) CanConvertToDec() bool {
if i.i == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Returning true for a nil Int makes the predicate disagree with the operation it guards: Int{}.ToDec() panics (big.Int.Mul on the nil returned by BigInt()), so a caller that trusts CanConvertToDec() gets a panic anyway. This is unreachable from NewDecCoinsFromCoins (the NewCoins call rejects such coins first), but CanConvertToDec is new exported API on sdk.Int and other callers won't have that guarantee. Returning false for nil — or documenting that the predicate assumes a non-nil Int — would make the contract match ToDec.

Minor, same lines: i.BigInt() already returns a fresh copy, so the wrapping new(big.Int).Set(...) is a redundant allocation.

return false
}
scaled := new(big.Int).Mul(i.BigInt(), precisionMultiplier(0))
return scaled.BitLen() <= maxDecBitLen
}

// Int64 converts Int to int64
// Panics if the value is out of range
func (i Int) Int64() int64 {
Expand Down
6 changes: 5 additions & 1 deletion sei-cosmos/x/auth/legacy/legacytx/stdtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ func (fee StdFee) GasPrices() sdk.DecCoins {
if fee.Gas > uint64(math.MaxInt64) {
panic(fmt.Sprintf("gas %d exceeds max int64", fee.Gas))
}
return sdk.NewDecCoinsFromCoins(fee.Amount...).QuoDec(sdk.NewDec(int64(fee.Gas))) //nolint:gosec G115 -- bounds checked above
decAmount, err := sdk.NewDecCoinsFromCoins(fee.Amount...)
if err != nil {
panic(err)
}
return decAmount.QuoDec(sdk.NewDec(int64(fee.Gas))) //nolint:gosec G115 -- bounds checked above
}

// StdTx is the legacy transaction format for wrapping a Msg with Fee and Signatures.
Expand Down
7 changes: 5 additions & 2 deletions sei-cosmos/x/distribution/keeper/allocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ func (k Keeper) AllocateTokens(
// (and distributed to the previous proposer)
feeCollector := k.authKeeper.GetModuleAccount(ctx, k.feeCollectorName)
feesCollectedInt := k.bankKeeper.GetAllBalances(ctx, feeCollector.GetAddress())
feesCollected := sdk.NewDecCoinsFromCoins(feesCollectedInt...)
feesCollected, err := sdk.NewDecCoinsFromCoins(feesCollectedInt...)
if err != nil {
panic(err)
}

// transfer collected fees to the distribution module account
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, k.feeCollectorName, types.ModuleName, feesCollectedInt)
err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, k.feeCollectorName, types.ModuleName, feesCollectedInt)
if err != nil {
panic(err)
}
Expand Down
9 changes: 7 additions & 2 deletions sei-cosmos/x/distribution/keeper/fee_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ import (
func (k Keeper) DistributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) error {
feePool := k.GetFeePool(ctx)

decAmount, err := sdk.NewDecCoinsFromCoins(amount...)
if err != nil {
return err
}

// NOTE the community pool isn't a module account, however its coins
// are held in the distribution module account. Thus the community pool
// must be reduced separately from the SendCoinsFromModuleToAccount call
newPool, negative := feePool.CommunityPool.SafeSub(sdk.NewDecCoinsFromCoins(amount...))
newPool, negative := feePool.CommunityPool.SafeSub(decAmount)
if negative {
return types.ErrBadDistribution
}

feePool.CommunityPool = newPool

err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiveAddr, amount)
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiveAddr, amount)
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion sei-cosmos/x/distribution/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,9 @@ func (suite *KeeperTestSuite) TestGRPCCommunityPool() {
suite.Require().Nil(err)
req = &types.QueryCommunityPoolRequest{}

expPool = &types.QueryCommunityPoolResponse{Pool: sdk.NewDecCoinsFromCoins(amount...)}
pool, err := sdk.NewDecCoinsFromCoins(amount...)
suite.Require().NoError(err)
expPool = &types.QueryCommunityPoolResponse{Pool: pool}
},
true,
},
Expand Down
6 changes: 5 additions & 1 deletion sei-cosmos/x/distribution/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, _ sdk.ConsAddress, valAddr
}
} else {
feePool := h.k.GetFeePool(ctx)
feePool.CommunityPool = feePool.CommunityPool.Add(sdk.NewDecCoinsFromCoins(coins...)...)
decCoins, err := sdk.NewDecCoinsFromCoins(coins...)
if err != nil {
panic(err)
}
feePool.CommunityPool = feePool.CommunityPool.Add(decCoins...)
h.k.SetFeePool(ctx, feePool)
}
}
Expand Down
14 changes: 12 additions & 2 deletions sei-cosmos/x/distribution/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,16 @@ func (k Keeper) WithdrawValidatorCommission(ctx sdk.Context, valAddr sdk.ValAddr
}

commission, remainder := accumCommission.Commission.TruncateDecimal()
commissionDec, err := sdk.NewDecCoinsFromCoins(commission...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] The conversion check runs after SetValidatorAccumulatedCommission has already written at line 130, so an error here returns with the accumulated commission truncated to remainder while outstanding rewards are untouched and no coins were sent. Every production caller (msg server, precompiles) runs in a revertible context so this can't corrupt committed state today, but it's the opposite ordering from what this same PR does in FundCommunityPool and DistributeFromFeePool, where the conversion is deliberately hoisted above the first mutation. Moving the NewDecCoinsFromCoins(commission...) call up between TruncateDecimal() and SetValidatorAccumulatedCommission makes the validate-then-mutate order uniform and removes the dependence on the caller's revert behavior.

if err != nil {
return nil, err
}

k.SetValidatorAccumulatedCommission(ctx, valAddr, types.ValidatorAccumulatedCommission{Commission: remainder}) // leave remainder to withdraw later

// update outstanding
outstanding := k.GetValidatorOutstandingRewards(ctx, valAddr).Rewards
k.SetValidatorOutstandingRewards(ctx, valAddr, types.ValidatorOutstandingRewards{Rewards: outstanding.Sub(sdk.NewDecCoinsFromCoins(commission...))})
k.SetValidatorOutstandingRewards(ctx, valAddr, types.ValidatorOutstandingRewards{Rewards: outstanding.Sub(commissionDec)})

if !commission.IsZero() {
accAddr := sdk.AccAddress(valAddr)
Expand Down Expand Up @@ -169,12 +174,17 @@ func (k Keeper) GetTotalRewards(ctx sdk.Context) (totalRewards sdk.DecCoins) {
// added to the pool. An error is returned if the amount cannot be sent to the
// module account.
func (k Keeper) FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error {
decAmount, err := sdk.NewDecCoinsFromCoins(amount...)
if err != nil {
return err
}

if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount); err != nil {
return err
}

feePool := k.GetFeePool(ctx)
feePool.CommunityPool = feePool.CommunityPool.Add(sdk.NewDecCoinsFromCoins(amount...)...)
feePool.CommunityPool = feePool.CommunityPool.Add(decAmount...)
k.SetFeePool(ctx, feePool)

return nil
Expand Down
29 changes: 24 additions & 5 deletions sei-cosmos/x/distribution/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ func TestFundCommunityPool(t *testing.T) {
err := app.DistrKeeper.FundCommunityPool(ctx, amount, addr[0])
assert.Nil(t, err)

assert.Equal(t, initPool.CommunityPool.Add(sdk.NewDecCoinsFromCoins(amount...)...), app.DistrKeeper.GetFeePool(ctx).CommunityPool)
wantPool, err := sdk.NewDecCoinsFromCoins(amount...)
require.NoError(t, err)
assert.Equal(t, initPool.CommunityPool.Add(wantPool...), app.DistrKeeper.GetFeePool(ctx).CommunityPool)
assert.Empty(t, app.BankKeeper.GetAllBalances(ctx, addr[0]))
}

Expand All @@ -322,11 +324,28 @@ func TestFundCommunityPoolRejectsOutOfRangeAmount(t *testing.T) {
coins := sdk.NewCoins(sdk.NewCoin("bigcoin", maxAmt))
require.NoError(t, apptesting.FundAccount(app.BankKeeper, ctx, addr[0], coins))

require.Panics(t, func() {
_ = app.DistrKeeper.FundCommunityPool(ctx, coins, addr[0])
}, "funding the community pool with an out-of-range amount must be rejected")
require.NotPanics(t, func() {
err := app.DistrKeeper.FundCommunityPool(ctx, coins, addr[0])
require.Error(t, err)
})

// The stored fee pool must be unchanged after the rejected attempt.
require.NotPanics(t, func() { app.DistrKeeper.GetFeePool(ctx) })
require.True(t, app.DistrKeeper.GetFeePool(ctx).CommunityPool.IsZero())
require.Equal(t, coins, app.BankKeeper.GetAllBalances(ctx, addr[0]))
}

func TestDistributeFromFeePoolRejectsOutOfRangeAmount(t *testing.T) {
app := seiapp.Setup(t, false, false, false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})

recipient := seiapp.AddTestAddrs(app, ctx, 1, sdk.ZeroInt())[0]
maxAmt := sdk.NewIntFromBigInt(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)))
coins := sdk.NewCoins(sdk.NewCoin("bigcoin", maxAmt))

require.NotPanics(t, func() {
err := app.DistrKeeper.DistributeFromFeePool(ctx, coins, recipient)
require.Error(t, err)
})
require.True(t, app.DistrKeeper.GetFeePool(ctx).CommunityPool.IsZero())
require.True(t, app.BankKeeper.GetAllBalances(ctx, recipient).IsZero())
}
25 changes: 24 additions & 1 deletion sei-cosmos/x/distribution/proposal_handler_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package distribution_test

import (
"math/big"
"testing"

tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types"
Expand Down Expand Up @@ -42,7 +43,9 @@ func TestProposalHandlerPassed(t *testing.T) {
require.True(t, app.BankKeeper.GetAllBalances(ctx, account.GetAddress()).IsZero())

feePool := app.DistrKeeper.GetFeePool(ctx)
feePool.CommunityPool = sdk.NewDecCoinsFromCoins(amount...)
poolCoins, err := sdk.NewDecCoinsFromCoins(amount...)
require.NoError(t, err)
feePool.CommunityPool = poolCoins
app.DistrKeeper.SetFeePool(ctx, feePool)

tp := testProposal(recipient, amount)
Expand Down Expand Up @@ -70,3 +73,23 @@ func TestProposalHandlerFailed(t *testing.T) {
balances := app.BankKeeper.GetAllBalances(ctx, recipient)
require.True(t, balances.IsZero())
}

func TestProposalHandlerRejectsOutOfRangeAmount(t *testing.T) {
app := seiapp.Setup(t, false, false, false)
ctx := app.BaseApp.NewContext(false, tmproto.Header{})

recipient := delAddr1
account := app.AccountKeeper.NewAccountWithAddress(ctx, recipient)
app.AccountKeeper.SetAccount(ctx, account)

maxAmt := sdk.NewIntFromBigInt(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)))
huge := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, maxAmt))
tp := testProposal(recipient, huge)
require.Error(t, tp.ValidateBasic(), "ValidateBasic must reject unconvertible spend amounts")

hdlr := distribution.NewCommunityPoolSpendProposalHandler(app.DistrKeeper)
require.NotPanics(t, func() {
require.Error(t, hdlr(ctx, tp))
})
require.True(t, app.BankKeeper.GetAllBalances(ctx, recipient).IsZero())
}
3 changes: 3 additions & 0 deletions sei-cosmos/x/distribution/types/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ func (msg MsgFundCommunityPool) ValidateBasic() error {
if !msg.Amount.IsValid() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String())
}
if _, err := sdk.NewDecCoinsFromCoins(msg.Amount...); err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, err.Error())
}
if msg.Depositor == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Depositor)
}
Expand Down
3 changes: 3 additions & 0 deletions sei-cosmos/x/distribution/types/msg_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

import (
"math/big"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -75,6 +76,7 @@ func TestMsgWithdrawValidatorCommission(t *testing.T) {

// test ValidateBasic for MsgDepositIntoCommunityPool
func TestMsgDepositIntoCommunityPool(t *testing.T) {
maxAmt := sdk.NewIntFromBigInt(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)))
tests := []struct {
amount sdk.Coins
depositor sdk.AccAddress
Expand All @@ -83,6 +85,7 @@ func TestMsgDepositIntoCommunityPool(t *testing.T) {
{sdk.NewCoins(sdk.NewInt64Coin("uatom", 10000)), sdk.AccAddress{}, false},
{sdk.Coins{sdk.NewInt64Coin("uatom", 10), sdk.NewInt64Coin("uatom", 10)}, delAddr1, false},
{sdk.NewCoins(sdk.NewInt64Coin("uatom", 1000)), delAddr1, true},
{sdk.NewCoins(sdk.NewCoin("uatom", maxAmt)), delAddr1, false},
}
for i, tc := range tests {
msg := NewMsgFundCommunityPool(tc.amount, tc.depositor)
Expand Down
3 changes: 3 additions & 0 deletions sei-cosmos/x/distribution/types/proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ func (csp *CommunityPoolSpendProposal) ValidateBasic() error {
if !csp.Amount.IsValid() {
return ErrInvalidProposalAmount
}
if _, err := sdk.NewDecCoinsFromCoins(csp.Amount...); err != nil {
return ErrInvalidProposalAmount
}
if csp.Recipient == "" {
return ErrEmptyProposalRecipient
}
Expand Down
Loading