Summary
Because of mistake in the burn function in RToken, people can withdraw more funds that they have deposited.
Vulnerability Details
RToken is scaled token that uses LiquidityPool's index to scale the users balance. In RToken burn fn implementation, the amount parameter refers to the unscaled 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);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
_burn(from, amount.toUint128());
This is problematic because this token overrides the base contract fn _update
:
function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
As you can see, each time token is minted or burned, it assumes that the amount is going to be scaled. Because unscaled amount is passed, it would mean that much lower token amount would be burned.
Impact
Malicious users can drain funds, if they deposit and withdraw from the lending pool contract.
Tools Used
Manual review
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);
}
//@note balance returns scaled?
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());
+ _burn(from, amountScaled.toUint128());
// in the tests i found this note - crvUSD minted to the rToken contract. But in the lending pool i dont see any funds transfered back to this contract.
if (receiverOfUnderlying != address(this)) {
//@note it looks like there are no assets send to this contract?
IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}