package keeper import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/address" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/bank/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" ) // SendKeeper defines a module interface that facilitates the transfer of coins // between accounts without the possibility of creating coins. type SendKeeper interface { ViewKeeper InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error GetParams(ctx sdk.Context) types.Params SetParams(ctx sdk.Context, params types.Params) IsSendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error BlockedAddr(addr sdk.AccAddress) bool } var _ SendKeeper = (*BaseSendKeeper)(nil) // BaseSendKeeper only allows transfers between accounts without the possibility of // creating coins. It implements the SendKeeper interface. type BaseSendKeeper struct { BaseViewKeeper cdc codec.BinaryCodec ak types.AccountKeeper storeKey storetypes.StoreKey paramSpace paramtypes.Subspace // list of addresses that are restricted from receiving transactions blockedAddrs map[string]bool } func NewBaseSendKeeper( cdc codec.BinaryCodec, storeKey storetypes.StoreKey, ak types.AccountKeeper, paramSpace paramtypes.Subspace, blockedAddrs map[string]bool, ) BaseSendKeeper { return BaseSendKeeper{ BaseViewKeeper: NewBaseViewKeeper(cdc, storeKey, ak), cdc: cdc, ak: ak, storeKey: storeKey, paramSpace: paramSpace, blockedAddrs: blockedAddrs, } } // GetParams returns the total set of bank parameters. func (k BaseSendKeeper) GetParams(ctx sdk.Context) (params types.Params) { k.paramSpace.GetParamSet(ctx, ¶ms) return params } // SetParams sets the total set of bank parameters. func (k BaseSendKeeper) SetParams(ctx sdk.Context, params types.Params) { k.paramSpace.SetParamSet(ctx, ¶ms) } // InputOutputCoins performs multi-send functionality. It accepts a series of // inputs that correspond to a series of outputs. It returns an error if the // inputs and outputs don't lineup or if any single transfer of tokens fails. func (k BaseSendKeeper) InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error { // Safety check ensuring that when sending coins the keeper must maintain the // Check supply invariant and validity of Coins. if err := types.ValidateInputsOutputs(inputs, outputs); err != nil { return err } for _, in := range inputs { inAddress, err := sdk.AccAddressFromBech32(in.Address) if err != nil { return err } err = k.subUnlockedCoins(ctx, inAddress, in.Coins) if err != nil { return err } ctx.EventManager().EmitEvent( sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(types.AttributeKeySender, in.Address), ), ) } for _, out := range outputs { outAddress, err := sdk.AccAddressFromBech32(out.Address) if err != nil { return err } err = k.addCoins(ctx, outAddress, out.Coins) if err != nil { return err } ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeTransfer, sdk.NewAttribute(types.AttributeKeyRecipient, out.Address), sdk.NewAttribute(sdk.AttributeKeyAmount, out.Coins.String()), ), ) // Create account if recipient does not exist. // // NOTE: This should ultimately be removed in favor a more flexible approach // such as delegated fee messages. accExists := k.ak.HasAccount(ctx, outAddress) if !accExists { defer telemetry.IncrCounter(1, "new", "account") k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, outAddress)) } } return nil } // SendCoins transfers amt coins from a sending account to a receiving account. // An error is returned upon failure. func (k BaseSendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error { err := k.subUnlockedCoins(ctx, fromAddr, amt) if err != nil { return err } err = k.addCoins(ctx, toAddr, amt) if err != nil { return err } // Create account if recipient does not exist. // // NOTE: This should ultimately be removed in favor a more flexible approach // such as delegated fee messages. accExists := k.ak.HasAccount(ctx, toAddr) if !accExists { defer telemetry.IncrCounter(1, "new", "account") k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr)) } ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeTransfer, sdk.NewAttribute(types.AttributeKeyRecipient, toAddr.String()), sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()), sdk.NewAttribute(sdk.AttributeKeyAmount, amt.String()), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()), ), }) return nil } // subUnlockedCoins removes the unlocked amt coins of the given account. An error is // returned if the resulting balance is negative or the initial amount is invalid. // A coin_spent event is emitted after. func (k BaseSendKeeper) subUnlockedCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error { if !amt.IsValid() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } lockedCoins := k.LockedCoins(ctx, addr) for _, coin := range amt { balance := k.GetBalance(ctx, addr, coin.Denom) locked := sdk.NewCoin(coin.Denom, lockedCoins.AmountOf(coin.Denom)) spendable := balance.Sub(locked) _, hasNeg := sdk.Coins{spendable}.SafeSub(sdk.Coins{coin}) if hasNeg { return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%s is smaller than %s", spendable, coin) } newBalance := balance.Sub(coin) err := k.setBalance(ctx, addr, newBalance) if err != nil { return err } } // emit coin spent event ctx.EventManager().EmitEvent( types.NewCoinSpentEvent(addr, amt), ) return nil } // addCoins increase the addr balance by the given amt. Fails if the provided amt is invalid. // It emits a coin received event. func (k BaseSendKeeper) addCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error { if !amt.IsValid() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } for _, coin := range amt { balance := k.GetBalance(ctx, addr, coin.Denom) newBalance := balance.Add(coin) err := k.setBalance(ctx, addr, newBalance) if err != nil { return err } } // emit coin received event ctx.EventManager().EmitEvent( types.NewCoinReceivedEvent(addr, amt), ) return nil } // initBalances sets the balance (multiple coins) for an account by address. // An error is returned upon failure. func (k BaseSendKeeper) initBalances(ctx sdk.Context, addr sdk.AccAddress, balances sdk.Coins) error { accountStore := k.getAccountStore(ctx, addr) denomPrefixStores := make(map[string]prefix.Store) // memoize prefix stores for i := range balances { balance := balances[i] if !balance.IsValid() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, balance.String()) } // x/bank invariants prohibit persistence of zero balances if !balance.IsZero() { amount, err := balance.Amount.Marshal() if err != nil { return err } accountStore.Set([]byte(balance.Denom), amount) denomPrefixStore, ok := denomPrefixStores[balance.Denom] if !ok { denomPrefixStore = k.getDenomAddressPrefixStore(ctx, balance.Denom) denomPrefixStores[balance.Denom] = denomPrefixStore } // Store a reverse index from denomination to account address with a // sentinel value. denomAddrKey := address.MustLengthPrefix(addr) if !denomPrefixStore.Has(denomAddrKey) { denomPrefixStore.Set(denomAddrKey, []byte{0}) } } } return nil } // setBalance sets the coin balance for an account by address. func (k BaseSendKeeper) setBalance(ctx sdk.Context, addr sdk.AccAddress, balance sdk.Coin) error { if !balance.IsValid() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, balance.String()) } accountStore := k.getAccountStore(ctx, addr) denomPrefixStore := k.getDenomAddressPrefixStore(ctx, balance.Denom) // x/bank invariants prohibit persistence of zero balances if balance.IsZero() { accountStore.Delete([]byte(balance.Denom)) denomPrefixStore.Delete(address.MustLengthPrefix(addr)) } else { amount, err := balance.Amount.Marshal() if err != nil { return err } accountStore.Set([]byte(balance.Denom), amount) // Store a reverse index from denomination to account address with a // sentinel value. denomAddrKey := address.MustLengthPrefix(addr) if !denomPrefixStore.Has(denomAddrKey) { denomPrefixStore.Set(denomAddrKey, []byte{0}) } } return nil } // IsSendEnabledCoins checks the coins provide 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 BaseSendKeeper) IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error { for _, coin := range coins { if !k.IsSendEnabledCoin(ctx, coin) { return sdkerrors.Wrapf(types.ErrSendDisabled, "%s transfers are currently disabled", coin.Denom) } } return nil } // IsSendEnabledCoin returns the current SendEnabled status of the provided coin's denom func (k BaseSendKeeper) IsSendEnabledCoin(ctx sdk.Context, coin sdk.Coin) bool { return k.GetParams(ctx).SendEnabledDenom(coin.Denom) } // BlockedAddr checks if a given address is restricted from // receiving funds. func (k BaseSendKeeper) BlockedAddr(addr sdk.AccAddress) bool { return k.blockedAddrs[addr.String()] }