Summary
The liquidityIndex in the RToken contract needs to be updated every time the reserve interests are updated in the ReserveLibrary contract. Failure to do so results in incorrect calculations of the scaled amount in the transferFrom function.
Vulnerability Details
The transferFrom function in the RToken contract depends on the liquidityIndex to calculate the scaled amount:
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
return super.transferFrom(sender, recipient, scaledAmount);
}
updateLiquidityIndex function updates the liquidity index in RToken
* @notice Updates the liquidity index
* @param newLiquidityIndex The new liquidity index
*/
function updateLiquidityIndex(uint256 newLiquidityIndex) external override onlyReservePool {
if (newLiquidityIndex < _liquidityIndex) revert InvalidAmount();
_liquidityIndex = newLiquidityIndex;
emit LiquidityIndexUpdated(newLiquidityIndex);
}
However, the liquidityIndex is not updated every time the reserve interests are updated in the ReserveLibrary contract, leading to incorrect calculations.
link to the issues:
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L98
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L224C7-L224C8
Impact
This issue can lead to incorrect calculations of the scaled amount in the transferFrom function, potentially causing issues with token transfers and liquidity management within the protocol.
Tools Used
Manual code review.
Recommendations
Ensure that the liquidityIndex in the RToken contract is updated every time the reserve interests are updated in the ReserveLibrary contract. This can be achieved by adding a call to update the liquidityIndex in the updateReserveInterests function.
Example update in ReserveLibrary:
function updateReserveInterests(ReserveData storage reserve, ReserveRateData storage rateData) internal {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}
uint256 oldLiquidityIndex = reserve.liquidityIndex;
if (oldLiquidityIndex < 1) revert LiquidityIndexIsZero();
reserve.liquidityIndex = calculateLiquidityIndex(
rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
);
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
reserve.lastUpdateTimestamp = uint40(block.timestamp);
IRToken(reserve.reserveRTokenAddress).updateLiquidityIndex(reserve.liquidityIndex);
emit ReserveInterestsUpdated(reserve.liquidityIndex, reserve.usageIndex);
}
This ensures that the liquidityIndex is always up-to-date, leading to correct calculations in the transferFrom function.