mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +00:00
364 lines
13 KiB
Go
364 lines
13 KiB
Go
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.
|
|
fullEmissionCoins := sdk.NewCoins(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 fullEmissionCoins.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, fullEmissionCoins.String()),
|
|
),
|
|
banktypes.NewCoinSpentEvent(from, fullEmissionCoins),
|
|
banktypes.NewCoinReceivedEvent(to, fullEmissionCoins),
|
|
})
|
|
|
|
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 {
|
|
reserveAddr := k.ak.GetModuleAddress(types.ModuleName)
|
|
|
|
// We use SendCoins instead of SendCoinsFromModuleToAccount to avoid
|
|
// the blocked addrs check. Blocked accounts should not be checked in
|
|
// a SendCoins operation. Only SendCoinsFromModuleToAccount should check
|
|
// blocked addrs which is done by the parent SendCoinsFromModuleToAccount
|
|
// method.
|
|
carryCoin := sdk.NewCoin(types.IntegerCoinDenom, sdk.NewInt(1))
|
|
if err := k.bk.SendCoins(
|
|
ctx,
|
|
reserveAddr,
|
|
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,
|
|
)
|
|
}
|