Summary
The function DebtToken::totalSupply()
implementation is incorrect that can cause the reserve total usage is tracked wrongly.
Vulnerability Details
The function DebtToken::totalSupply()
returns the wrong value such that it returns scaled_supply / normalized_debt
. It should be scaled_supply * normalized_debt
instead
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
@> return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
}
It can result the LendingPool
to track the wrong total usage
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
...
@> (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;
...
}
Hence, there will be wrong reserve parameters computed, such as utilization rate, borrow rate and liquidity rate -> also affect liquidity index and usage index. It means that the whole lending protocol is broken.
function updateInterestRatesAndLiquidity(ReserveData storage reserve,ReserveRateData storage rateData,uint256 liquidityAdded,uint256 liquidityTaken) internal {
if (liquidityAdded > 0) {
reserve.totalLiquidity = reserve.totalLiquidity + liquidityAdded.toUint128();
}
if (liquidityTaken > 0) {
if (reserve.totalLiquidity < liquidityTaken) revert InsufficientLiquidity();
reserve.totalLiquidity = reserve.totalLiquidity - liquidityTaken.toUint128();
}
uint256 totalLiquidity = reserve.totalLiquidity;
uint256 totalDebt = reserve.totalUsage;
uint256 computedDebt = getNormalizedDebt(reserve, rateData);
uint256 computedLiquidity = getNormalizedIncome(reserve, rateData);
@> uint256 utilizationRate = calculateUtilizationRate(reserve.totalLiquidity, reserve.totalUsage);
@> rateData.currentUsageRate = calculateBorrowRate(
rateData.primeRate,
rateData.baseRate,
rateData.optimalRate,
rateData.maxRate,
rateData.optimalUtilizationRate,
@> utilizationRate
);
@> rateData.currentLiquidityRate = calculateLiquidityRate(
@> utilizationRate,
rateData.currentUsageRate,
rateData.protocolFeeRate,
totalDebt
);
updateReserveInterests(reserve, rateData);
emit InterestRatesUpdated(rateData.currentLiquidityRate, rateData.currentUsageRate);
}
function calculateUtilizationRate(uint256 totalLiquidity, uint256 totalDebt) internal pure returns (uint256) {
if (totalLiquidity < 1) {
return WadRayMath.RAY;
}
@> uint256 utilizationRate = totalDebt.rayDiv(totalLiquidity + totalDebt).toUint128();
return utilizationRate;
}
function updateReserveInterests(ReserveData storage reserve,ReserveRateData storage rateData) internal {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}
uint256 oldLiquidityIndex = reserve.liquidityIndex;
if (oldLiquidityIndex < 1) revert LiquidityIndexIsZero();
@> reserve.liquidityIndex = calculateLiquidityIndex(
@> rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
);
@> reserve.usageIndex = calculateUsageIndex(
@> rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
reserve.lastUpdateTimestamp = uint40(block.timestamp);
emit ReserveInterestsUpdated(reserve.liquidityIndex, reserve.usageIndex);
}
PoC
Add this test to file test/unit/core/pools/LendingPool/LendingPool.test.js
it.only("test total usage/debt", async function(){
const borrowAmount = ethers.parseEther("60");
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [30 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await raacHousePrices.setHousePrice(2, ethers.parseEther("100"));
const amountToPay = ethers.parseEther("100");
await token.mint(user2.address, amountToPay);
await token.connect(user2).approve(raacNFT.target, amountToPay);
const tokenId = 2;
await raacNFT.connect(user2).mint(tokenId, amountToPay);
await raacNFT.connect(user2).approve(lendingPool.target, tokenId);
await lendingPool.connect(user2).depositNFT(tokenId);
await lendingPool.connect(user2).borrow(ethers.parseEther("1"));
await ethers.provider.send("evm_increaseTime", [30 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
let sumUsersDebt = await debtToken.balanceOf(user1.address) + await debtToken.balanceOf(user2.address)
let totalSupply = await debtToken.totalSupply();
let reserve = await lendingPool.reserve();
console.log(`--> before updating state\ntotal debt\t${sumUsersDebt}\ntotal supply\t${totalSupply}\nreserve total usage\t${reserve[4]}`)
await lendingPool.connect(user1).updateState();
totalSupply = await debtToken.totalSupply();
reserve = await lendingPool.reserve();
console.log(`\n--> after updating state\ntotal debt\t${sumUsersDebt}\ntotal supply\t${totalSupply}\nreserve total usage\t${reserve[4]}`)
})
Run the test and console show:
LendingPool
Borrow and Repay
--> before updating state
total debt 61148689538325432940
total supply 60846740072285777813
reserve total usage 60846740072285777813
--> after updating state
total debt 61148689538325432940
total supply 60695961312110307280
reserve total usage 60846740072285777813
✔ test total usage/debt (39ms)
1 passing (2s)
It means that the total usage is incorrect when compared with the total debt sum from all positions
Impact
Tools Used
Manual
Recommendations
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
- return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
+ return scaledSupply.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}