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.
Example
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.
Mitigations
- Use
Blocklist
to prevent unexpected token transfers to specific addresses - Use
SendEnabled
parameter to prevent unexpected transfers of specific tokens (denominations) - Ensure that the blocklist is explicitly checked whenever a new functionality allowing for tokens transfers is implemented
External examples
- Umee was vulnerable to the token:uToken exchange rate manipulation (search for finding TOB-UMEE-21).
- Desmos incorrectly blocklisted addresses (check app.go file in the commits diff)