Skip to content

Commit

Permalink
feat(x/precisebank): Implement GetBalance (#1916)
Browse files Browse the repository at this point in the history
Implement GetBalance for extended balances which passes through to `x/bank` for non-extended denoms. This diverges from `x/evmutil` behavior which will panic on non-"akava" calls.

Add bank / account keeper mocks for testing, with mockery config for [mockery package setup](https://vektra.github.io/mockery/latest/migrating_to_packages/)
  • Loading branch information
drklee3 committed May 21, 2024
1 parent dbc3ad7 commit 4cf41d1
Show file tree
Hide file tree
Showing 14 changed files with 1,103 additions and 48 deletions.
16 changes: 16 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generate EXPECT() methods, type-safe methods to generate call expectations
with-expecter: true

# Generate mocks in adjacent mocks directory to the interfaces
dir: "{{.InterfaceDir}}/mocks"
mockname: "Mock{{.InterfaceName}}"
outpkg: "mocks"
filename: "Mock{{.InterfaceName}}.go"

packages:
github.com/kava-labs/kava/x/precisebank/types:
# package-specific config
config:
interfaces:
AccountKeeper:
BankKeeper:
2 changes: 2 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,8 @@ func NewApp(
app.precisebankKeeper = precisebankkeeper.NewKeeper(
app.appCodec,
keys[precisebanktypes.StoreKey],
app.bankKeeper,
app.accountKeeper,
)

evmBankKeeper := evmutilkeeper.NewEvmBankKeeper(app.evmutilKeeper, app.bankKeeper, app.accountKeeper)
Expand Down
10 changes: 5 additions & 5 deletions x/precisebank/keeper/fractional_balance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func TestSetGetFractionalBalance(t *testing.T) {
tk := NewTestKeeper()
tk := NewMockedTestData(t)
ctx, k := tk.ctx, tk.keeper

addr := sdk.AccAddress([]byte("test-address"))
Expand Down Expand Up @@ -97,7 +97,7 @@ func TestSetGetFractionalBalance(t *testing.T) {
}

func TestSetFractionalBalance_InvalidAddr(t *testing.T) {
tk := NewTestKeeper()
tk := NewMockedTestData(t)
ctx, k := tk.ctx, tk.keeper

require.PanicsWithError(
Expand All @@ -111,7 +111,7 @@ func TestSetFractionalBalance_InvalidAddr(t *testing.T) {
}

func TestSetFractionalBalance_ZeroDeletes(t *testing.T) {
tk := NewTestKeeper()
tk := NewMockedTestData(t)
ctx, k := tk.ctx, tk.keeper

addr := sdk.AccAddress([]byte("test-address"))
Expand Down Expand Up @@ -140,7 +140,7 @@ func TestSetFractionalBalance_ZeroDeletes(t *testing.T) {
}

func TestIterateFractionalBalances(t *testing.T) {
tk := NewTestKeeper()
tk := NewMockedTestData(t)
ctx, k := tk.ctx, tk.keeper

addrs := []sdk.AccAddress{}
Expand Down Expand Up @@ -168,7 +168,7 @@ func TestIterateFractionalBalances(t *testing.T) {
}

func TestGetAggregateSumFractionalBalances(t *testing.T) {
tk := NewTestKeeper()
tk := NewMockedTestData(t)
ctx, k := tk.ctx, tk.keeper

// Set balances from 1 to 10
Expand Down
35 changes: 35 additions & 0 deletions x/precisebank/keeper/invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func RegisterInvariants(
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))
ir.RegisterRoute(types.ModuleName, "fractional-denom-not-in-bank", FractionalDenomNotInBankInvariant(k))
}

// AllInvariants runs all invariants of the X/precisebank module.
Expand All @@ -37,6 +38,11 @@ func AllInvariants(k Keeper) sdk.Invariant {
return res, stop
}

res, stop = FractionalDenomNotInBankInvariant(k)(ctx)
if stop {
return res, stop
}

return "", false
}
}
Expand Down Expand Up @@ -121,3 +127,32 @@ func BalancedFractionalTotalInvariant(k Keeper) sdk.Invariant {
), broken
}
}

// FractionalDenomNotInBankInvariant checks that the bank does not hold any
// fractional denoms. These assets, e.g. akava, should only exist in the
// x/precisebank module as this is a decimal extension of ukava that shares
// the same total supply and is effectively the same asset. ukava held by this
// module in x/bank backs all fractional balances in x/precisebank. If akava
// somehow ends up in x/bank, then it would both break all expectations of this
// module as well as be double-counted in the total supply.
func FractionalDenomNotInBankInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
extBankSupply := k.bk.GetSupply(ctx, types.ExtendedCoinDenom)

broken := !extBankSupply.IsZero()
msg := ""

if broken {
msg = fmt.Sprintf(
"x/bank should not hold any %v but has supply of %v",
types.ExtendedCoinDenom,
extBankSupply,
)
}

return sdk.FormatInvariant(
types.ModuleName, "fractional-denom-not-in-bank",
msg,
), broken
}
}
101 changes: 73 additions & 28 deletions x/precisebank/keeper/invariants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,26 @@ import (

"github.com/kava-labs/kava/x/precisebank/keeper"
"github.com/kava-labs/kava/x/precisebank/types"
"github.com/kava-labs/kava/x/precisebank/types/mocks"
"github.com/stretchr/testify/require"
)

func TestBalancedFractionalTotalInvariant(t *testing.T) {
var ctx sdk.Context
var k keeper.Keeper

tests := []struct {
name string
setupFn func()
setupFn func(ctx sdk.Context, k keeper.Keeper)
wantBroken bool
wantMsg string
}{
{
"valid - empty state",
func() {},
func(_ sdk.Context, _ keeper.Keeper) {},
false,
"",
},
{
"valid - balances, 0 remainder",
func() {
func(ctx sdk.Context, k keeper.Keeper) {
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2))
},
Expand All @@ -40,7 +38,7 @@ func TestBalancedFractionalTotalInvariant(t *testing.T) {
},
{
"valid - balances, non-zero remainder",
func() {
func(ctx sdk.Context, k keeper.Keeper) {
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2).SubRaw(1))

Expand All @@ -51,7 +49,7 @@ func TestBalancedFractionalTotalInvariant(t *testing.T) {
},
{
"invalid - balances, 0 remainder",
func() {
func(ctx sdk.Context, k keeper.Keeper) {
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2).SubRaw(1))
},
Expand All @@ -60,7 +58,7 @@ func TestBalancedFractionalTotalInvariant(t *testing.T) {
},
{
"invalid - invalid balances, non-zero (insufficient) remainder",
func() {
func(ctx sdk.Context, k keeper.Keeper) {
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())
Expand All @@ -70,7 +68,7 @@ func TestBalancedFractionalTotalInvariant(t *testing.T) {
},
{
"invalid - invalid balances, non-zero (excess) remainder",
func() {
func(ctx sdk.Context, k keeper.Keeper) {
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))
Expand All @@ -83,13 +81,12 @@ func TestBalancedFractionalTotalInvariant(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset each time
tk := NewTestKeeper()
ctx, k = tk.ctx, tk.keeper
td := NewMockedTestData(t)

tt.setupFn()
tt.setupFn(td.ctx, td.keeper)

invariantFn := keeper.BalancedFractionalTotalInvariant(k)
msg, broken := invariantFn(ctx)
invariantFn := keeper.BalancedFractionalTotalInvariant(td.keeper)
msg, broken := invariantFn(td.ctx)

if tt.wantBroken {
require.True(t, broken, "invariant should be broken but is not")
Expand All @@ -102,25 +99,21 @@ func TestBalancedFractionalTotalInvariant(t *testing.T) {
}

func TestValidFractionalAmountsInvariant(t *testing.T) {
var ctx sdk.Context
var k keeper.Keeper
var storeKey storetypes.StoreKey

tests := []struct {
name string
setupFn func()
setupFn func(ctx sdk.Context, k keeper.Keeper, storeKey storetypes.StoreKey)
wantBroken bool
wantMsg string
}{
{
"valid - empty state",
func() {},
func(_ sdk.Context, _ keeper.Keeper, _ storetypes.StoreKey) {},
false,
"",
},
{
"valid - valid balances",
func() {
func(ctx sdk.Context, k keeper.Keeper, _ storetypes.StoreKey) {
k.SetFractionalBalance(ctx, sdk.AccAddress{1}, types.ConversionFactor().QuoRaw(2))
k.SetFractionalBalance(ctx, sdk.AccAddress{2}, types.ConversionFactor().QuoRaw(2))
},
Expand All @@ -129,7 +122,7 @@ func TestValidFractionalAmountsInvariant(t *testing.T) {
},
{
"invalid - exceeds max balance",
func() {
func(ctx sdk.Context, _ keeper.Keeper, storeKey storetypes.StoreKey) {
// Requires manual store manipulation so it is unlikely to have
// invalid state in practice. SetFractionalBalance will validate
// before setting.
Expand All @@ -151,13 +144,65 @@ func TestValidFractionalAmountsInvariant(t *testing.T) {
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
td := NewMockedTestData(t)

tt.setupFn(td.ctx, td.keeper, td.storeKey)

invariantFn := keeper.ValidFractionalAmountsInvariant(td.keeper)
msg, broken := invariantFn(td.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 TestFractionalDenomNotInBankInvariant(t *testing.T) {
tests := []struct {
name string
setupFn func(ctx sdk.Context, bk *mocks.MockBankKeeper)
wantBroken bool
wantMsg string
}{
{
"valid - integer denom (ukava) supply",
func(ctx sdk.Context, bk *mocks.MockBankKeeper) {
// No fractional balance in x/bank
// This also enforces there is no GetSupply() call for IntegerCoinDenom / ukava
bk.EXPECT().
GetSupply(ctx, types.ExtendedCoinDenom).
Return(sdk.NewCoin(types.ExtendedCoinDenom, sdkmath.ZeroInt())).
Once()
},
false,
"",
},
{
"invalid - x/bank contains fractional denom (akava)",
func(ctx sdk.Context, bk *mocks.MockBankKeeper) {
bk.EXPECT().
GetSupply(ctx, types.ExtendedCoinDenom).
Return(sdk.NewCoin(types.ExtendedCoinDenom, sdk.NewInt(1000))).
Once()
},
true,
"precisebank: fractional-denom-not-in-bank invariant\nx/bank should not hold any akava but has supply of 1000akava\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset each time
td := NewMockedTestData(t)

tt.setupFn()
tt.setupFn(td.ctx, td.bk)

invariantFn := keeper.ValidFractionalAmountsInvariant(k)
msg, broken := invariantFn(ctx)
invariantFn := keeper.FractionalDenomNotInBankInvariant(td.keeper)
msg, broken := invariantFn(td.ctx)

if tt.wantBroken {
require.True(t, broken, "invariant should be broken but is not")
Expand Down
28 changes: 25 additions & 3 deletions x/precisebank/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,43 @@ package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
evmtypes "github.com/evmos/ethermint/x/evm/types"

"github.com/kava-labs/kava/x/precisebank/types"
)

// TODO: Enforce that Keeper implements the expected keeper interfaces
// var _ types.BankKeeper = Keeper{}
// Enforce that Keeper implements the expected keeper interfaces
var _ evmtypes.BankKeeper = Keeper{}

// Keeper defines the precisebank module's keeper
type Keeper struct {
cdc codec.BinaryCodec
storeKey storetypes.StoreKey

bk types.BankKeeper
ak types.AccountKeeper
}

// NewKeeper creates a new keeper
func NewKeeper(cdc codec.BinaryCodec, storeKey storetypes.StoreKey) Keeper {
func NewKeeper(
cdc codec.BinaryCodec,
storeKey storetypes.StoreKey,
bk types.BankKeeper,
ak types.AccountKeeper,
) Keeper {
return Keeper{
cdc: cdc,
storeKey: storeKey,
bk: bk,
ak: ak,
}
}

func (k Keeper) MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error {
panic("unimplemented")
}

func (k Keeper) BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error {
panic("unimplemented")
}

0 comments on commit 4cf41d1

Please sign in to comment.