Skip to content

Commit 17337d7

Browse files
authored
[WIP] Not so smart cosmos (#117)
Add not so smart cosmos
1 parent 8bf3238 commit 17337d7

File tree

9 files changed

+616
-0
lines changed

9 files changed

+616
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# (Not So) Smart Cosmos
2+
3+
This repository contains examples of common Cosmos applications vulnerabilities, including code from real applications. Use Not So Smart Cosmos to learn about Cosmos (Tendermint) vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools.
4+
5+
## Features
6+
7+
Each _Not So Smart Cosmos_ includes a standard set of information:
8+
9+
* Description of the vulnerability type
10+
* Attack scenarios to exploit the vulnerability
11+
* Recommendations to eliminate or mitigate the vulnerability
12+
* Real-world contracts that exhibit the flaw
13+
* References to third-party resources with more information
14+
15+
## Vulnerabilities
16+
17+
| Not So Smart Contract | Description |
18+
| --- | --- |
19+
| [Incorrect signers](incorrect_getsigners) | Broken access controls due to incorrect signers validation |
20+
| [Non-determinism](non_determinism) | Consensus failure because of non-determinism |
21+
| [Not prioritized messages](messages_priority) | Risks arising from usage of not prioritized message types |
22+
| [Slow ABCI methods](abci_fast) | Consensus failure because of slow ABCI methods |
23+
| [ABCI methods panic](abci_panic) | Chain halt due to panics in ABCI methods |
24+
| [Broken bookkeeping](broken_bookkeeping) | Exploit mismatch between different modules' views on balances |
25+
| [Rounding errors](rounding_errors) | Bugs related to imprecision of finite precision arithmetic |
26+
| [Unregistered message handler](unregistered_msg_handler) | Broken functionality because of unregistered msg handler |
27+
28+
## Credits
29+
30+
These examples are developed and maintained by [Trail of Bits](https://www.trailofbits.com/).
31+
32+
If you have questions, problems, or just want to learn more, then join the #ethereum channel on the [Empire Hacking Slack](https://empireslacking.herokuapp.com/) or [contact us](https://www.trailofbits.com/contact/) directly.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Slow ABCI methods
2+
3+
ABCI methods (like `EndBlocker`) [are not constrained by `gas`](https://docs.cosmos.network/v0.45/basics/app-anatomy.html#beginblocker-and-endblocker). Therefore, it is essential to ensure that they always will finish in a reasonable time. Otherwise, the chain will halt.
4+
5+
## Example
6+
7+
Below you can find part of a tokens lending application. Before a block is executed, the `BeginBlocker` method charges an interest for each borrower.
8+
9+
```go
10+
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
11+
updatePrices(ctx, k)
12+
accrueInterest(ctx, k)
13+
}
14+
15+
func accrueInterest(ctx sdk.Context, k keeper.Keeper) {
16+
for _, pool := range k.GetLendingPools() {
17+
poolAssets := k.GetPoolAssets(ctx, pool.Id)
18+
for userId, _ := range k.GetAllUsers() {
19+
for _, asset := range poolAssets {
20+
for _, loan := range k.GetUserLoans(ctx, pool, asset, userId) {
21+
if err := k.AccrueInterest(ctx, loan); err != nil {
22+
k.PunishUser(ctx, userId)
23+
}
24+
}
25+
}
26+
}
27+
}
28+
}
29+
```
30+
31+
The `accrueInterest` contains multiple nested for loops and is obviously too complex to be efficient. Mischievous
32+
users can take a lot of small loans to slow down computation to a point where the chain is not able to keep up with blocks production and halts.
33+
34+
## Mitigations
35+
36+
- Estimate computational complexity of all implemented ABCI methods and ensure that they will scale correctly with the application's usage growth
37+
- Implement stress tests for the ABCI methods
38+
- [Ensure that minimal fees are enforced on all messages](https://docs.cosmos.network/v0.46/basics/gas-fees.html#introduction-to-gas-and-fees) to prevent spam
39+
40+
## External examples
41+
- [Gravity Bridge's `slashing` method was executed in the `EndBlocker` and contained a computationally expensive, nested loop](https://github.com/althea-net/cosmos-gravity-bridge/issues/347).
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# ABCI methods panic
2+
3+
A `panic` inside an ABCI method (e.g., `EndBlocker`) will stop the chain. There should be no unanticipated `panic`s in these methods.
4+
5+
Some less expected `panic` sources are:
6+
* [`Coins`, `DecCoins`, `Dec`, `Int`, and `UInt` types panics a lot](https://github.com/cosmos/cosmos-sdk/blob/afbb0bd1941f7ad36e086913153af02eb6a68f5a/types/coin.go#L68), [for example on overflows](https://github.com/cosmos/cosmos-sdk/blob/afbb0bd1941f7ad36e086913153af02eb6a68f5a/types/dec_coin.go#L105) and [rounding errors](https://github.com/cosmos/cosmos-sdk/blob/afbb0bd1941f7ad36e086913153af02eb6a68f5a/types/decimal.go#L648)
7+
* [`new Dec` panics](https://pkg.go.dev/github.com/cosmos/cosmos-sdk/types@v0.45.5#Dec)
8+
* [`x/params`'s `SetParamSet` panics if arguments are invalid](https://github.com/cosmos/cosmos-sdk/blob/1b1dbf8ab722e4689e14a5a2a1fc433b69bc155e/x/params/doc.go#L107-L108)
9+
10+
## Example
11+
12+
The application below enforces limits on how much coins can be borrowed globally. If the `loan.Borrowed` array of Coins can be forced to be not-sorted (by coins' denominations), the `Add` method will `panic`.
13+
14+
Moreover, the `Mul` may panic if some asset's price becomes large.
15+
16+
```go
17+
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
18+
if !validateTotalBorrows(ctx, k) {
19+
k.PauseNewLoans(ctx)
20+
}
21+
}
22+
23+
func validateTotalBorrows(ctx sdk.Context, k keeper.Keeper) {
24+
total := sdk.NewCoins()
25+
for _, loan := range k.GetUsersLoans() {
26+
total.Add(loan.Borrowed...)
27+
}
28+
29+
for _, totalOneAsset := range total {
30+
if totalOneAsset.Amount.Mul(k.GetASsetPrice(totalOneAsset.Denom)).GTE(k.GetGlobalMaxBorrow()) {
31+
return false
32+
}
33+
}
34+
return true
35+
}
36+
```
37+
38+
## Mitigations
39+
40+
- [Use CodeQL static analysis](https://github.com/crypto-com/cosmos-sdk-codeql/blob/main/src/beginendblock-panic.ql) to detect `panic`s in ABCI methods
41+
- Review the code against unexpected `panic`s
42+
43+
## External examples
44+
- [Gravity Bridge can `panic` in multiple locations in the `EndBlocker` method](https://giters.com/althea-net/cosmos-gravity-bridge/issues/348)
45+
- [Agoric `panic`s purposefully if the `PushAction` method returns an error](https://github.com/Agoric/agoric-sdk/blob/9116ede69169ebb252faf069d90022e8e05c6a4e/golang/cosmos/x/vbank/module.go#L166)
46+
- [Setting invalid parameters in `x/distribution` module causes `panic` in `BeginBlocker`](https://github.com/cosmos/cosmos-sdk/issues/5808). Valid parameters are [described in the documentation](https://docs.cosmos.network/v0.45/modules/distribution/07_params.html).
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Broken Bookkeeping
2+
3+
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.
4+
5+
## Example
6+
7+
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.
8+
9+
```go
10+
func BalanceInvariant(k Keeper) sdk.Invariant {
11+
return func(ctx sdk.Context) (string, bool) {
12+
weAreFine := true
13+
msg := "hodling hard"
14+
15+
weHold := k.bankKeeper.SpendableCoins(authtypes.NewModuleAddress(types.ModuleName)).AmountOf("BTC")
16+
usersDeposited := k.GetTotalDeposited("BTC")
17+
18+
if weHold != usersDeposited {
19+
msg = fmt.Sprintf("%dBTC missing! Halting chain.\n", usersDeposited - weHold)
20+
weAreFine = false
21+
}
22+
23+
return sdk.FormatInvariant(types.ModuleName, "hodl-balance",), weAreFine
24+
}
25+
}
26+
```
27+
28+
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`.
29+
30+
Because an invariant's failure stops the chain, the bug constitutes a simple Denial-of-Service attack vector.
31+
32+
## Example 2
33+
34+
An example application implements a lending platform. It allows users to deposit Tokens in exchange for xTokens - similarly to the [Compound's cTokens](https://compound.finance/docs/ctokens#exchange-rate). Token:xToken exchange rate is calculated as `(amount of Tokens borrowed + amount of Tokens held by the module account) / (amount of uTokens in circulation)`.
35+
36+
Implementation of the `GetExchangeRate` method computing an exchange rate is presented below.
37+
38+
```go
39+
func (k Keeper) GetExchangeRate(tokenDenom string) sdk.Coin {
40+
uTokenDenom := createUDenom(tokenDenom)
41+
42+
tokensHeld := k.bankKeeper.SpendableCoins(authtypes.NewModuleAddress(types.ModuleName)).AmountOf(tokenDenom).ToDec()
43+
tokensBorrowed := k.GetTotalBorrowed(tokenDenom)
44+
uTokensInCirculation := k.bankKeeper.GetSupply(uTokenDenom).Amount
45+
46+
return (tokensHeld + tokensBorrowed) / uTokensInCirculation
47+
}
48+
49+
```
50+
51+
A malicious user can screw an exchange rate in two ways:
52+
53+
* by force-sending Tokens to the module, changing the `tokensHeld` value
54+
* by transferring uTokens to another chain via IBC, chaning `uTokensInCirculation` value
55+
56+
The first "attack" could be pulled of by sending [`MsgSend`](https://docs.cosmos.network/main/modules/bank/03_messages.html#msgsend) message. However, it would be not profitable (probably), as executing it would irreversibly decrease an attacker's resources.
57+
58+
The second one works because the IBC module [burns transferred coins in the source chain](https://github.com/cosmos/ibc-go/blob/48a6ae512b4ea42c29fdf6c6f5363f50645591a2/modules/apps/transfer/keeper/relay.go#L135-L136) 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.
59+
60+
61+
## Mitigations
62+
63+
- Use [`Blocklist` to prevent unexpected token transfers](https://docs.cosmos.network/v0.45/modules/bank/02_keepers.html#blocklisting-addresses) to specific addresses
64+
- Use [`SendEnabled` parameter to prevent unexpected transfers](https://docs.cosmos.network/v0.45/modules/bank/05_params.html#parameters) of specific tokens (denominations)
65+
- Ensure that the blocklist is explicitly checked [whenever a new functionality allowing for tokens transfers is implemented](https://github.com/cosmos/cosmos-sdk/issues/8463#issuecomment-801046285)
66+
67+
## External examples
68+
69+
- [Umee was vulnerable to the token:uToken exchange rate manipulation](https://github.com/trailofbits/publications/blob/master/reviews/Umee.pdf) (search for finding TOB-UMEE-21).
70+
- [Desmos incorrectly blocklisted addresses](https://github.com/desmos-labs/desmos/blob/e3c89e2f9ddd5dfde5d11c3ad5319e3c249cacb3/CHANGELOG.md#version-0154) (check app.go file in [the commits diff](https://github.com/desmos-labs/desmos/compare/v0.15.3...v0.15.4))
71+
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Incorrect Signers
2+
3+
In Cosmos, transaction's signature(s) are validated against public keys (addresses) taken from the transaction itself,
4+
where locations of the keys [are specified in `GetSigners` methods](https://docs.cosmos.network/v0.46/core/transactions.html#signing-transactions).
5+
6+
In the simplest case there is just one signer required, and its address is simple to use correctly.
7+
However, in more complex scenarios like when multiple signatures are required or a delegation schema is implemented,
8+
it is possible to make mistakes about what addresses in the transaction (the message) are actually authenticated.
9+
10+
Fortunately, mistakes in `GetSigners` should make part of application's intended functionality not working,
11+
making it easy to spot the bug.
12+
13+
## Example
14+
15+
The example application allows an author to create posts. A post can be created with a `MsgCreatePost` message, which has `signer` and `author` fields.
16+
17+
```proto
18+
service Msg {
19+
rpc CreatePost(MsgCreatePost) returns (MsgCreatePostResponse);
20+
}
21+
22+
message MsgCreatePost {
23+
string signer = 1;
24+
string author = 2;
25+
string title = 3;
26+
string body = 4;
27+
}
28+
29+
message MsgCreatePostResponse {
30+
uint64 id = 1;
31+
}
32+
33+
message Post {
34+
string author = 1;
35+
uint64 id = 2;
36+
string title = 3;
37+
string body = 4;
38+
}
39+
```
40+
41+
The `signer` field is used for signature verification - as can be seen in `GetSigners` method below.
42+
43+
```go
44+
func (msg *MsgCreatePost) GetSigners() []sdk.AccAddress {
45+
signer, err := sdk.AccAddressFromBech32(msg.Signer)
46+
if err != nil {
47+
panic(err)
48+
}
49+
return []sdk.AccAddress{Signer}
50+
}
51+
52+
func (msg *MsgCreatePost) GetSignBytes() []byte {
53+
bz := ModuleCdc.MustMarshalJSON(msg)
54+
return sdk.MustSortJSON(bz)
55+
}
56+
57+
func (msg *MsgCreatePost) ValidateBasic() error {
58+
_, err := sdk.AccAddressFromBech32(msg.Signer)
59+
if err != nil {
60+
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
61+
}
62+
return nil
63+
}
64+
```
65+
66+
The `author` field is saved along with the post's content:
67+
68+
```go
69+
func (k msgServer) CreatePost(goCtx context.Context, msg *types.MsgCreatePost) (*types.MsgCreatePostResponse, error) {
70+
ctx := sdk.UnwrapSDKContext(goCtx)
71+
72+
var post = types.Post{
73+
Author: msg.Author,
74+
Title: msg.Title,
75+
Body: msg.Body,
76+
}
77+
78+
id := k.AppendPost(ctx, post)
79+
80+
return &types.MsgCreatePostResponse{Id: id}, nil
81+
}
82+
```
83+
84+
The bug here - mismatch between the message signer address and the stored address - allows users to impersonate other users by sending an arbitrary `author` field.
85+
86+
## Mitigations
87+
88+
- Keep signers-related logic simple
89+
- Implement basic sanity tests for all functionalities
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Not prioritized messages
2+
3+
Some message types may be more important than others and should have priority over them. That is, the more significant a message type is, the more quickly it should be included in a block, before other messages are.
4+
5+
Failing to prioritize message types allows attackers to front-run them, possibly gaining unfair advantage. Moreover, during high network congestion, the message may be simply not included in a block for a long period, causing the system to malfunction.
6+
7+
In the Cosmos's mempool, transactions are [ordered in first-in-first-out (FIFO) manner](https://github.com/tendermint/tendermint/blob/f9e0f77af333f4ab7bfa1c0c303f7db47cec0c9e/docs/architecture/adr-067-mempool-refactor.md#context) by default. Especially, there is no fee-based ordering.
8+
9+
## Example
10+
11+
An example application implements a lending platform. It uses a price oracle mechanism, where privileged entities can vote on new assets' prices. The mechanism is implemented as standard messages.
12+
13+
```go
14+
service Msg {
15+
rpc Lend(MsgLend) returns (MsgLendResponse);
16+
17+
rpc Borrow(MsgBorrow) returns (MsgBorrowResponse);
18+
19+
rpc Liquidate(MsgLiquidate) returns (MsgLiquidateResponse);
20+
21+
rpc OracleCommitPrice(MsgOracleCommitPrice) returns (MsgOracleCommitPriceResponse);
22+
23+
rpc OracleRevealPrice(MsgOracleRevealPrice) returns (MsgOracleRevealPriceResponse);
24+
}
25+
```
26+
27+
Prices ought to be updated (committed and revealed) after every voting period. However, an attacker can spam the network with low-cost transactions to completely fill blocks, leaving no space for price updates. He can then profit from the fact that the system uses outdated, stale prices.
28+
29+
## Example 2
30+
31+
Lets consider a liquidity pool application that implements the following message types:
32+
33+
```go
34+
service Msg {
35+
rpc CreatePool(MsgCreatePool) returns (MsgCreatePoolResponse);
36+
37+
rpc Deposit(MsgDeposit) returns (MsgDepositResponse);
38+
39+
rpc Withdraw(MsgWithdraw) returns (MsgWithdrawResponse);
40+
41+
rpc Swap(MsgSwap) returns (MsgSwapResponse);
42+
43+
rpc Pause(MsgPause) returns (MsgPauseResponse);
44+
45+
rpc Resume(MsgResume) returns (MsgResumeResponse);
46+
}
47+
```
48+
49+
There is the `Pause` message, which allows privileged users to stop the pool.
50+
51+
Once a bug in pool's implementation is discovered, attackers and the pool's operators will compete for whose message is first executed (`Swap` vs `Pause`). Prioritizing `Pause` messages will help pool's operators to prevent exploitation, but in this case it won't stop the attackers completely. They can outrun the `Pause` message by order of magnitude - so the priority will not matter - or even cooperate with a malicious validator node - who can order his mempool in an arbitrary way.
52+
53+
54+
## Mitigations
55+
56+
- [Use `CheckTx`'s `priority` return value](https://github.com/tendermint/spec/blob/v0.7.1/spec/abci/abci.md#checktx-1) to prioritize messages. Please note that this feature has a transaction (not a message) granularity - users can send multiple messages in a single transaction, and it is the transaction that will have to be prioritized.
57+
- Perform authorization for prioritized transactions as early as possible. That is, during the `CheckTx` phase. This will prevent attackers from filling whole blocks with invalid, but prioritized transactions. In other words, implement a mechanism that will prevent validators from accepting not-authorized, prioritized messages into a mempool.
58+
- Alternatively, charge a high fee for prioritized transactions to disincentivize attackers.
59+
60+
## External examples
61+
- [Terra Money's oracle messages were not prioritized](https://cryptorisks.substack.com/p/ust-december-2021) (search for "priority"). It was [fixed with modifications to Tendermint](https://github.com/terra-money/tendermint/commit/6805b4866bdbd6933000eb0e761acbf15edd8ed6).
62+
- [Umee oracle and orchestrator messages were not prioritized](https://github.com/trailofbits/publications/blob/master/reviews/Umee.pdf) (search for finding TOB-UMEE-20 and TOB-UMEE-31).

0 commit comments

Comments
 (0)