Core Contracts

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

Excess Repayment Funds Not Refunded to Users in the `DebtToken.sol` and `LendingPool.sol` Contracts

Summary

In the LendingPool contract, when a user repays more than their actual debt, the excess funds are not refunded back to the user. This issue arises because the contract transfers the full repayment amount without properly handling and returning any excess funds, violating the principle of minimal surprise and potentially causing user fund loss.

Vulnerability Details

In the burn function of the DebtToken contract, when the repayment amount exceeds the user’s balance, the excess funds are not tracked or refunded.

contracts/core/tokens/DebtToken.sol:burn#L202-L204

function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
uint256 userBalance = balanceOf(from);
// @audit ❌ Limiting amount but not recording or returning the excess
...
if (amount > userBalance) {
amount = userBalance;
}
...
_burn(from, amount.toUint128());
}

In the ._repay function of the LendingPool.sol contract, the excess repayment amount is transferred without any refund mechanism.

contracts/core/pools/LendingPool/LendingPool.sol:_repay#L418-L422

function _repay(uint256 amount, address onBehalfOf) internal {
// ...
(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 // @audit ❌ Transfer full amount, no refund for excess
);
}

Reproduction Steps:

Scenario:

  1. User debt: 100 ETH

  2. User repayment: 101 ETH

Result:

  • Only 100 ETH is used to repay the debt.

  • The excess 1 ETH is locked in the contract.

  • The user is unable to retrieve the excess 1 ETH.

Impact

  1. Loss of User Funds: If a user repays more than their outstanding debt, the excess funds are locked in the contract, leading to a loss of funds.

  2. Users Funds Permanently Locked: The excess repayment is not refunded, which could discourage users from overpaying or making additional repayments, potentially hindering liquidity.

  3. Increased Risk of Repayment Failure: Users might refrain from making overpayments for fear of excess funds being lost, thus increasing the likelihood of repayment failures.

Tools Used

Manual code review

Recommendations

It is recommended to modify the burn function of the DebtToken.sol contract. Implement a refund mechanism to return any excess repayment amount to the user.

function burn(
address from,
uint256 amount,
uint256 index
- ) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
+ ) external override onlyReservePool returns (uint256, uint256, uint256, uint256uint256) {
...
uint256 userBalance = balanceOf(from);
- if(amount > userBalance){
- amount = userBalance;
- }
+ actualAmount = amount > userBalance ? userBalance : amount;
+ refundAmount = amount - actualAmount; // Calculate excess funds
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
_burn(from, actualAmount.toUint128());
emit Burn(from, amountScaled, index);
- return (amount, totalSupply(), amountScaled, balanceIncrease);
+ return (actualAmount, totalSupply(), balanceIncreaseamountScaled, balanceIncrease, refundAmount);
}

Modify LendingPool._repay: Transfer the actual repayment amount and refund the excess funds to the user.

function _repay(uint256 amount, address onBehalfOf) internal {
// ...
- (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
- (uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease, uint256 refundAmount) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
// Transfer the actual amount repaid
- IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
- IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, actualAmount);
// Refund any excess repayment
+ if (refundAmount > 0) {
+ IERC20(reserve.reserveAssetAddress).safeTransfer(
+ msg.sender,
+ refundAmount
+ );
+ emit RepayRefund(msg.sender, refundAmount);
+ }
}
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!