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": {}
},
"precisebank": {},
"precisebank": {
"remainder": "0"
},
"pricefeed": {
"params": {
"markets": [

View File

@ -6660,6 +6660,7 @@ GenesisState defines the precisebank module's genesis state.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `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.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

View File

@ -3,6 +3,7 @@ package precisebank
import (
"fmt"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/precisebank/keeper"
@ -14,24 +15,43 @@ func InitGenesis(
ctx sdk.Context,
keeper keeper.Keeper,
ak types.AccountKeeper,
bk types.BankKeeper,
gs *types.GenesisState,
) {
// Ensure the genesis state is valid
if err := gs.Validate(); err != nil {
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 {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
// TODO:
// - Set balances
// - Ensure reserve account exists
// - Ensure reserve balance matches sum of all fractional balances
// Check module balance matches sum of fractional balances + remainder
// This is always a whole integer amount, as previously verified in
// GenesisState.Validate()
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.
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
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/testutil"
"github.com/kava-labs/kava/x/precisebank/types"
"github.com/stretchr/testify/suite"
)
type KeeperTestSuite struct {
type GenesisTestSuite struct {
testutil.Suite
}
func (suite *KeeperTestSuite) TestInitGenesis() {
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}
func (suite *GenesisTestSuite) TestInitGenesis() {
tests := []struct {
name string
setupFn func()
genesisState *types.GenesisState
shouldPanic bool
panicMsg string
}{
{
"default genesisState",
"valid - default genesisState",
func() {},
types.DefaultGenesisState(),
false,
"",
},
{
"empty genesisState",
"valid - empty genesisState",
func() {},
&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",
&types.GenesisState{},
false,
// Other GenesisState.Validate() tests are in types/genesis_test.go
"invalid genesisState - GenesisState.Validate() is called",
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 {
suite.Run(tc.name, func() {
if tc.shouldPanic {
suite.Require().Panics(func() {
precisebank.InitGenesis(suite.Ctx, suite.Keeper, suite.AccountKeeper, tc.genesisState)
}, tc.panicMsg)
suite.SetupTest()
tc.setupFn()
if tc.panicMsg != "" {
suite.Require().PanicsWithValue(
tc.panicMsg,
func() {
precisebank.InitGenesis(
suite.Ctx,
suite.Keeper,
suite.AccountKeeper,
suite.BankKeeper,
tc.genesisState,
)
},
)
return
}
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
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
// - Verify balances
// - Ensure reserve account exists
// - Ensure reserve balance matches sum of all fractional balances
// Verify balances
// IterateBalances() or something
// Ensure reserve balance matches sum of all fractional balances
// sum up IterateBalances()
// - etc
})
}
}
func (suite *KeeperTestSuite) TestExportGenesis_Valid() {
func (suite *GenesisTestSuite) TestExportGenesis_Valid() {
// ExportGenesis(moduleState) should return a valid genesis state
tests := []struct {
name string
maleate func()
@ -79,6 +189,7 @@ func (suite *KeeperTestSuite) TestExportGenesis_Valid() {
suite.Ctx,
suite.Keeper,
suite.AccountKeeper,
suite.BankKeeper,
types.DefaultGenesisState(),
)
},
@ -96,7 +207,7 @@ func (suite *KeeperTestSuite) TestExportGenesis_Valid() {
}
}
func (suite *KeeperTestSuite) TestExportImportedState() {
func (suite *GenesisTestSuite) TestExportImportedState() {
// ExportGenesis(InitGenesis(genesisState)) == genesisState
tests := []struct {
@ -116,6 +227,7 @@ func (suite *KeeperTestSuite) TestExportImportedState() {
suite.Ctx,
suite.Keeper,
suite.AccountKeeper,
suite.BankKeeper,
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
cdc.MustUnmarshalJSON(gs, &genState)
InitGenesis(ctx, am.keeper, am.accountKeeper, &genState)
InitGenesis(ctx, am.keeper, am.accountKeeper, am.bankKeeper, &genState)
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"
)
// maxFractionalAmount is the largest valid value in a FractionalBalance amount.
// This is for direct internal use so that there are no extra allocations.
var maxFractionalAmount = sdkmath.NewInt(1_000_000_000_000).SubRaw(1)
var (
// conversionFactor is used to convert the fractional balance to integer
// 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
// amount.
// FractionalBalance contains **only** the fractional balance of an address.
// We want to extend the current KAVA decimal digits from 6 to 18, or in other
// words add 12 fractional digits to ukava.
// With 12 digits, the valid amount is 1 - 999_999_999_999.
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())
// ConversionFactor returns a copy of the conversionFactor used to convert the
// fractional balance to integer balances. This is also 1 greater than the max
// valid fractional amount (999_999_999_999):
// 0 < FractionalBalance < conversionFactor
func ConversionFactor() sdkmath.Int {
return sdkmath.NewIntFromBigIntMut(conversionFactor.BigInt())
}
// FractionalBalance returns a new FractionalBalance with the given address and

View File

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

View File

@ -3,6 +3,8 @@ package types
import (
fmt "fmt"
"strings"
sdkmath "cosmossdk.io/math"
)
// FractionalBalances is a slice of FractionalBalance
@ -33,3 +35,14 @@ func (fbs FractionalBalances) Validate() error {
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
import (
"math/big"
"math/rand"
"strings"
"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
import "fmt"
import (
"fmt"
// Validate performs basic validation of genesis data returning an error for
// 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
}
sdkmath "cosmossdk.io/math"
)
// NewGenesisState creates a new genesis state.
func NewGenesisState(balances FractionalBalances) *GenesisState {
func NewGenesisState(
balances FractionalBalances,
remainder sdkmath.Int,
) *GenesisState {
return &GenesisState{
Balances: balances,
Balances: balances,
Remainder: remainder,
}
}
// DefaultGenesisState returns a default genesis state.
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 {
// 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"`
// 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{} }
@ -122,28 +125,29 @@ func init() {
func init() { proto.RegisterFile("kava/precisebank/v1/genesis.proto", fileDescriptor_7f1c47a86fb0d2e0) }
var fileDescriptor_7f1c47a86fb0d2e0 = []byte{
// 331 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xcc, 0x4e, 0x2c, 0x4b,
0xd4, 0x2f, 0x28, 0x4a, 0x4d, 0xce, 0x2c, 0x4e, 0x4d, 0x4a, 0xcc, 0xcb, 0xd6, 0x2f, 0x33, 0xd4,
0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x06,
0x29, 0xd1, 0x43, 0x52, 0xa2, 0x57, 0x66, 0x28, 0x25, 0x99, 0x9c, 0x5f, 0x9c, 0x9b, 0x5f, 0x1c,
0x0f, 0x56, 0xa2, 0x0f, 0xe1, 0x40, 0xd4, 0x4b, 0x89, 0xa4, 0xe7, 0xa7, 0xe7, 0x43, 0xc4, 0x41,
0x2c, 0x88, 0xa8, 0x52, 0x1e, 0x17, 0x8f, 0x3b, 0xc4, 0xd8, 0xe0, 0x92, 0xc4, 0x92, 0x54, 0xa1,
0x38, 0x2e, 0x8e, 0xa4, 0xc4, 0x9c, 0xc4, 0xbc, 0xe4, 0xd4, 0x62, 0x09, 0x46, 0x05, 0x66, 0x0d,
0x6e, 0x23, 0x35, 0x3d, 0x2c, 0x16, 0xe9, 0xb9, 0x15, 0x25, 0x26, 0x97, 0x64, 0xe6, 0xe7, 0x25,
0xe6, 0x38, 0x41, 0x94, 0x3b, 0x49, 0x9d, 0xb8, 0x27, 0xcf, 0xb0, 0xea, 0xbe, 0xbc, 0x10, 0x86,
0x54, 0x71, 0x10, 0xdc, 0x4c, 0xa5, 0x69, 0x8c, 0x5c, 0x82, 0x18, 0x0a, 0x84, 0x8c, 0xb8, 0xd8,
0x13, 0x53, 0x52, 0x8a, 0x52, 0x8b, 0x41, 0x96, 0x32, 0x6a, 0x70, 0x3a, 0x49, 0x5c, 0xda, 0xa2,
0x2b, 0x02, 0x75, 0xbe, 0x23, 0x44, 0x26, 0xb8, 0xa4, 0x28, 0x33, 0x2f, 0x3d, 0x08, 0xa6, 0x50,
0xc8, 0x99, 0x8b, 0x2d, 0x31, 0x37, 0xbf, 0x34, 0xaf, 0x44, 0x82, 0x09, 0xac, 0x45, 0x1b, 0x64,
0xff, 0xad, 0x7b, 0xf2, 0xa2, 0x10, 0x6d, 0xc5, 0x29, 0xd9, 0x7a, 0x99, 0xf9, 0xfa, 0xb9, 0x89,
0x25, 0x19, 0x7a, 0x9e, 0x79, 0x25, 0x97, 0xb6, 0xe8, 0x72, 0x41, 0xcd, 0xf3, 0xcc, 0x2b, 0x09,
0x82, 0x6a, 0xb5, 0xe2, 0xe8, 0x58, 0x20, 0xcf, 0xf0, 0x62, 0x81, 0x3c, 0x83, 0x93, 0xfb, 0x89,
0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3,
0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0xe9, 0xa6, 0x67, 0x96, 0x64, 0x94, 0x26,
0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x83, 0x82, 0x42, 0x37, 0x27, 0x31, 0xa9, 0x18, 0xcc, 0xd2, 0xaf,
0x40, 0x89, 0xa2, 0x92, 0xca, 0x82, 0xd4, 0xe2, 0x24, 0x36, 0x70, 0xc0, 0x1a, 0x03, 0x02, 0x00,
0x00, 0xff, 0xff, 0xf4, 0x2e, 0xbf, 0x96, 0xc3, 0x01, 0x00, 0x00,
// 351 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x91, 0x3f, 0x4f, 0xc2, 0x40,
0x18, 0xc6, 0x7b, 0x9a, 0x20, 0x9c, 0x2e, 0x56, 0x4c, 0x90, 0xa1, 0x45, 0x06, 0x43, 0x62, 0x7a,
0x17, 0x70, 0x73, 0xb3, 0x26, 0x12, 0x56, 0xd8, 0x1c, 0x34, 0xd7, 0xf6, 0x52, 0x2e, 0xd0, 0x3b,
0x72, 0x77, 0x10, 0xfd, 0x06, 0x8e, 0x4e, 0xce, 0xcc, 0xce, 0x2c, 0x7e, 0x03, 0x46, 0xc2, 0x64,
0x1c, 0xd0, 0xc0, 0xe2, 0xc7, 0x30, 0xe5, 0xf0, 0x5f, 0x70, 0x72, 0x7b, 0xfb, 0xbe, 0xbf, 0xe7,
0x79, 0x9f, 0xde, 0x0b, 0x0f, 0x3b, 0x64, 0x40, 0x70, 0x4f, 0xd2, 0x90, 0x29, 0x1a, 0x10, 0xde,
0xc1, 0x83, 0x2a, 0x8e, 0x29, 0xa7, 0x8a, 0x29, 0xd4, 0x93, 0x42, 0x0b, 0x7b, 0x2f, 0x45, 0xd0,
0x0f, 0x04, 0x0d, 0xaa, 0xc5, 0x83, 0x50, 0xa8, 0x44, 0xa8, 0xeb, 0x25, 0x82, 0xcd, 0x87, 0xe1,
0x8b, 0xf9, 0x58, 0xc4, 0xc2, 0xf4, 0xd3, 0xca, 0x74, 0xcb, 0x4f, 0x00, 0xee, 0xd4, 0x8d, 0x6f,
0x4b, 0x13, 0x4d, 0xed, 0x2b, 0x98, 0x0d, 0x48, 0x97, 0xf0, 0x90, 0xaa, 0x02, 0x28, 0x6d, 0x56,
0xb6, 0x6b, 0x47, 0xe8, 0x8f, 0x4d, 0xe8, 0x42, 0x92, 0x50, 0x33, 0xc1, 0x49, 0xd7, 0x37, 0xb8,
0x5f, 0x1c, 0xcf, 0x5c, 0xeb, 0xf1, 0xd5, 0xb5, 0xd7, 0x46, 0xaa, 0xf9, 0xe5, 0x69, 0x37, 0x60,
0x4e, 0xd2, 0x84, 0x30, 0x1e, 0x51, 0x59, 0xd8, 0x28, 0x81, 0x4a, 0xce, 0x3f, 0x4e, 0x85, 0x2f,
0x33, 0x77, 0xdf, 0xe4, 0x55, 0x51, 0x07, 0x31, 0x81, 0x13, 0xa2, 0xdb, 0xa8, 0xc1, 0xf5, 0x74,
0xe4, 0xc1, 0xd5, 0x8f, 0x34, 0xb8, 0x6e, 0x7e, 0xab, 0xcb, 0x0f, 0x00, 0xee, 0xae, 0xed, 0xb2,
0x6b, 0x70, 0x8b, 0x44, 0x91, 0xa4, 0x2a, 0xcd, 0x9f, 0xda, 0x17, 0xa6, 0x23, 0x2f, 0xbf, 0x72,
0x38, 0x33, 0x93, 0x96, 0x96, 0x8c, 0xc7, 0xcd, 0x4f, 0xd0, 0x3e, 0x87, 0x19, 0x92, 0x88, 0x3e,
0xd7, 0xff, 0x49, 0xb4, 0x92, 0x9e, 0x66, 0xef, 0x86, 0xae, 0xf5, 0x3e, 0x74, 0x2d, 0xbf, 0x3e,
0x9e, 0x3b, 0x60, 0x32, 0x77, 0xc0, 0xdb, 0xdc, 0x01, 0xf7, 0x0b, 0xc7, 0x9a, 0x2c, 0x1c, 0xeb,
0x79, 0xe1, 0x58, 0x97, 0x5e, 0xcc, 0x74, 0xbb, 0x1f, 0xa0, 0x50, 0x24, 0x38, 0x7d, 0x55, 0xaf,
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) {
@ -166,6 +170,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = 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 {
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))
}
}
l = m.Remainder.Size()
n += 1 + l + sovGenesis(uint64(l))
return n
}
@ -333,6 +349,40 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error {
return err
}
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:
iNdEx = preIndex
skippy, err := skipGenesis(dAtA[iNdEx:])

View File

@ -5,25 +5,62 @@ import (
sdkmath "cosmossdk.io/math"
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/stretchr/testify/require"
)
func TestGenesisStateValidate(t *testing.T) {
func TestGenesisStateValidate_Basic(t *testing.T) {
app.SetSDKConfig()
testCases := []struct {
name string
genesisState *types.GenesisState
expErr bool
wantErr string
}{
{
"empty genesisState",
&types.GenesisState{},
false,
"valid - default genesisState",
types.DefaultGenesisState(),
"",
},
{
"valid genesisState - nil",
types.NewGenesisState(nil),
false,
"valid - empty balances, zero remainder",
&types.GenesisState{
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()",
@ -32,18 +69,42 @@ func TestGenesisStateValidate(t *testing.T) {
types.NewFractionalBalance(sdk.AccAddress{1}.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.FractionalBalances{
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) {
err := tc.genesisState.Validate()
if tc.expErr {
require.Error(tt, err)
} else {
if tc.wantErr == "" {
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())
})
}