The Cashio stablecoin (CASH) protocol recently lost $50M in an attack. The attacker was able to mint 2,000,000,000 CASH tokens for almost free. The root cause is a vulnerability in the Cashio’s brrr smart contract.
Soteria team conducted an in-depth analysis of the attack and found that
(1) The attacker was sophisticated (started preparation five days before the attack)
(2) The vulnerability lies deeply in a missing check of an input (bank) account
(3) C̶a̶s̶h̶i̶o̶’̶s̶ ̶p̶a̶t̶c̶h̶ ̶t̶o̶ ̶f̶i̶x̶ ̶t̶h̶e̶ ̶m̶i̶s̶s̶i̶n̶g̶ ̶c̶h̶e̶c̶k̶ ̶i̶s̶ ̶s̶t̶i̶l̶l̶ ̶i̶n̶s̶u̶f̶f̶i̶c̶i̶e̶n̶t̶ (however, the brrr smart contract has been disabled at the time of writing). (Correction: after a careful discussion with @siintemal from Neodyme, we found that the patch indeed fixes the fake bank issue. More detail in Section “Cashio’s patch”. Credits: @sinntemal and @nick_soteria).
Importantly, the vulnerability can be automatically detected by Soteria’s Premium auto auditor. This article elaborates on the details.
The Cashio brrr program’s functionality is simple: print and burn CASH tokens. It has only two instructions: print_cash and burn_cash :
The complex part is that Cashio uses Saber LP Arrows as collateral. Both instructions must provide a sequence of input accounts related to Saber and Arrow. In total, there are 12 accounts declared in a struct BrrrCommon:
(The accounts 6–10 are wrapped in a struct SaberSwapAccounts).
Because all input accounts are supplied by untrusted users and thus could be faked, a challenge here is how to properly validate them. The brrr program includes the following checks:
Although these assert_keys_eq checks look a bit overwhelming, they are still insufficient: a crucial check on the validity of the bank account is missing! If the attacker could supply a fake bank account, then all the other checks become meaningless since bank is the root of trust.
In fact, that’s exactly what the attacker did: out of these 12 input accounts, the attacker created 8 fake accounts to pass all the validity checks.
To establish the bank account’s validity, the input accounts must satisfy two conditions:
Condition 1: The bank account’s crate_token must be valid:
Condition 2. The bank account’s crate_mint must be valid:
Condition (1) ensures that bank cannot be faked if the supplied crate_token account is valid. The reason is that bank is a PDA initialized in the bankman smart contract, with crate_token as a part of the seeds:
Condition (2) ensures that the supplied crate_mint cannot be faked — it is the same as the bank account’s crate_mint . In this context, it ensures that bank.crate_mint is indeed the CASH token mint.
T̶h̶e̶ ̶p̶a̶t̶c̶h̶ ̶i̶n̶ ̶v̶0̶.̶2̶.̶1̶ ̶i̶s̶ ̶i̶n̶s̶u̶f̶f̶i̶c̶i̶e̶n̶t̶ ̶b̶e̶c̶a̶u̶s̶e̶ ̶i̶t̶ ̶o̶n̶l̶y̶ ̶e̶n̶s̶u̶r̶e̶s̶ ̶C̶o̶n̶d̶i̶t̶i̶o̶n̶ ̶2̶,̶ ̶b̶u̶t̶ ̶n̶o̶t̶ ̶C̶o̶n̶d̶i̶t̶i̶o̶n̶ ̶1̶.̶ ̶T̶h̶e̶ ̶a̶t̶t̶a̶c̶k̶e̶r̶ ̶c̶o̶u̶l̶d̶ ̶s̶t̶i̶l̶l̶ ̶c̶r̶e̶a̶t̶e̶ ̶a̶ ̶f̶a̶k̶e̶ ̶b̶a̶n̶k̶ ̶t̶h̶a̶t̶ ̶s̶a̶t̶i̶s̶f̶i̶e̶s̶ ̶C̶o̶n̶d̶i̶t̶i̶o̶n̶ ̶2̶,̶ ̶a̶n̶d̶ ̶s̶i̶m̶i̶l̶a̶r̶l̶y̶ ̶c̶r̶e̶a̶t̶e̶ ̶7̶ ̶o̶t̶h̶e̶r̶ ̶f̶a̶k̶e̶d̶ ̶a̶c̶c̶o̶u̶n̶t̶s̶ ̶t̶o̶ ̶p̶a̶s̶s̶ ̶a̶l̶l̶ ̶t̶h̶e̶ ̶o̶t̶h̶e̶r̶ ̶v̶a̶l̶i̶d̶i̶t̶y̶ ̶c̶h̶e̶c̶k̶s̶.̶
Correction: After this post, @siintemal and Soteria team had a careful discussion on the patch in v0.2.1. It turns out the patch is indeed sufficient to also ensure Condition 1 (due to an invariant enforced in bankman, as pointed out by @nick_soteria)!
Specifically, bank and crate_token are both PDAs uniquely determined by crate_mint , due to an invariant bank.crate_mint == bank.crate_token.mint enforced in the new_bank instruction of bankman:
Thus, crate_mint to crate_token and bank relations are both 1:1.
Additional Note: With the patch, the input bank account can no longer be faked, but the bank’s curator may still mint CASH tokens for free.
The bankman contract has an authorize_collateral instruction to add a bank to a collateral account by bank’s curator. Thus, a fake collateral account can be authorized by the bank’s curator to satisfy the validity check:
However, we expect that the bank’s curator is a part of the trust base and must not be compromised.
The Soteria Premium Auto Auditor has a checker that automatically detects untrustful accounts (such as bank in this case) .
The checker uses an algorithm that infers the relationships and constraints among all input accounts. If any input account is not validated (i.e., it does not satisfy the inferred constraints) along any code path, then a potential vulnerability will be flagged.
The following shows a screenshot of a vulnerability detected on the bank account:
The tool also flags the other unvalidated accounts, such as the arrow account:
The premium version is currently open to a small number of pilot customers. sec3 team has been working hard with pilot customers to release a version to the community as soon as possible.
sec3 (formerly Soteria)is founded by leading minds in the fields of blockchain security and software verification.