How Abracadabra was hacked for $6.5m worth MIM(Magic Internet Money)
EIP 4626 vault like hack.
Table of contents
"A rounding donation attack requires three things:
A way to get one key value to empty / almost empty
A way to massively inflate another key number
A way to exploit the resulting rounding errors"
Note: Transaction trace of the attack...
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 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);
}
User | BorrowAmount | BorrowShares |
1. | 61044875481177018152 | 202284465696344636046 |
2. | 13024696314701303623 | 43159949366903061281 |
3. | 31641401399971525699 | 9548659121510118956 |
4. | 377656843307606598 | 1251441864085761231 |
5. | 2335699056858135268 | 7739808329852196274 |
6. | 91252409304910033 | 302383201116359505 |
7. | 24086015685524257091 | 7268614637802482637 |
8. | 1274011181744690454130 | 4221692143017937064243 |
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