Core Contracts

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

User will not be able to fully repay the debt due to wrong value added to increase balance in debtMint function

Summary

When a user borrows for the second time, extra debt tokens are minted, calculated from the difference of last usage index to the current index. However, the user's debt only increases by the new borrow amount. This discrepancy prevents the user from fully repaying the debt, as repayment is calculated using the user's debt token balance.

Vulnerability Details

When ever user take borrow from lending pool we mint debt tokens to the user and also update state of user borrow with new borrow amount as follows :

/contracts/core/pools/LendingPool/LendingPool.sol:333
333: function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
334: if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
335:
336: UserData storage user = userData[msg.sender];
337:
...
347: uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
...
353:
354: // Update user's scaled debt balance
355: uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
358: // Mint DebtTokens to the user (scaled amount)
359: (bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
360:
364: user.scaledDebtBalance += scaledAmount; // here wee added the scaledamount in userDebt
...
376: }

we calculate scaledAmount and add it to the user debt state scaledDebtBalance. Now let check the debt token minting logic:

/contracts/core/tokens/DebtToken.sol:137
137: function mint(
138: address user,
139: address onBehalfOf,
140: uint256 amount,
141: uint256 index
142: ) external override onlyReservePool returns (bool, uint256, uint256) {
143: if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
144: if (amount == 0) {
145: return (false, 0, totalSupply());
146: }
147:
148: uint256 amountScaled = amount.rayDiv(index);
149: if (amountScaled == 0) revert InvalidAmount();
150:
151: uint256 scaledBalance = balanceOf(onBehalfOf);
152: bool isFirstMint = scaledBalance == 0;
153:
154: uint256 balanceIncrease = 0;
155: if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
156: balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
157: }
158:
159: _userState[onBehalfOf].index = index.toUint128();
160:
161: uint256 amountToMint = amount + balanceIncrease;
162:
163: _mint(onBehalfOf, amountToMint.toUint128());
164:
165: emit Transfer(address(0), onBehalfOf, amountToMint);
166: emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
167:
168: return (scaledBalance == 0, amountToMint, totalSupply());
169: }

From the above code it is ecident that the extra token will be minted to borrower if he borrow again. but we increment the usedDebt only via new barrow amount. this inconsistency could reuslt in two different impact check the impact section.

POC :

Add the following test cases to LendingPool.test.js section describe("Borrow and Repay" and run with command npx hardhat test.

  1. First test case show that the user still has debt token and debt state variable > 0 but the debt in state variable is less than debt balance.

  2. 2nd test case show that the user debt state variable is 0 but the user debt balance is > 0.

it.only("Case 1 :repay using debt token balance", async function () { // @audit : POC to show that user still hold debt token
const borrowAmount = ethers.parseEther("50");
await lendingPool.connect(user1).borrow(borrowAmount);
const crvUSDBalance = await crvusd.balanceOf(user1.address);
expect(crvUSDBalance).to.equal(ethers.parseEther("1050"));
let debtBalance = await debtToken.balanceOf(user1.address);
console.log("after first loan balance user 1 " , debtBalance);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await lendingPool.updateState();
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
debtBalance = await debtToken.balanceOf(user1.address);
console.log("after 2nd loan balance user 1 " , debtBalance);
const poolBalanceBefore = await crvusd.balanceOf(rToken.target);
let debtToBePayed = debtBalance;
await crvusd.connect(user1).approve(rToken.target, debtBalance + ethers.parseEther("1"));
await lendingPool.connect(user1).repay(debtBalance+ethers.parseEther("0.0011"));
debtBalance = await debtToken.balanceOf(user1.address);
console.log("after full repay balance user 1 " , debtBalance);
const poolBalance = await crvusd.balanceOf(rToken.target);
console.log("poolbalanceAfter " , poolBalance);
console.log("poolbalance Befor " , poolBalanceBefore);
let userDebt = await lendingPool.getUserDebt(user1.address);
console.log("userDebt" , userDebt);
expect(userDebt).to.lt(debtBalance);
});
it.only("Case 2: repay using userdebt state variable", async function () { // @audit : POC to show that user still hold debt token
const borrowAmount = ethers.parseEther("50");
await lendingPool.connect(user1).borrow(borrowAmount);
const crvUSDBalance = await crvusd.balanceOf(user1.address);
expect(crvUSDBalance).to.equal(ethers.parseEther("1050"));
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await lendingPool.updateState();
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
let debtBalance = await debtToken.balanceOf(user1.address);
console.log("after first loan balance user 1 " , debtBalance);
debtBalance = await debtToken.balanceOf(user1.address);
console.log("after 2nd loan balance user 1 " , debtBalance);
let userDebt = await lendingPool.getUserDebt(user1.address);
const poolBalanceBefore = await crvusd.balanceOf(rToken.target);
let debtToBePayed = userDebt;
await crvusd.connect(user1).approve(rToken.target, userDebt + ethers.parseEther("1"));
await lendingPool.connect(user1).repay(userDebt); // this will not clear all the debt
userDebt = await lendingPool.getUserDebt(user1.address);
// await expect(lendingPool.connect(user1).repay(userDebt)).to.be.rejectedWith("InvalidAmount");
await lendingPool.connect(user1).repay(userDebt)
debtBalance = await debtToken.balanceOf(user1.address); // after repay
console.log("after full repay balance user 1 " , debtBalance);
const poolBalance = await crvusd.balanceOf(rToken.target);
console.log("poolbalanceAfter " , poolBalance);
console.log("poolbalance Befor " , poolBalanceBefore);
userDebt = await lendingPool.getUserDebt(user1.address);
console.log("userDebt" , userDebt);
expect(userDebt).to.lt(debtBalance);
});

Logs :

1st test case :
after full repay balance user 1 39589034594399475n
poolbalanceAfter 2000038571312498346168n
poolbalance Befor 1900000000000000000000n
userDebt 20842247460145145n
----------------------------------------------------------
2nd test case :
after full repay balance user 1 18746787121880098n
poolbalanceAfter 2000059413559970647862n
poolbalance Befor 1900000000000000000000n
userDebt 0n

Impact

This inconsistency between the debt token balance and the debt state variable can cause repayment transactions to revert in some cases. Additionally, it may fail to burn all debt tokens from the user, leading to incorrect debt tracking in the protocol.

Tools Used

Manual Review

Recommendations

Doe not mint extra token to the user in case when user borrow again . AAVE only emit the extra amount in event.

diff --git a/contracts/core/tokens/DebtToken.sol b/contracts/core/tokens/DebtToken.sol
index b7368e6..65abae4 100644
--- a/contracts/core/tokens/DebtToken.sol
+++ b/contracts/core/tokens/DebtToken.sol
function mint(
address user,
address onBehalfOf,
uint256 amount,
@@ -154,14 +154,11 @@ contract DebtToken is ERC20, ERC20Permit, IDebtToken, Ownable {
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;
+ uint256 amountToMint = amount;
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

Support

FAQs

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

Give us feedback!