mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-04-04 15:55:23 +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
	 drklee3
						drklee3