Sturdy Finance Under Siege: A Forensic Analysis of the $800K Exploit and Tips to Fortify DeFi
Step by step dissection of the 357-transfer reentrancy attack.
EigenPhi is holding a Twitter Space discussing Cross Chain MEV Market. Join all the experts and from Ethereum Foundation, deBridge, and oDos, and let’s find out how to build a fair marketplace for it!
⏰ Time: 9:30 EST, July 12.
🔗 https://twitter.com/i/spaces/1YpKkgZRZBoKj
Don’t forget to set up your reminder!
At 01:06:35 a.m. UTC time on June 12th, 2023, a reentrancy attack hit Sturdy Finance, resulting in a loss of approximately 442 ETH, equivalent to $800k in value [1,2]. Sturdy Finance is a decentralized finance (DeFi) lending protocol. Unlike other lending protocols that only allow lenders to earn yield from interest rates, Sturdy lets lenders earn yield from the farming profits that borrowers make. The attacker used a flash loan of 110,000 ETH tokens to manipulate the price of B-wst-ETH, causing it to surge from approximately 1 ETH to 3 ETH. Then, the attacker used B-wst-ETH as collateral to borrow WETH from Sturdy Finance. The attacker executed this operation five times in a single transaction. The Balancer Vault reentrancy vulnerability discovered in February made this exploit possible [3]. In this essay, we will dive into a comprehensive analysis of the hack and suggest preventive measures to fend off such attacks in the future.
When and How Long? The Attack Timeline.
Let's dive into the detailed timeline of this attack.
On June 12, 2023, at 01:06:35 AM +UTC, during block 17460610, the attacker launched the initial attack. You can identify the transaction associated with the attack by its hash value: 0xeb87ebc0a18aca7d2a9ffcabf61aa69c9e8d3c6efade9e2303f8857717fb9eb7. The attacker acquired approximately 442 ETH at this stage.
After successfully launching the attack, the attacker gradually transferred a total of 447.7 ETH out of the system (including the initial capital used in the attack) between blocks 17460618 and 17460719.
On June 12, 2023, at 08:25:35 PM +UTC, during block 17466325, Sturdy Finance detected the attack. In response, Sturdy Finance sent a message to the attacker in transaction 0xda7fda2146ec0cc6f22920451978b41f9a9ae7f01ce6e4878b454eb2efdc9fec:
To the exploiter: as we have seen with recent hacks, exploits are not as easy to escape from as they used to be. That said, we are willing to offer you $100k as a bounty, and will not pursue you further if you send the remaining funds to 0x4e489d9863c9bAAc6C4917E1221274760BA889F5.
In the next two sections, we will walk you through the detailed process of our analysis.
Who Got the Booty? Attacker Account Balance Changes.
Based on the Account Balance Changes section on EigenPhi's website regarding this attack, it's clear that the attacker systematically created a new contract each time and drained the pool's entire balance. As a result, Sturdy Finance's contract had to liquidate the collateral to minimize losses. The attacker repeated this process with the goal of depleting the balance completely. Five distinct bots, all created specifically for the attack, carried out the entire operation. Eventually, the attacker stopped, leaving Sturdy Finance with only 11 ETH remaining.
The addresses of these contracts were generated at the time of the attack are
0x555003433D6A51E9Cc7798752DBDd4ED28D61DE5
0xA6181b779dC2F93033e82584b692b3714b184163
0x89676bEF260bbd56dBe97C81300d7a4f63d344D8
0x7750F94314a4fe3C78337dD8c9661864C4C77Ad6
0xF2ea7eACf7b042Fd85690d71b9D2957830BDcF02
Where and How? The Weak Point & The Attack.
The flaw exists in the calculation of the B-wst-ETH price. B-wst-ETH, a compound token representing various assets, gets its price by dividing the total underlying balances (V) by the total token supply (S). However, when someone exits the pool, the subtraction order of the numerator and denominator must be one by one. When you subtract one of them while the other remains unchanged, the price calculation goes haywire. This vulnerability lets the attacker manipulate the price to look higher, enabling them to borrow more money using it as collateral.
Now, let's dive into the call trace of contract 0x5550. The execution involves these key steps:
Create contract 0x555003433d6a51e9cc7798752dbdd4ed28d61de5.
Transfer a massive amount of funds through a flash loan: 58,900 WETH, 50,000 stETH, and 1,023.797 steCRV into the new contract.
Call the yoink method of the new contract.
Deposit 57,000 ETH and 50,000 stETH (equivalent to 107,000 ETH) in the Vault (0xba12).
Deposit 1,000 steCRV as collateral and 233.348 B-wst-ETH in Sturdy Finance (0xa36b).
Borrow 513.367 WETH from Sturdy Finance (0xa36b).
Withdraw 56,522 ETH and 50,208 wstETH, totaling 106,730 ETH, from the Vault (0xba12). This is where the attack happens.
As the exit nears completion, the fallback method of the attacker's contract 0x5550 triggers.
Within the fallback method, the attacker uses their 233.348 B-wst-ETH as collateral. At this point, the price of B-wst-ETH skyrockets to 3.008 ETH, letting the attacker easily secure collateral at 2.2 times their borrowed WETH.
Withdraw the 1,000 steCRV collateral from Sturdy Finance.
A check of the account status at Sturdy Finance shows collateral amounting to 241.436 ETH, with 513.367 ETH borrowed. This nets a profit of 271.931 ETH.
Sturdy Finance kicks off the liquidation process and secures 236.702 ETH.
Exit the Pool (0xba12) by withdrawing 107.208 wstETH and 120.690 ETH.
Repay the flash loan by transferring 58,820.232 WETH, 50,315.927 wstETH, and 1,023.797 steCRV back.
In the first iteration, the attacker uses 233.348 B-wst-ETH to borrow 513.367 WETH. After the liquidation process, Sturdy Finance ends up with 236.702 ETH. In the second iteration, a new contract (0xa618) is created, and this contract uses 107.591 B-wst-ETH, resulting in 236.702 WETH borrowed and leaving 109.138 ETH for Sturdy Finance. In the third iteration, the attacker uses 49.608 B-wst-ETH to borrow 109.138 ETH, leaving 50.321 ETH for Sturdy Finance. After the fourth iteration, only 23.202 ETH remains in Sturdy Finance. Finally, after the fifth iteration, a mere 10.697 ETH is left.
Open the Wound: The Vulnerable Source Code.
In the attack, the attacker manipulated the price of B-wst-ETH to skyrocket to 3 ETH, a significant increase from its actual value of 1 ETH. Let's dive into how Sturdy Finance's contract calculates this price. SturdyOracle.sol serves as the entry point for this calculation.
function getAssetPrice(address asset) public view override returns (uint256) {
address source = assetsSources[asset];
if (asset == BASE_CURRENCY) {
return BASE_CURRENCY_UNIT;
} else if (source == address(0)) {
return _fallbackOracle.getAssetPrice(asset);
} else {
int256 price = IChainlinkAggregator(source).latestAnswer();
if (price > 0) {
return uint256(price);
} else {
return _fallbackOracle.getAssetPrice(asset);
}
}
}
Regarding B-wst-ETH, the getAssetPrice
function calls the latestAnswer
function in BALWSTETHWETHOracle, which in turn calls the _get
function.
function latestAnswer() external view override returns (int256 rate) {
return int256(_get());
}
function _get() internal view returns (uint256) {
(, int256 stETHPrice, , uint256 updatedAt, ) = STETH.latestRoundData();
require(updatedAt > block.timestamp - 1 days, Errors.O_WRONG_PRICE);
require(stETHPrice > 0, Errors.O_WRONG_PRICE);
uint256 minValue = Math.min(uint256(stETHPrice), 1e18);
return (BALWSTETHWETH.getRate() * minValue) / 1e18;
}
The getRate
function, called within the _get
function, is susceptible to this attack. During the withdrawal process, the totalSupply()
is not accurately calculated, leading to an artificially inflated price.
function getRate() public view override returns (uint256) {
(, uint256[] memory balances, ) = getVault().getPoolTokens(getPoolId());
// When calculating the current BPT rate, we may not have paid the protocol fees, therefore
// the invariant should be smaller than its current value. Then, we round down overall.
(uint256 currentAmp, ) = _getAmplificationParameter();
_upscaleArray(balances, _scalingFactors());
uint256 invariant = StableMath._calculateInvariant(currentAmp, balances, false);
return invariant.divDown(totalSupply());
}
What Are the Lessons and How to Avoid?
During the Sturdy Finance exploit, the attacker leveraged the time lag between changes in the denominator and numerator during a calculation to manipulate the price of B-wst-ETH. The attacker then used B-wst-ETH as collateral to borrow WETH from Sturdy Finance at an inflated price, exploiting the fallback function provided by Vault. This drained nearly all funds from Sturdy Finance's pool. We conducted an in-depth analysis using the EigenPhi website and call traces and reviewed the problematic code.
Here are the key takeaways, along with advice and insights to help prevent similar attacks:
Always update to the latest library version and apply patches promptly. Balancer Vault issued a vulnerability notice in February [3] urging upgrades, but Sturdy Finance ignored it. Note that even if your code is error-free, you can still suffer losses due to others' mistakes.
The time lag between changes in the denominator and numerator is inherent during deposits or withdrawals. Deposits change the denominator first, followed by the numerator. Withdrawals should be the opposite. This issue stems from the lack of atomicity in EVM. Adopting operations that ensure atomicity in EVM could permanently resolve this issue.
Fallback functions are risky. Pay close attention to context and permissions when incorporating fallback functions in contracts.
Implementing a minimum number of blocks between deposits and withdrawals can be beneficial. Though not a foolproof solution, it can help detect and mitigate attacks early.
Restrict the types of tokens that can be used as collateral to minimize risks associated with poorly maintained collateral.
Limit the proportion of total reserves that an individual account can borrow from the pool. This won’t prevent attacks but can create friction, forcing attackers to halt sooner.
Lastly, a word of caution to all DeFi liquidation providers: loan contracts carry high risks; you could lose all your money.
References
[1] Twitter of PeckShield Inc.
https://twitter.com/peckshield/status/1668072853802037248
[2] Twitter SunSec
https://twitter.com/1nf0s3cpt/status/1668175807364341760?cxt=HHwWgIDRzd7DxqYuAAAA
[3] https://docs.balancer.fi/concepts/advanced/valuing-bpt/valuing-bpt.html#informational-price-evaluation
Follow us via these to dig more hidden wisdom of DeFi:
Website | Discord | Twitter | YouTube | Substack | Medium | Telegram