Summary
RAAC protocol's DebtToken
takes from AAVE's idea of using self-interest-bearing tokens by applying indexes to minted/burned amounts. However, the current implementation is flawed, as when the token is minted, the amount to mint is inflated with a double-scaled balanceIncrease
value, which is intended to serve as an event emission input. This over-inflation leads to a mismatch between the internal user.scaledDebtBalance
variable in LendingPool
and the burned amount of DebtTokens when repaying, resulting in an underflow error DOS-ing the LendingPool::repay()
function.
Vulnerability Details
Let's look at RAAC's DebtToken
mint:
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
uint256 amountToMint = amount + balanceIncrease;
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}
And let's look at AAVE's scaled token minting:
function _mintScaled(
address caller,
address onBehalfOf,
uint256 amount,
uint256 index
) internal returns (bool) {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT);
uint256 scaledBalance = super.balanceOf(onBehalfOf);
uint256 balanceIncrease = scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[onBehalfOf].additionalData);
_userState[onBehalfOf].additionalData = index.toUint128();
_mint(onBehalfOf, amountScaled.toUint128());
uint256 amountToMint = amount + balanceIncrease;
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0);
}
From the above, we can see that there are numerous inconsistencies:
When taking the scaledBalance
, RAAC uses the balanceOf
function of the DebtToken
and then does rayMul
on it again to calculate the balanceIncrease
. This is incorrect as balanceOf
returns the already normalized amount, leading to double scaling (this is a different issue leading to invalid event emission).
RAAC then goes to add the balanceIncrease
to the amount
that is meant to be minted, where this balanceIncrease
is meant to be used as an event input, to indicate how much increase there is due to the changes in borrowing indexes.
Combining both points from above results in an over-inflated DebtToken
mint, which does not match with what is added to the internal user.scaledDebtBalance
in LendingPool::borrow()
. Let's analyze:
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
__SNIP__
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
@> user.scaledDebtBalance += scaledAmount;
reserve.totalUsage = newTotalSupply;
__SNIP__
}
4 The actual minted amount differs from what is added to user.scaledDebtBalance
, which means that the internal debt storage is now lower than the actual balance of debt tokens.
5 Now, when the user wants to fully repay, an underflow will happen, as it will try to deduct the actual burned tokens from the lowered internal storage:
function _repay(uint256 amount, address onBehalfOf) internal {
__SNIP__
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
@> user.scaledDebtBalance -= amountBurned;
__SNIP__
}
Impact
Users cannot repay debt due to DoS.
Tools Used
Manual review
Recommendations
Do not add the balance increase to the amount that is going to be minted when borrowing.