Josh Fraser 2021-2-13.
Alpha Finance Lab is an ecosystem of DeFi products w/ a focus on innovation.
This attack was on Alpha Homora v2, which is a protocol for leveraging your position in yield farming pools. ETH lenders can earn high interest on ETH, and yield farmers can get even higher farming APY from taking on leveraged positions on yield farming.
The Iron Bank is a new project from Andre Cronje that allows trusted protocols to borrow funds from Cream v2 w/o posting any collateral. Borrowers have a credit limit and must be whitelisted. Yearn and Alpha Finance were two of the initial whitelisted partners.
To get a credit line from the Iron Bank, a user must have one of the following three backstops to ensure the debt is paid back:
- A treasury large enough to cover credit
- Cover Protocol insurance large enough to cover credit
- Nexus Mutual insurance large enough to cover credit
It appears this attacker pretended to be Alpha and used Alpha's privledged access in order to take out a huge loan from the Iron Bank which was sent to the attacker instead of being used for the intended purpose of generating yield for the protocol.
Alpha Homora is a borrower from Iron Bank, but it's also a lending platform itself with it's own borrowers and lenders. Currently ibETHv2 collateral is at 100% utilization on Alpha Homora and lenders are locked from withdrawing their capital. Their other pools have unusally high utilization factors.
As if often the case, this attack involved a flash loan and multiple DeFi players, including the newly launched IronBank from Cream. At first glance, it looked like the money came from Cream Finance, but communication from both the Cream & Alpha teams quickly identified the root cause and victim of the attack as Alpha Finance.
Within the last few days, Alpha Homora had deployed an upgrade aimed at preventing flash loans by adding an onlyEOA()
modifier. As a result of this code change, this attack required 9 seperate transactions instead of 1. However, while this flash-loan prevention slowed down the attack, it still didn't prevent $37.5M worth of ETH and stablecoins from being taken.
As result of this attack, Alpha finance is left with a debt that they need to pay back to the Iron Bank. Alpha Homora's treasury currently holds $1.6B worth of ALPHA tokens. It is unclear whether this treasury will be used to pay off the debt or they will resolve this debt in some other way.
After the attack, the hacker sent 1k ETH back to Alpha's deployer address, 1k ETH to CREAM Finance’s deployer address. 100 ETH to Tornado Cash, and 100 ETH to the Gitcoin grant for Tornado.
-
Attackers wallet: 0x905315602ed9a854e325f692ff82f58799beab57
-
Attack contract: 0x560a8e3b79d23b0a525e15c6f3486c6a293ddad2
Alpha Homora was previously audited by Quantstamp and Peckshield. Quantstamp found 4 high-severity issues which were all subsequently fixed. Peckshield did not report any high-severity issues.
Here are the steps of the attack, copied verbatim from the Alpha's post-mortem published on their blog.
-
The attacker created an evil spell (can think of this as equivalent to Yearn’s strategy). https://etherscan.io/tx/0x2b419173c1f116e94e43afed15a46e3b3a109e118aba166fcca0ba583f686d23
-
Attacker swaps ETH -> UNI, and supply ETH + UNI to Uniswap pool (obtaining ETH/UNI LP token). In the same tx, swap ETH -> sUSD on Uniswap and deposit sUSD to Cream’s Iron Bank (getting cysUSD) https://etherscan.io/tx/0x4441eefe434fbef9d9b3acb169e35eb7b3958763b74c5617b39034decd4dd3ad
-
Call execute to HomoraBankV2 using the evil spell (creating position 883), performing:
- Borrow 1000e18 sUSD
- Deposit UNI-WETH LP to WERC20, and use as collateral (to bypass the collateral > borrow check)
- In the process, the attacker has 1000e18 sUSD debt shares (because the attacker is the first borrower) https://etherscan.io/tx/0xcc57ac77dc3953de7832162ea4cd925970e064ead3f6861ee40076aca8e7e571
- Call execute to HomoraBankV2 using the evil spell again (to position 883), performing:
- Repay 1000000098548938710983 sUSD (actual debt with interest accrued is 1000000098548938710984 sUSD), resulting in a repay share of 1 less than the total share.
- As a result, the attacker now has 1 minisUSD debt and 1 debt share. https://etherscan.io/tx/0xf31ee9d9e83db3592601b854fe4f8b872cecd0ea2a3247c475eea8062a20dd41
-
Call resolveReserve on sUSD bank, accruing 19709787742196 debt, while totalShare remains 1. Current state: totalDebt = 19709787742197, while totalShare = 1
https://etherscan.io/tx/0x98f623af655f1e27e1c04ffe0bc8c9bbdb35d39999913bedfe712d4058c67c0e
-
Call execute to HomoraBankV2 using the evil spell again, performing (repeat 16 times, each time doubling the borrowed amount):
-
Borrow 19709787742196 minisUSD and transfer to the attacker (doubling each time, since totalDebt doubles each time the borrow is successful). Each borrow is 1 less than the totalDebt value, causing the corresponding borrow share = 0, so the protocol treats this as no debt borrowing.
At the end of tx, the attacker deposits 19.54 sUSD to Cream’s Iron Bank. https://etherscan.io/tx/0x2e387620bb31c067efc878346742637d650843210596e770d4e2d601de5409e3
-
Continue the process: call execute to HomoraBankV2 using the evil spell again, performing (repeat 10 times, each time doubling the borrowed amount). At the end of tx, the attacker deposits 1321 sUSD to Cream’s Iron Bank. https://etherscan.io/tx/0x64de824a7aa339ff41b1487194ca634a9ce35a32c65f4e78eb3893cc183532a4
-
Flashloan from aave (borrowing 1,800,000 USDC)
-
Swap 1,800,000 USDC to 1,770,757.56254472419047906 sUSD, and deposit to Cream to have enough liquidity for the attacker to borrow using the custom spell
-
Continued doubling the sUSD borrow from 1,322.70 sUSD to 677,223.15 sUSD (total of 10 times).
-
Swap 1,353,123.59 sUSD to 1,374,960.72 USDC on Curve
-
Borrow 426,659.27 USDC from Cream (since the attacker deposited sUSD already in step b.)
https://etherscan.io/tx/0x7eb2436eedd39c8865fcc1e51ae4a245e89765f4c64a13200c623f676b3912f9
-
Repeat step 8, but with ~10M USDC (no USDC borrowing at the end) https://etherscan.io/tx/0xd7a91172c3fd09acb75a9447189e1178ae70517698f249b84062681f43f0e26e
-
Repeat with 10M USDC (no USDC borrowing at the end)
https://etherscan.io/tx/0xacec6ddb7db4baa66c0fb6289c25a833d93d2d9eb4fbe9a8d8495e5bfa24ba57
- Borrow 13,244.63 WETH + 3.6M USDC + 5.6M USDT + 4.26M DAI
- Supply the stablecoins to Aave (to get aTokens, so USDC & USDT can’t be frozen)
- Supply aDAI, aUSDT, aUSDC to Curve a3Crv pool https://etherscan.io/tx/0x745ddedf268f60ea4a038991d46b33b7a1d4e5a9ff2767cdba2d3af69f43eb1b
-
Add a3Crv LP token to Curve’s liquidity gauge
https://etherscan.io/tx/0xc60bc6ab561af2a19ebc9e57b44b21774e489bb07f75cb367d69841b372fe896
-
The rest of txs are supplying to Tornade Cash, GitCoin Grants. 1k ETH is sent to each of Cream’s and Alpha’s deployer addresses.
Alpha's newly deployed Bank contract includes a whitelist of 5 spells that was missing in the published version of this contract in Github. This initially lead me to believe that this missing validation check was the issue, but Banteg & Samczsun let me know that spells were open to anyone by design. It's clear from Alpha's post-mortem that this was the sledgehammer approach to fixing the issue quickly and not the actual vulnerability.
function execute(
uint positionId,
address spell,
bytes memory data
) external payable lock onlyEOA returns (uint) {
require(
spell == 0x17c0b6568F5d72b796269e0F43dDd881AC13110b ||
spell == 0xc671B7251a789de0835a2fa33c83c8D4afB39092 ||
spell == 0x42C750024E02816eE32EB2eB4DA79ff5BF343D30 ||
spell == 0x15B79c184A6a8E19a4CA1F637081270343E4D15D ||
spell == 0x21Fa95485f4571A3a0d0c396561cF4D8D13D445d
);
if (positionId == 0) {
positionId = nextPositionId++;
positions[positionId].owner = msg.sender;
} else {
require(positionId < nextPositionId, 'position id not exists');
require(msg.sender == positions[positionId].owner, 'not position owner');
}
POSITION_ID = positionId;
SPELL = spell;
HomoraCaster(caster).cast{value: msg.value}(spell, data);
uint collateralValue = getCollateralETHValue(positionId);
uint borrowValue = getBorrowETHValue(positionId);
require(collateralValue >= borrowValue, 'insufficient collateral');
POSITION_ID = _NO_ID;
SPELL = _NO_ADDRESS;
return positionId;
}
In addition to the spell whitelist, Alpha deployed two other changes:
resolveReserve()
function can now only be called by governor- Can only borrow & repay 4 tokens (ETH, DAI, USDC, USDT)
Alpha Finance deployed two new contracts following the attack to upgrade the HomoraBank.
-
Alpha deployed this contract: https://etherscan.io/address/0x6f80c10eafa1d3f7d8cc9f36bf39d301c7a7ad86#code
-
And initalized it
-
Then updated their proxy to point to this newly deployed contact
-
They then deployed a new version of HomoraBank: https://etherscan.io/address/0x525d911b9459966ed6e90f3d44613bc17dfc8be6#code
-
And initialized it
-
Finally, they updated their proxy to point to this newly deployed contact
- Attacker funded their wallet using Tornado Cash at 2021-02-12 8:55 AM +UTC
- Attacker deployed the contract at 2021-02-13 5:37 AM +UTC
- Attackers first attempt ran out of gas at 2021-02-13 5:40 AM +UTC
- Attacker started the first of many success transactions at 2021-02-13 5:59 AM +UTC
- Attacker returns funds to Alpha, CREAM, Tornado Cash, and Gitcoin starting at 2021-02-13 6:21 AM +UTC
- Jose Baredes sounded the alarm on Twitter at 2021-02-13 6:29 +UTC
- Alpha deployed a fixed contract at 2021-02-13 08:44:30 +UTC
- This first draft of this report was published at 2021-02-13 18:18 +UTC
- Alpha published their post-mortem at 2021-02-13 19:33 +UTC
At this time, there is no reason to believe that OUSD would be impacted by the attack. It's clear that the attack is specific to Alpha and possibly the Iron Bank / Cream.
As usual, the key takeaway is that securing smart contracts is really hard. This was clearly a highly sophisticated hack involving complex interactions between multiple DeFi protocols. It's hard to guarantee the security of a system this complex. The jury is still out on whether it's a good idea to offer uncollateralized loans to trusted smart contracts or not.