Summary
LendingPool
contract tracks position debt incorrectly such that the accrued interest is not taken into account, so the tracked debt is less than the actual accumulated debt
Vulnerability Details
When an user calls LendingPool::borrow()
, the function DebtToken::mint()
is called to handles to interest accrual logic and also mint the DebtToken
for the user. The mint amount takes into account the balanceIncrease
which is the user position's interest accrued since last usage index.
The problem arises when the accrued interest is not taken into account in the function LendingPool::borrow()
, such that user scaled debt is increased user.scaledDebtBalance += scaledAmount
when scaledAmount = amount.rayDiv(reserve.usageIndex)
. Note that this scaledAmount
does not include interest. As a result, the mint amount of DebtToken
includes interest, but LendingPool's user.scaledDebtBalance
does not include interest.
This can cause the value user.scaledDebtBalance
less than expected. It also leads to scenario that borrowers can not repay total debt because arithmetic underflow happens in LendingPool::_repay()
.
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
...
@> 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;
...
}
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);
reserve.totalUsage = newTotalSupply;
@> user.scaledDebtBalance -= amountBurned;
...
}
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
...
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());
...
}
PoC
Add this test to test/unit/core/pools/LendingPool/LendingPool.test.js
:
describe("Borrow and Repay", function () {
...
it.only("test debt tracked", async function(){
const collateralValue = await lendingPool.getUserCollateralValue(user1.address);
const totalBorrowable = collateralValue * 100n / 80n;
const borrowAmount = totalBorrowable / 3n;
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [90 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [90 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await lendingPool.connect(user1).updateState();
let userData = await lendingPool.userData(user1.address);
let reserveData = await lendingPool.reserve();
let ray = ethers.parseUnits("1", 27);
let userTotalDebt = (userData[0] * reserveData[6] + (ray/2n)) / ray
let debt = await debtToken.balanceOf(user1.address);
expect(debt).to.not.eq(userTotalDebt);
await lendingPool.connect(user1).borrow(totalBorrowable - userTotalDebt);
debt = await debtToken.balanceOf(user1.address);
expect(debt).to.gt(totalBorrowable)
await lendingPool.connect(user1).repay(debt);
})
Run the test and it failed:
0 passing (2s)
1 failing
1) LendingPool
Borrow and Repay
test debt tracked:
Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
at LendingPool._repay (contracts/core/pools/LendingPool/LendingPool.sol:458)
at LendingPool.repay (contracts/core/pools/LendingPool/LendingPool.sol:404)
...
Impact
Tools Used
Manual
Recommendations
- user.scaledDebtBalance += scaledAmount;
+ user.scaledDebtBalance += amountMinted;