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" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "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. // Note: This method is not used directly by x/evm, but is still required as // part of authtypes.BankKeeper. x/evm uses auth methods that require this // interface. func (k Keeper) IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error { // Simply pass through to x/bank 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, amt sdk.Coins, ) error { // 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. 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 } } // Send the extended coin amount through x/precisebank if extendedCoinAmount.IsPositive() { if err := k.sendExtendedCoins(ctx, from, to, extendedCoinAmount); err != nil { return err } } // Get a full extended coin amount (passthrough integer + fractional) ONLY // for event attributes. fullEmissionCoin := types.SumExtendedCoin(amt) // If no passthrough integer nor fractional coins, then no event emission. // We also want to emit the event with the whole equivalent extended coin // if only integer coins are sent. if fullEmissionCoin.IsZero() { return nil } // Emit transfer event of extended denom for the FULL equivalent value. ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( banktypes.EventTypeTransfer, sdk.NewAttribute(banktypes.AttributeKeyRecipient, to.String()), sdk.NewAttribute(banktypes.AttributeKeySender, from.String()), sdk.NewAttribute(sdk.AttributeKeyAmount, fullEmissionCoin.String()), ), }) return nil } // 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 { 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 { // 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, ) }