Cashio was a decentralised stablecoin fully backed by interest-bearing Saber USD liquidity provider tokens.

What happened

Like with the Wormhole hack, the attacker used fake accounts to mint Cashio’s CASH tokens and stole over $52M. It is worth mentioning that Cashio was never audited by a third party.

Due to a collateral token validation flaw, the attacker minted 2 billion CASH tokens using a faked worthless token as collateral. He then burnt part of the tokens for the Saber USDT-USDC LP tokens that he swapped for $16.4 USDC and $10.8 USDT, respectively. The remaining CASH tokens were swapped out for $8.6M UST and $17M USDC through Saber. What’s curious is that the hacker embedded a hidden message in the transaction that, after viewing the input data as UTF-8 says:

“Accounts with less than 100k have been returned. All other money will be donated to charity”.

Exploit Details

Cashio has two programs: Bankman and Brrr. The Bank manager, or Bankman for short, keeps track of the collateral that is allowed to be used as a backing for the $CASH token. The Brrr program handles the printing and burning of $CASH, using Saber LP Arrows as collateral.

In order to print $CASH, the brrr program performs checks that the exploiter could circumvent. First of all, the supplied bank account must correspond to the bank of the collateral used.

impl<'info> Validate<'info> for BrrrCommon<'info> {

    fn validate(&self) -> Result<()> {

//these checks passed due to fake bank account supplied
        assert_keys_eq!(self.bank, self.collateral.bank);

        assert_keys_eq!(self.bank.crate_mint, self.crate_mint);

        assert_keys_eq!(self.crate_token, self.crate_collateral_tokens.owner);

        assert_keys_eq!(self.crate_mint, self.crate_token.mint);

        assert_keys_eq!(self.crate_collateral_tokens.mint, self.collateral.mint);

        // saber swap

        self.saber_swap.validate()?;

        assert_keys_eq!(self.collateral.mint, self.saber_swap.arrow.mint);

        Ok(())

    }

}

To pass the bank verification, the attacker simply created a new bank with the same mint as his worthless token mint.

Next, the Saber swap accounts are verified; however, the verification missed the critical check if the

saber_swap.mint 

actually matches the expected mint.

impl<'info> Validate<'info> for SaberSwapAccounts<'info> {

    fn validate(&self) -> Result<()> {

        // missing verification that the saber_swap.mint is correct

        assert_keys_eq!(self.arrow.vendor_miner.mint, self.pool_mint);

        assert_keys_eq!(self.saber_swap.pool_mint, self.pool_mint);

        assert_keys_eq!(self.saber_swap.token_a.reserves, self.reserve_a);

        assert_keys_eq!(self.saber_swap.token_b.reserves, self.reserve_b);

        Ok(())

    }

}

Therefore, the exploiter supplied his fake worthless token as collateral and passed the checks proving that the collateral token corresponds to the expected token from the bank. There was no check to ensure that the token was indeed a Saber Arrow token leading to a successful transaction.

In simple words, the exploiter had to perform multiple steps and supply the worthless tokens he created earlier as collateral to mint 2 billion CASH tokens. The reason why he managed to do this boils down to the flawed verification of input accounts.

Reference