Summary
The burn
function in RToken contract is defined as follows:
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);
}
We can see that amount
is returned twice, but as explained in the function documentation, return values should be:
* @return A tuple containing:
* - uint256: The amount of scaled tokens burned
* - uint256: The new total supply after burning
* - uint256: The amount of underlying asset transferred
*/
This means the third return value should be amountScaled
, not amount
, as it should be the amount of underlying assets, i.e., the amount
multiplied by the index.
The problem arises in ReserveLibrary that makes the call to the burn
function:
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);
}
Both burnedScaledAmount
and amountUnderlying
represent the amount of interest-bearing RToken that has been burned.
updateInterestRatesAndLiquidity
uses amountUnderlying
to update interests rates, but this is incorrect as amountUnderlying
is not the amount of underlying assets being transferred back to the user.
Also, the Withdraw
event is emitted with the same value twice for underlying assets and RToken burned amount, which is incorrect and might lead to front-end integration issues.
Impact
The impact of this vulnerability can be considered as high as it leads to incorrect interest rates computation.
Tools Used
Manual review.
Recommendations
Make sure to return the correct values in burn
function so that interests rates are correctly computed in the lending pool after withdrawals. Note that amountScaled
is not correctly computed but this is another issue.
function burn(address from, address receiverOfUnderlying, uint256 amount, uint256 index)
external
override
onlyReservePool
returns (uint256, uint256, uint256)
{
...
return (amount, totalSupply(), amountScaled);
}