mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 07:45:18 +00:00
feat(x/precisebank): Add keeper methods for store (#1912)
- Add store methods to get/set/delete/etc account fractional balances & remainder amount - Add invariants to ensure stored state is correct
This commit is contained in:
parent
d66b7d2705
commit
4ff43eb270
109
x/precisebank/keeper/fractional_balance.go
Normal file
109
x/precisebank/keeper/fractional_balance.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetFractionalBalance returns the fractional balance for an address.
|
||||||
|
func (k *Keeper) GetFractionalBalance(
|
||||||
|
ctx sdk.Context,
|
||||||
|
address sdk.AccAddress,
|
||||||
|
) (sdkmath.Int, bool) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.FractionalBalancePrefix)
|
||||||
|
|
||||||
|
bz := store.Get(types.FractionalBalanceKey(address))
|
||||||
|
if bz == nil {
|
||||||
|
return sdkmath.ZeroInt(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
var bal sdkmath.Int
|
||||||
|
if err := bal.Unmarshal(bz); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to unmarshal fractional balance: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bal, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFractionalBalance sets the fractional balance for an address.
|
||||||
|
func (k *Keeper) SetFractionalBalance(
|
||||||
|
ctx sdk.Context,
|
||||||
|
address sdk.AccAddress,
|
||||||
|
amount sdkmath.Int,
|
||||||
|
) {
|
||||||
|
if address.Empty() {
|
||||||
|
panic(errors.New("address cannot be empty"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount.IsZero() {
|
||||||
|
k.DeleteFractionalBalance(ctx, address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the fractional balance is valid before setting it. Use the
|
||||||
|
// NewFractionalAmountFromInt wrapper to use its Validate() method.
|
||||||
|
if err := types.NewFractionalAmountFromInt(amount).Validate(); err != nil {
|
||||||
|
panic(fmt.Errorf("amount is invalid: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.FractionalBalancePrefix)
|
||||||
|
|
||||||
|
amountBytes, err := amount.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to marshal fractional balance: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Set(types.FractionalBalanceKey(address), amountBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFractionalBalance deletes the fractional balance for an address.
|
||||||
|
func (k *Keeper) DeleteFractionalBalance(
|
||||||
|
ctx sdk.Context,
|
||||||
|
address sdk.AccAddress,
|
||||||
|
) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.FractionalBalancePrefix)
|
||||||
|
store.Delete(types.FractionalBalanceKey(address))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterateFractionalBalances iterates over all fractional balances in the store
|
||||||
|
// and performs a callback function.
|
||||||
|
func (k *Keeper) IterateFractionalBalances(
|
||||||
|
ctx sdk.Context,
|
||||||
|
cb func(address sdk.AccAddress, amount sdkmath.Int) (stop bool),
|
||||||
|
) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.FractionalBalancePrefix)
|
||||||
|
|
||||||
|
iterator := store.Iterator(nil, nil)
|
||||||
|
defer iterator.Close()
|
||||||
|
|
||||||
|
for ; iterator.Valid(); iterator.Next() {
|
||||||
|
address := sdk.AccAddress(iterator.Key())
|
||||||
|
|
||||||
|
var amount sdkmath.Int
|
||||||
|
if err := amount.Unmarshal(iterator.Value()); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to unmarshal fractional balance: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cb(address, amount) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTotalSumFractionalBalances returns the sum of all fractional balances.
|
||||||
|
func (k *Keeper) GetTotalSumFractionalBalances(ctx sdk.Context) sdkmath.Int {
|
||||||
|
sum := sdkmath.ZeroInt()
|
||||||
|
|
||||||
|
k.IterateFractionalBalances(ctx, func(_ sdk.AccAddress, amount sdkmath.Int) bool {
|
||||||
|
sum = sum.Add(amount)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return sum
|
||||||
|
}
|
189
x/precisebank/keeper/fractional_balance_test.go
Normal file
189
x/precisebank/keeper/fractional_balance_test.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetGetFractionalBalance(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k := tk.ctx, tk.keeper
|
||||||
|
|
||||||
|
addr := sdk.AccAddress([]byte("test-address"))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
address sdk.AccAddress
|
||||||
|
amount sdkmath.Int
|
||||||
|
setPanicMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid - min amount",
|
||||||
|
addr,
|
||||||
|
sdkmath.NewInt(1),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid - positive amount",
|
||||||
|
addr,
|
||||||
|
sdkmath.NewInt(100),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid - max amount",
|
||||||
|
addr,
|
||||||
|
types.ConversionFactor().SubRaw(1),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid - zero amount (deletes)",
|
||||||
|
addr,
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid - negative amount",
|
||||||
|
addr,
|
||||||
|
sdkmath.NewInt(-1),
|
||||||
|
"amount is invalid: non-positive amount -1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid - over max amount",
|
||||||
|
addr,
|
||||||
|
types.ConversionFactor(),
|
||||||
|
"amount is invalid: amount 1000000000000 exceeds max of 999999999999",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.setPanicMsg != "" {
|
||||||
|
require.PanicsWithError(t, tt.setPanicMsg, func() {
|
||||||
|
k.SetFractionalBalance(ctx, tt.address, tt.amount)
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
k.SetFractionalBalance(ctx, tt.address, tt.amount)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If its zero balance, check it was deleted
|
||||||
|
if tt.amount.IsZero() {
|
||||||
|
_, exists := k.GetFractionalBalance(ctx, tt.address)
|
||||||
|
require.False(t, exists)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gotAmount, exists := k.GetFractionalBalance(ctx, tt.address)
|
||||||
|
require.True(t, exists)
|
||||||
|
require.Equal(t, tt.amount, gotAmount)
|
||||||
|
|
||||||
|
// Delete balance
|
||||||
|
k.DeleteFractionalBalance(ctx, tt.address)
|
||||||
|
|
||||||
|
_, exists = k.GetFractionalBalance(ctx, tt.address)
|
||||||
|
require.False(t, exists)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetFractionalBalance_InvalidAddr(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k := tk.ctx, tk.keeper
|
||||||
|
|
||||||
|
require.PanicsWithError(
|
||||||
|
t,
|
||||||
|
"address cannot be empty",
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{}, sdkmath.NewInt(100))
|
||||||
|
},
|
||||||
|
"setting balance with empty address should panic",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetFractionalBalance_ZeroDeletes(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k := tk.ctx, tk.keeper
|
||||||
|
|
||||||
|
addr := sdk.AccAddress([]byte("test-address"))
|
||||||
|
|
||||||
|
// Set balance
|
||||||
|
k.SetFractionalBalance(ctx, addr, sdkmath.NewInt(100))
|
||||||
|
|
||||||
|
bal, exists := k.GetFractionalBalance(ctx, addr)
|
||||||
|
require.True(t, exists)
|
||||||
|
require.Equal(t, sdkmath.NewInt(100), bal)
|
||||||
|
|
||||||
|
// Set zero balance
|
||||||
|
k.SetFractionalBalance(ctx, addr, sdkmath.ZeroInt())
|
||||||
|
|
||||||
|
_, exists = k.GetFractionalBalance(ctx, addr)
|
||||||
|
require.False(t, exists)
|
||||||
|
|
||||||
|
// Set zero balance again on non-existent balance
|
||||||
|
require.NotPanics(
|
||||||
|
t,
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, addr, sdkmath.ZeroInt())
|
||||||
|
},
|
||||||
|
"deleting non-existent balance should not panic",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterateFractionalBalances(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k := tk.ctx, tk.keeper
|
||||||
|
|
||||||
|
addrs := []sdk.AccAddress{}
|
||||||
|
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
addr := sdk.AccAddress([]byte{byte(i)})
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
|
||||||
|
// Set balance same as their address byte
|
||||||
|
k.SetFractionalBalance(ctx, addr, sdkmath.NewInt(int64(i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
seenAddrs := []sdk.AccAddress{}
|
||||||
|
|
||||||
|
k.IterateFractionalBalances(ctx, func(addr sdk.AccAddress, bal sdkmath.Int) bool {
|
||||||
|
seenAddrs = append(seenAddrs, addr)
|
||||||
|
|
||||||
|
// Balance is same as first address byte
|
||||||
|
require.Equal(t, int64(addr.Bytes()[0]), bal.Int64())
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
require.ElementsMatch(t, addrs, seenAddrs, "all addresses should be seen")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAggregateSumFractionalBalances(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k := tk.ctx, tk.keeper
|
||||||
|
|
||||||
|
// Set balances from 1 to 10
|
||||||
|
sum := sdkmath.ZeroInt()
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
addr := sdk.AccAddress([]byte{byte(i)})
|
||||||
|
amt := sdkmath.NewInt(int64(i))
|
||||||
|
|
||||||
|
sum = sum.Add(amt)
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
k.SetFractionalBalance(ctx, addr, amt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
gotSum := k.GetTotalSumFractionalBalances(ctx)
|
||||||
|
require.Equal(t, sum, gotSum)
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package keeper
|
package keeper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"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/types"
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
)
|
)
|
||||||
@ -11,11 +14,110 @@ func RegisterInvariants(
|
|||||||
k Keeper,
|
k Keeper,
|
||||||
bk types.BankKeeper,
|
bk types.BankKeeper,
|
||||||
) {
|
) {
|
||||||
|
ir.RegisterRoute(types.ModuleName, "balance-remainder-total", BalancedFractionalTotalInvariant(k))
|
||||||
|
ir.RegisterRoute(types.ModuleName, "valid-fractional-balances", ValidFractionalAmountsInvariant(k))
|
||||||
|
ir.RegisterRoute(types.ModuleName, "valid-remainder-amount", ValidRemainderAmountInvariant(k))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllInvariants runs all invariants of the X/precisebank module.
|
// AllInvariants runs all invariants of the X/precisebank module.
|
||||||
func AllInvariants(k Keeper) sdk.Invariant {
|
func AllInvariants(k Keeper) sdk.Invariant {
|
||||||
return func(ctx sdk.Context) (string, bool) {
|
return func(ctx sdk.Context) (string, bool) {
|
||||||
|
res, stop := BalancedFractionalTotalInvariant(k)(ctx)
|
||||||
|
if stop {
|
||||||
|
return res, stop
|
||||||
|
}
|
||||||
|
|
||||||
|
res, stop = ValidFractionalAmountsInvariant(k)(ctx)
|
||||||
|
if stop {
|
||||||
|
return res, stop
|
||||||
|
}
|
||||||
|
|
||||||
|
res, stop = ValidRemainderAmountInvariant(k)(ctx)
|
||||||
|
if stop {
|
||||||
|
return res, stop
|
||||||
|
}
|
||||||
|
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidFractionalAmountsInvariant checks that all individual fractional
|
||||||
|
// balances are valid.
|
||||||
|
func ValidFractionalAmountsInvariant(k Keeper) sdk.Invariant {
|
||||||
|
return func(ctx sdk.Context) (string, bool) {
|
||||||
|
var (
|
||||||
|
msg string
|
||||||
|
count int
|
||||||
|
)
|
||||||
|
|
||||||
|
k.IterateFractionalBalances(ctx, func(addr sdk.AccAddress, amount sdkmath.Int) bool {
|
||||||
|
if err := types.NewFractionalAmountFromInt(amount).Validate(); err != nil {
|
||||||
|
count++
|
||||||
|
msg += fmt.Sprintf("\t%s has an invalid fractional amount of %s\n", addr, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
broken := count != 0
|
||||||
|
|
||||||
|
return sdk.FormatInvariant(
|
||||||
|
types.ModuleName, "valid-fractional-balances",
|
||||||
|
fmt.Sprintf("amount of invalid fractional balances found %d\n%s", count, msg),
|
||||||
|
), broken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidRemainderAmountInvariant checks that the remainder amount is valid.
|
||||||
|
func ValidRemainderAmountInvariant(k Keeper) sdk.Invariant {
|
||||||
|
return func(ctx sdk.Context) (string, bool) {
|
||||||
|
var (
|
||||||
|
msg string
|
||||||
|
broken bool
|
||||||
|
)
|
||||||
|
|
||||||
|
remainderAmount := k.GetRemainderAmount(ctx)
|
||||||
|
|
||||||
|
if !remainderAmount.IsZero() {
|
||||||
|
// Only validate if non-zero, as zero is default value
|
||||||
|
if err := types.NewFractionalAmountFromInt(remainderAmount).Validate(); err != nil {
|
||||||
|
broken = true
|
||||||
|
msg = fmt.Sprintf("remainder amount is invalid: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.FormatInvariant(
|
||||||
|
types.ModuleName, "valid-remainder-amount",
|
||||||
|
msg,
|
||||||
|
), broken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalancedFractionalTotalInvariant checks that the sum of fractional balances
|
||||||
|
// and the remainder amount is divisible by the conversion factor without any
|
||||||
|
// leftover amount.
|
||||||
|
func BalancedFractionalTotalInvariant(k Keeper) sdk.Invariant {
|
||||||
|
return func(ctx sdk.Context) (string, bool) {
|
||||||
|
fractionalBalSum := k.GetTotalSumFractionalBalances(ctx)
|
||||||
|
remainderAmount := k.GetRemainderAmount(ctx)
|
||||||
|
|
||||||
|
total := fractionalBalSum.Add(remainderAmount)
|
||||||
|
fractionalAmount := total.Mod(types.ConversionFactor())
|
||||||
|
|
||||||
|
broken := false
|
||||||
|
msg := ""
|
||||||
|
|
||||||
|
if !fractionalAmount.IsZero() {
|
||||||
|
broken = true
|
||||||
|
msg = fmt.Sprintf(
|
||||||
|
"(sum(FractionalBalances) + remainder) %% conversionFactor should be 0 but got %v",
|
||||||
|
fractionalAmount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.FormatInvariant(
|
||||||
|
types.ModuleName, "balance-remainder-total",
|
||||||
|
msg,
|
||||||
|
), broken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
170
x/precisebank/keeper/invariants_test.go
Normal file
170
x/precisebank/keeper/invariants_test.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||||
|
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBalancedFractionalTotalInvariant(t *testing.T) {
|
||||||
|
var ctx sdk.Context
|
||||||
|
var k keeper.Keeper
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupFn func()
|
||||||
|
wantBroken bool
|
||||||
|
wantMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid - empty state",
|
||||||
|
func() {},
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid - balances, 0 remainder",
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid - balances, non-zero remainder",
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2).SubRaw(1))
|
||||||
|
|
||||||
|
k.SetRemainderAmount(ctx, sdkmath.OneInt())
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid - balances, 0 remainder",
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2).SubRaw(1))
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
"precisebank: balance-remainder-total invariant\n(sum(FractionalBalances) + remainder) % conversionFactor should be 0 but got 999999999999\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid - invalid balances, non-zero (insufficient) remainder",
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2).SubRaw(2))
|
||||||
|
k.SetRemainderAmount(ctx, sdkmath.OneInt())
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
"precisebank: balance-remainder-total invariant\n(sum(FractionalBalances) + remainder) % conversionFactor should be 0 but got 999999999999\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid - invalid balances, non-zero (excess) remainder",
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2).SubRaw(2))
|
||||||
|
k.SetRemainderAmount(ctx, sdkmath.NewInt(5))
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
"precisebank: balance-remainder-total invariant\n(sum(FractionalBalances) + remainder) % conversionFactor should be 0 but got 3\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Reset each time
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k = tk.ctx, tk.keeper
|
||||||
|
|
||||||
|
tt.setupFn()
|
||||||
|
|
||||||
|
invariantFn := keeper.BalancedFractionalTotalInvariant(k)
|
||||||
|
msg, broken := invariantFn(ctx)
|
||||||
|
|
||||||
|
if tt.wantBroken {
|
||||||
|
require.True(t, broken, "invariant should be broken but is not")
|
||||||
|
require.Equal(t, tt.wantMsg, msg)
|
||||||
|
} else {
|
||||||
|
require.False(t, broken, "invariant should not be broken but is")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidFractionalAmountsInvariant(t *testing.T) {
|
||||||
|
var ctx sdk.Context
|
||||||
|
var k keeper.Keeper
|
||||||
|
var storeKey storetypes.StoreKey
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupFn func()
|
||||||
|
wantBroken bool
|
||||||
|
wantMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid - empty state",
|
||||||
|
func() {},
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid - valid balances",
|
||||||
|
func() {
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2))
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid - exceeds max balance",
|
||||||
|
func() {
|
||||||
|
// Requires manual store manipulation so it is unlikely to have
|
||||||
|
// invalid state in practice. SetFractionalBalance will validate
|
||||||
|
// before setting.
|
||||||
|
addr := sdk.AccAddress{1}
|
||||||
|
amount := types.ConversionFactor()
|
||||||
|
|
||||||
|
store := prefix.NewStore(ctx.KVStore(storeKey), types.FractionalBalancePrefix)
|
||||||
|
|
||||||
|
amountBytes, err := amount.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
store.Set(types.FractionalBalanceKey(addr), amountBytes)
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
"precisebank: valid-fractional-balances invariant\namount of invalid fractional balances found 1\n\tkava1qy0xn7za has an invalid fractional amount of 1000000000000\n\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Reset each time
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k, storeKey = tk.ctx, tk.keeper, tk.storeKey
|
||||||
|
|
||||||
|
tt.setupFn()
|
||||||
|
|
||||||
|
invariantFn := keeper.ValidFractionalAmountsInvariant(k)
|
||||||
|
msg, broken := invariantFn(ctx)
|
||||||
|
|
||||||
|
if tt.wantBroken {
|
||||||
|
require.True(t, broken, "invariant should be broken but is not")
|
||||||
|
require.Equal(t, tt.wantMsg, msg)
|
||||||
|
} else {
|
||||||
|
require.False(t, broken, "invariant should not be broken but is")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
36
x/precisebank/keeper/keeper_test.go
Normal file
36
x/precisebank/keeper/keeper_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/testutil"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testKeeper defines necessary fields for testing keeper store methods that
|
||||||
|
// don't require a full app setup.
|
||||||
|
type testKeeper struct {
|
||||||
|
ctx sdk.Context
|
||||||
|
keeper keeper.Keeper
|
||||||
|
storeKey *storetypes.KVStoreKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestKeeper() testKeeper {
|
||||||
|
storeKey := sdk.NewKVStoreKey(types.ModuleName)
|
||||||
|
// Not required by module, but needs to be non-nil for context
|
||||||
|
tKey := sdk.NewTransientStoreKey("transient_test")
|
||||||
|
ctx := testutil.DefaultContext(storeKey, tKey)
|
||||||
|
|
||||||
|
tApp := app.NewTestApp()
|
||||||
|
cdc := tApp.AppCodec()
|
||||||
|
k := keeper.NewKeeper(cdc, storeKey)
|
||||||
|
|
||||||
|
return testKeeper{
|
||||||
|
ctx: ctx,
|
||||||
|
keeper: k,
|
||||||
|
storeKey: storeKey,
|
||||||
|
}
|
||||||
|
}
|
66
x/precisebank/keeper/remainder_amount.go
Normal file
66
x/precisebank/keeper/remainder_amount.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRemainderAmount returns the internal remainder amount.
|
||||||
|
func (k *Keeper) GetRemainderAmount(
|
||||||
|
ctx sdk.Context,
|
||||||
|
) sdkmath.Int {
|
||||||
|
store := ctx.KVStore(k.storeKey)
|
||||||
|
|
||||||
|
bz := store.Get(types.RemainderBalanceKey)
|
||||||
|
if bz == nil {
|
||||||
|
return sdkmath.ZeroInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
var bal sdkmath.Int
|
||||||
|
if err := bal.Unmarshal(bz); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to unmarshal remainder amount: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bal
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRemainderAmount sets the internal remainder amount.
|
||||||
|
func (k *Keeper) SetRemainderAmount(
|
||||||
|
ctx sdk.Context,
|
||||||
|
amount sdkmath.Int,
|
||||||
|
) {
|
||||||
|
// Prevent storing zero amounts. In practice, the remainder amount should
|
||||||
|
// only be non-zero during transactions as mint and burns should net zero
|
||||||
|
// due to only being used for EVM transfers.
|
||||||
|
if amount.IsZero() {
|
||||||
|
k.DeleteRemainderAmount(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the remainder is valid before setting it. Follows the same
|
||||||
|
// validation as FractionalBalance with the same value range.
|
||||||
|
if err := types.NewFractionalAmountFromInt(amount).Validate(); err != nil {
|
||||||
|
panic(fmt.Errorf("remainder amount is invalid: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
store := ctx.KVStore(k.storeKey)
|
||||||
|
|
||||||
|
amountBytes, err := amount.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to marshal remainder amount: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Set(types.RemainderBalanceKey, amountBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRemainderAmount deletes the internal remainder amount.
|
||||||
|
func (k *Keeper) DeleteRemainderAmount(
|
||||||
|
ctx sdk.Context,
|
||||||
|
) {
|
||||||
|
store := ctx.KVStore(k.storeKey)
|
||||||
|
store.Delete(types.RemainderBalanceKey)
|
||||||
|
}
|
71
x/precisebank/keeper/remainder_amount_test.go
Normal file
71
x/precisebank/keeper/remainder_amount_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSetRemainderAmount(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k, storeKey := tk.ctx, tk.keeper, tk.storeKey
|
||||||
|
|
||||||
|
// Set amount
|
||||||
|
k.SetRemainderAmount(ctx, sdkmath.NewInt(100))
|
||||||
|
|
||||||
|
amt := k.GetRemainderAmount(ctx)
|
||||||
|
require.Equal(t, sdkmath.NewInt(100), amt)
|
||||||
|
|
||||||
|
// Set zero balance
|
||||||
|
k.SetRemainderAmount(ctx, sdkmath.ZeroInt())
|
||||||
|
|
||||||
|
amt = k.GetRemainderAmount(ctx)
|
||||||
|
require.Equal(t, sdkmath.ZeroInt(), amt)
|
||||||
|
|
||||||
|
// Get directly from store to make sure it was actually deleted
|
||||||
|
store := ctx.KVStore(storeKey)
|
||||||
|
bz := store.Get(types.RemainderBalanceKey)
|
||||||
|
require.Nil(t, bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidRemainderAmount(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k := tk.ctx, tk.keeper
|
||||||
|
|
||||||
|
// Set negative amount
|
||||||
|
require.PanicsWithError(t, "remainder amount is invalid: non-positive amount -1", func() {
|
||||||
|
k.SetRemainderAmount(ctx, sdkmath.NewInt(-1))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set amount over max
|
||||||
|
require.PanicsWithError(t, "remainder amount is invalid: amount 1000000000000 exceeds max of 999999999999", func() {
|
||||||
|
k.SetRemainderAmount(ctx, types.ConversionFactor())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRemainderAmount(t *testing.T) {
|
||||||
|
tk := NewTestKeeper()
|
||||||
|
ctx, k, storeKey := tk.ctx, tk.keeper, tk.storeKey
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
k.DeleteRemainderAmount(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set amount
|
||||||
|
k.SetRemainderAmount(ctx, sdkmath.NewInt(100))
|
||||||
|
|
||||||
|
amt := k.GetRemainderAmount(ctx)
|
||||||
|
require.Equal(t, sdkmath.NewInt(100), amt)
|
||||||
|
|
||||||
|
// Delete amount
|
||||||
|
k.DeleteRemainderAmount(ctx)
|
||||||
|
|
||||||
|
amt = k.GetRemainderAmount(ctx)
|
||||||
|
require.Equal(t, sdkmath.ZeroInt(), amt)
|
||||||
|
|
||||||
|
store := ctx.KVStore(storeKey)
|
||||||
|
bz := store.Get(types.RemainderBalanceKey)
|
||||||
|
require.Nil(t, bz)
|
||||||
|
}
|
41
x/precisebank/types/fractional_amount.go
Normal file
41
x/precisebank/types/fractional_amount.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
fmt "fmt"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FractionalAmount represents a fractional amount between the valid range of 1
|
||||||
|
// and maxFractionalAmount. This wraps an sdkmath.Int to provide additional
|
||||||
|
// validation methods so it can be re-used in multiple places.
|
||||||
|
type FractionalAmount struct {
|
||||||
|
sdkmath.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFractionalAmountFromInt creates a new FractionalAmount from an sdkmath.Int.
|
||||||
|
func NewFractionalAmountFromInt(i sdkmath.Int) FractionalAmount {
|
||||||
|
return FractionalAmount{i}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFractionalAmount creates a new FractionalAmount from an int64.
|
||||||
|
func NewFractionalAmount(i int64) FractionalAmount {
|
||||||
|
return FractionalAmount{sdkmath.NewInt(i)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the FractionalAmount is valid.
|
||||||
|
func (f FractionalAmount) Validate() error {
|
||||||
|
if f.IsNil() {
|
||||||
|
return fmt.Errorf("nil amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.IsPositive() {
|
||||||
|
return fmt.Errorf("non-positive amount %v", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.GT(maxFractionalAmount) {
|
||||||
|
return fmt.Errorf("amount %v exceeds max of %v", f, maxFractionalAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
fmt "fmt"
|
|
||||||
|
|
||||||
sdkmath "cosmossdk.io/math"
|
sdkmath "cosmossdk.io/math"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
)
|
)
|
||||||
@ -40,17 +38,6 @@ func (fb FractionalBalance) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if fb.Amount.IsNil() {
|
// Validate the amount with the FractionalAmount wrapper
|
||||||
return fmt.Errorf("nil amount")
|
return NewFractionalAmountFromInt(fb.Amount).Validate()
|
||||||
}
|
|
||||||
|
|
||||||
if !fb.Amount.IsPositive() {
|
|
||||||
return fmt.Errorf("non-positive amount %v", fb.Amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fb.Amount.GT(maxFractionalAmount) {
|
|
||||||
return fmt.Errorf("amount %v exceeds max of %v", fb.Amount, maxFractionalAmount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
|
import sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ModuleName name that will be used throughout the module
|
// ModuleName name that will be used throughout the module
|
||||||
ModuleName = "precisebank"
|
ModuleName = "precisebank"
|
||||||
@ -9,3 +11,18 @@ const (
|
|||||||
// RouterKey Top level router key
|
// RouterKey Top level router key
|
||||||
RouterKey = ModuleName
|
RouterKey = ModuleName
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// key prefixes for store
|
||||||
|
var (
|
||||||
|
FractionalBalancePrefix = []byte{0x01} // address -> fractional balance
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keys for store that are not prefixed
|
||||||
|
var (
|
||||||
|
RemainderBalanceKey = []byte{0x02} // fractional balance remainder
|
||||||
|
)
|
||||||
|
|
||||||
|
// FractionalBalanceKey returns a key from an address
|
||||||
|
func FractionalBalanceKey(address sdk.AccAddress) []byte {
|
||||||
|
return address.Bytes()
|
||||||
|
}
|
||||||
|
17
x/precisebank/types/keys_test.go
Normal file
17
x/precisebank/types/keys_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFractionalBalanceKey(t *testing.T) {
|
||||||
|
addr := sdk.AccAddress([]byte("test-address"))
|
||||||
|
|
||||||
|
key := types.FractionalBalanceKey(addr)
|
||||||
|
require.Equal(t, addr.Bytes(), key)
|
||||||
|
require.Equal(t, addr, sdk.AccAddress(key), "key should be able to be converted back to address")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user