Summary
The RToken's burn function returns incorrect scaled amounts, causing reserve accounting errors in the protocol. The function calculates but doesn't use the proper scaled amount, instead returning the unscaled amount which breaks interest accounting mechanisms.
Vulnerability Details
The vulnerability exists in the RToken's burn function:
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
uint256 amountScaled = amount.rayMul(index);
_burn(from, amount.toUint128());
return (amount, totalSupply(), amount);
}
The ReserveLibrary::withdraw expects the first return value to be the scaled amount for proper accounting:
(uint256 burnedScaledAmount, uint256 newTotalSupply, uint256 amountUnderlying) =
IRToken(reserve.reserveRTokenAddress).burn(
recipient,
recipient,
amount,
reserve.liquidityIndex
);
The mismatch occurs because:
The burn function calculates amountScaled but never uses it
Returns the unscaled amount instead of the scaled amount
ReserveLibrary uses this incorrect value for reserve accounting
Impact
High severity due to:
Incorrect reserve accounting leading to protocol insolvency risks
Interest accrual calculations become inaccurate
Users receive incorrect token amounts during withdrawals
Protocol's economic model breaks down over time as interest accrues
Proof of Concept
it("causes reserve accounting errors in ReserveLibrary", async function () {
const ReserveLibraryMock = await ethers.getContractFactory("ReserveLibraryMock");
const reserveLibrary = await ReserveLibraryMock.deploy();
await reserveLibrary.setRToken(rToken.address);
await reserveLibrary.deposit(INITIAL_DEPOSIT);
const newIndex = RAY * 2n;
await reserveLibrary.setLiquidityIndex(newIndex);
const withdrawAmount = INITIAL_DEPOSIT / 2n;
await reserveLibrary.withdraw(withdrawAmount);
const reserveData = await reserveLibrary.getReserveData();
const expectedLiquidity = INITIAL_DEPOSIT - (withdrawAmount / 2n);
expect(reserveData.totalLiquidity).to.not.equal(expectedLiquidity,
"Reserve accounting is incorrect due to wrong scaled amount");
});
Tools Used
Manual code review
Unit tests with Hardhat
Recommendations
Update the burn function to return the correct scaled amount:
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);
if(amount > userBalance) {
amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(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 (amountScaled, totalSupply(), amount);
}