feat(x/precisebank): Add remainder amount to genesis (#1911)

- Validate total fractional amounts in genesis type
- Validate against fractional balances such that `(sum(balances) + remainder) % conversionFactor == 0`
- Add new utility type `SplitBalance` for splitting up full balances into each
This commit is contained in:
drklee3 2024-05-15 14:07:24 -07:00 committed by GitHub
parent 94914d4ca1
commit 025b7b2cdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 837 additions and 111 deletions

View File

@ -3006,7 +3006,9 @@
}, },
"in_flight_packets": {} "in_flight_packets": {}
}, },
"precisebank": {}, "precisebank": {
"remainder": "0"
},
"pricefeed": { "pricefeed": {
"params": { "params": {
"markets": [ "markets": [

View File

@ -6660,6 +6660,7 @@ GenesisState defines the precisebank module's genesis state.
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| `balances` | [FractionalBalance](#kava.precisebank.v1.FractionalBalance) | repeated | balances is a list of all the balances in the precisebank module. | | `balances` | [FractionalBalance](#kava.precisebank.v1.FractionalBalance) | repeated | balances is a list of all the balances in the precisebank module. |
| `remainder` | [string](#string) | | remainder is an internal value of how much extra fractional digits are still backed by the reserve, but not assigned to any account. |

View File

@ -13,6 +13,14 @@ message GenesisState {
(gogoproto.castrepeated) = "FractionalBalances", (gogoproto.castrepeated) = "FractionalBalances",
(gogoproto.nullable) = false (gogoproto.nullable) = false
]; ];
// remainder is an internal value of how much extra fractional digits are
// still backed by the reserve, but not assigned to any account.
string remainder = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false
];
} }
// FractionalBalance defines the fractional portion of an account balance // FractionalBalance defines the fractional portion of an account balance

View File

@ -3,6 +3,7 @@ package precisebank
import ( import (
"fmt" "fmt"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/precisebank/keeper" "github.com/kava-labs/kava/x/precisebank/keeper"
@ -14,24 +15,43 @@ func InitGenesis(
ctx sdk.Context, ctx sdk.Context,
keeper keeper.Keeper, keeper keeper.Keeper,
ak types.AccountKeeper, ak types.AccountKeeper,
bk types.BankKeeper,
gs *types.GenesisState, gs *types.GenesisState,
) { ) {
// Ensure the genesis state is valid
if err := gs.Validate(); err != nil { if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err)) panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err))
} }
// initialize module account // Initialize module account
if moduleAcc := ak.GetModuleAccount(ctx, types.ModuleName); moduleAcc == nil { if moduleAcc := ak.GetModuleAccount(ctx, types.ModuleName); moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
} }
// TODO: // Check module balance matches sum of fractional balances + remainder
// - Set balances // This is always a whole integer amount, as previously verified in
// - Ensure reserve account exists // GenesisState.Validate()
// - Ensure reserve balance matches sum of all fractional balances totalAmt := gs.TotalAmountWithRemainder()
moduleAddr := ak.GetModuleAddress(types.ModuleName)
moduleBal := bk.GetBalance(ctx, moduleAddr, types.IntegerCoinDenom)
moduleBalExtended := moduleBal.Amount.Mul(types.ConversionFactor())
// Compare balances in full precise extended amounts
if !totalAmt.Equal(moduleBalExtended) {
panic(fmt.Sprintf(
"module account balance does not match sum of fractional balances and remainder, balance is %s but expected %v%s (%v%s)",
moduleBal,
totalAmt, types.ExtendedCoinDenom,
totalAmt.Quo(types.ConversionFactor()), types.IntegerCoinDenom,
))
}
// TODO: After keeper methods are implemented
// - Set account FractionalBalances
} }
// ExportGenesis returns a GenesisState for a given context and keeper. // ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState { func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState {
return types.NewGenesisState(nil) return types.NewGenesisState(types.FractionalBalances{}, sdkmath.ZeroInt())
} }

View File

@ -1,73 +1,183 @@
package precisebank_test package precisebank_test
import ( import (
"testing"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/precisebank" "github.com/kava-labs/kava/x/precisebank"
"github.com/kava-labs/kava/x/precisebank/testutil" "github.com/kava-labs/kava/x/precisebank/testutil"
"github.com/kava-labs/kava/x/precisebank/types" "github.com/kava-labs/kava/x/precisebank/types"
"github.com/stretchr/testify/suite"
) )
type KeeperTestSuite struct { type GenesisTestSuite struct {
testutil.Suite testutil.Suite
} }
func (suite *KeeperTestSuite) TestInitGenesis() { func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}
func (suite *GenesisTestSuite) TestInitGenesis() {
tests := []struct { tests := []struct {
name string name string
setupFn func()
genesisState *types.GenesisState genesisState *types.GenesisState
shouldPanic bool
panicMsg string panicMsg string
}{ }{
{ {
"default genesisState", "valid - default genesisState",
func() {},
types.DefaultGenesisState(), types.DefaultGenesisState(),
false,
"", "",
}, },
{ {
"empty genesisState", "valid - empty genesisState",
func() {},
&types.GenesisState{}, &types.GenesisState{},
false, "failed to validate precisebank genesis state: nil remainder amount",
},
{
"valid - module balance matches non-zero amount",
func() {
// Set module account balance to expected amount
err := suite.BankKeeper.MintCoins(
suite.Ctx,
types.ModuleName,
sdk.NewCoins(sdk.NewCoin(types.IntegerCoinDenom, sdkmath.NewInt(2))),
)
suite.Require().NoError(err)
},
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), types.ConversionFactor().SubRaw(1)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), types.ConversionFactor().SubRaw(1)),
},
// 2 leftover from 0.999... + 0.999...
sdkmath.NewInt(2),
),
"", "",
}, },
{ {
"TODO: invalid genesisState", // Other GenesisState.Validate() tests are in types/genesis_test.go
&types.GenesisState{}, "invalid genesisState - GenesisState.Validate() is called",
false, func() {},
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
},
sdkmath.ZeroInt(),
),
"failed to validate precisebank genesis state: invalid balances: duplicate address kava1qy0xn7za",
},
{
"invalid - module balance insufficient",
func() {},
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), types.ConversionFactor().SubRaw(1)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), types.ConversionFactor().SubRaw(1)),
},
// 2 leftover from 0.999... + 0.999...
sdkmath.NewInt(2),
),
"module account balance does not match sum of fractional balances and remainder, balance is 0ukava but expected 2000000000000akava (2ukava)",
},
{
"invalid - module balance excessive",
func() {
// Set module account balance to greater than expected amount
err := suite.BankKeeper.MintCoins(
suite.Ctx,
types.ModuleName,
sdk.NewCoins(sdk.NewCoin(types.IntegerCoinDenom, sdkmath.NewInt(100))),
)
suite.Require().NoError(err)
},
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), types.ConversionFactor().SubRaw(1)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), types.ConversionFactor().SubRaw(1)),
},
sdkmath.NewInt(2),
),
"module account balance does not match sum of fractional balances and remainder, balance is 100ukava but expected 2000000000000akava (2ukava)",
},
{
"sets module account",
func() {
// Delete the module account first to ensure it's created here
moduleAcc := suite.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName)
suite.AccountKeeper.RemoveAccount(suite.Ctx, moduleAcc)
// Ensure module account is deleted in state.
// GetModuleAccount() will always return non-nil and does not
// necessarily equate to the account being stored in the account store.
suite.Require().Nil(suite.AccountKeeper.GetAccount(suite.Ctx, moduleAcc.GetAddress()))
},
types.DefaultGenesisState(),
"", "",
}, },
} }
for _, tc := range tests { for _, tc := range tests {
suite.Run(tc.name, func() { suite.Run(tc.name, func() {
if tc.shouldPanic { suite.SetupTest()
suite.Require().Panics(func() { tc.setupFn()
precisebank.InitGenesis(suite.Ctx, suite.Keeper, suite.AccountKeeper, tc.genesisState)
}, tc.panicMsg) if tc.panicMsg != "" {
suite.Require().PanicsWithValue(
tc.panicMsg,
func() {
precisebank.InitGenesis(
suite.Ctx,
suite.Keeper,
suite.AccountKeeper,
suite.BankKeeper,
tc.genesisState,
)
},
)
return return
} }
suite.Require().NotPanics(func() { suite.Require().NotPanics(func() {
precisebank.InitGenesis(suite.Ctx, suite.Keeper, suite.AccountKeeper, tc.genesisState) precisebank.InitGenesis(
suite.Ctx,
suite.Keeper,
suite.AccountKeeper,
suite.BankKeeper,
tc.genesisState,
)
}) })
// Ensure module account is created // Ensure module account is created
moduleAcc := suite.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName) moduleAcc := suite.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName)
suite.NotNil(moduleAcc, "module account should be created") suite.NotNil(moduleAcc)
suite.NotNil(
suite.AccountKeeper.GetAccount(suite.Ctx, moduleAcc.GetAddress()),
"module account should be created & stored in account store",
)
// TODO: Check module state once implemented // TODO: Check module state once implemented
// - Verify balances // Verify balances
// - Ensure reserve account exists // IterateBalances() or something
// - Ensure reserve balance matches sum of all fractional balances
// Ensure reserve balance matches sum of all fractional balances
// sum up IterateBalances()
// - etc // - etc
}) })
} }
} }
func (suite *KeeperTestSuite) TestExportGenesis_Valid() { func (suite *GenesisTestSuite) TestExportGenesis_Valid() {
// ExportGenesis(moduleState) should return a valid genesis state // ExportGenesis(moduleState) should return a valid genesis state
tests := []struct { tests := []struct {
name string name string
maleate func() maleate func()
@ -79,6 +189,7 @@ func (suite *KeeperTestSuite) TestExportGenesis_Valid() {
suite.Ctx, suite.Ctx,
suite.Keeper, suite.Keeper,
suite.AccountKeeper, suite.AccountKeeper,
suite.BankKeeper,
types.DefaultGenesisState(), types.DefaultGenesisState(),
) )
}, },
@ -96,7 +207,7 @@ func (suite *KeeperTestSuite) TestExportGenesis_Valid() {
} }
} }
func (suite *KeeperTestSuite) TestExportImportedState() { func (suite *GenesisTestSuite) TestExportImportedState() {
// ExportGenesis(InitGenesis(genesisState)) == genesisState // ExportGenesis(InitGenesis(genesisState)) == genesisState
tests := []struct { tests := []struct {
@ -116,6 +227,7 @@ func (suite *KeeperTestSuite) TestExportImportedState() {
suite.Ctx, suite.Ctx,
suite.Keeper, suite.Keeper,
suite.AccountKeeper, suite.AccountKeeper,
suite.BankKeeper,
tc.initGenesisState, tc.initGenesisState,
) )
}) })

View File

@ -131,7 +131,7 @@ func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.Ra
// Initialize global index to index in genesis state // Initialize global index to index in genesis state
cdc.MustUnmarshalJSON(gs, &genState) cdc.MustUnmarshalJSON(gs, &genState)
InitGenesis(ctx, am.keeper, am.accountKeeper, &genState) InitGenesis(ctx, am.keeper, am.accountKeeper, am.bankKeeper, &genState)
return []abci.ValidatorUpdate{} return []abci.ValidatorUpdate{}
} }

View File

@ -0,0 +1,155 @@
package testutil
import (
crand "crypto/rand"
"math/rand"
"testing"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
"github.com/kava-labs/kava/x/precisebank/types"
"github.com/stretchr/testify/require"
)
// randRange returns a random number in the range [min, max)
// meaning max is never returned
func randRange(min, max int64) int64 {
return rand.Int63n(max-min) + min
}
func randAccAddress() sdk.AccAddress {
addrBytes := make([]byte, address.MaxAddrLen)
_, err := crand.Read(addrBytes)
if err != nil {
panic(err)
}
addr := sdk.AccAddress(addrBytes)
if addr.Empty() {
panic("empty address")
}
return addr
}
// GenerateEqualFractionalBalances generates count number of FractionalBalances
// with randomly generated amounts such that the sum of all amounts is a
// multiple of types.CONVERSION_FACTOR. If a remainder is desired, any single
// FractionalBalance can be removed from the returned slice and used as the
// remainder.
func GenerateEqualFractionalBalances(
t *testing.T,
count int,
) types.FractionalBalances {
t.Helper()
// 1 account is not valid, as the total amount needs to be a multiple of
// conversionFactor. 0 < account balance < conversionFactor, so there must
// be at least 2
// NOTE: THIS IS ONLY TRUE WITH 0 REMAINDER
// GenerateEqualFractionalBalancesWithRemainder repurposes the last balance
// as the remainder, so this >= 2 requirement is not true in production code.
require.GreaterOrEqual(t, count, 2, "count must be at least 2 to generate balances")
fbs := make(types.FractionalBalances, count)
sum := sdkmath.ZeroInt()
// Random amounts for count - 1 FractionalBalances
for i := 0; i < count-1; i++ {
// Not just using sdk.AccAddress{byte(count)} since that has limited
// range
addr := randAccAddress().String()
// Random 1 < amt < ConversionFactor
// POSITIVE and less than ConversionFactor
// If it's 0, Validate() will error.
// Why start at 2 instead of 1? We want to make sure its divisible
// for the last account, more details below.
amt := randRange(2, types.ConversionFactor().Int64())
amtInt := sdkmath.NewInt(amt)
fb := types.NewFractionalBalance(addr, amtInt)
require.NoError(t, fb.Validate())
fbs[i] = fb
sum = sum.Add(amtInt)
}
// Last FractionalBalance must make sum of all balances equal to have 0
// fractional remainder. Effectively the amount needed to round up to the
// nearest integer amount to make this true.
// (sum + lastAmt) % CONVERSION_FACTOR = 0
// aka
// CONVERSION_FACTOR - (sum % CONVERSION_FACTOR) = lastAmt
addr := randAccAddress().String()
// Why do we need to Mod(conversionFactor) again?
// Edge case without: If sum == ConversionFactor, then lastAmt == 0 not ConversionFactor
// 1_000_000_000_000 - (1_000_000_000_000 % 1_000_000_000_000)
// = 1_000_000_000_000 - 0
// = 1_000_000_000_000 (invalid!)
// Note that we only have this issue in tests since we want to calculate a
// new valid remainder, but we only validate in the actual code.
amt := types.ConversionFactor().
Sub(sum.Mod(types.ConversionFactor())).
Mod(types.ConversionFactor())
// We only want to generate VALID FractionalBalances - zero would not be
// valid, so let's just borrow half of the previous amount. We generated
// amounts from 2 to ConversionFactor, so we know the previous amount is
// at least 2 and thus able to be split into two valid balances.
if amt.IsZero() {
fbs[count-2].Amount = fbs[count-2].Amount.QuoRaw(2)
amt = fbs[count-2].Amount
}
fb := types.NewFractionalBalance(addr, amt)
require.NoError(t, fb.Validate())
fbs[count-1] = fb
// Lets double check this before returning
verificationSum := sdkmath.ZeroInt()
for _, fb := range fbs {
verificationSum = verificationSum.Add(fb.Amount)
}
require.True(t, verificationSum.Mod(types.ConversionFactor()).IsZero())
// Also make sure no duplicate addresses
require.NoError(t, fbs.Validate())
return fbs
}
// GenerateEqualFractionalBalancesWithRemainder generates count number of
// FractionalBalances with randomly generated amounts as well as a non-zero
// remainder.
// 0 == (sum(FractionalBalances) + remainder) % conversionFactor
// Where remainder > 0
func GenerateEqualFractionalBalancesWithRemainder(
t *testing.T,
count int,
) (types.FractionalBalances, sdkmath.Int) {
t.Helper()
require.GreaterOrEqual(t, count, 3, "count must be at least 3 to generate both balances and remainder")
countWithRemainder := count + 1
// Generate 1 additional FractionalBalance so we can use one as remainder
fbs := GenerateEqualFractionalBalances(t, countWithRemainder)
// Use the last one as remainder
remainder := fbs[countWithRemainder-1].Amount
// Remove the balance used as remainder from the slice
fbs = fbs[:countWithRemainder-1]
require.Len(t, fbs, count)
require.NotZero(t, remainder.Int64(), "remainder must be non-zero")
return fbs, remainder
}

View File

@ -0,0 +1,11 @@
package types
// IntegerCoinDenom is the denomination for integer coins that are managed by
// x/bank. This is the "true" denomination of the coin, and is also used for
// the reserve to back all fractional coins.
const IntegerCoinDenom = "ukava"
// ExtendedCoinDenom is the denomination for the extended IntegerCoinDenom. This
// not only represents the fractional balance, but the total balance of
// integer + fractional balances.
const ExtendedCoinDenom = "akava"

View File

@ -7,22 +7,21 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
// maxFractionalAmount is the largest valid value in a FractionalBalance amount. var (
// This is for direct internal use so that there are no extra allocations. // conversionFactor is used to convert the fractional balance to integer
var maxFractionalAmount = sdkmath.NewInt(1_000_000_000_000).SubRaw(1) // balances.
conversionFactor = sdkmath.NewInt(1_000_000_000_000)
// maxFractionalAmount is the largest valid value in a FractionalBalance amount.
// This is for direct internal use so that there are no extra allocations.
maxFractionalAmount = conversionFactor.SubRaw(1)
)
// MaxFractionalAmount returns the largest valid value in a FractionalBalance // ConversionFactor returns a copy of the conversionFactor used to convert the
// amount. // fractional balance to integer balances. This is also 1 greater than the max
// FractionalBalance contains **only** the fractional balance of an address. // valid fractional amount (999_999_999_999):
// We want to extend the current KAVA decimal digits from 6 to 18, or in other // 0 < FractionalBalance < conversionFactor
// words add 12 fractional digits to ukava. func ConversionFactor() sdkmath.Int {
// With 12 digits, the valid amount is 1 - 999_999_999_999. return sdkmath.NewIntFromBigIntMut(conversionFactor.BigInt())
func MaxFractionalAmount() sdkmath.Int {
// BigInt() returns a copy of the internal big.Int, so it's safe to directly
// use it for a new Int instead of creating another big.Int internally.
// We need to copy it because the internal value can be accessed and
// modified via Int.BigIntMut()
return sdkmath.NewIntFromBigIntMut(maxFractionalAmount.BigInt())
} }
// FractionalBalance returns a new FractionalBalance with the given address and // FractionalBalance returns a new FractionalBalance with the given address and

View File

@ -10,12 +10,12 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMaxFractionalAmount_Immutable(t *testing.T) { func TestConversionFactor_Immutable(t *testing.T) {
max1 := types.MaxFractionalAmount() cf1 := types.ConversionFactor()
origInt64 := max1.Int64() origInt64 := cf1.Int64()
// Get the internal pointer to the big.Int without copying // Get the internal pointer to the big.Int without copying
internalBigInt := max1.BigIntMut() internalBigInt := cf1.BigIntMut()
// Mutate the big.Int -- .Add() mutates in place // Mutate the big.Int -- .Add() mutates in place
internalBigInt.Add(internalBigInt, big.NewInt(5)) internalBigInt.Add(internalBigInt, big.NewInt(5))
@ -23,24 +23,33 @@ func TestMaxFractionalAmount_Immutable(t *testing.T) {
require.Equal(t, origInt64+5, internalBigInt.Int64()) require.Equal(t, origInt64+5, internalBigInt.Int64())
// Fetch the max amount again // Fetch the max amount again
max2 := types.MaxFractionalAmount() cf2 := types.ConversionFactor()
require.Equal( require.Equal(
t, t,
origInt64, origInt64,
max2.Int64(), cf2.Int64(),
"max amount should be immutable", "conversion factor should be immutable",
) )
} }
func TestMaxFractionalAmount_Copied(t *testing.T) { func TestConversionFactor_Copied(t *testing.T) {
max1 := types.MaxFractionalAmount().BigIntMut() max1 := types.ConversionFactor().BigIntMut()
max2 := types.MaxFractionalAmount().BigIntMut() max2 := types.ConversionFactor().BigIntMut()
// Checks that the returned two pointers do not reference the same object // Checks that the returned two pointers do not reference the same object
require.NotSame(t, max1, max2, "max fractional amount should be copied") require.NotSame(t, max1, max2, "max fractional amount should be copied")
} }
func TestConversionFactor(t *testing.T) {
require.Equal(
t,
sdkmath.NewInt(1_000_000_000_000),
types.ConversionFactor(),
"conversion factor should have 12 decimal points",
)
}
func TestNewFractionalBalance(t *testing.T) { func TestNewFractionalBalance(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -94,7 +103,7 @@ func TestFractionalBalance_Validate(t *testing.T) {
{ {
"valid - max balance", "valid - max balance",
"kava1gpxd677pp8zr97xvy3pmgk70a9vcpagsakv0tx", "kava1gpxd677pp8zr97xvy3pmgk70a9vcpagsakv0tx",
types.MaxFractionalAmount(), types.ConversionFactor().SubRaw(1),
"", "",
}, },
{ {
@ -136,7 +145,7 @@ func TestFractionalBalance_Validate(t *testing.T) {
{ {
"invalid - max amount + 1", "invalid - max amount + 1",
"kava1gpxd677pp8zr97xvy3pmgk70a9vcpagsakv0tx", "kava1gpxd677pp8zr97xvy3pmgk70a9vcpagsakv0tx",
types.MaxFractionalAmount().AddRaw(1), types.ConversionFactor(),
"amount 1000000000000 exceeds max of 999999999999", "amount 1000000000000 exceeds max of 999999999999",
}, },
{ {

View File

@ -3,6 +3,8 @@ package types
import ( import (
fmt "fmt" fmt "fmt"
"strings" "strings"
sdkmath "cosmossdk.io/math"
) )
// FractionalBalances is a slice of FractionalBalance // FractionalBalances is a slice of FractionalBalance
@ -33,3 +35,14 @@ func (fbs FractionalBalances) Validate() error {
return nil return nil
} }
// SumAmount returns the sum of all the amounts in the slice.
func (fbs FractionalBalances) SumAmount() sdkmath.Int {
sum := sdkmath.ZeroInt()
for _, fb := range fbs {
sum = sum.Add(fb.Amount)
}
return sum
}

View File

@ -1,6 +1,8 @@
package types_test package types_test
import ( import (
"math/big"
"math/rand"
"strings" "strings"
"testing" "testing"
@ -84,3 +86,74 @@ func TestFractionalBalances_Validate(t *testing.T) {
}) })
} }
} }
func TestFractionalBalances_SumAmount(t *testing.T) {
generateRandomFractionalBalances := func(n int) (types.FractionalBalances, sdkmath.Int) {
balances := make(types.FractionalBalances, n)
sum := sdkmath.ZeroInt()
for i := 0; i < n; i++ {
addr := sdk.AccAddress{byte(i)}.String()
amount := sdkmath.NewInt(rand.Int63())
balances[i] = types.NewFractionalBalance(addr, amount)
sum = sum.Add(amount)
}
return balances, sum
}
multiBalances, sum := generateRandomFractionalBalances(10)
tests := []struct {
name string
balances types.FractionalBalances
wantSum sdkmath.Int
}{
{
"empty",
types.FractionalBalances{},
sdkmath.ZeroInt(),
},
{
"single",
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(100)),
},
sdkmath.NewInt(100),
},
{
"multiple",
multiBalances,
sum,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sum := tt.balances.SumAmount()
require.Equal(t, tt.wantSum, sum)
})
}
}
func TestFractionalBalances_SumAmount_Overflow(t *testing.T) {
// 2^256 - 1
maxInt := new(big.Int).Sub(
new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil),
big.NewInt(1),
)
fbs := types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(100)),
// This is NOT valid, but just to test overflows will panic
types.NewFractionalBalance(
sdk.AccAddress{2}.String(),
sdkmath.NewIntFromBigInt(maxInt),
),
}
require.PanicsWithError(t, sdkmath.ErrIntOverflow.Error(), func() {
_ = fbs.SumAmount()
})
}

View File

@ -1,30 +1,70 @@
package types package types
import "fmt" import (
"fmt"
// Validate performs basic validation of genesis data returning an error for sdkmath "cosmossdk.io/math"
// any failed validation criteria. )
func (gs *GenesisState) Validate() error {
if err := gs.Balances.Validate(); err != nil {
return fmt.Errorf("invalid balances: %w", err)
}
// TODO:
// - Validate remainder amount
// - Validate sum(fractionalBalances) + remainder = whole integer value
// - Cannot validate here: reserve account exists & balance match
return nil
}
// NewGenesisState creates a new genesis state. // NewGenesisState creates a new genesis state.
func NewGenesisState(balances FractionalBalances) *GenesisState { func NewGenesisState(
balances FractionalBalances,
remainder sdkmath.Int,
) *GenesisState {
return &GenesisState{ return &GenesisState{
Balances: balances, Balances: balances,
Remainder: remainder,
} }
} }
// DefaultGenesisState returns a default genesis state. // DefaultGenesisState returns a default genesis state.
func DefaultGenesisState() *GenesisState { func DefaultGenesisState() *GenesisState {
return NewGenesisState(FractionalBalances{}) return NewGenesisState(FractionalBalances{}, sdkmath.ZeroInt())
}
// Validate performs basic validation of genesis data returning an error for
// any failed validation criteria.
func (gs *GenesisState) Validate() error {
// Validate all FractionalBalances
if err := gs.Balances.Validate(); err != nil {
return fmt.Errorf("invalid balances: %w", err)
}
if gs.Remainder.IsNil() {
return fmt.Errorf("nil remainder amount")
}
// Validate remainder, 0 <= remainder <= maxFractionalAmount
if gs.Remainder.IsNegative() {
return fmt.Errorf("negative remainder amount %s", gs.Remainder)
}
if gs.Remainder.GTE(conversionFactor) {
return fmt.Errorf("remainder %v exceeds max of %v", gs.Remainder, maxFractionalAmount)
}
// Determine if sum(fractionalBalances) + remainder = whole integer value
// i.e total of all fractional balances + remainder == 0 fractional digits
sum := gs.Balances.SumAmount()
sumWithRemainder := sum.Add(gs.Remainder)
offBy := sumWithRemainder.Mod(conversionFactor)
if !offBy.IsZero() {
return fmt.Errorf(
"sum of fractional balances %v + remainder %v is not a multiple of %v",
sum,
gs.Remainder,
conversionFactor,
)
}
return nil
}
// TotalAmountWithRemainder returns the total amount of all balances in the
// genesis state, including both fractional balances and the remainder. A bit
// more verbose WithRemainder to ensure its clearly different from SumAmount().
func (gs *GenesisState) TotalAmountWithRemainder() sdkmath.Int {
return gs.Balances.SumAmount().Add(gs.Remainder)
} }

View File

@ -29,6 +29,9 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type GenesisState struct { type GenesisState struct {
// balances is a list of all the balances in the precisebank module. // balances is a list of all the balances in the precisebank module.
Balances FractionalBalances `protobuf:"bytes,1,rep,name=balances,proto3,castrepeated=FractionalBalances" json:"balances"` Balances FractionalBalances `protobuf:"bytes,1,rep,name=balances,proto3,castrepeated=FractionalBalances" json:"balances"`
// remainder is an internal value of how much extra fractional digits are
// still backed by the reserve, but not assigned to any account.
Remainder cosmossdk_io_math.Int `protobuf:"bytes,2,opt,name=remainder,proto3,customtype=cosmossdk.io/math.Int" json:"remainder"`
} }
func (m *GenesisState) Reset() { *m = GenesisState{} } func (m *GenesisState) Reset() { *m = GenesisState{} }
@ -122,28 +125,29 @@ func init() {
func init() { proto.RegisterFile("kava/precisebank/v1/genesis.proto", fileDescriptor_7f1c47a86fb0d2e0) } func init() { proto.RegisterFile("kava/precisebank/v1/genesis.proto", fileDescriptor_7f1c47a86fb0d2e0) }
var fileDescriptor_7f1c47a86fb0d2e0 = []byte{ var fileDescriptor_7f1c47a86fb0d2e0 = []byte{
// 331 bytes of a gzipped FileDescriptorProto // 351 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xcc, 0x4e, 0x2c, 0x4b, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x91, 0x3f, 0x4f, 0xc2, 0x40,
0xd4, 0x2f, 0x28, 0x4a, 0x4d, 0xce, 0x2c, 0x4e, 0x4d, 0x4a, 0xcc, 0xcb, 0xd6, 0x2f, 0x33, 0xd4, 0x18, 0xc6, 0x7b, 0x9a, 0x20, 0x9c, 0x2e, 0x56, 0x4c, 0x90, 0xa1, 0x45, 0x06, 0x43, 0x62, 0x7a,
0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x06, 0x17, 0x70, 0x73, 0xb3, 0x26, 0x12, 0x56, 0xd8, 0x1c, 0x34, 0xd7, 0xf6, 0x52, 0x2e, 0xd0, 0x3b,
0x29, 0xd1, 0x43, 0x52, 0xa2, 0x57, 0x66, 0x28, 0x25, 0x99, 0x9c, 0x5f, 0x9c, 0x9b, 0x5f, 0x1c, 0x72, 0x77, 0x10, 0xfd, 0x06, 0x8e, 0x4e, 0xce, 0xcc, 0xce, 0x2c, 0x7e, 0x03, 0x46, 0xc2, 0x64,
0x0f, 0x56, 0xa2, 0x0f, 0xe1, 0x40, 0xd4, 0x4b, 0x89, 0xa4, 0xe7, 0xa7, 0xe7, 0x43, 0xc4, 0x41, 0x1c, 0xd0, 0xc0, 0xe2, 0xc7, 0x30, 0xe5, 0xf0, 0x5f, 0x70, 0x72, 0x7b, 0xfb, 0xbe, 0xbf, 0xe7,
0x2c, 0x88, 0xa8, 0x52, 0x1e, 0x17, 0x8f, 0x3b, 0xc4, 0xd8, 0xe0, 0x92, 0xc4, 0x92, 0x54, 0xa1, 0x79, 0x9f, 0xde, 0x0b, 0x0f, 0x3b, 0x64, 0x40, 0x70, 0x4f, 0xd2, 0x90, 0x29, 0x1a, 0x10, 0xde,
0x38, 0x2e, 0x8e, 0xa4, 0xc4, 0x9c, 0xc4, 0xbc, 0xe4, 0xd4, 0x62, 0x09, 0x46, 0x05, 0x66, 0x0d, 0xc1, 0x83, 0x2a, 0x8e, 0x29, 0xa7, 0x8a, 0x29, 0xd4, 0x93, 0x42, 0x0b, 0x7b, 0x2f, 0x45, 0xd0,
0x6e, 0x23, 0x35, 0x3d, 0x2c, 0x16, 0xe9, 0xb9, 0x15, 0x25, 0x26, 0x97, 0x64, 0xe6, 0xe7, 0x25, 0x0f, 0x04, 0x0d, 0xaa, 0xc5, 0x83, 0x50, 0xa8, 0x44, 0xa8, 0xeb, 0x25, 0x82, 0xcd, 0x87, 0xe1,
0xe6, 0x38, 0x41, 0x94, 0x3b, 0x49, 0x9d, 0xb8, 0x27, 0xcf, 0xb0, 0xea, 0xbe, 0xbc, 0x10, 0x86, 0x8b, 0xf9, 0x58, 0xc4, 0xc2, 0xf4, 0xd3, 0xca, 0x74, 0xcb, 0x4f, 0x00, 0xee, 0xd4, 0x8d, 0x6f,
0x54, 0x71, 0x10, 0xdc, 0x4c, 0xa5, 0x69, 0x8c, 0x5c, 0x82, 0x18, 0x0a, 0x84, 0x8c, 0xb8, 0xd8, 0x4b, 0x13, 0x4d, 0xed, 0x2b, 0x98, 0x0d, 0x48, 0x97, 0xf0, 0x90, 0xaa, 0x02, 0x28, 0x6d, 0x56,
0x13, 0x53, 0x52, 0x8a, 0x52, 0x8b, 0x41, 0x96, 0x32, 0x6a, 0x70, 0x3a, 0x49, 0x5c, 0xda, 0xa2, 0xb6, 0x6b, 0x47, 0xe8, 0x8f, 0x4d, 0xe8, 0x42, 0x92, 0x50, 0x33, 0xc1, 0x49, 0xd7, 0x37, 0xb8,
0x2b, 0x02, 0x75, 0xbe, 0x23, 0x44, 0x26, 0xb8, 0xa4, 0x28, 0x33, 0x2f, 0x3d, 0x08, 0xa6, 0x50, 0x5f, 0x1c, 0xcf, 0x5c, 0xeb, 0xf1, 0xd5, 0xb5, 0xd7, 0x46, 0xaa, 0xf9, 0xe5, 0x69, 0x37, 0x60,
0xc8, 0x99, 0x8b, 0x2d, 0x31, 0x37, 0xbf, 0x34, 0xaf, 0x44, 0x82, 0x09, 0xac, 0x45, 0x1b, 0x64, 0x4e, 0xd2, 0x84, 0x30, 0x1e, 0x51, 0x59, 0xd8, 0x28, 0x81, 0x4a, 0xce, 0x3f, 0x4e, 0x85, 0x2f,
0xff, 0xad, 0x7b, 0xf2, 0xa2, 0x10, 0x6d, 0xc5, 0x29, 0xd9, 0x7a, 0x99, 0xf9, 0xfa, 0xb9, 0x89, 0x33, 0x77, 0xdf, 0xe4, 0x55, 0x51, 0x07, 0x31, 0x81, 0x13, 0xa2, 0xdb, 0xa8, 0xc1, 0xf5, 0x74,
0x25, 0x19, 0x7a, 0x9e, 0x79, 0x25, 0x97, 0xb6, 0xe8, 0x72, 0x41, 0xcd, 0xf3, 0xcc, 0x2b, 0x09, 0xe4, 0xc1, 0xd5, 0x8f, 0x34, 0xb8, 0x6e, 0x7e, 0xab, 0xcb, 0x0f, 0x00, 0xee, 0xae, 0xed, 0xb2,
0x82, 0x6a, 0xb5, 0xe2, 0xe8, 0x58, 0x20, 0xcf, 0xf0, 0x62, 0x81, 0x3c, 0x83, 0x93, 0xfb, 0x89, 0x6b, 0x70, 0x8b, 0x44, 0x91, 0xa4, 0x2a, 0xcd, 0x9f, 0xda, 0x17, 0xa6, 0x23, 0x2f, 0xbf, 0x72,
0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x38, 0x33, 0x93, 0x96, 0x96, 0x8c, 0xc7, 0xcd, 0x4f, 0xd0, 0x3e, 0x87, 0x19, 0x92, 0x88, 0x3e,
0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0xe9, 0xa6, 0x67, 0x96, 0x64, 0x94, 0x26, 0xd7, 0xff, 0x49, 0xb4, 0x92, 0x9e, 0x66, 0xef, 0x86, 0xae, 0xf5, 0x3e, 0x74, 0x2d, 0xbf, 0x3e,
0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x83, 0x82, 0x42, 0x37, 0x27, 0x31, 0xa9, 0x18, 0xcc, 0xd2, 0xaf, 0x9e, 0x3b, 0x60, 0x32, 0x77, 0xc0, 0xdb, 0xdc, 0x01, 0xf7, 0x0b, 0xc7, 0x9a, 0x2c, 0x1c, 0xeb,
0x40, 0x89, 0xa2, 0x92, 0xca, 0x82, 0xd4, 0xe2, 0x24, 0x36, 0x70, 0xc0, 0x1a, 0x03, 0x02, 0x00, 0x79, 0xe1, 0x58, 0x97, 0x5e, 0xcc, 0x74, 0xbb, 0x1f, 0xa0, 0x50, 0x24, 0x38, 0x7d, 0x55, 0xaf,
0x00, 0xff, 0xff, 0xf4, 0x2e, 0xbf, 0x96, 0xc3, 0x01, 0x00, 0x00, 0x4b, 0x02, 0xb5, 0xac, 0xf0, 0xcd, 0xaf, 0x73, 0xeb, 0xdb, 0x1e, 0x55, 0x41, 0x66, 0x79, 0xa4,
0x93, 0x8f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbd, 0xf6, 0xf2, 0x8d, 0x0f, 0x02, 0x00, 0x00,
} }
func (m *GenesisState) Marshal() (dAtA []byte, err error) { func (m *GenesisState) Marshal() (dAtA []byte, err error) {
@ -166,6 +170,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i _ = i
var l int var l int
_ = l _ = l
{
size := m.Remainder.Size()
i -= size
if _, err := m.Remainder.MarshalTo(dAtA[i:]); err != nil {
return 0, err
}
i = encodeVarintGenesis(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x12
if len(m.Balances) > 0 { if len(m.Balances) > 0 {
for iNdEx := len(m.Balances) - 1; iNdEx >= 0; iNdEx-- { for iNdEx := len(m.Balances) - 1; iNdEx >= 0; iNdEx-- {
{ {
@ -246,6 +260,8 @@ func (m *GenesisState) Size() (n int) {
n += 1 + l + sovGenesis(uint64(l)) n += 1 + l + sovGenesis(uint64(l))
} }
} }
l = m.Remainder.Size()
n += 1 + l + sovGenesis(uint64(l))
return n return n
} }
@ -333,6 +349,40 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error {
return err return err
} }
iNdEx = postIndex iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Remainder", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenesis
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenesis
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenesis
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := m.Remainder.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipGenesis(dAtA[iNdEx:]) skippy, err := skipGenesis(dAtA[iNdEx:])

View File

@ -5,25 +5,62 @@ import (
sdkmath "cosmossdk.io/math" sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/precisebank/testutil"
"github.com/kava-labs/kava/x/precisebank/types" "github.com/kava-labs/kava/x/precisebank/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestGenesisStateValidate(t *testing.T) { func TestGenesisStateValidate_Basic(t *testing.T) {
app.SetSDKConfig()
testCases := []struct { testCases := []struct {
name string name string
genesisState *types.GenesisState genesisState *types.GenesisState
expErr bool wantErr string
}{ }{
{ {
"empty genesisState", "valid - default genesisState",
&types.GenesisState{}, types.DefaultGenesisState(),
false, "",
}, },
{ {
"valid genesisState - nil", "valid - empty balances, zero remainder",
types.NewGenesisState(nil), &types.GenesisState{
false, Remainder: sdkmath.ZeroInt(),
},
"",
},
{
"valid - nil balances",
types.NewGenesisState(nil, sdkmath.ZeroInt()),
"",
},
{
"valid - max remainder amount",
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
},
types.ConversionFactor().SubRaw(1),
),
"",
},
{
"invalid - empty genesisState (nil remainder)",
&types.GenesisState{},
"nil remainder amount",
},
{
"valid - balances add up",
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
},
sdkmath.ZeroInt(),
),
"invalid balances: duplicate address kava1qy0xn7za",
}, },
{ {
"invalid - calls (single) FractionalBalance.Validate()", "invalid - calls (single) FractionalBalance.Validate()",
@ -32,18 +69,42 @@ func TestGenesisStateValidate(t *testing.T) {
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)), types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), sdkmath.NewInt(-1)), types.NewFractionalBalance(sdk.AccAddress{2}.String(), sdkmath.NewInt(-1)),
}, },
sdkmath.ZeroInt(),
), ),
true, "invalid balances: invalid fractional balance for kava1qg7c45n6: non-positive amount -1",
}, },
{ {
"invalid - calls (multi) FractionalBalances.Validate()", "invalid - calls (slice) FractionalBalances.Validate()",
types.NewGenesisState( types.NewGenesisState(
types.FractionalBalances{ types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)), types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)), types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
}, },
sdkmath.ZeroInt(),
), ),
true, "invalid balances: duplicate address kava1qy0xn7za",
},
{
"invalid - negative remainder",
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), sdkmath.NewInt(1)),
},
sdkmath.NewInt(-1),
),
"negative remainder amount -1",
},
{
"invalid - too large remainder",
types.NewGenesisState(
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.NewInt(1)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), sdkmath.NewInt(1)),
},
types.ConversionFactor(),
),
"remainder 1000000000000 exceeds max of 999999999999",
}, },
} }
@ -52,11 +113,183 @@ func TestGenesisStateValidate(t *testing.T) {
t.Run(tc.name, func(tt *testing.T) { t.Run(tc.name, func(tt *testing.T) {
err := tc.genesisState.Validate() err := tc.genesisState.Validate()
if tc.expErr { if tc.wantErr == "" {
require.Error(tt, err)
} else {
require.NoError(tt, err) require.NoError(tt, err)
} else {
require.Error(tt, err)
require.EqualError(tt, err, tc.wantErr)
} }
}) })
} }
} }
func TestGenesisStateValidate_Total(t *testing.T) {
testCases := []struct {
name string
buildGenesisState func() *types.GenesisState
containsErr string
}{
{
"valid - empty balances, zero remainder",
func() *types.GenesisState {
return types.NewGenesisState(nil, sdkmath.ZeroInt())
},
"",
},
{
"valid - non-zero balances, zero remainder",
func() *types.GenesisState {
fbs := testutil.GenerateEqualFractionalBalances(t, 100)
require.Len(t, fbs, 100)
return types.NewGenesisState(fbs, sdkmath.ZeroInt())
},
"",
},
{
"valid - non-zero balances, non-zero remainder",
func() *types.GenesisState {
fbs, remainder := testutil.GenerateEqualFractionalBalancesWithRemainder(t, 100)
require.Len(t, fbs, 100)
require.NotZero(t, remainder.Int64())
t.Log("remainder:", remainder)
return types.NewGenesisState(fbs, remainder)
},
"",
},
{
"invalid - non-zero balances, invalid remainder",
func() *types.GenesisState {
fbs, remainder := testutil.GenerateEqualFractionalBalancesWithRemainder(t, 100)
require.Len(t, fbs, 100)
require.NotZero(t, remainder.Int64())
// Wrong remainder - should be non-zero
return types.NewGenesisState(fbs, sdkmath.ZeroInt())
},
// balances are randomly generated so we can't set the exact value in the error message
// "sum of fractional balances 52885778295370 ... "
"+ remainder 0 is not a multiple of 1000000000000",
},
{
"invalid - empty balances, non-zero remainder",
func() *types.GenesisState {
return types.NewGenesisState(types.FractionalBalances{}, sdkmath.NewInt(1))
},
"sum of fractional balances 0 + remainder 1 is not a multiple of 1000000000000",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(tt *testing.T) {
err := tc.buildGenesisState().Validate()
if tc.containsErr == "" {
require.NoError(tt, err)
} else {
require.Error(tt, err)
require.ErrorContains(tt, err, tc.containsErr)
}
})
}
}
func TestGenesisState_TotalAmountWithRemainder(t *testing.T) {
tests := []struct {
name string
giveBalances types.FractionalBalances
giveRemainder sdkmath.Int
wantTotalAmountWithRemainder sdkmath.Int
}{
{
"empty balances, zero remainder",
types.FractionalBalances{},
sdkmath.ZeroInt(),
sdkmath.ZeroInt(),
},
{
"non-empty balances, zero remainder",
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), types.ConversionFactor().QuoRaw(2)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), types.ConversionFactor().QuoRaw(2)),
},
sdkmath.ZeroInt(),
types.ConversionFactor(),
},
{
"non-empty balances, 1 remainder",
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), types.ConversionFactor().QuoRaw(2)),
types.NewFractionalBalance(sdk.AccAddress{2}.String(), types.ConversionFactor().QuoRaw(2).SubRaw(1)),
},
sdkmath.OneInt(),
types.ConversionFactor(),
},
{
"non-empty balances, max remainder",
types.FractionalBalances{
types.NewFractionalBalance(sdk.AccAddress{1}.String(), sdkmath.OneInt()),
},
types.ConversionFactor().SubRaw(1),
types.ConversionFactor(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gs := types.NewGenesisState(
tt.giveBalances,
tt.giveRemainder,
)
require.NoError(t, gs.Validate(), "genesis state should be valid before testing total amount")
totalAmt := gs.TotalAmountWithRemainder()
require.Equal(t, tt.wantTotalAmountWithRemainder, totalAmt, "total amount should be balances + remainder")
})
}
}
func FuzzGenesisStateValidate_NonZeroRemainder(f *testing.F) {
f.Add(5)
f.Add(100)
f.Add(30)
f.Fuzz(func(t *testing.T, count int) {
// Need at least 2 so we can generate both balances and remainder
if count < 2 {
t.Skip("count < 2")
}
fbs, remainder := testutil.GenerateEqualFractionalBalancesWithRemainder(t, count)
t.Logf("count: %v", count)
t.Logf("remainder: %v", remainder)
gs := types.NewGenesisState(fbs, remainder)
require.NoError(t, gs.Validate())
})
}
func FuzzGenesisStateValidate_ZeroRemainder(f *testing.F) {
f.Add(5)
f.Add(100)
f.Add(30)
f.Fuzz(func(t *testing.T, count int) {
// Need at least 2 as 1 account with non-zero balance & no remainder is not valid
if count < 2 {
t.Skip("count < 2")
}
fbs := testutil.GenerateEqualFractionalBalances(t, count)
gs := types.NewGenesisState(fbs, sdkmath.ZeroInt())
require.NoError(t, gs.Validate())
})
}