mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-10-31 18:27:26 +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
	 drklee3
						drklee3