My co-worker Jason just published a super sick bug in the main implementation of EVM integration in Cosmos. Under the hood, the execution is done with Geth but the integration with Cosmos is complicated to do correctly with many different areas of state to consider.
The stateDB in Geth contains a single journal of all state changes in the current transaction. When a new execution context is created, such as on a function call, a snapshot is taken. This adds a new revision and an index in the journal. In case of a revert, all changes within a particular jouralindex can be undone.
In Cosmos, Commit() on a cached storage is what stores the data into permanent memory. It is crucial to ensure that the Cosmos storage and the Geth journal storage line up. However, during Evmos specific precompiles (such as for the Staking and Distribution module), it's possible to desync these two.
The steps are outlined in the post well with a simple looking yet specific PoC contract:
- Contract calls an external function within a
try/catch block.
- A new contract is created for a contract that will transfer ETH.
- A call is made to the Distribution precompile contract. This will trigger the
Commit(). In particular, the balance of the ETH is saved in the target contract.
- Revert the call within the try/catch block. The rollback done by the EVM's state is not accurate now! Even though the contract shouldn't exist, it's still in storage and holds a balance.
- Withdraw the balance from the
target contract that shouldn't exist still.
In point 4 above, they have some notes on WHY this happens. The rollback on the revert does not reflect the changes in permanent storage. Since the contract creation happened post snapshot, the dirty mappings are removed after the revert. Since only the dirty accounts are touched, there's no reason to make any changes to the created contract. As a result, when the actual state is updated at the end of execution, the target contract is valid and alive, even though it should have been destroyed.
This vulnerability required a very deep insight into how the state handling was being done by the EVM execution and by EVMOS. Overall, a solid vulnerability that was hard to wrap my head around but is amazing none-the-less.