Broken Bookkeeping

The x/bank module is the standard way to manage tokens in a cosmos-sdk based applications. The module allows to mint, burn, and transfer coins between both users' and modules' accounts. If an application implements its own, internal bookkeeping, it must carefully use the x/bank's features.


An application enforces the following invariant as a sanity-check: amount of tokens owned by a module equals to the amount of tokens deposited by users via the custom x/hodl module.

func BalanceInvariant(k Keeper) sdk.Invariant {
    return func(ctx sdk.Context) (string, bool) {
        weAreFine := true
        msg := "hodling hard"

        weHold := k.bankKeeper.SpendableCoins(authtypes.NewModuleAddress(types.ModuleName)).AmountOf("BTC")
        usersDeposited := k.GetTotalDeposited("BTC")

        if weHold != usersDeposited {
            msg = fmt.Sprintf("%dBTC missing! Halting chain.\n", usersDeposited - weHold)
            weAreFine = false

        return sdk.FormatInvariant(types.ModuleName, "hodl-balance",), weAreFine

A spiteful user can simply transfer a tiny amount of BTC tokens directly to the x/hodl module via a message to the x/bank module. That would bypass accounting of the x/hodl, so the GetTotalDeposited function would report a not-updated amount, smaller than the module's SpendableCoins.

Because an invariant's failure stops the chain, the bug constitutes a simple Denial-of-Service attack vector.

Example 2

An example application implements a lending platform. It allows users to deposit Tokens in exchange for xTokens - similarly to the Compound's cTokens. Token:xToken exchange rate is calculated as (amount of Tokens borrowed + amount of Tokens held by the module account) / (amount of uTokens in circulation).

Implementation of the GetExchangeRate method computing an exchange rate is presented below.

func (k Keeper) GetExchangeRate(tokenDenom string) sdk.Coin {
    uTokenDenom := createUDenom(tokenDenom)

    tokensHeld := k.bankKeeper.SpendableCoins(authtypes.NewModuleAddress(types.ModuleName)).AmountOf(tokenDenom).ToDec()
    tokensBorrowed := k.GetTotalBorrowed(tokenDenom)
    uTokensInCirculation := k.bankKeeper.GetSupply(uTokenDenom).Amount

    return (tokensHeld + tokensBorrowed) / uTokensInCirculation

A malicious user can screw an exchange rate in two ways:

  • by force-sending Tokens to the module, changing the tokensHeld value
  • by transferring uTokens to another chain via IBC, chaning uTokensInCirculation value

The first "attack" could be pulled of by sending MsgSend message. However, it would be not profitable (probably), as executing it would irreversibly decrease an attacker's resources.

The second one works because the IBC module burns transferred coins in the source chain and mints corresponding tokens in the destination chain. Therefore, it will decrease the supply reported by the x/bank module, increasing the exchange rate. After the attack the malicious user can just transfer back uTokens.


External examples