Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Invalid

When the borrowers repay their loan the interest/reserveAssets accrued over time are not taken from them

Summary

When the borrowers borrows reserveAssets via borrow function, they are minted with debtTokens and when the borrowes decide to repay their loan, those debtTokens minted at the time of borrow are burned and reserveAssets that they borrowed are supposed to be taken back from them with some additional interest accrued on their loan that they are supposed to pay along with the borrowedAmount. However due to flaws in the logic that accrued interest is not charged or taken from them

Vulnerability Details

When the borrowers borrows reserveAssets via borrow function, they are minted with debtTokens and when the borrows decide to repay their loan, those debtTokens minted at the time of borrow are burned and reserveAssets that they borrowed are supposed to be taken back from them with some additional interest accrued on their loan that they are supposed to pay along with the borrowedAmount. This interest that is accrued is computed and accrued accordingly on the basis of a gloabl reserve.usageIndex and the index of the user i.e UserState.index The usageIndex keeps increasing as the liquidity is used from the reserve and based on this some interest is accrued that is supposed to be charged to the borrower at the time of repaying the loan along with borrowed amount. This accrued amount or interest is computed as balanceIncrease here when the DebtToken's burn function is called in the LendingPool's _repay function. Now what the DebtToken.burn function does is that it computes that accrued amount that is to be charged from the borrower as balanceIncrease and along with this the DebtTokens are burned from the borrower that were minted at the time of borrow and after burning the those tokens this function(burn) returns a few things to the LendingPool's _repay function and one of these are the balanceIncrease var i.e the interest that the borrower is supposed to pay along with the amount at the time of repayment. But the issue, here is that this balanceIncrease is computed but when the amount is transferred back from the user to the reserve here in line of code only the amount that was passed in by the user is taken from him and the balanceIncrease that was computed and was supposed to be charged is not included in this transfer. This is very clearly wrong and unexpected behaviour as the borrower just basically got a loan at literally 0 interest. Lets take an example of how much the borrower paid back and how much was supposed to be charged from him. --> lets say that a borrower borrowed 10000e18 crvUsd from the reserve and at that time lets say: usageIndex = 1e27 and UserState.index = 1e27 initially. But time passed and users kept coming and interacting with the LendingPool contract. Now providing that the global reserve.usageIndex is updated everytime a core function of the contract is called, so lets say that when the borrower decided to repay is debt he called the repay function and at that time the usageIndex got to = 1000712723634978242463146739. Now the balIncrease calculation willbe like-: balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index); the userBal here will be 10000e18 as mentioned above. -> 10000e18.rayMul(1000712723634978242463146739) - 10000e18.rayMul(1e27) = 7127236349782424631(7e18 approx) As can be seen that this is the interest that was accrued over the time but was not charged to the borrower

Impact

The impact here is quite good, the reasoning for that is that even though the loss for the protocol is not that much, but still the fact that this is basically a crucial invariant for the protocol, like first of all the borrower gets a loan without any interest which does not make much sense and also the fact that the interest that was accrued was supposed to be for the reserve, and reserve didnt get that because of tis vulnerability and flawed logic.

Tools Used

Manual Review

Recommendations

add the balanceIncrease that was computed in the Debttoken.burn function to the safeTransferFrom function, where the reserve assets are taken back from the user. Like This:-

- IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
+ IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, balanceIncrease + amountScaled);

Code Snippets

function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
if (onBehalfOf == address(0)) revert AddressCannotBeZero();
UserData storage user = userData[onBehalfOf];
// Update reserve state before repayment
ReserveLibrary.updateReserveState(reserve, rateData);
// Calculate the user's debt (for the onBehalfOf address)
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex);
// If amount is greater than userDebt, cap it at userDebt
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount;
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex);
// Burn DebtTokens from the user whose debt is being repaid (onBehalfOf)
// is not actualRepayAmount because we want to allow paying extra dust and we will then cap there
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = <-
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
// Transfer reserve assets from the caller (msg.sender) to the reserve
-> IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);```
function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
-> balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index); AUDIT- this is being computed and returned but not used or included when taking back the tokens from the borrowers
amount = amount;
}```
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

DebtToken::burn calculates balanceIncrease (interest) but never applies it, allowing borrowers to repay loans without paying accrued interest

Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!