Vulnerability Details
As stated in in-line docs, the DebtToken::mint
aligns with AaveV3's VariableDebtToken implementation. Aave's VariableDebtToken
implementation increases debt balance of a user by scaled amount via amount.rayDiv(index)
. It also calculates balanceIncrease
, but uses it solely for event emission.
Snippet from Aave's _mintScaled
:
uint256 amountScaled = amount.rayDiv(index);
...
...
...
@> _mint(onBehalfOf, amountScaled.toUint128());
@> uint256 amountToMint = amount + balanceIncrease;
@> emit Transfer(address(0), onBehalfOf, amountToMint);
@> emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index);
In current DebtToken::mint
implementation, the balanceIncrease
calculated is added to the amount that's being minted before calling _mint
.
@> uint256 amountToMint = amount + balanceIncrease;
@> _mint(onBehalfOf, amountToMint.toUint128());
The compounded interest is accounted for in DebtToken::_update
so adding it here results in inflated debt of user on each borrow. This can have the following consequences,
User would have to unnecessarily repay more than what they borrowed, even after accounting for interest.
User's debt is overstated which can lead to liquidation of user's collateral even when they have collateral
that's worth enough.
Impact
Current implementation a significant deviation from Aave V3’s debt token model that overstates user's debt leading to unnecessary liquidations and loss of assets.
Tools Used
Manual Review + Hardhat Testing
Proof-Of-Code
The following custom function was added to DebtToken
for testing only,
function rayDivOperation(uint256 amount, uint256 _index) external pure returns (uint256) {
return amount.rayDiv(_index);
}
Place the following test under Borrow and Repay
in LendingPool.test.js
it("borrow and repay with inflated amount after change in usageIndex", async() => {
const borrowAmount = ethers.parseEther("20");
const borrowAmount2 = ethers.parseEther("30");
const borrowAmount3 = ethers.parseEther("10");
await lendingPool.connect(user2).borrow(borrowAmount2);
await debtToken.balanceOf(user2.address);
await lendingPool.connect(user3).borrow(borrowAmount3);
await debtToken.balanceOf(user3.address);
const reserve = await lendingPool.reserve();
reserve.usageIndex;
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await lendingPool.updateState();
const reserve2 = await lendingPool.reserve();
const currentUsageIndex = reserve2.usageIndex;
const scaledBalanceOfUser = await debtToken.balanceOf(user1.address);
const scaledAmountMinted = await debtToken.rayDivOperation(borrowAmount, currentUsageIndex);
const expectedBalance = scaledBalanceOfUser + scaledAmountMinted;
await lendingPool.connect(user1).borrow(borrowAmount);
const actualBalance = await debtToken.balanceOf(user1.address);
console.log("expected balance of user1: ", expectedBalance);
console.log("actual balance of user1: ", actualBalance);
expect(actualBalance).to.gt(expectedBalance);
})
Recommendations
Remove balanceIncrease
from amount being minted and use scaledBalanceOf
for balanceIncrease
calculation,
- uint256 scaledBalance = balanceOf(onBehalfOf);
+ uint256 scaledBalance = scaledBalanceOf(onBehalfOf);
...
- uint256 amountToMint = amount + balanceIncrease;
- _mint(onBehalfOf, amountToMint.toUint128());
+ _mint(onBehalfOf, amount.toUint128());