Summary
In the current implementation of the lending pool contract, when a liquidity provider withdraws funds, the protocol fails to account for accrued interest based on the liquidityIndex
. The issue stems from the incorrect transfer logic within the burn()
function of the RToken
contract, where the underlying asset is transferred without scaling the amount by the liquidityIndex
.
As per the docs here: [Lending](https://docs.raac.io/quickstart/about-raac?id=lending)
The user depositing the curvUsd receives Rtokens which represents the amount he has deposited along with accured interest so after particular amount of time after which he has deposited the curvUsd if he withdraws by burning all Rtokens he should receive the curvUsd which he had deposited along with any interests which are accumalated.But here due to the issue mentioned only he receives the amount he had deposited and not any accured interest.
Vulnerability Details
Let us consider the scenario where he wants to burn all the rtokens he received after depositing the curvUsd. He calls function LendingPool::withdraw
, as highlighted below here it uses reserve library to call withdraw.
function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (withdrawalsPaused) revert WithdrawalsArePaused();
ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
@> (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
@> reserve,
@> rateData,
@> amount,
@> msg.sender
@> );
_rebalanceLiquidity();
emit Withdraw(msg.sender, amountWithdrawn);
}
Now the withdraw function in reserve library calls the burn function of rtoken address which also transfers the underlying asset to the liquidity provider.
ReserveLibrary::withdraw
:
function withdraw(
ReserveData storage reserve,
ReserveRateData storage rateData,
uint256 amount,
address recipient
) internal returns (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) {
if (amount < 1) revert InvalidAmount();
updateReserveInterests(reserve, rateData);
(uint256 burnedScaledAmount, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).burn(
recipient,
recipient,
amount,
reserve.liquidityIndex
);
amountWithdrawn = burnedScaledAmount;
updateInterestRatesAndLiquidity(reserve, rateData, 0, amountUnderlying);
emit Withdraw(recipient, amountUnderlying, burnedScaledAmount);
return (amountUnderlying, burnedScaledAmount, amountUnderlying);
}
now in the below rtoken contract where the rtokens are burnt, as the ratio of initally deposited amount of liquidity provider and the rtokens he got is in the ratio 1:1. As we considered that he is burning all rtokens he as here the amountScaled
is calculated in the below function which is never used meaning the amount actually user should get is the amountScaled
but he is just receiving the amount he had trasnferred and not receiving any interest.
RToken::burn
:
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
@> uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
_burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
@> IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}
Impact
Financial Loss: Liquidity providers do not receive their accrued interest when withdrawing their funds.
Tools Used
Manual
Recommendations
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
_burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
- IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
+ IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amountScaled);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}