How Abracadabra was hacked for $6.5m worth MIM(Magic Internet Money)

EIP 4626 vault like hack.

·

6 min read

How Abracadabra was hacked for $6.5m worth MIM(Magic Internet Money)

"A rounding donation attack requires three things:

  1. A way to get one key value to empty / almost empty

  2. A way to massively inflate another key number

  3. A way to exploit the resulting rounding errors"

Daniel Von Fange

Note: Transaction trace of the attack...

CodeBase

Abracadabra implements a 1:1 shares representation of assets deposited, just like is obtainable in EIP4626 style vaults. The exploit happened cause the user was able to change the ratio of the shares to asset significantly due to a feature on the contract itself.

The effected contract CauldronV4 is a single lending pool with mAPE as collateral and MIM as lending Token.

$$Shares = \frac{Asset * TotalShares}{TotalAssets}$$

$$Asset = \frac{Shares * TotalAssets}{TotalAsset}$$

This is a 1:1 ratio representation of asset amount to shares. The contract also rounds down in favour of the vault when depositing and rounds up when borrowing and repaying loans. To begin this attack the attacker supplier collateral worth about about 10k mAPE and got a resulting share representation. Then proceeded to borrow 5k MIM.

part here represents shares in the Cauldron contract, wile shares represent shares in DegenBox vault

(Part in the image represents shares in the Cauldron contract and Shares presents shares in the DegenBox vault)

CauldronV4 balances

TotalBorrowShares = 310465465543797797127

TotalBorrowAmount = 367702635609352029399

Collateral Amount = 10024734352538017028101 mAPE

$$BorrowShares = \frac{(amount * TotalBorrowShares)}{ TotalBorrowAmount}$$

$$BorrowShares = \frac{(5000000000000000000000*310465465543797797127)} {367702635609352029399}$$

$$BorrowShares = 4221692143017937064250.8 = 4221692143017937064250$$

$$BorrowShares < BorrowAmount ? BorrowShares + 1 = 4221692143017937064251$$

$$BorrowShares = 4221692143017937064251$$

So attacker borrow share balance is incremented by

$$BorrowShares = 4221692143017937064251$$

$$TotalBorrowShares = 310465465543797797127 + 4221692143017937064251 = 4532157608561734861378$$

$$TotalBorrowAmount= 367702635609352029399 + 5000000000000000000000 = 5367702635609352029399$$

Repay all BorrowBalances

CauldronV4 allows open debt positions to be repaid by anyone, at once using the repayAll function.

function repayForAll(uint128 amount, bool skim) public returns(uint128) {
        accrue();

        if(skim) {
            // ignore amount and take every mim in this contract since it could be taken by anyone, the next block.
            amount = uint128(magicInternetMoney.balanceOf(address(this)));
            bentoBox.deposit(magicInternetMoney, address(this), address(this), amount, 0);
        } else {
            bentoBox.transfer(magicInternetMoney, msg.sender, address(this), bentoBox.toShare(magicInternetMoney, amount, true));
        }

        uint128 previousElastic = totalBorrow.elastic;

        require(previousElastic - amount > 1000 * 1e18, "Total Elastic too small");

        totalBorrow.elastic = previousElastic - amount;

        emit LogRepayForAll(amount, previousElastic, totalBorrow.elastic);
        return amount;
    }

The challenge with this is that, when repaying the open debt positions, it only reduces the TotalBorrows. Elastic in the contract is equivalent to asset amount, and base equivalent to shares.

totalBorrow.elastic = previousElastic - amount;

So because of this, the attacker exploits this function to unbalance the share to asset ratio representation.

A repayment of of 4kMIM is made.

V4 balances before repayment.

TotalBorrowShares = 4532157608561734861378

TotalBorrowAmount= 5367702635609352029399

After

TotalBorrowAmount= 5367702635609352029399 - 4k1e18 = 1367702635609352029399

Exchange rate Before Repayment

Exchange Rate = TotalAssets / TotalShares

ExchangeRate = 5367702635609352029399 / 4532157608561734861378 = 1.184 = 1

1 share = 1 Asset

After repayment

Share Rate \= 1367702635609352029399 / 4532157608561734861378 = 0.30 = 0

1 Share = 0 Asset

4 Shares = 1 Asset

Repay all balances individually.

The attacker then proceeds to repay each account balance individually, this time using the repay function, which reduces both the shares and amount.

    function _repay(
        address to,
        bool skim,
        uint256 part
    ) internal returns (uint256 amount) {
        (totalBorrow, amount) = totalBorrow.sub(part, true);
        userBorrowPart[to] = userBorrowPart[to].sub(part);

        uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);
        bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);
        emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);
    }

    /// @notice Repays a loan.
    /// @param to Address of the user this payment should go.
    /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.
    /// False if tokens from msg.sender in `bentoBox` should be transferred.
    /// @param part The amount to repay. See `userBorrowPart`.
    /// @return amount The total amount repayed.
    function repay(
        address to,
        bool skim,
        uint256 part
    ) public returns (uint256 amount) {
        accrue();
        amount = _repay(to, skim, part);
    }


    /// @notice Sub `base` from `total` and update `total.elastic`.
    /// @return (Rebase) The new total.
    /// @return elastic in relationship to `base`.
    function sub(
        Rebase memory total,
        uint256 base,
        bool roundUp
    ) internal pure returns (Rebase memory, uint256 elastic) {
        elastic = toElastic(total, base, roundUp);
        total.elastic -= uint128(elastic);
        total.base -= uint128(base);
        return (total, elastic);
    }
UserBorrowAmountBorrowShares
1.61044875481177018152202284465696344636046
2.1302469631470130362343159949366903061281
3.316414013999715256999548659121510118956
4.3776568433076065981251441864085761231
5.23356990568581352687739808329852196274
6.91252409304910033302383201116359505
7.240860156855242570917268614637802482637
8.12740111817446904541304221692143017937064243

TotalRepaidAmount = 13024696314701303623 + 9548659121510118956 + 377656843307606598 + 2335699056858135268 + 91252409304910033 + 7268614637802482637 + 1274011181744690454130 + 61044875481177018152 = 1367702635609352029397

TotalRepaidShares = 61044875481177018152+43159949366903061281+31641401399971525699+1251441864085761231+7739808329852196274+302383201116359505+24086015685524257091+4221692143017937064243+ 202284465696344636046 = 4532157608561734861370

TotalBorrowShares = TotalBorrowShares - TotalRepaidShares

TotalBorrowShares = 4532157608561734861378 - 4532157608561734861370 = 8

TotalBorrowAmount = TotalBorrowAmount - TotalRepaidAmount.

TotalBorrowAmount = 1367702635609352029399 - 1367702635609352029397 = 2

Repay own loan

The remaining 2 amount belongs to the balance of the attacker, it then repays it too.

Amount = Shares * TotalAmount / TotalShares = 1 * 2 / 8 = 0.5 < Shares ? 0 + 1 = 1

Amount = Shares * TotalAmount / TotalShares = 1 * 1 / 7 = 0.1 < Shares ? 0+ 1 = 1

TotalAmount = 2-2 = 0

TotalShares = 8 - 2 = 6

Inflate dem shares

The attacker then proceeds to give execution to a second attack contract it controls,

Attacker contract 2: 0xd4d29229848efaa0af4ba731754df5f44d5bca29

The second attack contract, then proceeds to add 100 amount collateral to the market, which gets it 100 CollateralShares.

After which it goes on a borrow and repayment spree of just 1 asset or share each time, in a bid to inflate the total shares and keep the total amount at either 1 or zero. Part in the image represents shares and amount asset amount borrowed.

The first three transactions:

TotalAmount = 0

TotalShares = 6.

Borrow 1 amount

Shares = (amount totalShares) / TotalAsset = 1 \ 6 / 0 = 1;*

This is because the toBase function that converts amount to shares

function toBase(
        Rebase memory total,
        uint256 elastic,
        bool roundUp
    ) internal pure returns (uint256 base) {
        if (total.elastic == 0) {
            base = elastic;
        } else {
            base = (elastic * total.base) / total.elastic;
            if (roundUp && (base * total.elastic) / total.base < elastic) {
                base++;
            }
        }
    }

Like seen above, returns base = elastic (share = amount) if totalAmount/TotalElastic is 0.

TotalShares = 6+1 = 7;

TotalAmount = 0+1=1

Borrow 1 amount

Share = amount \ TotalShare / TotalAmount = 1 \ 7 / 1 = 7;

TotalShares = 7 + 7 = 14;

TotalAmount = 1 + 1 = 2

Repay 1 share of borrowBalance

Amount = (Share \TotalAmount) / TotalShare = 1 \ 2 / 14 = 0.1 = 0 + 1 (roundup) = 1

TotalShares = 14 - 1 = 13

TotalAsset = 2-1 = 1;

Continues

The attacker performs this action, of borrow and repay, in uniform, to maintain amount at 1 and keep shares inflated. The at the end, it repays twice, to keep TotalAmount at 0 and TotalShares at an inflated value.

TotalAmount = 0

TotalShares = 237684487542793012780631851008

Borrow 2000000369516986964469862 worth of MIM on 100 collateral shares

Because it had inflated the shares astronomically, then proceeds to borrow 2000000369516986964469862 worth of MIM on just 100 shares worth of collateral.

Resources

  1. Transaction trace

  2. Kankodu on inflation attack

  3. Daniel Von Fange on Inflation attack

  4. hoshiyari420 on inflation