How Was Raft Protocol Exploited?
TL;DR
On November 11, 2023, the Raft Protocol was exploited on the Ethereum Mainnet due to a smart contract vulnerability, which resulted in a loss of 1577 ETH, worth approximately $3,300,000.
Introduction to Raft
Raft enables the generation of R, a decentralized USD stablecoin, by opening a collateralized debt position or by depositing stablecoins into the Raft protocol reserve.
Vulnerability Assessment
The root cause of the exploit is a precision loss error when minting share tokens, which enabled the exploiter to obtain extra share tokens.
Steps
Step 1:
We attempt to analyze the attack transaction executed by the exploiter.
Step 2:
In the IRPM contract, the collateral token (rcbETH-c) was calculated using the parameters amount * ONE / index
. This means that depositing 1 wei at the index rate of 22528727648486976868105072582590464 results in receiving approximately 0.0000000000000000443 wei of rcbETH-c.
function mint(address to, uint256 amount) public virtual override onlyPositionManager {
_mint(to, amount.divUp(storedIndex));
}
function divUp(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
} else {
return (((a * ONE) - 1) / b) + 1;
}
}
Step 3:
The liquidate function in the affected contracts uses the balanceOf validation, which could be easily manipulated by an attack. Essentially, this function of the protocol returns the balance * currentIndex
which will result in the rcbETH-c balance of users increasing by 0.022 rcbETH-c.
function liquidate(address position) external override {
IERC20 collateralToken = collateralTokenForPosition[position];
CollateralTokenInfo storage collateralTokenInfo = collateralInfo[collateralToken];
IERC20Indexable raftCollateralToken = collateralTokenInfo.collateralToken;
IERC20Indexable raftDebtToken = collateralTokenInfo.debtToken;
ISplitLiquidationCollateral splitLiquidation = collateralTokenInfo.splitLiquidation;
if (address(collateralToken) == address(0)) {
revert NothingToLiquidate();
}
(uint256 price, ) = collateralTokenInfo.priceFeed.fetchPrice();
uint256 entireCollateral = raftCollateralToken.balanceOf(position);
uint256 entireDebt = raftDebtToken.balanceOf(position);
uint256 icr = MathUtils._computeCR(entireCollateral, entireDebt, price);
if (icr >= splitLiquidation.MCR()) {
revert NothingToLiquidate();
}
uint256 totalDebt = raftDebtToken.totalSupply();
if (entireDebt == totalDebt) {
revert CannotLiquidateLastPosition();
}
bool isRedistribution = icr <= MathUtils._100_PERCENT;
// prettier: ignore
(uint256 collateralLiquidationFee, uint256 collateralToSendToLiquidator) = splitLiquidation.split(entireCollateral, entireDebt, price, isRedistribution);
if (!isRedistribution) {
_burnRTokens(msg.sender, entireDebt);
totalDebt -= entireDebt;
// Collateral is sent to protocol as a fee only in case of liquidation
collateralToken.safeTransfer(feeRecipient, collateralLiquidationFee);
}
collateralToken.safeTransfer(msg.sender, collateralToSendToLiquidator);
_closePosition(raftCollateralToken, raftDebtToken, position, true);
_updateDebtAndCollateralIndex(collateralToken, raftCollateralToken, raftDebtToken, totalDebt);
emit Liquidation(msg.sender, position, collateralToken, entireDebt, entireCollateral, collateralToSendToLiquidator, collateralLiquidationFee, isRedistribution);
}
function balanceOf(address account) public view virtual override(IERC20, ERC20) returns (uint256) {
return ERC20.balanceOf(account).mulDown(currentIndex());
}
Step 4:
The attacker initially donated approximately 1,061 cbETH into the IRPM contract and subsequently liquidated their position, which resulted in the manipulation of the collateralIndex value to a very large number.
function _updateDebtAndCollateralIndex(IERC20 collateralToken, IERC20Indexable raftCollateralToken, IERC20Indexable raftDebtToken, uint256 totalDebtForCollateral) internal {
raftDebtToken.setIndex(totalDebtForCollateral);
raftCollateralToken.setIndex(collateralToken.balanceOf(address(this)));
}
Step 5:
The exploiter then repeatedly called the managePosition function to increase his position by 1 wei worth of cbETH as collateral, receiving 1 share each time. The mint function in the rcbETH-c contract uses rounding to calculate the number of shares to be minted.
function managePosition(
IERC20 collateralToken,
address position,
uint256 collateralChange,
bool isCollateralIncrease,
uint256 debtChange,
bool isDebtIncrease,
uint256 maxFeePercentage,
ERC20PermitSignature calldata permitSignature
) public virtual override(IPositionManager, PositionManager) returns (uint256 actualCollateralChange, uint256 actualDebtChange) {
if (address(permitSignature.token) == address(r)) {
PermitHelper.applyPermit(permitSignature, msg.sender, address(this));
}
return super.managePosition(collateralToken, position, collateralChange, isCollateralIncrease, debtChange, isDebtIncrease, maxFeePercentage, permitSignature);
}
Step 6:
Due to the precision loss issue, the attacker received 1 share instead of the expected 0 shares when minting. The attacker subsequently redeemed 67.45 rcbETH-c each time, thereby minting approximately 6,705,028 R Tokens, which were swapped for 1577 WETH as profits.
Step 7:
However, the smart contracts to convert the yielded R tokens to ETH and transfer them to the exploiter were called from another contract using delegatecall. The key aspect of delegatecall is that it operates on the storage of the calling contract, not the called contract, in which the slot with the exploit address was not initialized.
Step 8:
Therefore, despite looting 1,577 ETH, the exploiter burned 1,570 ETH and sent the remaining 7 ETH to themselves. The exploiter had pulled approximately 18 ETH from Tornado Cash before the attack and was left with 14 ETH after the attack, thereby ultimately losing 4 ETH during the entire process of the heist.
Aftermath
The team acknowledged the occurrence of the exploit and stated that they were investigating the issue. The minting of R was temporarily suspended. They confirmed that the protocol experienced a complex security incident that resulted in the minting of approximately $6.7 million in unbacked R. They will also be working on a comprehensive recovery plan to compensate users affected by the incident. According to the post-mortem report, HatsFinance and Trail of Bits both audited the exploited codebase. However, the vulnerabilities that led to the incident were not detected in these audits.
Solution
To safeguard against vulnerabilities like those exposed in the Raft Protocol exploit, a multifaceted and comprehensive approach is essential. The foremost step is to prevent precision loss, a critical issue in the exploit. This requires the implementation of enhanced arithmetic handling within the smart contracts. The use of robust and tested arithmetic libraries, specifically designed to handle operations without losing precision, is crucial. These libraries must adeptly manage various arithmetic operations, particularly those involving extremely small or large numbers. Additionally, conducting rigorous testing for edge cases is vital. Automated tests should encompass scenarios that replicate the exploit conditions, such as minimal collateral deposits or exceptionally high index values. This ensures the system’s resilience against similar precision loss issues.
Employing formal verification tools is another key aspect of the solution. These tools mathematically validate the correctness of the smart contracts’ operations against their intended specifications. Formal verification is particularly crucial in complex mathematical functions where traditional testing might miss subtle errors. It’s important to continuously update and check these verification models to align with any changes or updates in the smart contract, maintaining their relevance and effectiveness.
Understanding the limitations of smart contract audits is essential. While audits are a critical component of smart contract security, they do not guarantee the absence of vulnerabilities. Recognizing that audits are part of a broader security strategy is crucial. Continuous monitoring and re-auditing, particularly post-major updates, are necessary practices. Changes in one part of a smart contract can inadvertently introduce vulnerabilities in previously secure parts, making ongoing vigilance essential. Involving the broader security research community is another critical strategy. This can be achieved through bug bounty programs, which incentivize independent security researchers to find and report potential security issues.
This article was originally published by Pukar Acharya elsewhere.
Enjoy Reading This Article?
Here are some more articles you might like to read next: