Analysis of the Beluga Protocol Exploit
TL;DR
On October 13, 2023, the Beluga Protocol was exploited across multiple transactions on the Arbitrum chain, resulting in a loss of funds worth approximately $175,000.
Introduction to Beluga Protocol
Beluga Protocol is a multichain stableswap AMM that aims to solve the problem of liquidity fragmentation with seamless cross-chain swaps.
Vulnerability Assessment
The root cause of the exploit is price manipulation of the underlying assets.
Steps
Step 1:
We attempt to analyze one of the attack transactions executed by the exploiter.
Step 2:
Let’s understand the liquidity to be withdrawn via the withdrawFrom function of the affected contract.
The liability to burn becomes a dynamic value, and thus would be changed if the asset liability or the total supply values were to change.
/**
* @notice Calculates fee and liability to burn in case of withdrawal
* @param asset The asset willing to be withdrawn
* @param liquidity The liquidity willing to be withdrawn
* @return amount Total amount to be withdrawn from Pool
* @return liabilityToBurn Total liability to be burned by Pool
* @return fee The fee of the withdraw operation
*/
function _withdrawFrom(Asset asset, uint256 liquidity) private view returns (uint256 amount, uint256 liabilityToBurn, uint256 fee, bool enoughCash) {
liabilityToBurn = (asset.liability() * liquidity) / asset.totalSupply();
require(liabilityToBurn > 0, "INSUFFICIENT_LIQ_BURN");
fee = _withdrawalFee(_slippageParamK, _slippageParamN, _c1, _xThreshold, asset.cash(), asset.liability(), liabilityToBurn);
// Get equilibrium coverage ratio before withdraw
uint256 eqCov = getEquilibriumCoverageRatio();
// Init enoughCash to true
enoughCash = true;
// Apply impairment in the case eqCov < 1
uint256 amountAfterImpairment;
if (eqCov < ETH_UNIT) {
amountAfterImpairment = (liabilityToBurn).wmul(eqCov);
} else {
amountAfterImpairment = liabilityToBurn;
}
// Prevent underflow in case withdrawal fees >= liabilityToBurn, user would only burn his underlying liability
if (amountAfterImpairment > fee) {
amount = amountAfterImpairment - fee;
// If not enough cash
if (asset.cash() < amount) {
amount = asset.cash(); // When asset does not contain enough cash, just withdraw the remaining cash
fee = 0;
enoughCash = false;
}
} else {
fee = amountAfterImpairment; // fee overcomes the amount to withdraw. User would be just burning liability
amount = 0;
enoughCash = false;
}
}
Step 3:
The swap function will then update the liability value, but use the Oracle price to perform the token exchange.
/**
* @notice Swap fromToken for toToken, ensures deadline and minimumToAmount and sends quoted amount to `to` address
* @param fromToken The token being inserted into Pool by user for swap
* @param toToken The token wanted by user, leaving the Pool
* @param fromAmount The amount of from token inserted
* @param minimumToAmount The minimum amount that will be accepted by user as result
* @param to The user receiving the result of swap
* @param deadline The deadline to be respected
* @return actualToAmount The actual amount user receive
* @return haircut The haircut that would be applied
*/
function swap(
address fromToken,
address toToken,
uint256 fromAmount,
uint256 minimumToAmount,
address to,
uint256 deadline
) external ensure(deadline) nonReentrant whenNotPaused returns (uint256 actualToAmount, uint256 haircut) {
require(fromToken != address(0), "ZERO");
require(toToken != address(0), "ZERO");
require(fromToken != toToken, "SAME_ADDRESS");
require(fromAmount > 0, "ZERO_FROM_AMOUNT");
require(to != address(0), "ZERO");
IERC20 fromERC20 = IERC20(fromToken);
Asset fromAsset = _assetOf(fromToken);
Asset toAsset = _assetOf(toToken);
// Intrapool swapping only
require(toAsset.aggregateAccount() == fromAsset.aggregateAccount(), "DIFF_AGG_ACC");
(actualToAmount, haircut) = _quoteFrom(fromAsset, toAsset, fromAmount);
require(minimumToAmount <= actualToAmount, "AMOUNT_TOO_LOW");
fromERC20.safeTransferFrom(address(msg.sender), address(fromAsset), fromAmount);
fromAsset.addCash(fromAmount);
toAsset.removeCash(actualToAmount);
toAsset.addLiability(_dividend(haircut, _retentionRatio));
toAsset.transferUnderlyingToken(to, actualToAmount);
emit Swap(msg.sender, fromToken, toToken, fromAmount, actualToAmount, to);
}
Step 4:
Therefore, an attacker could deposit USDT tokens and then use the swap between USDT and USDC_E to update the asset liability.
Due to the stable coin price deduced from the Oracle, the ratio of the USDC_E to USDT swap is consistent, but the withdrawal amount gets impacted, letting the attacker spend less USDT to withdraw back larger amounts of profits.
Step 5:
The attacker has since then transferred 113.3 ETH, worth approximately $175,681, to this address and then laundered them into MEXC.
Aftermath
The team hasn’t shared any details of the incident on their social media platforms at Twitter (X) at the time of this writing, but has sent an on-chain message to the exploiter to return the user’s funds for a white-hat bounty reward of 20% of the stolen assets.
Solution
In the wake of this exploit on Beluga Protocol, we find it crucial to emphasize the significance of utilizing reputable, multifaceted, and robust oracle solutions. The unfortunate price manipulation incident, stemming from such a vulnerability, highlights the importance of creating resilient systems that can anticipate and guard against malicious actors.
In scenarios like these, where price manipulation stands central, it becomes imperative to leverage oracle systems like ChainLink, which amalgamate data from numerous sources to provide accurate price feeds. Time-weighted average prices (TWAPs) play a pivotal role in ensuring price stability, mitigating abrupt, and likely manipulative, price changes. Adequate liquidity within pools, which the oracle leans on for price determination, is also a cardinal element in maintaining a fortified defense against manipulation.
This article was originally published by Pukar Acharya elsewhere.
Enjoy Reading This Article?
Here are some more articles you might like to read next: