Skip to content

Commit

Permalink
feat(x/precisebank): Display 0 reserve balance to module consumers (#…
Browse files Browse the repository at this point in the history
…1958)

Module reserve represents fractional balances, so it should be hidden to consumers to not have a misleading total balance that doubles the fractional balances. This modifies GetBalance() and SpendableCoin() to always return zero coins when fetching the reserve address balance for fractional amounts.
  • Loading branch information
drklee3 authored Jul 10, 2024
1 parent ce6aac3 commit 9de9de6
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 4 deletions.
8 changes: 4 additions & 4 deletions x/precisebank/keeper/burn_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func (suite *burnIntegrationTestSuite) TestBurnCoins_Remainder() {

// Burn 0.1 until balance is 0
for {
reserveBalBefore := suite.Keeper.GetBalance(
reserveBalBefore := suite.BankKeeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
Expand Down Expand Up @@ -291,7 +291,7 @@ func (suite *burnIntegrationTestSuite) TestBurnCoins_Remainder() {
moduleAddr,
types.ExtendedCoinDenom,
)
reserveBalAfter := suite.Keeper.GetBalance(
reserveBalAfter := suite.BankKeeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
Expand Down Expand Up @@ -357,7 +357,7 @@ func (suite *burnIntegrationTestSuite) TestBurnCoins_Spread_Remainder() {

// Burn 0.1 from each account
for _, addr := range addrs {
reserveBalBefore := suite.Keeper.GetBalance(
reserveBalBefore := suite.BankKeeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
Expand Down Expand Up @@ -395,7 +395,7 @@ func (suite *burnIntegrationTestSuite) TestBurnCoins_Spread_Remainder() {
addr,
types.ExtendedCoinDenom,
)
reserveBalAfter := suite.Keeper.GetBalance(
reserveBalAfter := suite.BankKeeper.GetBalance(
suite.Ctx,
reserveAddr,
types.IntegerCoinDenom,
Expand Down
14 changes: 14 additions & 0 deletions x/precisebank/keeper/view.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/precisebank/types"
)
Expand All @@ -13,6 +14,14 @@ func (k Keeper) GetBalance(
addr sdk.AccAddress,
denom string,
) sdk.Coin {
// Module balance should display as empty for extended denom. Module
// balances are **only** for the reserve which backs the fractional
// balances. Returning the backing balances if querying extended denom would
// result in a double counting of the fractional balances.
if denom == types.ExtendedCoinDenom && addr.Equals(k.ak.GetModuleAddress(types.ModuleName)) {
return sdk.NewCoin(denom, sdkmath.ZeroInt())
}

// Pass through to x/bank for denoms except ExtendedCoinDenom
if denom != types.ExtendedCoinDenom {
return k.bk.GetBalance(ctx, addr, denom)
Expand Down Expand Up @@ -41,6 +50,11 @@ func (k Keeper) SpendableCoin(
addr sdk.AccAddress,
denom string,
) sdk.Coin {
// Same as GetBalance, extended denom balances are transparent to consumers.
if denom == types.ExtendedCoinDenom && addr.Equals(k.ak.GetModuleAddress(types.ModuleName)) {
return sdk.NewCoin(denom, sdkmath.ZeroInt())
}

// Pass through to x/bank for denoms except ExtendedCoinDenom
if denom != types.ExtendedCoinDenom {
return k.bk.SpendableCoin(ctx, addr, denom)
Expand Down
71 changes: 71 additions & 0 deletions x/precisebank/keeper/view_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,74 @@ func (suite *viewIntegrationTestSuite) TestKeeper_SpendableCoin() {
})
}
}

func (suite *viewIntegrationTestSuite) TestKeeper_HiddenReserve() {
// Reserve balances should not be shown to consumers of x/precisebank, as it
// represents the fractional balances of accounts.

moduleAddr := authtypes.NewModuleAddress(types.ModuleName)
addr1 := sdk.AccAddress{1}

// Make the reserve hold a non-zero balance
// Mint fractional coins to an account, which should cause a mint of 1
// integer coin to the reserve to back it.
extCoin := sdk.NewCoin(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(1000))
unrelatedCoin := sdk.NewCoin("unrelated", sdk.NewInt(1000))
suite.MintToAccount(
addr1,
sdk.NewCoins(
extCoin,
unrelatedCoin,
),
)

// Check underlying x/bank balance for reserve
reserveIntCoin := suite.BankKeeper.GetBalance(suite.Ctx, moduleAddr, types.IntegerCoinDenom)
suite.Require().Equal(
sdkmath.NewInt(1),
reserveIntCoin.Amount,
"reserve should hold 1 integer coin",
)

tests := []struct {
name string
giveAddr sdk.AccAddress
giveDenom string
wantAmount sdkmath.Int
}{
{
"reserve account - hidden extended denom",
moduleAddr,
types.ExtendedCoinDenom,
sdkmath.ZeroInt(),
},
{
"reserve account - visible integer denom",
moduleAddr,
types.IntegerCoinDenom,
sdkmath.OneInt(),
},
{
"user account - visible extended denom",
addr1,
types.ExtendedCoinDenom,
extCoin.Amount,
},
{
"user account - visible integer denom",
addr1,
types.IntegerCoinDenom,
extCoin.Amount.Quo(types.ConversionFactor()),
},
}

for _, tt := range tests {
suite.Run(tt.name, func() {
coin := suite.Keeper.GetBalance(suite.Ctx, tt.giveAddr, tt.giveDenom)
suite.Require().Equal(tt.wantAmount.Int64(), coin.Amount.Int64())

spendableCoin := suite.Keeper.SpendableCoin(suite.Ctx, tt.giveAddr, tt.giveDenom)
suite.Require().Equal(tt.wantAmount.Int64(), spendableCoin.Amount.Int64())
})
}
}
70 changes: 70 additions & 0 deletions x/precisebank/keeper/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/kava-labs/kava/x/precisebank/types"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -90,6 +91,13 @@ func TestKeeper_GetBalance(t *testing.T) {
// Set fractional balance in store before query
tk.keeper.SetFractionalBalance(tk.ctx, addr, tt.giveFractionalBal)

// Checks address if its a reserve denom
if tt.giveDenom == types.ExtendedCoinDenom {
tk.ak.EXPECT().GetModuleAddress(types.ModuleName).
Return(authtypes.NewModuleAddress(types.ModuleName)).
Once()
}

if tt.giveDenom == types.ExtendedCoinDenom {
// No balance pass through
tk.bk.EXPECT().
Expand Down Expand Up @@ -198,6 +206,13 @@ func TestKeeper_SpendableCoin(t *testing.T) {
// Set fractional balance in store before query
tk.keeper.SetFractionalBalance(tk.ctx, addr, tt.giveFractionalBal)

// If its a reserve denom, module address is checked
if tt.giveDenom == types.ExtendedCoinDenom {
tk.ak.EXPECT().GetModuleAddress(types.ModuleName).
Return(authtypes.NewModuleAddress(types.ModuleName)).
Once()
}

if tt.giveDenom == types.ExtendedCoinDenom {
// No balance pass through
tk.bk.EXPECT().
Expand All @@ -224,3 +239,58 @@ func TestKeeper_SpendableCoin(t *testing.T) {
})
}
}

func TestHiddenReserve(t *testing.T) {
// Reserve balances should not be shown to consumers of x/precisebank, as it
// represents the fractional balances of accounts.

tk := NewMockedTestData(t)

moduleAddr := authtypes.NewModuleAddress(types.ModuleName)

// No mock bankkeeper expectations, which means the zero coin is returned
// directly for reserve address. So the mock bankkeeper doesn't need to have
// a handler for getting underlying balance.

tests := []struct {
name string
denom string
expectedBalance sdk.Coin
}{
{"akava", types.ExtendedCoinDenom, sdk.NewCoin(types.ExtendedCoinDenom, sdkmath.ZeroInt())},
{"ukava", types.IntegerCoinDenom, sdk.NewCoin(types.IntegerCoinDenom, sdkmath.NewInt(1))},
{"unrelated denom", "cat", sdk.NewCoin("cat", sdkmath.ZeroInt())},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 2 calls for GetBalance and SpendableCoin, only for reserve coins
if tt.denom == "akava" {
tk.ak.EXPECT().GetModuleAddress(types.ModuleName).
Return(moduleAddr).
Twice()
} else {
// Passthrough to x/bank for non-reserve denoms
tk.bk.EXPECT().
GetBalance(tk.ctx, moduleAddr, tt.denom).
Return(sdk.NewCoin(tt.denom, sdkmath.ZeroInt())).
Once()

tk.bk.EXPECT().
SpendableCoin(tk.ctx, moduleAddr, tt.denom).
Return(sdk.NewCoin(tt.denom, sdkmath.ZeroInt())).
Once()
}

// GetBalance should return zero balance for reserve address
coin := tk.keeper.GetBalance(tk.ctx, moduleAddr, tt.denom)
require.Equal(t, tt.denom, coin.Denom)
require.Equal(t, sdkmath.ZeroInt(), coin.Amount)

// SpendableCoin should return zero balance for reserve address
spendableCoin := tk.keeper.SpendableCoin(tk.ctx, moduleAddr, tt.denom)
require.Equal(t, tt.denom, spendableCoin.Denom)
require.Equal(t, sdkmath.ZeroInt(), spendableCoin.Amount)
})
}
}

0 comments on commit 9de9de6

Please sign in to comment.