mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 09:17:28 +00:00 
			
		
		
		
	feat(x/precisebank): Implement SendCoins (#1923)
Implements methods SendCoins, SendCoinsFromModuleToAccount, SendCoinsFromAccountToModule
This commit is contained in:
		
							parent
							
								
									4c3f6533a0
								
							
						
					
					
						commit
						409841c79c
					
				
							
								
								
									
										15
									
								
								.github/workflows/ci-default.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/ci-default.yml
									
									
									
									
										vendored
									
									
								
							@ -35,6 +35,21 @@ jobs:
 | 
			
		||||
        run: make test
 | 
			
		||||
      - name: run e2e tests
 | 
			
		||||
        run: make docker-build test-e2e
 | 
			
		||||
  fuzz:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: checkout repo from current commit
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: true
 | 
			
		||||
      - name: Set up Go
 | 
			
		||||
        uses: actions/setup-go@v4
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          cache-dependency-path: |
 | 
			
		||||
            go.sum
 | 
			
		||||
      - name: run fuzz tests
 | 
			
		||||
        run: make test-fuzz
 | 
			
		||||
  ibc-test:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Makefile
									
									
									
									
									
								
							@ -334,6 +334,7 @@ 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=FuzzSendCoins ./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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -142,6 +142,10 @@ func (tApp TestApp) GetKVStoreKey(key string) *storetypes.KVStoreKey {
 | 
			
		||||
	return tApp.keys[key]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (tApp TestApp) GetBlockedMaccAddrs() map[string]bool {
 | 
			
		||||
	return tApp.loadBlockedMaccAddrs()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LegacyAmino returns the app's amino codec.
 | 
			
		||||
func (app *App) LegacyAmino() *codec.LegacyAmino {
 | 
			
		||||
	return app.legacyAmino
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,29 @@
 | 
			
		||||
package keeper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	errorsmod "cosmossdk.io/errors"
 | 
			
		||||
	sdkmath "cosmossdk.io/math"
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 | 
			
		||||
	"github.com/kava-labs/kava/x/precisebank/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// IsSendEnabledCoins uses the parent x/bank keeper to check the coins provided
 | 
			
		||||
// and returns an ErrSendDisabled if any of the coins are not configured for
 | 
			
		||||
// sending. Returns nil if sending is enabled for all provided coin
 | 
			
		||||
func (k Keeper) IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error {
 | 
			
		||||
	panic("unimplemented")
 | 
			
		||||
	// TODO: This does not actually seem to be used by x/evm, so it should be
 | 
			
		||||
	// removed from the expected_interface in x/evm.
 | 
			
		||||
	return k.bk.IsSendEnabledCoins(ctx, coins...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendCoins transfers amt coins from a sending account to a receiving account.
 | 
			
		||||
// An error is returned upon failure. This handles transfers including
 | 
			
		||||
// ExtendedCoinDenom and supports non-ExtendedCoinDenom transfers by passing
 | 
			
		||||
// through to x/bank.
 | 
			
		||||
func (k Keeper) SendCoins(
 | 
			
		||||
	ctx sdk.Context,
 | 
			
		||||
	from, to sdk.AccAddress,
 | 
			
		||||
@ -19,23 +32,298 @@ func (k Keeper) SendCoins(
 | 
			
		||||
	// IsSendEnabledCoins() is only used in x/bank in msg server, not in keeper,
 | 
			
		||||
	// so we should also not use it here to align with x/bank behavior.
 | 
			
		||||
 | 
			
		||||
	panic("unimplemented")
 | 
			
		||||
	if !amt.IsValid() {
 | 
			
		||||
		return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	passthroughCoins := amt
 | 
			
		||||
	extendedCoinAmount := amt.AmountOf(types.ExtendedCoinDenom)
 | 
			
		||||
 | 
			
		||||
	// Remove the extended coin amount from the passthrough coins
 | 
			
		||||
	if extendedCoinAmount.IsPositive() {
 | 
			
		||||
		subCoin := sdk.NewCoin(types.ExtendedCoinDenom, extendedCoinAmount)
 | 
			
		||||
		passthroughCoins = amt.Sub(subCoin)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Send the passthrough coins through x/bank
 | 
			
		||||
	if passthroughCoins.IsAllPositive() {
 | 
			
		||||
		if err := k.bk.SendCoins(ctx, from, to, passthroughCoins); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If there is no extended coin amount, we are done
 | 
			
		||||
	if extendedCoinAmount.IsZero() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Send the extended coin amount through x/precisebank
 | 
			
		||||
	return k.sendExtendedCoins(ctx, from, to, extendedCoinAmount)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendExtendedCoins transfers amt extended coins from a sending account to a
 | 
			
		||||
// receiving account. An error is returned upon failure. This function is
 | 
			
		||||
// called by SendCoins() and should not be called directly.
 | 
			
		||||
//
 | 
			
		||||
// This method covers 4 cases between two accounts - sender and receiver.
 | 
			
		||||
// Depending on the fractional balance and the amount being transferred:
 | 
			
		||||
// Sender account:
 | 
			
		||||
//  1. Arithmetic borrow 1 integer equivalent amount of fractional coins if the
 | 
			
		||||
//     fractional balance is insufficient.
 | 
			
		||||
//  2. No borrow if fractional balance is sufficient.
 | 
			
		||||
//
 | 
			
		||||
// Receiver account:
 | 
			
		||||
//  1. Arithmetic carry 1 integer equivalent amount of fractional coins if the
 | 
			
		||||
//     received amount exceeds max fractional balance.
 | 
			
		||||
//  2. No carry if received amount does not exceed max fractional balance.
 | 
			
		||||
//
 | 
			
		||||
// The 4 cases are:
 | 
			
		||||
// 1. Sender borrow, receiver carry
 | 
			
		||||
// 2. Sender borrow, NO receiver carry
 | 
			
		||||
// 3. NO sender borrow, receiver carry
 | 
			
		||||
// 4. NO sender borrow, NO receiver carry
 | 
			
		||||
//
 | 
			
		||||
// Truth table:
 | 
			
		||||
// | Sender Borrow | Receiver Carry | Direct Transfer |
 | 
			
		||||
// | --------------|----------------|-----------------|
 | 
			
		||||
// | T             | T              | T               |
 | 
			
		||||
// | T             | F              | F               |
 | 
			
		||||
// | F             | T              | F               |
 | 
			
		||||
// | F             | F              | F               |
 | 
			
		||||
func (k Keeper) sendExtendedCoins(
 | 
			
		||||
	ctx sdk.Context,
 | 
			
		||||
	from, to sdk.AccAddress,
 | 
			
		||||
	amt sdkmath.Int,
 | 
			
		||||
) error {
 | 
			
		||||
	// Sufficient balance check is done by bankkeeper.SendCoins(), for both
 | 
			
		||||
	// integer and fractional-only sends. E.g. If fractional balance is
 | 
			
		||||
	// insufficient, it will still incur a integer borrow which will fail if the
 | 
			
		||||
	// sender does not have sufficient integer balance.
 | 
			
		||||
 | 
			
		||||
	// Load required state: Account old balances
 | 
			
		||||
	senderFracBal := k.GetFractionalBalance(ctx, from)
 | 
			
		||||
	recipientFracBal := k.GetFractionalBalance(ctx, to)
 | 
			
		||||
 | 
			
		||||
	// -------------------------------------------------------------------------
 | 
			
		||||
	// Pure stateless calculations
 | 
			
		||||
	integerAmt := amt.Quo(types.ConversionFactor())
 | 
			
		||||
	fractionalAmt := amt.Mod(types.ConversionFactor())
 | 
			
		||||
 | 
			
		||||
	// Account new fractional balances
 | 
			
		||||
	senderNewFracBal, senderNeedsBorrow := subFromFractionalBalance(senderFracBal, fractionalAmt)
 | 
			
		||||
	recipientNewFracBal, recipientNeedsCarry := addToFractionalBalance(recipientFracBal, fractionalAmt)
 | 
			
		||||
 | 
			
		||||
	// Case #1: Sender borrow, recipient carry
 | 
			
		||||
	if senderNeedsBorrow && recipientNeedsCarry {
 | 
			
		||||
		// Can directly transfer borrow/carry - increase the direct transfer by 1
 | 
			
		||||
		integerAmt = integerAmt.AddRaw(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// -------------------------------------------------------------------------
 | 
			
		||||
	// Stateful operations for transfers
 | 
			
		||||
 | 
			
		||||
	// This includes ALL transfers of >= conversionFactor AND Case #1
 | 
			
		||||
	// Full integer amount transfer, including direct transfer of borrow/carry
 | 
			
		||||
	// if any.
 | 
			
		||||
	if integerAmt.IsPositive() {
 | 
			
		||||
		transferCoin := sdk.NewCoin(types.IntegerCoinDenom, integerAmt)
 | 
			
		||||
		if err := k.bk.SendCoins(ctx, from, to, sdk.NewCoins(transferCoin)); err != nil {
 | 
			
		||||
			return k.updateInsufficientFundsError(ctx, from, amt, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Case #2: Sender borrow, NO recipient carry
 | 
			
		||||
	// Sender borrows by transferring 1 integer amount to reserve to account for
 | 
			
		||||
	// lack of fractional balance.
 | 
			
		||||
	if senderNeedsBorrow && !recipientNeedsCarry {
 | 
			
		||||
		borrowCoin := sdk.NewCoin(types.IntegerCoinDenom, sdk.NewInt(1))
 | 
			
		||||
		if err := k.bk.SendCoinsFromAccountToModule(
 | 
			
		||||
			ctx,
 | 
			
		||||
			from, // sender borrowing
 | 
			
		||||
			types.ModuleName,
 | 
			
		||||
			sdk.NewCoins(borrowCoin),
 | 
			
		||||
		); err != nil {
 | 
			
		||||
			return k.updateInsufficientFundsError(ctx, from, amt, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Case #3: NO sender borrow, recipient carry.
 | 
			
		||||
	// Recipient's fractional balance carries over to integer balance by 1.
 | 
			
		||||
	// Always send carry from reserve before receiving borrow from sender to
 | 
			
		||||
	// ensure reserve always has sufficient balance starting from 0.
 | 
			
		||||
	if !senderNeedsBorrow && recipientNeedsCarry {
 | 
			
		||||
		carryCoin := sdk.NewCoin(types.IntegerCoinDenom, sdk.NewInt(1))
 | 
			
		||||
		if err := k.bk.SendCoinsFromModuleToAccount(
 | 
			
		||||
			ctx,
 | 
			
		||||
			types.ModuleName,
 | 
			
		||||
			to, // recipient carrying
 | 
			
		||||
			sdk.NewCoins(carryCoin),
 | 
			
		||||
		); err != nil {
 | 
			
		||||
			// Panic instead of returning error, as this will only error
 | 
			
		||||
			// with invalid state or logic. Reserve should always have
 | 
			
		||||
			// sufficient balance to carry fractional coins.
 | 
			
		||||
			panic(fmt.Errorf("failed to carry fractional coins to %s: %w", to, err))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Case #4: NO sender borrow, NO recipient carry
 | 
			
		||||
	// No additional operations required, as the transfer of fractional coins
 | 
			
		||||
	// does not incur any integer borrow or carry. New fractional balances
 | 
			
		||||
	// already calculated and just need to be set.
 | 
			
		||||
 | 
			
		||||
	// Persist new fractional balances to store.
 | 
			
		||||
	k.SetFractionalBalance(ctx, from, senderNewFracBal)
 | 
			
		||||
	k.SetFractionalBalance(ctx, to, recipientNewFracBal)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// subFromFractionalBalance subtracts a fractional amount from the provided
 | 
			
		||||
// current fractional balance, returning the new fractional balance and true if
 | 
			
		||||
// an integer borrow is required.
 | 
			
		||||
func subFromFractionalBalance(
 | 
			
		||||
	currentFractionalBalance sdkmath.Int,
 | 
			
		||||
	amountToSub sdkmath.Int,
 | 
			
		||||
) (sdkmath.Int, bool) {
 | 
			
		||||
	// Enforce that currentFractionalBalance is not a full balance.
 | 
			
		||||
	if currentFractionalBalance.GTE(types.ConversionFactor()) {
 | 
			
		||||
		panic("currentFractionalBalance must be less than ConversionFactor")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if amountToSub.GTE(types.ConversionFactor()) {
 | 
			
		||||
		panic("amountToSub must be less than ConversionFactor")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newFractionalBalance := currentFractionalBalance.Sub(amountToSub)
 | 
			
		||||
 | 
			
		||||
	// Insufficient fractional balance, so we need to borrow.
 | 
			
		||||
	borrowRequired := newFractionalBalance.IsNegative()
 | 
			
		||||
 | 
			
		||||
	if borrowRequired {
 | 
			
		||||
		// Borrowing 1 integer equivalent amount of fractional coins. We need to
 | 
			
		||||
		// add 1 integer equivalent amount to the fractional balance otherwise
 | 
			
		||||
		// the new fractional balance will be negative.
 | 
			
		||||
		newFractionalBalance = newFractionalBalance.Add(types.ConversionFactor())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newFractionalBalance, borrowRequired
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// addToFractionalBalance adds a fractional amount to the provided current
 | 
			
		||||
// fractional balance, returning the new fractional balance and true if a carry
 | 
			
		||||
// is required.
 | 
			
		||||
func addToFractionalBalance(
 | 
			
		||||
	currentFractionalBalance sdkmath.Int,
 | 
			
		||||
	amountToAdd sdkmath.Int,
 | 
			
		||||
) (sdkmath.Int, bool) {
 | 
			
		||||
	// Enforce that currentFractionalBalance is not a full balance.
 | 
			
		||||
	if currentFractionalBalance.GTE(types.ConversionFactor()) {
 | 
			
		||||
		panic("currentFractionalBalance must be less than ConversionFactor")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if amountToAdd.GTE(types.ConversionFactor()) {
 | 
			
		||||
		panic("amountToAdd must be less than ConversionFactor")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newFractionalBalance := currentFractionalBalance.Add(amountToAdd)
 | 
			
		||||
 | 
			
		||||
	// New balance exceeds max fractional balance, so we need to carry it over
 | 
			
		||||
	// to the integer balance.
 | 
			
		||||
	carryRequired := newFractionalBalance.GTE(types.ConversionFactor())
 | 
			
		||||
 | 
			
		||||
	if carryRequired {
 | 
			
		||||
		// Carry over to integer amount
 | 
			
		||||
		newFractionalBalance = newFractionalBalance.Sub(types.ConversionFactor())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newFractionalBalance, carryRequired
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendCoinsFromModuleToModule transfers coins from a ModuleAccount to another.
 | 
			
		||||
// It will panic if either module account does not exist. An error is returned
 | 
			
		||||
// if the recipient module is the x/precisebank module account or if sending the
 | 
			
		||||
// tokens fails.
 | 
			
		||||
func (k Keeper) SendCoinsFromAccountToModule(
 | 
			
		||||
	ctx sdk.Context,
 | 
			
		||||
	senderAddr sdk.AccAddress,
 | 
			
		||||
	recipientModule string,
 | 
			
		||||
	amt sdk.Coins,
 | 
			
		||||
) error {
 | 
			
		||||
	panic("unimplemented")
 | 
			
		||||
	recipientAcc := k.ak.GetModuleAccount(ctx, recipientModule)
 | 
			
		||||
	if recipientAcc == nil {
 | 
			
		||||
		panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", recipientModule))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if recipientModule == types.ModuleName {
 | 
			
		||||
		return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s is not allowed to receive funds", types.ModuleName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return k.SendCoins(ctx, senderAddr, recipientAcc.GetAddress(), amt)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendCoinsFromModuleToAccount transfers coins from a ModuleAccount to an AccAddress.
 | 
			
		||||
// It will panic if the module account does not exist. An error is returned if
 | 
			
		||||
// the recipient address is blocked, if the sender is the x/precisebank module
 | 
			
		||||
// account, or if sending the tokens fails.
 | 
			
		||||
func (k Keeper) SendCoinsFromModuleToAccount(
 | 
			
		||||
	ctx sdk.Context,
 | 
			
		||||
	senderModule string,
 | 
			
		||||
	recipientAddr sdk.AccAddress,
 | 
			
		||||
	amt sdk.Coins,
 | 
			
		||||
) error {
 | 
			
		||||
	panic("unimplemented")
 | 
			
		||||
	// Identical panics to x/bank
 | 
			
		||||
	senderAddr := k.ak.GetModuleAddress(senderModule)
 | 
			
		||||
	if senderAddr == nil {
 | 
			
		||||
		panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", senderModule))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Custom error to prevent external modules from modifying x/precisebank
 | 
			
		||||
	// balances. x/precisebank module account balance is for internal reserve
 | 
			
		||||
	// use only.
 | 
			
		||||
	if senderModule == types.ModuleName {
 | 
			
		||||
		return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "module account %s is not allowed to send funds", types.ModuleName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Uses x/bank BlockedAddr, no need to modify. x/precisebank should be
 | 
			
		||||
	// blocked.
 | 
			
		||||
	if k.bk.BlockedAddr(recipientAddr) {
 | 
			
		||||
		return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", recipientAddr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return k.SendCoins(ctx, senderAddr, recipientAddr, amt)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateInsufficientFundsError returns a modified ErrInsufficientFunds with
 | 
			
		||||
// extended coin amounts if the error is due to insufficient funds. Otherwise,
 | 
			
		||||
// it returns the original error. This is used since x/bank transfers will
 | 
			
		||||
// return errors with integer coins, but we want the more accurate error that
 | 
			
		||||
// contains the full extended coin balance and send amounts.
 | 
			
		||||
func (k Keeper) updateInsufficientFundsError(
 | 
			
		||||
	ctx sdk.Context,
 | 
			
		||||
	addr sdk.AccAddress,
 | 
			
		||||
	amt sdkmath.Int,
 | 
			
		||||
	err error,
 | 
			
		||||
) error {
 | 
			
		||||
	if !errors.Is(err, sdkerrors.ErrInsufficientFunds) {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check balance is sufficient
 | 
			
		||||
	bal := k.GetBalance(ctx, addr, types.ExtendedCoinDenom)
 | 
			
		||||
	coin := sdk.NewCoin(types.ExtendedCoinDenom, amt)
 | 
			
		||||
 | 
			
		||||
	// TODO: This checks spendable coins and returns error with spendable
 | 
			
		||||
	// coins, not full balance. If GetBalance() is modified to return the
 | 
			
		||||
	// full, including locked, balance then this should be updated to deduct
 | 
			
		||||
	// locked coins.
 | 
			
		||||
 | 
			
		||||
	// Use sdk.NewCoins() so that it removes empty balances - ie. prints
 | 
			
		||||
	// empty string if balance is 0. This is to match x/bank behavior.
 | 
			
		||||
	spendable := sdk.NewCoins(bal)
 | 
			
		||||
 | 
			
		||||
	return errorsmod.Wrapf(
 | 
			
		||||
		sdkerrors.ErrInsufficientFunds,
 | 
			
		||||
		"spendable balance %s is smaller than %s",
 | 
			
		||||
		spendable, coin,
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										683
									
								
								x/precisebank/keeper/send_integration_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										683
									
								
								x/precisebank/keeper/send_integration_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,683 @@
 | 
			
		||||
package keeper_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
 | 
			
		||||
	"github.com/kava-labs/kava/app"
 | 
			
		||||
	"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 sendIntegrationTestSuite struct {
 | 
			
		||||
	testutil.Suite
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) SetupTest() {
 | 
			
		||||
	suite.Suite.SetupTest()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSendIntegrationTestSuite(t *testing.T) {
 | 
			
		||||
	suite.Run(t, new(sendIntegrationTestSuite))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) TestSendCoinsFromAccountToModule_MatchingErrors() {
 | 
			
		||||
	// No specific errors for SendCoinsFromAccountToModule, only 1 panic if
 | 
			
		||||
	// the module account does not exist
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name            string
 | 
			
		||||
		sender          sdk.AccAddress
 | 
			
		||||
		recipientModule string
 | 
			
		||||
		sendAmount      sdk.Coins
 | 
			
		||||
		wantPanic       string
 | 
			
		||||
	}{
 | 
			
		||||
		// SendCoinsFromAccountToModule specific errors/panics
 | 
			
		||||
		{
 | 
			
		||||
			"missing module account - passthrough",
 | 
			
		||||
			sdk.AccAddress([]byte{2}),
 | 
			
		||||
			"cat",
 | 
			
		||||
			cs(c("usdc", 1000)),
 | 
			
		||||
			"module account cat does not exist: unknown address",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"missing module account - extended",
 | 
			
		||||
			sdk.AccAddress([]byte{2}),
 | 
			
		||||
			"cat",
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
			"module account cat does not exist: unknown address",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		suite.Run(tt.name, func() {
 | 
			
		||||
			// Reset
 | 
			
		||||
			suite.SetupTest()
 | 
			
		||||
 | 
			
		||||
			suite.Require().NotEmpty(tt.wantPanic, "test case must have a wantPanic")
 | 
			
		||||
 | 
			
		||||
			suite.Require().PanicsWithError(tt.wantPanic, func() {
 | 
			
		||||
				suite.BankKeeper.SendCoinsFromAccountToModule(suite.Ctx, tt.sender, tt.recipientModule, tt.sendAmount)
 | 
			
		||||
			}, "wantPanic should match x/bank SendCoinsFromAccountToModule panic")
 | 
			
		||||
 | 
			
		||||
			suite.Require().PanicsWithError(tt.wantPanic, func() {
 | 
			
		||||
				suite.Keeper.SendCoinsFromAccountToModule(suite.Ctx, tt.sender, tt.recipientModule, tt.sendAmount)
 | 
			
		||||
			}, "x/precisebank panic should match x/bank SendCoinsFromAccountToModule panic")
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) TestSendCoinsFromModuleToAccount_MatchingErrors() {
 | 
			
		||||
	// Ensure errors match x/bank errors AND panics. This needs to be well
 | 
			
		||||
	// tested before SendCoins as all send tests rely on this to initialize
 | 
			
		||||
	// account balances.
 | 
			
		||||
	// No unit test with mock x/bank for SendCoinsFromModuleToAccount since
 | 
			
		||||
	// we only are testing the errors/panics specific to the method and
 | 
			
		||||
	// remaining logic is the same as SendCoins.
 | 
			
		||||
 | 
			
		||||
	blockedMacAddrs := suite.App.GetBlockedMaccAddrs()
 | 
			
		||||
	precisebankAddr := suite.AccountKeeper.GetModuleAddress(types.ModuleName)
 | 
			
		||||
 | 
			
		||||
	var blockedAddr sdk.AccAddress
 | 
			
		||||
	// Get the first blocked address
 | 
			
		||||
	for addr, isBlocked := range blockedMacAddrs {
 | 
			
		||||
		// Skip x/precisebank module account
 | 
			
		||||
		if addr == precisebankAddr.String() {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if isBlocked {
 | 
			
		||||
			blockedAddr = sdk.MustAccAddressFromBech32(addr)
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We need a ModuleName of another module account to send funds from.
 | 
			
		||||
	// x/precisebank is blocked from use with SendCoinsFromModuleToAccount as we
 | 
			
		||||
	// don't want external modules to modify x/precisebank balances.
 | 
			
		||||
	var senderModuleName string
 | 
			
		||||
	macPerms := app.GetMaccPerms()
 | 
			
		||||
	for moduleName := range macPerms {
 | 
			
		||||
		if moduleName != types.ModuleName {
 | 
			
		||||
			senderModuleName = moduleName
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	suite.Require().NotEmpty(blockedAddr, "no blocked addresses found")
 | 
			
		||||
	suite.Require().NotEmpty(senderModuleName, "no sender module name found")
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name         string
 | 
			
		||||
		senderModule string
 | 
			
		||||
		recipient    sdk.AccAddress
 | 
			
		||||
		sendAmount   sdk.Coins
 | 
			
		||||
		wantErr      string
 | 
			
		||||
		wantPanic    string
 | 
			
		||||
	}{
 | 
			
		||||
		// SendCoinsFromModuleToAccount specific errors/panics
 | 
			
		||||
		{
 | 
			
		||||
			"missing module account - passthrough",
 | 
			
		||||
			"cat",
 | 
			
		||||
			sdk.AccAddress([]byte{2}),
 | 
			
		||||
			cs(c("usdc", 1000)),
 | 
			
		||||
			"",
 | 
			
		||||
			"module account cat does not exist: unknown address",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"missing module account - extended",
 | 
			
		||||
			"cat",
 | 
			
		||||
			sdk.AccAddress([]byte{2}),
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
			"",
 | 
			
		||||
			"module account cat does not exist: unknown address",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"blocked recipient address - passthrough",
 | 
			
		||||
			senderModuleName,
 | 
			
		||||
			blockedAddr,
 | 
			
		||||
			cs(c("usdc", 1000)),
 | 
			
		||||
			fmt.Sprintf("%s is not allowed to receive funds: unauthorized", blockedAddr.String()),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"blocked recipient address - extended",
 | 
			
		||||
			senderModuleName,
 | 
			
		||||
			blockedAddr,
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
			fmt.Sprintf("%s is not allowed to receive funds: unauthorized", blockedAddr.String()),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		// SendCoins specific errors/panics
 | 
			
		||||
		{
 | 
			
		||||
			"invalid coins",
 | 
			
		||||
			senderModuleName,
 | 
			
		||||
			sdk.AccAddress([]byte{2}),
 | 
			
		||||
			sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)}},
 | 
			
		||||
			"-1ukava: invalid coins",
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"insufficient balance - passthrough",
 | 
			
		||||
			senderModuleName,
 | 
			
		||||
			sdk.AccAddress([]byte{2}),
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 1000)),
 | 
			
		||||
			"spendable balance  is smaller than 1000ukava: insufficient funds",
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"insufficient balance - extended",
 | 
			
		||||
			senderModuleName,
 | 
			
		||||
			sdk.AccAddress([]byte{2}),
 | 
			
		||||
			// We can still test insufficient bal errors with "akava" since
 | 
			
		||||
			// we also expect it to not exist in x/bank
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
			"spendable balance  is smaller than 1000akava: insufficient funds",
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		suite.Run(tt.name, func() {
 | 
			
		||||
			// Reset
 | 
			
		||||
			suite.SetupTest()
 | 
			
		||||
 | 
			
		||||
			if tt.wantPanic == "" && tt.wantErr == "" {
 | 
			
		||||
				suite.FailNow("test case must have a wantErr or wantPanic")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if tt.wantPanic != "" {
 | 
			
		||||
				suite.Require().Empty(tt.wantErr, "test case must not have a wantErr if wantPanic is set")
 | 
			
		||||
 | 
			
		||||
				suite.Require().PanicsWithError(tt.wantPanic, func() {
 | 
			
		||||
					suite.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
 | 
			
		||||
				}, "wantPanic should match x/bank SendCoinsFromModuleToAccount panic")
 | 
			
		||||
 | 
			
		||||
				suite.Require().PanicsWithError(tt.wantPanic, func() {
 | 
			
		||||
					suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
 | 
			
		||||
				}, "x/precisebank panic should match x/bank SendCoinsFromModuleToAccount panic")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				bankErr := suite.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
 | 
			
		||||
				suite.Require().Error(bankErr)
 | 
			
		||||
				suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank SendCoins error")
 | 
			
		||||
 | 
			
		||||
				pbankErr := suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, tt.senderModule, tt.recipient, tt.sendAmount)
 | 
			
		||||
				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 SendCoins error",
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) TestSendCoins_MatchingErrors() {
 | 
			
		||||
	// Ensure errors match x/bank errors
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		initialAmount sdk.Coins
 | 
			
		||||
		sendAmount    sdk.Coins
 | 
			
		||||
		wantErr       string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"invalid coins",
 | 
			
		||||
			cs(),
 | 
			
		||||
			sdk.Coins{sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)}},
 | 
			
		||||
			"-1ukava: invalid coins",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"insufficient empty balance - passthrough",
 | 
			
		||||
			cs(),
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 1000)),
 | 
			
		||||
			"spendable balance  is smaller than 1000ukava: insufficient funds",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"insufficient empty balance - extended",
 | 
			
		||||
			cs(),
 | 
			
		||||
			// We can still test insufficient bal errors with "akava" since
 | 
			
		||||
			// we also expect it to not exist in x/bank
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
			"spendable balance  is smaller than 1000akava: insufficient funds",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"insufficient non-empty balance - passthrough",
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 100), c("usdc", 1000)),
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 1000)),
 | 
			
		||||
			"spendable balance 100ukava is smaller than 1000ukava: insufficient funds",
 | 
			
		||||
		},
 | 
			
		||||
		// non-empty akava transfer error is tested in SendCoins, not here since
 | 
			
		||||
		// x/bank doesn't hold akava
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		suite.Run(tt.name, func() {
 | 
			
		||||
			// Reset
 | 
			
		||||
			suite.SetupTest()
 | 
			
		||||
			sender := sdk.AccAddress([]byte{1})
 | 
			
		||||
			recipient := sdk.AccAddress([]byte{2})
 | 
			
		||||
 | 
			
		||||
			suite.Require().NotEmpty(tt.wantErr, "test case must have a wantErr")
 | 
			
		||||
 | 
			
		||||
			suite.MintToAccount(sender, tt.initialAmount)
 | 
			
		||||
 | 
			
		||||
			bankErr := suite.BankKeeper.SendCoins(suite.Ctx, sender, recipient, tt.sendAmount)
 | 
			
		||||
			suite.Require().Error(bankErr)
 | 
			
		||||
			suite.Require().EqualError(bankErr, tt.wantErr, "expected error should match x/bank SendCoins error")
 | 
			
		||||
 | 
			
		||||
			pbankErr := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, tt.sendAmount)
 | 
			
		||||
			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 SendCoins error",
 | 
			
		||||
			)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) TestSendCoins() {
 | 
			
		||||
	// SendCoins is tested mostly in this integration test, as a unit test with
 | 
			
		||||
	// mocked BankKeeper overcomplicates expected keepers and makes initializing
 | 
			
		||||
	// balances very complex.
 | 
			
		||||
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name                  string
 | 
			
		||||
		giveStartBalSender    sdk.Coins
 | 
			
		||||
		giveStartBalRecipient sdk.Coins
 | 
			
		||||
		giveAmt               sdk.Coins
 | 
			
		||||
		wantErr               string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"insufficient balance error denom matches",
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 10), c("usdc", 1000)),
 | 
			
		||||
			cs(),
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
			"spendable balance 10akava is smaller than 1000akava: insufficient funds",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"passthrough - unrelated",
 | 
			
		||||
			cs(c("cats", 1000)),
 | 
			
		||||
			cs(),
 | 
			
		||||
			cs(c("cats", 1000)),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"passthrough - integer denom",
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 1000)),
 | 
			
		||||
			cs(),
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 1000)),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"passthrough & extended",
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 1000)),
 | 
			
		||||
			cs(),
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1)),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"akava send - 1akava to 0 balance",
 | 
			
		||||
			// Starting balances
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5))),
 | 
			
		||||
			cs(),
 | 
			
		||||
			// Send amount
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1)), // akava
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"sender borrow from integer",
 | 
			
		||||
			// 1ukava, 0 fractional
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
 | 
			
		||||
			cs(),
 | 
			
		||||
			// Send 1 with 0 fractional balance
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1)),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"sender borrow from integer - max fractional amount",
 | 
			
		||||
			// 1ukava, 0 fractional
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor())),
 | 
			
		||||
			cs(),
 | 
			
		||||
			// Max fractional amount
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"receiver carry",
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
			// max fractional amount, carries over to integer
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 1)),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"receiver carry - max fractional amount",
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().MulRaw(5))),
 | 
			
		||||
			// max fractional amount, carries over to integer
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
 | 
			
		||||
			cs(ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
 | 
			
		||||
			"",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		suite.Run(tt.name, func() {
 | 
			
		||||
			suite.SetupTest()
 | 
			
		||||
 | 
			
		||||
			sender := sdk.AccAddress([]byte{1})
 | 
			
		||||
			recipient := sdk.AccAddress([]byte{2})
 | 
			
		||||
 | 
			
		||||
			// Initialize balances
 | 
			
		||||
			suite.MintToAccount(sender, tt.giveStartBalSender)
 | 
			
		||||
			suite.MintToAccount(recipient, tt.giveStartBalRecipient)
 | 
			
		||||
 | 
			
		||||
			senderBalBefore := suite.GetAllBalances(sender)
 | 
			
		||||
			recipientBalBefore := suite.GetAllBalances(recipient)
 | 
			
		||||
 | 
			
		||||
			err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, tt.giveAmt)
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				suite.Require().Error(err)
 | 
			
		||||
				suite.Require().EqualError(err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
			// Check balances
 | 
			
		||||
			senderBalAfter := suite.GetAllBalances(sender)
 | 
			
		||||
			recipientBalAfter := suite.GetAllBalances(recipient)
 | 
			
		||||
 | 
			
		||||
			// Convert send amount coins to extended coins. i.e. if send coins
 | 
			
		||||
			// includes ukava, convert it so that its the equivalent akava
 | 
			
		||||
			// amount so its easier to compare. Compare extended coins only.
 | 
			
		||||
			sendAmountExtended := tt.giveAmt
 | 
			
		||||
			sendAmountInteger := tt.giveAmt.AmountOf(types.IntegerCoinDenom)
 | 
			
		||||
			if !sendAmountInteger.IsZero() {
 | 
			
		||||
				integerCoin := sdk.NewCoin(types.IntegerCoinDenom, sendAmountInteger)
 | 
			
		||||
				sendAmountExtended = sendAmountExtended.Sub(integerCoin)
 | 
			
		||||
 | 
			
		||||
				// Add equivalent extended coin
 | 
			
		||||
				extendedCoinAmount := sendAmountInteger.Mul(types.ConversionFactor())
 | 
			
		||||
				extendedCoin := sdk.NewCoin(types.ExtendedCoinDenom, extendedCoinAmount)
 | 
			
		||||
				sendAmountExtended = sendAmountExtended.Add(extendedCoin)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			suite.Require().Equal(
 | 
			
		||||
				senderBalBefore.Sub(sendAmountExtended...),
 | 
			
		||||
				senderBalAfter,
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			suite.Require().Equal(
 | 
			
		||||
				recipientBalBefore.Add(sendAmountExtended...),
 | 
			
		||||
				recipientBalAfter,
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			invariantFn := keeper.AllInvariants(suite.Keeper)
 | 
			
		||||
			res, stop := invariantFn(suite.Ctx)
 | 
			
		||||
			suite.Require().False(stop, "invariants should not stop")
 | 
			
		||||
			suite.Require().Empty(res, "invariants should not return any messages")
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) TestSendCoins_Matrix() {
 | 
			
		||||
	// SendCoins is tested mostly in this integration test, as a unit test with
 | 
			
		||||
	// mocked BankKeeper overcomplicates expected keepers and makes initializing
 | 
			
		||||
	// balances very complex.
 | 
			
		||||
 | 
			
		||||
	type startBalance struct {
 | 
			
		||||
		name string
 | 
			
		||||
		bal  sdk.Coins
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Run through each combination of start sender/recipient balance & send amt
 | 
			
		||||
	// Test matrix fields:
 | 
			
		||||
	startBalances := []startBalance{
 | 
			
		||||
		{"empty", cs()},
 | 
			
		||||
		{"integer only", cs(c(types.IntegerCoinDenom, 1000))},
 | 
			
		||||
		{"extended only", cs(c(types.ExtendedCoinDenom, 1000))},
 | 
			
		||||
		{"integer & extended", cs(c(types.IntegerCoinDenom, 1000), c(types.ExtendedCoinDenom, 1000))},
 | 
			
		||||
		{"integer & extended - max fractional", cs(c(types.IntegerCoinDenom, 1000), ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1)))},
 | 
			
		||||
		{"integer & extended - min fractional", cs(c(types.IntegerCoinDenom, 1000), c(types.ExtendedCoinDenom, 1))},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sendAmts := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		amt  sdk.Coins
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"empty",
 | 
			
		||||
			cs(),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"integer only",
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 10)),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"extended only",
 | 
			
		||||
			cs(c(types.ExtendedCoinDenom, 10)),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"integer & extended",
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1000)),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"integer & extended - max fractional",
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 10), ci(types.ExtendedCoinDenom, types.ConversionFactor().SubRaw(1))),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"integer & extended - min fractional",
 | 
			
		||||
			cs(c(types.IntegerCoinDenom, 10), c(types.ExtendedCoinDenom, 1)),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, senderStartBal := range startBalances {
 | 
			
		||||
		for _, recipientStartBal := range startBalances {
 | 
			
		||||
			for _, sendAmt := range sendAmts {
 | 
			
		||||
				testName := fmt.Sprintf(
 | 
			
		||||
					"%s -> %s (%s -> %s), send %s (%s)",
 | 
			
		||||
					senderStartBal.name, senderStartBal.bal,
 | 
			
		||||
					recipientStartBal.name, recipientStartBal.bal,
 | 
			
		||||
					sendAmt.name, sendAmt.amt,
 | 
			
		||||
				)
 | 
			
		||||
 | 
			
		||||
				suite.Run(testName, func() {
 | 
			
		||||
					suite.SetupTest()
 | 
			
		||||
 | 
			
		||||
					sender := sdk.AccAddress([]byte{1})
 | 
			
		||||
					recipient := sdk.AccAddress([]byte{2})
 | 
			
		||||
 | 
			
		||||
					// Initialize balances
 | 
			
		||||
					suite.MintToAccount(sender, senderStartBal.bal)
 | 
			
		||||
					suite.MintToAccount(recipient, recipientStartBal.bal)
 | 
			
		||||
 | 
			
		||||
					// balances & send amount will only contain total equivalent
 | 
			
		||||
					// extended coins and no integer coins so its easier to compare
 | 
			
		||||
					senderBalBefore := suite.GetAllBalances(sender)
 | 
			
		||||
					recipientBalBefore := suite.GetAllBalances(recipient)
 | 
			
		||||
 | 
			
		||||
					sendAmtNormalized := testutil.ConvertCoinsToExtendedCoinDenom(sendAmt.amt)
 | 
			
		||||
 | 
			
		||||
					err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, sendAmt.amt)
 | 
			
		||||
 | 
			
		||||
					hasSufficientBal := senderBalBefore.IsAllGTE(sendAmtNormalized)
 | 
			
		||||
 | 
			
		||||
					if hasSufficientBal {
 | 
			
		||||
						suite.Require().NoError(err)
 | 
			
		||||
					} else {
 | 
			
		||||
						suite.Require().Error(err, "expected insufficient funds error")
 | 
			
		||||
						// No balance checks if insufficient funds
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// Check balances
 | 
			
		||||
					senderBalAfter := suite.GetAllBalances(sender)
 | 
			
		||||
					recipientBalAfter := suite.GetAllBalances(recipient)
 | 
			
		||||
 | 
			
		||||
					// Convert send amount coins to extended coins. i.e. if send coins
 | 
			
		||||
					// includes ukava, convert it so that its the equivalent akava
 | 
			
		||||
					// amount so its easier to compare. Compare extended coins only.
 | 
			
		||||
 | 
			
		||||
					suite.Require().Equal(
 | 
			
		||||
						senderBalBefore.Sub(sendAmtNormalized...),
 | 
			
		||||
						senderBalAfter,
 | 
			
		||||
					)
 | 
			
		||||
 | 
			
		||||
					suite.Require().Equal(
 | 
			
		||||
						recipientBalBefore.Add(sendAmtNormalized...),
 | 
			
		||||
						recipientBalAfter,
 | 
			
		||||
					)
 | 
			
		||||
 | 
			
		||||
					invariantFn := keeper.AllInvariants(suite.Keeper)
 | 
			
		||||
					res, stop := invariantFn(suite.Ctx)
 | 
			
		||||
					suite.Require().False(stop, "invariants should not stop")
 | 
			
		||||
					suite.Require().Empty(res, "invariants should not return any messages")
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) TestSendCoinsFromAccountToModule() {
 | 
			
		||||
	// Ensure recipient correctly matches the specified module account. Specific
 | 
			
		||||
	// send amount and cases are handled by SendCoins() tests, so we are only
 | 
			
		||||
	// checking SendCoinsFromAccountToModule specific behavior here.
 | 
			
		||||
 | 
			
		||||
	sender := sdk.AccAddress([]byte{1})
 | 
			
		||||
	recipientModule := minttypes.ModuleName
 | 
			
		||||
	recipientAddr := suite.AccountKeeper.GetModuleAddress(recipientModule)
 | 
			
		||||
 | 
			
		||||
	sendAmt := cs(c(types.ExtendedCoinDenom, 1000))
 | 
			
		||||
 | 
			
		||||
	suite.MintToAccount(sender, sendAmt)
 | 
			
		||||
 | 
			
		||||
	err := suite.Keeper.SendCoinsFromAccountToModule(
 | 
			
		||||
		suite.Ctx,
 | 
			
		||||
		sender,
 | 
			
		||||
		recipientModule,
 | 
			
		||||
		sendAmt,
 | 
			
		||||
	)
 | 
			
		||||
	suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
	// Check balances
 | 
			
		||||
	senderBalAfter := suite.GetAllBalances(sender)
 | 
			
		||||
	recipientBalAfter := suite.GetAllBalances(recipientAddr)
 | 
			
		||||
 | 
			
		||||
	suite.Require().Equal(
 | 
			
		||||
		cs(),
 | 
			
		||||
		senderBalAfter,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	suite.Require().Equal(
 | 
			
		||||
		sendAmt,
 | 
			
		||||
		recipientBalAfter,
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (suite *sendIntegrationTestSuite) TestSendCoinsFromModuleToAccount() {
 | 
			
		||||
	// Ensure sender correctly matches the specified module account. Opposite
 | 
			
		||||
	// of SendCoinsFromAccountToModule, so we are only checking the correct
 | 
			
		||||
	// addresses are being used.
 | 
			
		||||
 | 
			
		||||
	senderModule := "community"
 | 
			
		||||
	senderAddr := suite.AccountKeeper.GetModuleAddress(senderModule)
 | 
			
		||||
 | 
			
		||||
	recipient := sdk.AccAddress([]byte{1})
 | 
			
		||||
 | 
			
		||||
	sendAmt := cs(c(types.ExtendedCoinDenom, 1000))
 | 
			
		||||
 | 
			
		||||
	suite.MintToAccount(senderAddr, sendAmt)
 | 
			
		||||
 | 
			
		||||
	err := suite.Keeper.SendCoinsFromModuleToAccount(
 | 
			
		||||
		suite.Ctx,
 | 
			
		||||
		senderModule,
 | 
			
		||||
		recipient,
 | 
			
		||||
		sendAmt,
 | 
			
		||||
	)
 | 
			
		||||
	suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
	// Check balances
 | 
			
		||||
	senderBalAfter := suite.GetAllBalances(senderAddr)
 | 
			
		||||
	recipientBalAfter := suite.GetAllBalances(recipient)
 | 
			
		||||
 | 
			
		||||
	suite.Require().Equal(
 | 
			
		||||
		cs(),
 | 
			
		||||
		senderBalAfter,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	suite.Require().Equal(
 | 
			
		||||
		sendAmt,
 | 
			
		||||
		recipientBalAfter,
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FuzzSendCoins(f *testing.F) {
 | 
			
		||||
	f.Add(uint64(100), uint64(0), uint64(2))
 | 
			
		||||
	f.Add(uint64(100), uint64(100), uint64(5))
 | 
			
		||||
	f.Add(types.ConversionFactor().Uint64(), uint64(0), uint64(500))
 | 
			
		||||
	f.Add(
 | 
			
		||||
		types.ConversionFactor().MulRaw(2).AddRaw(123948723).Uint64(),
 | 
			
		||||
		types.ConversionFactor().MulRaw(2).Uint64(),
 | 
			
		||||
		types.ConversionFactor().Uint64(),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	f.Fuzz(func(
 | 
			
		||||
		t *testing.T,
 | 
			
		||||
		startBalSender uint64,
 | 
			
		||||
		startBalReceiver uint64,
 | 
			
		||||
		sendAmount uint64,
 | 
			
		||||
	) {
 | 
			
		||||
		// Manually setup test suite since no direct Fuzz support in test suites
 | 
			
		||||
		suite := new(sendIntegrationTestSuite)
 | 
			
		||||
		suite.SetT(t)
 | 
			
		||||
		suite.SetS(suite)
 | 
			
		||||
		suite.SetupTest()
 | 
			
		||||
 | 
			
		||||
		sender := sdk.AccAddress([]byte{1})
 | 
			
		||||
		recipient := sdk.AccAddress([]byte{2})
 | 
			
		||||
 | 
			
		||||
		// Initial balances
 | 
			
		||||
		suite.MintToAccount(sender, cs(c(types.ExtendedCoinDenom, int64(startBalSender))))
 | 
			
		||||
		suite.MintToAccount(recipient, cs(c(types.ExtendedCoinDenom, int64(startBalReceiver))))
 | 
			
		||||
 | 
			
		||||
		// Send amount
 | 
			
		||||
		sendCoins := cs(c(types.ExtendedCoinDenom, int64(sendAmount)))
 | 
			
		||||
		err := suite.Keeper.SendCoins(suite.Ctx, sender, recipient, sendCoins)
 | 
			
		||||
		if startBalSender < sendAmount {
 | 
			
		||||
			suite.Require().Error(err, "expected insufficient funds error")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
		// Check FULL balances
 | 
			
		||||
		balSender := suite.GetAllBalances(sender)
 | 
			
		||||
		balReceiver := suite.GetAllBalances(recipient)
 | 
			
		||||
 | 
			
		||||
		suite.Require().Equal(
 | 
			
		||||
			startBalSender-sendAmount,
 | 
			
		||||
			balSender.AmountOf(types.ExtendedCoinDenom).Uint64(),
 | 
			
		||||
		)
 | 
			
		||||
		suite.Require().Equal(
 | 
			
		||||
			startBalReceiver+sendAmount,
 | 
			
		||||
			balReceiver.AmountOf(types.ExtendedCoinDenom).Uint64(),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		// 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)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								x/precisebank/keeper/send_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								x/precisebank/keeper/send_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
package keeper_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
 | 
			
		||||
	"github.com/kava-labs/kava/x/precisebank/types"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSendCoinsFromAccountToModule_BlockedReserve(t *testing.T) {
 | 
			
		||||
	// Other modules shouldn't be able to send x/precisebank coins as the module
 | 
			
		||||
	// account balance is for internal reserve use only.
 | 
			
		||||
 | 
			
		||||
	td := NewMockedTestData(t)
 | 
			
		||||
	td.ak.EXPECT().
 | 
			
		||||
		GetModuleAccount(td.ctx, types.ModuleName).
 | 
			
		||||
		Return(authtypes.NewModuleAccount(
 | 
			
		||||
			authtypes.NewBaseAccountWithAddress(sdk.AccAddress{100}),
 | 
			
		||||
			types.ModuleName,
 | 
			
		||||
		)).
 | 
			
		||||
		Once()
 | 
			
		||||
 | 
			
		||||
	fromAddr := sdk.AccAddress([]byte{1})
 | 
			
		||||
	err := td.keeper.SendCoinsFromAccountToModule(td.ctx, fromAddr, types.ModuleName, cs(c("busd", 1000)))
 | 
			
		||||
 | 
			
		||||
	require.Error(t, err)
 | 
			
		||||
	require.EqualError(t, err, "module account precisebank is not allowed to receive funds: unauthorized")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSendCoinsFromModuleToAccount_BlockedReserve(t *testing.T) {
 | 
			
		||||
	// Other modules shouldn't be able to send x/precisebank module account
 | 
			
		||||
	// funds.
 | 
			
		||||
 | 
			
		||||
	td := NewMockedTestData(t)
 | 
			
		||||
	td.ak.EXPECT().
 | 
			
		||||
		GetModuleAddress(types.ModuleName).
 | 
			
		||||
		Return(sdk.AccAddress{100}).
 | 
			
		||||
		Once()
 | 
			
		||||
 | 
			
		||||
	toAddr := sdk.AccAddress([]byte{1})
 | 
			
		||||
	err := td.keeper.SendCoinsFromModuleToAccount(td.ctx, types.ModuleName, toAddr, cs(c("busd", 1000)))
 | 
			
		||||
 | 
			
		||||
	require.Error(t, err)
 | 
			
		||||
	require.EqualError(t, err, "module account precisebank is not allowed to send funds: unauthorized")
 | 
			
		||||
}
 | 
			
		||||
@ -12,11 +12,13 @@ import (
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
	authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
 | 
			
		||||
	bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
 | 
			
		||||
	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
 | 
			
		||||
	"github.com/evmos/ethermint/crypto/ethsecp256k1"
 | 
			
		||||
	"github.com/stretchr/testify/suite"
 | 
			
		||||
 | 
			
		||||
	"github.com/kava-labs/kava/app"
 | 
			
		||||
	"github.com/kava-labs/kava/x/precisebank/keeper"
 | 
			
		||||
	"github.com/kava-labs/kava/x/precisebank/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Suite struct {
 | 
			
		||||
@ -87,3 +89,63 @@ func (suite *Suite) Commit() {
 | 
			
		||||
	// update ctx
 | 
			
		||||
	suite.Ctx = suite.App.NewContext(false, header)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MintToAccount mints coins to an account with the x/precisebank methods. This
 | 
			
		||||
// must be used when minting extended coins, ie. akava coins. This depends on
 | 
			
		||||
// the methods to be properly tested to be implemented correctly.
 | 
			
		||||
func (suite *Suite) MintToAccount(addr sdk.AccAddress, amt sdk.Coins) {
 | 
			
		||||
	accBalancesBefore := suite.GetAllBalances(addr)
 | 
			
		||||
 | 
			
		||||
	err := suite.Keeper.MintCoins(suite.Ctx, minttypes.ModuleName, amt)
 | 
			
		||||
	suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
	err = suite.Keeper.SendCoinsFromModuleToAccount(suite.Ctx, minttypes.ModuleName, addr, amt)
 | 
			
		||||
	suite.Require().NoError(err)
 | 
			
		||||
 | 
			
		||||
	// Double check balances are correctly minted and sent to account
 | 
			
		||||
	accBalancesAfter := suite.GetAllBalances(addr)
 | 
			
		||||
 | 
			
		||||
	netIncrease := accBalancesAfter.Sub(accBalancesBefore...)
 | 
			
		||||
	suite.Require().Equal(ConvertCoinsToExtendedCoinDenom(amt), netIncrease)
 | 
			
		||||
 | 
			
		||||
	suite.T().Logf("minted %s to %s", amt, addr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAllBalances returns all the account balances for the given account address.
 | 
			
		||||
// This returns the extended coin balance if the account has a non-zero balance,
 | 
			
		||||
// WITHOUT the integer coin balance.
 | 
			
		||||
func (suite *Suite) GetAllBalances(addr sdk.AccAddress) sdk.Coins {
 | 
			
		||||
	// Get all balances for an account
 | 
			
		||||
	bankBalances := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)
 | 
			
		||||
 | 
			
		||||
	// Remove integer coins from the balance
 | 
			
		||||
	for _, coin := range bankBalances {
 | 
			
		||||
		if coin.Denom == types.IntegerCoinDenom {
 | 
			
		||||
			bankBalances = bankBalances.Sub(coin)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Replace the integer coin with the extended coin, from x/precisebank
 | 
			
		||||
	extendedBal := suite.Keeper.GetBalance(suite.Ctx, addr, types.ExtendedCoinDenom)
 | 
			
		||||
 | 
			
		||||
	return bankBalances.Add(extendedBal)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConvertCoinsToExtendedCoinDenom converts sdk.Coins that includes Integer denoms
 | 
			
		||||
// to sdk.Coins that includes Extended denoms of the same amount. This is useful
 | 
			
		||||
// for testing to make sure only extended amounts are compared instead of double
 | 
			
		||||
// counting balances.
 | 
			
		||||
func ConvertCoinsToExtendedCoinDenom(coins sdk.Coins) sdk.Coins {
 | 
			
		||||
	integerCoinAmt := coins.AmountOf(types.IntegerCoinDenom)
 | 
			
		||||
	if integerCoinAmt.IsZero() {
 | 
			
		||||
		return coins
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Remove the integer coin from the coins
 | 
			
		||||
	integerCoin := sdk.NewCoin(types.IntegerCoinDenom, integerCoinAmt)
 | 
			
		||||
 | 
			
		||||
	// Add the equivalent extended coin to the coins
 | 
			
		||||
	extendedCoin := sdk.NewCoin(types.ExtendedCoinDenom, integerCoinAmt.Mul(types.ConversionFactor()))
 | 
			
		||||
 | 
			
		||||
	return coins.Sub(integerCoin).Add(extendedCoin)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user