Jul 07, 2023
Sturdy Finance's Price Manipulation Vulnerability Explained
In a world where the adoption of decentralized finance (DeFi) is increasing exponentially, so are the vulnerabilities and risks associated with these nascent technologies. One such risk is re-entrancy attacks, a risk that recently hit the Sturdy project hard on June 12th, resulting in a loss of 442 WETH. Below, we at Scalebit Research Group, dig into this loophole and give a step-by-step account of the exploit by reproducing the attack against Sturdy Finance.
0x00: Stury Finance
Sturdy Finance is a lending protocol that offers several financial services including lending, borrowing, and yield farming.
The function setUserUseReserveAsCollateral[1] in the Sturdy Finance protocol is used to allow depositors to enable or disable a specific deposited asset as collateral.
Inside the function ,it checks that if it’s safe for a user to decrease their balance of a particular asset, in terms of the overall system’s health. It calls GenericLogic.validateSetUseReserveAsCollateral to check this condition in line 531.
The below code snippet provided is part of function validateSetUseReserveAsCollateral intended to calculate and manage a user’s collateral balance . In the line 184, an oracle is called upon to import real-world data into the blockchain environment by acquiring the current price of the reserve asset. Upon investigation, it was discovered that if the currentReserveAddress is set to Sturdy B-stETH-STABLE[2], the protocol will obtain its pricing data from the Balancer.
Let’s deep into the oracle.
The protocol initiates a call to the getAssetPrice function to acquire the asset’s current price. As shown in the code snippet below, this operation leads to a subsequent call to latestAnswer in line 111.
Inside latestAnswer, there is another internal function call to _get. Within the _get function, the protocol makes another crucial call to BALWSTETHWETH.getRate() in line 33. This function fetches the price from the Balancer Vault.
As shown in the code snippet below, It first retrieve the pool tokens and their respective balances from the Balancer vault in line 593 , using the pool’s id via the getVault().getPoolTokens(getPoolId()) call.After obtaining the balance, a series of calculations are performed(line 597-601). In essence, the function is conducting a division operation in line 602, where the calculated invariant (representing the obtained balance of assets in the pool) is divided by the total supply of the Balancer. This process yields the price of the asset in question.
Returning to the Sturdy Finance protocol, the line 189 below performs the crucial task of calculating the user’s total collateral balance, denominated in ETH. In this computation, the protocol first multiplies the user’s balance of the reserve asset (vars.compoundedLiquidityBalance) by the unit price of the reserve (vars.reserveUnitPrice). This product is then divided by a base unit conversion factor (vars.tokenUnit) to ensure precision and to convert the result into ETH.
After that, the line192, incorporates the newly computed collateral balance into the user’s total collateral balance, spanning multiple reserves. This cumulative collateral balance is crucial for the platform to manage and execute operations, including but not limited to borrowing, liquidations, and health factor calculations, thereby maintaining a fluid, stable, and secure lending environment.
0x01: Balancer
Balancer is an advanced automated portfolio manager and liquidity protocol that allows multiple tokens in one pool, each with custom weights, automatic rebalancing, and programmable liquidity. The exitPool function of the Balance Vault[3] serves as the starting point when a user wishes to withdraw liquidity from the pool.
Upon initiation of an exit request, the exitPool function triggers the _joinOrExit function. It’s critical to note that within the pool’s hook, the transfer of assets, potentially including tokens like ETH, is handled prior to any other operation. Once the asset transfer is completed, any due fees are settled, and the final balances are calculated. As shown in the figure below,only after these steps are completed, the state update takes place.
To implement the main aspect of the pool balance alteration process, the _joinOrExit function calls upon _callPoolBalanceChange. This function calculates the total balances for the pool’s tokens, retrieves the pool contract instance, and invokes the corresponding function (onJoinPool or onExitPool) based on the type of operation.
In the scenario where an exit operation is conducted, the onExitPool function is invoked, as stipulated by the pool contract. This function accommodates decimal precision by upscaling the token balances, computes the amount of tokens to be withdrawn and the corresponding fees due. At this stage, the pool tokens held by the sender are extinguished, or “burned”. This act of burning the tokens, which effectively reduces the total supply, signifies the decrease in the user’s share in the pool. Subsequently, the function downscales the amounts for the exit and fee to their original values, reflecting the final state of the pool after the exit operation.
After performing the appropriate operation, either the _processJoinPoolTransfers or _processExitPoolTransfers function is triggered, depending on the operation type. These functions supervise the actual transfer of assets, which could include tokens such as ETH, and the payment of fees. They then return the final balances of the tokens in the pool following the operation, providing a snapshot of the pool’s state after the join or exit transaction.
0x02: Loophole
In the Balancer protocol, a re-entrancy attack might be possible if the exitPool function is called and the protocol proceeds to transfer ETH funds to the user’s address. During this fund transfer, if a reentrant call to the setUserUseReserveAsCollateral function is made, an exploit can occur due to the mismatched timing of token burning and balance deduction in the protocol.Notably, despite Balancer’s use of the nonReentrant modifier, it does not prevent re-entrancy into third-party contracts
Specifically, when the exitPool function is called, Balancer protocol burns the pool tokens, causing a reduction in the total supply. However, the token balance in the user’s account is not deducted immediately, which creates a timing window for manipulation.
If the setUserUseReserveAsCollateral function is called during this timing window, it could potentially value the tokens at a higher price due to the artificially decreased supply. This inflated token price could allow the function to bypass the Sturdy’s protocol setUserUseReserveAsCollateral.balanceDecreaseAllowed check, and consequently, enable the de-collateralization of other assets.
When the exitPool function call completes, the token price then corrects itself as the user’s balance is updated to reflect the burnt tokens. This correction could lead to a situation where an asset that was previously fully collateralized in the sturdy protocol becomes under-collateralized, due to the incorrect unchecking of collateral that occurred during the inflated token price period. The under-collateralized asset could then be subject to forced liquidation, leading to a potential loss for the protocol.
0x03: Exploit
In the following section, we will explain how we use foundry[4] to reproduce the incident at block height 17460609 mined on Jul 12, 2023.
The following is an attack flowchart.
1.Initiate a flash loan:
As shown in the code snippet below, the contract starts by initiating a flash loan from Aave v3 in line 110, borrowing wstETH and WETH assets in the trigger() function.
2.Execute operation:
After receiving the loan assets, Aave calls the executeOperation() function in the contract. The function carries out the heart of the attack:
The code begins by converting 1100 WETH into ETH in line 126, which is necessary for liquidity addition. It then contributes this ETH as liquidity to the Curve pool known as steCRVPool in line 129. Subsequently, it obtains LP tokens by joining the Balancer pool using wstETH and WETH, initiating a JoinPoolRequest, and calling the joinPool function on the Balancer vault (line 132-166 ). These LP tokens are deposited as collateral in both the SetCRVSturdyPool and BstETHSturdyPool(line 169-178). Finally, the code borrows extra WETH from Sturdy’s Lending Pool, utilizing the deposited LP tokens as collateral in line 181.
3.Exit from the Balancer pool:
The contract prepares an ExitPoolRequest for Balancer. It sets a boolean variable opt to true and calls exitPool function on Balancer(line 210-212). This is where a re-entrancy attack occurs.
4.Re-entrancy:
When the exitPool function is called, the contract receives ETH from Balancer before its internal state is updated. During this time, the contract triggers its fallback function receive(), which calls setUserUseReserveAsCollateral function on Sturdy’s LendingPool, disabling csteCRV as collateral in line 295 of the code below. The re-entrancy attack allows this to occur before the state of the contract is updated after the exit pool operation, which can lead to an inconsistency between the actual token balances and the state of the contract.
5.Liquidation and profit:
Once the re-entrancy attack is successful and the collateral is withdrawn, the contract initiates a liquidation call on Sturdy’s LendingPool in line 221 of the code below. This essentially means that the attacker is liquidating their own position but due to the previously manipulated state, this leads to a profit. After liquidation, the contract exits from the Balancer pool again, resetting the manipulated state.
6.Repeated Operations
In this pool, the operations of joinPool, depositCollateral, borrow, exitPool, withdrawCollateral, and liquidationCall are repeated five times.
7.Return the flash loan
Finally, after removing the liquidity from the Curve pool in line 273 below, the contract unwraps the remaining wstETH to stETH, exchanges stETH to ETH on the Curve pool, and deposits ETH into WETH before returning the flash loan to Aave(line 284-291).
8.Make a profit
In our experiment, the bad actor walks away with around 442 WETH through this attack, which involves repeating the steps multiple times.
0x04: Conclusion
Both Sturdy Finance and dForce Network[5] were victims of exploits involving a read-only reentrancy vulnerability. In both cases, the attackers used this vulnerability to manipulate asset prices and steal funds from the protocols. Despite the known nature of this vulnerability and the existence of suggested mitigations, it was not effectively addressed in either case, leading to significant losses. These incidents highlight the importance of comprehensive and in-depth security audits in identifying and correcting such vulnerabilities in DeFi protocols. They also bring to light the potential risks associated with relying on oracles for price information
References
[1] Lending pool :
https://etherscan.io/address/0x9f72dc67cec672bb99e3d02cbea0a21536a2b657
[2] Sturdy B-stETH-STABLE :
https://etherscan.io/address/0x10aa9eea35a3102cc47d4d93bc0ba9ae45557746
[3] Balancer Vault:
https://etherscan.io/address/0xba12222222228d8ba445958a75a0704d566bf2c8
[4] foundry :
https://www.paradigm.xyz/2021/12/introducing-the-foundry-ethereum-development-toolbox
[5] dForce Network incident :
https://rekt.news/dforce-network-rekt/
[6] Attack transaction:
https://etherscan.io/tx/0xeb87ebc0a18aca7d2a9ffcabf61aa69c9e8d3c6efade9e2303f8857717fb9eb7
About ScaleBit
ScaleBit is a blockchain security team that provides security solutions for Mass Adoption of Web3. With expertise in scaling technologies like blockchain interoperability and zero-knowledge proofs, we provide meticulous and cutting-edge security audits for blockchain applications. The team comprises security professionals with extensive experience in both academia and enterprise. Our mission is to provide security solutions for Web3 Mass Adoption and make security accessible for all.
Website: https://www.scalebit.xyz/
Twitter: https://twitter.com/scalebit_
ScaleBit Research Group