mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 03:17:27 +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:
 | 
			
		||||
	@$(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.
 | 
			
		||||
# 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:
 | 
			
		||||
@ -347,4 +357,4 @@ update-kvtool:
 | 
			
		||||
	git submodule update
 | 
			
		||||
	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(
 | 
			
		||||
	ctx sdk.Context,
 | 
			
		||||
	address sdk.AccAddress,
 | 
			
		||||
) (sdkmath.Int, bool) {
 | 
			
		||||
) sdkmath.Int {
 | 
			
		||||
	store := prefix.NewStore(ctx.KVStore(k.storeKey), types.FractionalBalancePrefix)
 | 
			
		||||
 | 
			
		||||
	bz := store.Get(types.FractionalBalanceKey(address))
 | 
			
		||||
	if bz == nil {
 | 
			
		||||
		return sdkmath.ZeroInt(), false
 | 
			
		||||
		return sdkmath.ZeroInt()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var bal sdkmath.Int
 | 
			
		||||
@ -28,7 +28,7 @@ func (k *Keeper) GetFractionalBalance(
 | 
			
		||||
		panic(fmt.Errorf("failed to unmarshal fractional balance: %w", err))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bal, true
 | 
			
		||||
	return bal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetFractionalBalance sets the fractional balance for an address.
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	sdkmath "cosmossdk.io/math"
 | 
			
		||||
	"github.com/cosmos/cosmos-sdk/store/prefix"
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
 | 
			
		||||
@ -11,9 +12,6 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSetGetFractionalBalance(t *testing.T) {
 | 
			
		||||
	tk := NewMockedTestData(t)
 | 
			
		||||
	ctx, k := tk.ctx, tk.keeper
 | 
			
		||||
 | 
			
		||||
	addr := sdk.AccAddress([]byte("test-address"))
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
@ -63,6 +61,9 @@ func TestSetGetFractionalBalance(t *testing.T) {
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		tt := tt
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			td := NewMockedTestData(t)
 | 
			
		||||
			ctx, k := td.ctx, td.keeper
 | 
			
		||||
 | 
			
		||||
			if tt.setPanicMsg != "" {
 | 
			
		||||
				require.PanicsWithError(t, tt.setPanicMsg, func() {
 | 
			
		||||
					k.SetFractionalBalance(ctx, tt.address, tt.amount)
 | 
			
		||||
@ -75,23 +76,24 @@ func TestSetGetFractionalBalance(t *testing.T) {
 | 
			
		||||
				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() {
 | 
			
		||||
				_, exists := k.GetFractionalBalance(ctx, tt.address)
 | 
			
		||||
				require.False(t, exists)
 | 
			
		||||
				store := prefix.NewStore(ctx.KVStore(td.storeKey), types.FractionalBalancePrefix)
 | 
			
		||||
				bz := store.Get(types.FractionalBalanceKey(tt.address))
 | 
			
		||||
				require.Nil(t, bz)
 | 
			
		||||
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			gotAmount, exists := k.GetFractionalBalance(ctx, tt.address)
 | 
			
		||||
			require.True(t, exists)
 | 
			
		||||
			gotAmount := k.GetFractionalBalance(ctx, tt.address)
 | 
			
		||||
			require.Equal(t, tt.amount, gotAmount)
 | 
			
		||||
 | 
			
		||||
			// Delete balance
 | 
			
		||||
			k.DeleteFractionalBalance(ctx, tt.address)
 | 
			
		||||
 | 
			
		||||
			_, exists = k.GetFractionalBalance(ctx, tt.address)
 | 
			
		||||
			require.False(t, exists)
 | 
			
		||||
			store := prefix.NewStore(ctx.KVStore(td.storeKey), types.FractionalBalancePrefix)
 | 
			
		||||
			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) {
 | 
			
		||||
	tk := NewMockedTestData(t)
 | 
			
		||||
	ctx, k := tk.ctx, tk.keeper
 | 
			
		||||
	td := NewMockedTestData(t)
 | 
			
		||||
	ctx, k := td.ctx, td.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)
 | 
			
		||||
	bal := k.GetFractionalBalance(ctx, addr)
 | 
			
		||||
	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)
 | 
			
		||||
	// Check balance was deleted
 | 
			
		||||
	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
 | 
			
		||||
	require.NotPanics(
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ 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"
 | 
			
		||||
@ -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 {
 | 
			
		||||
	panic("unimplemented")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ package keeper_test
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	sdkmath "cosmossdk.io/math"
 | 
			
		||||
	storetypes "github.com/cosmos/cosmos-sdk/store/types"
 | 
			
		||||
	"github.com/cosmos/cosmos-sdk/testutil"
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
@ -46,3 +47,7 @@ func NewMockedTestData(t *testing.T) testData {
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
	// x/precisebank for fractional balance
 | 
			
		||||
	fractionalAmount, found := k.GetFractionalBalance(ctx, addr)
 | 
			
		||||
	if !found {
 | 
			
		||||
		fractionalAmount = sdk.ZeroInt()
 | 
			
		||||
	}
 | 
			
		||||
	fractionalAmount := k.GetFractionalBalance(ctx, addr)
 | 
			
		||||
 | 
			
		||||
	// (Integer * ConversionFactor) + Fractional
 | 
			
		||||
	fullAmount := integerAmount.
 | 
			
		||||
 | 
			
		||||
@ -135,7 +135,7 @@ func GenerateEqualFractionalBalancesWithRemainder(
 | 
			
		||||
) (types.FractionalBalances, sdkmath.Int) {
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user