mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 23:35:19 +00:00
feat(x/precisebank): Implement MintCoins (#1920)
Implement MintCoins method that matches x/bank MintCoins validation behavior
This commit is contained in:
parent
3d5f5902b8
commit
110adcab2c
12
Makefile
12
Makefile
@ -327,6 +327,16 @@ test-cli: build
|
|||||||
test-migrate:
|
test-migrate:
|
||||||
@$(GO_BIN) test -v -count=1 ./migrate/...
|
@$(GO_BIN) test -v -count=1 ./migrate/...
|
||||||
|
|
||||||
|
# Use the old Apple linker to workaround broken xcode - https://github.com/golang/go/issues/65169
|
||||||
|
ifeq ($(OS_FAMILY),Darwin)
|
||||||
|
FUZZLDFLAGS := -ldflags=-extldflags=-Wl,-ld_classic
|
||||||
|
endif
|
||||||
|
|
||||||
|
test-fuzz:
|
||||||
|
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzMintCoins ./x/precisebank/keeper
|
||||||
|
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzGenesisStateValidate_NonZeroRemainder ./x/precisebank/types
|
||||||
|
@$(GO_BIN) test $(FUZZLDFLAGS) -run NOTAREALTEST -v -fuzztime 10s -fuzz=FuzzGenesisStateValidate_ZeroRemainder ./x/precisebank/types
|
||||||
|
|
||||||
# Kick start lots of sims on an AWS cluster.
|
# Kick start lots of sims on an AWS cluster.
|
||||||
# This submits an AWS Batch job to run a lot of sims, each within a docker image. Results are uploaded to S3
|
# This submits an AWS Batch job to run a lot of sims, each within a docker image. Results are uploaded to S3
|
||||||
start-remote-sims:
|
start-remote-sims:
|
||||||
@ -347,4 +357,4 @@ update-kvtool:
|
|||||||
git submodule update
|
git submodule update
|
||||||
cd tests/e2e/kvtool && make install
|
cd tests/e2e/kvtool && make install
|
||||||
|
|
||||||
.PHONY: all build-linux install clean build test test-cli test-all test-rest test-basic start-remote-sims
|
.PHONY: all build-linux install clean build test test-cli test-all test-rest test-basic test-fuzz start-remote-sims
|
||||||
|
@ -15,12 +15,12 @@ import (
|
|||||||
func (k *Keeper) GetFractionalBalance(
|
func (k *Keeper) GetFractionalBalance(
|
||||||
ctx sdk.Context,
|
ctx sdk.Context,
|
||||||
address sdk.AccAddress,
|
address sdk.AccAddress,
|
||||||
) (sdkmath.Int, bool) {
|
) sdkmath.Int {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.FractionalBalancePrefix)
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.FractionalBalancePrefix)
|
||||||
|
|
||||||
bz := store.Get(types.FractionalBalanceKey(address))
|
bz := store.Get(types.FractionalBalanceKey(address))
|
||||||
if bz == nil {
|
if bz == nil {
|
||||||
return sdkmath.ZeroInt(), false
|
return sdkmath.ZeroInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
var bal sdkmath.Int
|
var bal sdkmath.Int
|
||||||
@ -28,7 +28,7 @@ func (k *Keeper) GetFractionalBalance(
|
|||||||
panic(fmt.Errorf("failed to unmarshal fractional balance: %w", err))
|
panic(fmt.Errorf("failed to unmarshal fractional balance: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return bal, true
|
return bal
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFractionalBalance sets the fractional balance for an address.
|
// SetFractionalBalance sets the fractional balance for an address.
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
sdkmath "cosmossdk.io/math"
|
sdkmath "cosmossdk.io/math"
|
||||||
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -11,9 +12,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSetGetFractionalBalance(t *testing.T) {
|
func TestSetGetFractionalBalance(t *testing.T) {
|
||||||
tk := NewMockedTestData(t)
|
|
||||||
ctx, k := tk.ctx, tk.keeper
|
|
||||||
|
|
||||||
addr := sdk.AccAddress([]byte("test-address"))
|
addr := sdk.AccAddress([]byte("test-address"))
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -63,6 +61,9 @@ func TestSetGetFractionalBalance(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
td := NewMockedTestData(t)
|
||||||
|
ctx, k := td.ctx, td.keeper
|
||||||
|
|
||||||
if tt.setPanicMsg != "" {
|
if tt.setPanicMsg != "" {
|
||||||
require.PanicsWithError(t, tt.setPanicMsg, func() {
|
require.PanicsWithError(t, tt.setPanicMsg, func() {
|
||||||
k.SetFractionalBalance(ctx, tt.address, tt.amount)
|
k.SetFractionalBalance(ctx, tt.address, tt.amount)
|
||||||
@ -75,23 +76,24 @@ func TestSetGetFractionalBalance(t *testing.T) {
|
|||||||
k.SetFractionalBalance(ctx, tt.address, tt.amount)
|
k.SetFractionalBalance(ctx, tt.address, tt.amount)
|
||||||
})
|
})
|
||||||
|
|
||||||
// If its zero balance, check it was deleted
|
// If its zero balance, check it was deleted in store
|
||||||
if tt.amount.IsZero() {
|
if tt.amount.IsZero() {
|
||||||
_, exists := k.GetFractionalBalance(ctx, tt.address)
|
store := prefix.NewStore(ctx.KVStore(td.storeKey), types.FractionalBalancePrefix)
|
||||||
require.False(t, exists)
|
bz := store.Get(types.FractionalBalanceKey(tt.address))
|
||||||
|
require.Nil(t, bz)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gotAmount, exists := k.GetFractionalBalance(ctx, tt.address)
|
gotAmount := k.GetFractionalBalance(ctx, tt.address)
|
||||||
require.True(t, exists)
|
|
||||||
require.Equal(t, tt.amount, gotAmount)
|
require.Equal(t, tt.amount, gotAmount)
|
||||||
|
|
||||||
// Delete balance
|
// Delete balance
|
||||||
k.DeleteFractionalBalance(ctx, tt.address)
|
k.DeleteFractionalBalance(ctx, tt.address)
|
||||||
|
|
||||||
_, exists = k.GetFractionalBalance(ctx, tt.address)
|
store := prefix.NewStore(ctx.KVStore(td.storeKey), types.FractionalBalancePrefix)
|
||||||
require.False(t, exists)
|
bz := store.Get(types.FractionalBalanceKey(tt.address))
|
||||||
|
require.Nil(t, bz)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,23 +113,24 @@ func TestSetFractionalBalance_InvalidAddr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetFractionalBalance_ZeroDeletes(t *testing.T) {
|
func TestSetFractionalBalance_ZeroDeletes(t *testing.T) {
|
||||||
tk := NewMockedTestData(t)
|
td := NewMockedTestData(t)
|
||||||
ctx, k := tk.ctx, tk.keeper
|
ctx, k := td.ctx, td.keeper
|
||||||
|
|
||||||
addr := sdk.AccAddress([]byte("test-address"))
|
addr := sdk.AccAddress([]byte("test-address"))
|
||||||
|
|
||||||
// Set balance
|
// Set balance
|
||||||
k.SetFractionalBalance(ctx, addr, sdkmath.NewInt(100))
|
k.SetFractionalBalance(ctx, addr, sdkmath.NewInt(100))
|
||||||
|
|
||||||
bal, exists := k.GetFractionalBalance(ctx, addr)
|
bal := k.GetFractionalBalance(ctx, addr)
|
||||||
require.True(t, exists)
|
|
||||||
require.Equal(t, sdkmath.NewInt(100), bal)
|
require.Equal(t, sdkmath.NewInt(100), bal)
|
||||||
|
|
||||||
// Set zero balance
|
// Set zero balance
|
||||||
k.SetFractionalBalance(ctx, addr, sdkmath.ZeroInt())
|
k.SetFractionalBalance(ctx, addr, sdkmath.ZeroInt())
|
||||||
|
|
||||||
_, exists = k.GetFractionalBalance(ctx, addr)
|
// Check balance was deleted
|
||||||
require.False(t, exists)
|
store := prefix.NewStore(ctx.KVStore(td.storeKey), types.FractionalBalancePrefix)
|
||||||
|
bz := store.Get(types.FractionalBalanceKey(addr))
|
||||||
|
require.Nil(t, bz)
|
||||||
|
|
||||||
// Set zero balance again on non-existent balance
|
// Set zero balance again on non-existent balance
|
||||||
require.NotPanics(
|
require.NotPanics(
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/cosmos/cosmos-sdk/codec"
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
evmtypes "github.com/evmos/ethermint/x/evm/types"
|
evmtypes "github.com/evmos/ethermint/x/evm/types"
|
||||||
|
|
||||||
"github.com/kava-labs/kava/x/precisebank/types"
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
@ -36,10 +37,6 @@ func NewKeeper(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func (k Keeper) BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error {
|
||||||
panic("unimplemented")
|
panic("unimplemented")
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package keeper_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||||
"github.com/cosmos/cosmos-sdk/testutil"
|
"github.com/cosmos/cosmos-sdk/testutil"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
@ -46,3 +47,7 @@ func NewMockedTestData(t *testing.T) testData {
|
|||||||
ak: ak,
|
ak: ak,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
|
||||||
|
func ci(denom string, amount sdkmath.Int) sdk.Coin { return sdk.NewCoin(denom, amount) }
|
||||||
|
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
|
||||||
|
138
x/precisebank/keeper/mint.go
Normal file
138
x/precisebank/keeper/mint.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
errorsmod "cosmossdk.io/errors"
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MintCoins creates new coins from thin air and adds it to the module account.
|
||||||
|
// If ExtendedCoinDenom is provided, the corresponding fractional amount is
|
||||||
|
// added to the module state.
|
||||||
|
// It will panic if the module account does not exist or is unauthorized.
|
||||||
|
func (k Keeper) MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error {
|
||||||
|
// Disallow minting to x/precisebank module
|
||||||
|
if moduleName == types.ModuleName {
|
||||||
|
panic(errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s cannot be minted to", moduleName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: MintingRestrictionFn is not used in x/precisebank
|
||||||
|
// Panic errors are identical to x/bank for consistency.
|
||||||
|
acc := k.ak.GetModuleAccount(ctx, moduleName)
|
||||||
|
if acc == nil {
|
||||||
|
panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", moduleName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !acc.HasPermission(authtypes.Minter) {
|
||||||
|
panic(errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s does not have permissions to mint tokens", moduleName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the coins are valid before minting
|
||||||
|
if !amt.IsValid() {
|
||||||
|
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get non-ExtendedCoinDenom coins
|
||||||
|
passthroughCoins := amt
|
||||||
|
|
||||||
|
extendedAmount := amt.AmountOf(types.ExtendedCoinDenom)
|
||||||
|
if extendedAmount.IsPositive() {
|
||||||
|
// Remove ExtendedCoinDenom from the coins as it is managed by x/precisebank
|
||||||
|
removeCoin := sdk.NewCoin(types.ExtendedCoinDenom, extendedAmount)
|
||||||
|
passthroughCoins = amt.Sub(removeCoin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coins unmanaged by x/precisebank are passed through to x/bank
|
||||||
|
if !passthroughCoins.Empty() {
|
||||||
|
if err := k.bk.MintCoins(ctx, moduleName, passthroughCoins); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more processing required if no ExtendedCoinDenom
|
||||||
|
if extendedAmount.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return k.mintExtendedCoin(ctx, moduleName, extendedAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mintExtendedCoin manages the minting of extended coins, and no other coins.
|
||||||
|
func (k Keeper) mintExtendedCoin(
|
||||||
|
ctx sdk.Context,
|
||||||
|
moduleName string,
|
||||||
|
amt sdkmath.Int,
|
||||||
|
) error {
|
||||||
|
moduleAddr := k.ak.GetModuleAddress(moduleName)
|
||||||
|
|
||||||
|
// Get current module account fractional balance - 0 if not found
|
||||||
|
fractionalAmount := k.GetFractionalBalance(ctx, moduleAddr)
|
||||||
|
|
||||||
|
// Get separated mint amounts
|
||||||
|
integerMintAmount := amt.Quo(types.ConversionFactor())
|
||||||
|
fractionalMintAmount := amt.Mod(types.ConversionFactor())
|
||||||
|
|
||||||
|
// Get new fractional balance after minting, this could be greater than
|
||||||
|
// the conversion factor and must be checked for carry over to integer mint
|
||||||
|
// amount as being set as-is may cause fractional balance exceeding max.
|
||||||
|
newFractionalBalance := fractionalAmount.Add(fractionalMintAmount)
|
||||||
|
|
||||||
|
// If it carries over, add 1 to integer mint amount. In this case, it will
|
||||||
|
// always be 1:
|
||||||
|
// fractional amounts x and y where both x and y < ConversionFactor
|
||||||
|
// x + y < (2 * ConversionFactor) - 2
|
||||||
|
// x + y < 1 integer amount + fractional amount
|
||||||
|
if newFractionalBalance.GTE(types.ConversionFactor()) {
|
||||||
|
// Carry over to integer mint amount
|
||||||
|
integerMintAmount = integerMintAmount.AddRaw(1)
|
||||||
|
// Subtract 1 integer equivalent amount of fractional balance. Same
|
||||||
|
// behavior as using .Mod() in this case.
|
||||||
|
newFractionalBalance = newFractionalBalance.Sub(types.ConversionFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mint new integer amounts in x/bank - including carry over from fractional
|
||||||
|
// amount if any.
|
||||||
|
if integerMintAmount.IsPositive() {
|
||||||
|
integerMintCoin := sdk.NewCoin(types.IntegerCoinDenom, integerMintAmount)
|
||||||
|
|
||||||
|
if err := k.bk.MintCoins(
|
||||||
|
ctx,
|
||||||
|
moduleName,
|
||||||
|
sdk.NewCoins(integerMintCoin),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign new fractional balance in x/precisebank
|
||||||
|
k.SetFractionalBalance(ctx, moduleAddr, newFractionalBalance)
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Update remainder & reserves to back minted fractional coins
|
||||||
|
prevRemainder := k.GetRemainderAmount(ctx)
|
||||||
|
// Deduct new remainder with minted fractional amount
|
||||||
|
newRemainder := prevRemainder.Sub(fractionalMintAmount)
|
||||||
|
|
||||||
|
if prevRemainder.LT(fractionalMintAmount) {
|
||||||
|
// Need additional 1 integer coin in reserve to back minted fractional
|
||||||
|
reserveMintCoins := sdk.NewCoins(sdk.NewCoin(types.IntegerCoinDenom, sdkmath.OneInt()))
|
||||||
|
if err := k.bk.MintCoins(ctx, types.ModuleName, reserveMintCoins); err != nil {
|
||||||
|
return fmt.Errorf("failed to mint %s for reserve: %w", reserveMintCoins, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update remainder with value of minted integer coin. newRemainder is
|
||||||
|
// currently negative at this point. This also means that it will always
|
||||||
|
// be < conversionFactor after this operation and not require a Mod().
|
||||||
|
newRemainder = newRemainder.Add(types.ConversionFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
k.SetRemainderAmount(ctx, newRemainder)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
359
x/precisebank/keeper/mint_integration_test.go
Normal file
359
x/precisebank/keeper/mint_integration_test.go
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||||
|
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||||
|
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||||
|
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/testutil"
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mintIntegrationTestSuite struct {
|
||||||
|
testutil.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *mintIntegrationTestSuite) SetupTest() {
|
||||||
|
suite.Suite.SetupTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMintIntegrationTest(t *testing.T) {
|
||||||
|
suite.Run(t, new(mintIntegrationTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *mintIntegrationTestSuite) TestBlockedRecipient() {
|
||||||
|
// Tests that sending funds to x/precisebank is disallowed.
|
||||||
|
// x/precisebank balance is used as the reserve funds and should not be
|
||||||
|
// directly interacted with by external modules or users.
|
||||||
|
msgServer := bankkeeper.NewMsgServerImpl(suite.BankKeeper)
|
||||||
|
|
||||||
|
fromAddr := sdk.AccAddress{1}
|
||||||
|
|
||||||
|
// To x/precisebank
|
||||||
|
toAddr := suite.AccountKeeper.GetModuleAddress(types.ModuleName)
|
||||||
|
amount := cs(c("ukava", 1000))
|
||||||
|
|
||||||
|
msg := banktypes.NewMsgSend(fromAddr, toAddr, amount)
|
||||||
|
|
||||||
|
_, err := msgServer.Send(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||||
|
suite.Require().Error(err)
|
||||||
|
|
||||||
|
suite.Require().EqualError(
|
||||||
|
err,
|
||||||
|
fmt.Sprintf("%s is not allowed to receive funds: unauthorized", toAddr.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *mintIntegrationTestSuite) TestMintCoins_MatchingErrors() {
|
||||||
|
// x/precisebank MintCoins should be identical to x/bank MintCoins to
|
||||||
|
// consumers. This test ensures that the panics & errors returned by
|
||||||
|
// x/precisebank are identical to x/bank.
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
recipientModule string
|
||||||
|
mintAmount sdk.Coins
|
||||||
|
wantErr string
|
||||||
|
wantPanic string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid module",
|
||||||
|
"notamodule",
|
||||||
|
cs(c("ukava", 1000)),
|
||||||
|
"",
|
||||||
|
"module account notamodule does not exist: unknown address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no mint permissions",
|
||||||
|
// Check app.go to ensure this module has no mint permissions
|
||||||
|
authtypes.FeeCollectorName,
|
||||||
|
cs(c("ukava", 1000)),
|
||||||
|
"",
|
||||||
|
"module account fee_collector does not have permissions to mint tokens: unauthorized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid amount",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdkmath.NewInt(-100)}},
|
||||||
|
"-100ukava: invalid coins",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
suite.Run(tt.name, func() {
|
||||||
|
// Reset
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
if tt.wantErr == "" && tt.wantPanic == "" {
|
||||||
|
suite.Fail("test must specify either wantErr or wantPanic")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
// Check x/bank MintCoins for identical error
|
||||||
|
bankErr := suite.BankKeeper.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
||||||
|
suite.Require().Error(bankErr)
|
||||||
|
suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank MintCoins error")
|
||||||
|
|
||||||
|
pbankErr := suite.Keeper.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
||||||
|
suite.Require().Error(pbankErr)
|
||||||
|
// Compare strings instead of errors, as error stack is still different
|
||||||
|
suite.Require().Equal(
|
||||||
|
bankErr.Error(),
|
||||||
|
pbankErr.Error(),
|
||||||
|
"x/precisebank error should match x/bank MintCoins error",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantPanic != "" {
|
||||||
|
// First check the wantPanic string is correct.
|
||||||
|
// Actually specify the panic string in the test since it makes
|
||||||
|
// it more clear we are testing specific and different cases.
|
||||||
|
suite.Require().PanicsWithError(tt.wantPanic, func() {
|
||||||
|
_ = suite.BankKeeper.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
||||||
|
}, "expected panic error should match x/bank MintCoins")
|
||||||
|
|
||||||
|
suite.Require().PanicsWithError(tt.wantPanic, func() {
|
||||||
|
_ = suite.Keeper.MintCoins(suite.Ctx, tt.recipientModule, tt.mintAmount)
|
||||||
|
}, "x/precisebank panic should match x/bank MintCoins")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *mintIntegrationTestSuite) TestMintCoins() {
|
||||||
|
type mintTest struct {
|
||||||
|
mintAmount sdk.Coins
|
||||||
|
// Expected **full** balances after MintCoins(mintAmount)
|
||||||
|
wantBalance sdk.Coins
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
recipientModule string
|
||||||
|
// Instead of having a start balance, we just have a list of mints to
|
||||||
|
// both test & get into desired non-default states.
|
||||||
|
mints []mintTest
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"passthrough - unrelated",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
[]mintTest{
|
||||||
|
{
|
||||||
|
mintAmount: cs(c("busd", 1000)),
|
||||||
|
wantBalance: cs(c("busd", 1000)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passthrough - integer denom",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
[]mintTest{
|
||||||
|
{
|
||||||
|
mintAmount: cs(c(types.IntegerCoinDenom, 1000)),
|
||||||
|
wantBalance: cs(c(types.ExtendedCoinDenom, 1000000000000000)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fractional only",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
[]mintTest{
|
||||||
|
{
|
||||||
|
mintAmount: cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
wantBalance: cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mintAmount: cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
wantBalance: cs(c(types.ExtendedCoinDenom, 2000)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"exact carry",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
[]mintTest{
|
||||||
|
{
|
||||||
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
||||||
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
||||||
|
},
|
||||||
|
// Carry again - exact amount
|
||||||
|
{
|
||||||
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
|
||||||
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2))),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"carry with extra",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
[]mintTest{
|
||||||
|
// MintCoins(C + 100)
|
||||||
|
{
|
||||||
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(100))),
|
||||||
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(100))),
|
||||||
|
},
|
||||||
|
// MintCoins(C + 5), total = 2C + 105
|
||||||
|
{
|
||||||
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().AddRaw(5))),
|
||||||
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2).AddRaw(105))),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"integer with fractional",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
[]mintTest{
|
||||||
|
{
|
||||||
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100))),
|
||||||
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mintAmount: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2).AddRaw(5))),
|
||||||
|
wantBalance: cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(7).AddRaw(105))),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"with passthrough",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
[]mintTest{
|
||||||
|
{
|
||||||
|
mintAmount: cs(
|
||||||
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100)),
|
||||||
|
c("busd", 1000),
|
||||||
|
),
|
||||||
|
wantBalance: cs(
|
||||||
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5).AddRaw(100)),
|
||||||
|
c("busd", 1000),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mintAmount: cs(
|
||||||
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(2).AddRaw(5)),
|
||||||
|
c("meow", 40),
|
||||||
|
),
|
||||||
|
wantBalance: cs(
|
||||||
|
ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(7).AddRaw(105)),
|
||||||
|
c("busd", 1000),
|
||||||
|
c("meow", 40),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
suite.Run(tt.name, func() {
|
||||||
|
// Reset
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
recipientAddr := suite.AccountKeeper.GetModuleAddress(tt.recipientModule)
|
||||||
|
|
||||||
|
for _, mt := range tt.mints {
|
||||||
|
err := suite.Keeper.MintCoins(suite.Ctx, tt.recipientModule, mt.mintAmount)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// Check FULL balances
|
||||||
|
// x/bank balances + x/precisebank balance
|
||||||
|
// Exclude "ukava" as x/precisebank balance will include it
|
||||||
|
bankCoins := suite.BankKeeper.GetAllBalances(suite.Ctx, recipientAddr)
|
||||||
|
|
||||||
|
// Only use x/bank balances for non-ukava denoms
|
||||||
|
var denoms []string
|
||||||
|
for _, coin := range bankCoins {
|
||||||
|
// Ignore integer coins, query the extended denom instead
|
||||||
|
if coin.Denom == types.IntegerCoinDenom {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
denoms = append(denoms, coin.Denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the extended denom to the list of denoms to balance check
|
||||||
|
// Will be included in balance check even if x/bank doesn't have
|
||||||
|
// ukava.
|
||||||
|
denoms = append(denoms, types.ExtendedCoinDenom)
|
||||||
|
|
||||||
|
// All balance queries through x/precisebank
|
||||||
|
afterBalance := sdk.NewCoins()
|
||||||
|
for _, denom := range denoms {
|
||||||
|
coin := suite.Keeper.GetBalance(suite.Ctx, recipientAddr, denom)
|
||||||
|
afterBalance = afterBalance.Add(coin)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Require().Equal(
|
||||||
|
mt.wantBalance.String(),
|
||||||
|
afterBalance.String(),
|
||||||
|
"unexpected balance after minting %s to %s",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure reserve is backing all minted fractions
|
||||||
|
allInvariantsFn := keeper.AllInvariants(suite.Keeper)
|
||||||
|
res, stop := allInvariantsFn(suite.Ctx)
|
||||||
|
suite.Require().False(stop, "invariant should not be broken")
|
||||||
|
suite.Require().Empty(res, "unexpected invariant message: %s", res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzMintCoins(f *testing.F) {
|
||||||
|
f.Add(int64(0))
|
||||||
|
f.Add(int64(100))
|
||||||
|
f.Add(types.ConversionFactor().Int64())
|
||||||
|
f.Add(types.ConversionFactor().MulRaw(5).Int64())
|
||||||
|
f.Add(types.ConversionFactor().MulRaw(2).AddRaw(123948723).Int64())
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, amount int64) {
|
||||||
|
// No negative amounts
|
||||||
|
if amount < 0 {
|
||||||
|
amount = -amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually setup test suite since no direct Fuzz support in test suites
|
||||||
|
suite := new(mintIntegrationTestSuite)
|
||||||
|
suite.SetT(t)
|
||||||
|
suite.SetS(suite)
|
||||||
|
suite.SetupTest()
|
||||||
|
|
||||||
|
mintCount := int64(10)
|
||||||
|
|
||||||
|
// Mint 10 times to include mints from non-zero balances
|
||||||
|
for i := int64(0); i < mintCount; i++ {
|
||||||
|
err := suite.Keeper.MintCoins(
|
||||||
|
suite.Ctx,
|
||||||
|
minttypes.ModuleName,
|
||||||
|
cs(c(types.ExtendedCoinDenom, amount)),
|
||||||
|
)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check FULL balances
|
||||||
|
recipientAddr := suite.AccountKeeper.GetModuleAddress(minttypes.ModuleName)
|
||||||
|
bal := suite.Keeper.GetBalance(suite.Ctx, recipientAddr, types.ExtendedCoinDenom)
|
||||||
|
|
||||||
|
suite.Require().Equalf(
|
||||||
|
amount*mintCount,
|
||||||
|
bal.Amount.Int64(),
|
||||||
|
"unexpected balance after minting %d 5 times",
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run Invariants to ensure remainder is backing all minted fractions
|
||||||
|
// and in a valid state
|
||||||
|
allInvariantsFn := keeper.AllInvariants(suite.Keeper)
|
||||||
|
res, stop := allInvariantsFn(suite.Ctx)
|
||||||
|
suite.Require().False(stop, "invariant should not be broken")
|
||||||
|
suite.Require().Empty(res, "unexpected invariant message: %s", res)
|
||||||
|
})
|
||||||
|
}
|
347
x/precisebank/keeper/mint_test.go
Normal file
347
x/precisebank/keeper/mint_test.go
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
sdkmath "cosmossdk.io/math"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||||
|
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/precisebank/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMintCoins_PanicValidations(t *testing.T) {
|
||||||
|
// panic tests for invalid inputs
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
recipientModule string
|
||||||
|
setupFn func(td testData)
|
||||||
|
mintAmount sdk.Coins
|
||||||
|
wantPanic string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid module",
|
||||||
|
"notamodule",
|
||||||
|
func(td testData) {
|
||||||
|
// Make module not found
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAccount(td.ctx, "notamodule").
|
||||||
|
Return(nil).
|
||||||
|
Once()
|
||||||
|
},
|
||||||
|
cs(c("ukava", 1000)),
|
||||||
|
"module account notamodule does not exist: unknown address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no permission",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
func(td testData) {
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAccount(td.ctx, minttypes.ModuleName).
|
||||||
|
Return(authtypes.NewModuleAccount(
|
||||||
|
authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
|
||||||
|
minttypes.ModuleName,
|
||||||
|
// no mint permission
|
||||||
|
)).
|
||||||
|
Once()
|
||||||
|
},
|
||||||
|
cs(c("ukava", 1000)),
|
||||||
|
"module account mint does not have permissions to mint tokens: unauthorized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"has mint permission",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
func(td testData) {
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAccount(td.ctx, minttypes.ModuleName).
|
||||||
|
Return(authtypes.NewModuleAccount(
|
||||||
|
authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
|
||||||
|
minttypes.ModuleName,
|
||||||
|
// includes minter permission
|
||||||
|
authtypes.Minter,
|
||||||
|
)).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
// Will call x/bank MintCoins coins
|
||||||
|
td.bk.EXPECT().
|
||||||
|
MintCoins(td.ctx, minttypes.ModuleName, cs(c("ukava", 1000))).
|
||||||
|
Return(nil).
|
||||||
|
Once()
|
||||||
|
},
|
||||||
|
cs(c("ukava", 1000)),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"disallow minting to x/precisebank",
|
||||||
|
types.ModuleName,
|
||||||
|
func(td testData) {
|
||||||
|
// No mock setup needed since this is checked before module
|
||||||
|
// account checks
|
||||||
|
},
|
||||||
|
cs(c("ukava", 1000)),
|
||||||
|
"module account precisebank cannot be minted to: unauthorized",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
td := NewMockedTestData(t)
|
||||||
|
tt.setupFn(td)
|
||||||
|
|
||||||
|
if tt.wantPanic != "" {
|
||||||
|
require.PanicsWithError(t, tt.wantPanic, func() {
|
||||||
|
_ = td.keeper.MintCoins(td.ctx, tt.recipientModule, tt.mintAmount)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
// Not testing errors, only panics for this test
|
||||||
|
_ = td.keeper.MintCoins(td.ctx, tt.recipientModule, tt.mintAmount)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMintCoins_Errors(t *testing.T) {
|
||||||
|
// returned errors, not panics
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
recipientModule string
|
||||||
|
setupFn func(td testData)
|
||||||
|
mintAmount sdk.Coins
|
||||||
|
wantError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid coins",
|
||||||
|
minttypes.ModuleName,
|
||||||
|
func(td testData) {
|
||||||
|
// Valid module account minter
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAccount(td.ctx, minttypes.ModuleName).
|
||||||
|
Return(authtypes.NewModuleAccount(
|
||||||
|
authtypes.NewBaseAccountWithAddress(sdk.AccAddress{1}),
|
||||||
|
minttypes.ModuleName,
|
||||||
|
// includes minter permission
|
||||||
|
authtypes.Minter,
|
||||||
|
)).
|
||||||
|
Once()
|
||||||
|
},
|
||||||
|
sdk.Coins{sdk.Coin{
|
||||||
|
Denom: "ukava",
|
||||||
|
Amount: sdk.NewInt(-1000),
|
||||||
|
}},
|
||||||
|
"-1000ukava: invalid coins",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
td := NewMockedTestData(t)
|
||||||
|
tt.setupFn(td)
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
err := td.keeper.MintCoins(td.ctx, tt.recipientModule, tt.mintAmount)
|
||||||
|
|
||||||
|
if tt.wantError != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tt.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMintCoins_ExpectedCalls(t *testing.T) {
|
||||||
|
// Tests the expected calls to the bank keeper when minting coins
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
// Only care about starting fractional balance.
|
||||||
|
// MintCoins() doesn't care about the previous integer balance.
|
||||||
|
startFractionalBalance sdkmath.Int
|
||||||
|
mintAmount sdk.Coins
|
||||||
|
// account x/precisebank balance (fractional amount)
|
||||||
|
wantPreciseBalance sdkmath.Int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"passthrough mint - integer denom",
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
cs(c("ukava", 1000)),
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"passthrough mint - unrelated denom",
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
cs(c("meow", 1000)),
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no carry - 0 starting fractional",
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
sdkmath.NewInt(1000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no carry - non-zero fractional",
|
||||||
|
sdkmath.NewInt(1_000_000),
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1000)),
|
||||||
|
sdkmath.NewInt(1_001_000),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fractional carry",
|
||||||
|
// max fractional amount
|
||||||
|
types.ConversionFactor().SubRaw(1),
|
||||||
|
cs(c(types.ExtendedCoinDenom, 1)), // +1 to carry
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fractional carry max",
|
||||||
|
// max fractional amount + max fractional amount
|
||||||
|
types.ConversionFactor().SubRaw(1),
|
||||||
|
cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
|
||||||
|
types.ConversionFactor().SubRaw(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"integer with fractional no carry",
|
||||||
|
sdkmath.NewInt(1234),
|
||||||
|
// mint 100 fractional
|
||||||
|
cs(c(types.ExtendedCoinDenom, 100)),
|
||||||
|
sdkmath.NewInt(1234 + 100),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"integer with fractional carry",
|
||||||
|
types.ConversionFactor().SubRaw(100),
|
||||||
|
// mint 105 fractional to carry
|
||||||
|
cs(c(types.ExtendedCoinDenom, 105)),
|
||||||
|
sdkmath.NewInt(5),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
td := NewMockedTestData(t)
|
||||||
|
|
||||||
|
// Set initial fractional balance
|
||||||
|
// Initial integer balance doesn't matter for this test
|
||||||
|
moduleAddr := sdk.AccAddress{1}
|
||||||
|
td.keeper.SetFractionalBalance(
|
||||||
|
td.ctx,
|
||||||
|
moduleAddr,
|
||||||
|
tt.startFractionalBalance,
|
||||||
|
)
|
||||||
|
fBal := td.keeper.GetFractionalBalance(td.ctx, moduleAddr)
|
||||||
|
require.Equal(t, tt.startFractionalBalance, fBal)
|
||||||
|
|
||||||
|
// Always calls GetModuleAccount() to check if module exists &
|
||||||
|
// has permission
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAccount(td.ctx, minttypes.ModuleName).
|
||||||
|
Return(authtypes.NewModuleAccount(
|
||||||
|
authtypes.NewBaseAccountWithAddress(
|
||||||
|
moduleAddr,
|
||||||
|
),
|
||||||
|
minttypes.ModuleName,
|
||||||
|
// Include minter permissions - not testing permission in
|
||||||
|
// this test
|
||||||
|
authtypes.Minter,
|
||||||
|
)).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Separate passthrough and extended coins
|
||||||
|
// Determine how much is passed through to x/bank
|
||||||
|
passthroughCoins := tt.mintAmount
|
||||||
|
|
||||||
|
found, extCoins := tt.mintAmount.Find(types.ExtendedCoinDenom)
|
||||||
|
if found {
|
||||||
|
// Remove extended coin from passthrough coins
|
||||||
|
passthroughCoins = passthroughCoins.Sub(extCoins)
|
||||||
|
} else {
|
||||||
|
extCoins = sdk.NewCoin(types.ExtendedCoinDenom, sdkmath.ZeroInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equalf(
|
||||||
|
t,
|
||||||
|
sdkmath.ZeroInt(),
|
||||||
|
passthroughCoins.AmountOf(types.ExtendedCoinDenom),
|
||||||
|
"expected pass through coins should not include %v",
|
||||||
|
types.ExtendedCoinDenom,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Set expectations for minting passthrough coins
|
||||||
|
// Only expect MintCoins to be called with passthrough coins with non-zero amount
|
||||||
|
if !passthroughCoins.Empty() {
|
||||||
|
t.Logf("Expecting MintCoins(%v)", passthroughCoins)
|
||||||
|
|
||||||
|
td.bk.EXPECT().
|
||||||
|
MintCoins(td.ctx, minttypes.ModuleName, passthroughCoins).
|
||||||
|
Return(nil).
|
||||||
|
Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Set expectations for minting fractional coins
|
||||||
|
if !extCoins.IsNil() && extCoins.IsPositive() {
|
||||||
|
td.ak.EXPECT().
|
||||||
|
GetModuleAddress(minttypes.ModuleName).
|
||||||
|
Return(moduleAddr).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
// Initial integer balance is always 0 for this test
|
||||||
|
totalNewBalance := tt.startFractionalBalance.Add(extCoins.Amount)
|
||||||
|
mintIntegerAmount := totalNewBalance.Quo(types.ConversionFactor())
|
||||||
|
|
||||||
|
mintCoins := cs(ci(types.IntegerCoinDenom, mintIntegerAmount))
|
||||||
|
|
||||||
|
// Only expect MintCoins to be called with mint coins with
|
||||||
|
// non-zero amount.
|
||||||
|
// Will fail if x/bank MintCoins is called with empty coins
|
||||||
|
if !mintCoins.Empty() {
|
||||||
|
t.Logf("Expecting MintCoins(%v)", mintCoins)
|
||||||
|
|
||||||
|
td.bk.EXPECT().
|
||||||
|
MintCoins(td.ctx, minttypes.ModuleName, mintCoins).
|
||||||
|
Return(nil).
|
||||||
|
Once()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Set expectations for reserve minting when fractional amounts
|
||||||
|
// are minted & remainder is insufficient
|
||||||
|
mintFractionalAmount := extCoins.Amount.Mod(types.ConversionFactor())
|
||||||
|
currentRemainder := td.keeper.GetRemainderAmount(td.ctx)
|
||||||
|
|
||||||
|
remainderEnough := currentRemainder.GTE(mintFractionalAmount)
|
||||||
|
if !remainderEnough {
|
||||||
|
reserveMintCoins := cs(ci(types.IntegerCoinDenom, sdkmath.OneInt()))
|
||||||
|
td.bk.EXPECT().
|
||||||
|
// Mints to x/precisebank
|
||||||
|
MintCoins(td.ctx, types.ModuleName, reserveMintCoins).
|
||||||
|
Return(nil).
|
||||||
|
Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Actual call after all setup and expectations
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
err := td.keeper.MintCoins(td.ctx, minttypes.ModuleName, tt.mintAmount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check final fractional balance
|
||||||
|
fBal = td.keeper.GetFractionalBalance(td.ctx, moduleAddr)
|
||||||
|
require.Equal(t, tt.wantPreciseBalance, fBal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -23,10 +23,7 @@ func (k Keeper) GetBalance(
|
|||||||
integerAmount := spendableCoins.AmountOf(types.IntegerCoinDenom)
|
integerAmount := spendableCoins.AmountOf(types.IntegerCoinDenom)
|
||||||
|
|
||||||
// x/precisebank for fractional balance
|
// x/precisebank for fractional balance
|
||||||
fractionalAmount, found := k.GetFractionalBalance(ctx, addr)
|
fractionalAmount := k.GetFractionalBalance(ctx, addr)
|
||||||
if !found {
|
|
||||||
fractionalAmount = sdk.ZeroInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// (Integer * ConversionFactor) + Fractional
|
// (Integer * ConversionFactor) + Fractional
|
||||||
fullAmount := integerAmount.
|
fullAmount := integerAmount.
|
||||||
|
@ -135,7 +135,7 @@ func GenerateEqualFractionalBalancesWithRemainder(
|
|||||||
) (types.FractionalBalances, sdkmath.Int) {
|
) (types.FractionalBalances, sdkmath.Int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
require.GreaterOrEqual(t, count, 3, "count must be at least 3 to generate both balances and remainder")
|
require.GreaterOrEqual(t, count, 2, "count must be at least 2 to generate both balances and remainder")
|
||||||
|
|
||||||
countWithRemainder := count + 1
|
countWithRemainder := count + 1
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user