Core Contracts

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

LendingPool::getNormalizedDebt() returns stale usage index

Summary

In LendingPool the functions that rely on the indexes (liquidity index, usage index) update them before executing state changes, in order to ensure they are working with accurate data.

On the other hand LendingPool::getNormalizedDebt() method does not refresh the usage index, returning a stale value.

Vulnerability Details

The usage index returned by LendingPool::getNormalizedDebt() is stale. All of the contracts that rely on this method will use stale data for performing calculations.

Examples

Impact

Calculations in DebtToken relying on the liquidity index will be performed with stale data, leading to incorrect results.

POC:

set up:

  1. run npm install and add .env file if you haven't already

  2. npm i --save-dev @nomicfoundation/hardhat-foundry - Install the hardhat-foundry plugin.

  3. Add require("@nomicfoundation/hardhat-foundry"); to the top of your hardhat.config.js file.

  4. Run npx hardhat init-foundry in your terminal. This will generate a foundry.toml file based on your Hardhat project’s existing configuration, and will install the forge-std library.

  5. mkdir test/foundry

  6. Create *.t.sol file inside /test/foundry and paste the POC inside.

  7. In foundry.toml update test = 'test/foundry'

running test:

forge test --match-test testGetNormalizedDebtReturnsStaleData -vv

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../../contracts/core/pools/LendingPool/LendingPool.sol";
import "../../contracts/libraries/pools/ReserveLibrary.sol";
import "../../lib/forge-std/src/Test.sol";
import "../../lib/forge-std/src/console.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../../contracts/core/tokens/DebtToken.sol";
import "../../contracts/core/tokens/RToken.sol";
import "../../contracts/core/tokens/RAACNFT.sol";
import "../../contracts/core/primitives/RAACHousePrices.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function balanceOf(address account) public view override returns (uint256) {
return super.balanceOf(account);
}
}
contract ReserveLibraryTest is Test {
//address that deploys and owns contracts
address owner;
address user;
LendingPool lendingPool;
MockERC20 reserveAsset;
RToken rToken;
DebtToken debtToken;
RAACNFT raacNFt;
address priceOracle;
RAACHousePrices housePrices;
ReserveLibrary.ReserveData reserve;
ReserveLibrary.ReserveRateData rateData;
function setUp() public {
owner = makeAddr("owner");
initializeContracts(owner);
configureContracts(owner);
user = makeAddr("user");
}
function initializeContracts(address _owner) internal {
reserveAsset = new MockERC20("reserveMock", "mkTkn");
rToken = new RToken("rMock", "mkTkn", _owner, address(reserveAsset));
debtToken = new DebtToken("debtMock", "mkTkn", _owner);
priceOracle = makeAddr("priceOracle");
housePrices = new RAACHousePrices(_owner);
raacNFt = new RAACNFT(address(reserveAsset), address(housePrices), _owner);
lendingPool = new LendingPool(
address(reserveAsset), address(rToken), address(debtToken), address(raacNFt), address(housePrices), 1e26
);
}
function configureContracts(address _owner) internal {
vm.startPrank(_owner);
housePrices.setOracle(priceOracle);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
vm.stopPrank();
vm.prank(priceOracle);
housePrices.setHousePrice(1, 10_000);
}
function testGetNormalizedDebtReturnsStaleData() public {
address borrower = makeAddr("borrower");
//starting usage index = 1e27
uint256 initialUsageIndex = lendingPool.getNormalizedDebt();
console.log("initial usage index: %d", initialUsageIndex);
//mint reserve asset
reserveAsset.mint(user, 10_000);
reserveAsset.mint(borrower, 10_000);
//deposit liquidity into the lending pool
vm.startPrank(user);
deposit(10_000);
vm.stopPrank();
//borrow
vm.startPrank(borrower);
mintNFT();
depositNFT();
lendingPool.borrow(1000);
vm.stopPrank();
//fast-forward 1 block
skip(12);
uint256 usageIndexAfterBorrow = lendingPool.getNormalizedDebt();
console.log("usage index after borrow: %d", usageIndexAfterBorrow);
assertEq(initialUsageIndex, usageIndexAfterBorrow);
}
function mintNFT() internal {
reserveAsset.approve(address(raacNFt), 10_000);
raacNFt.mint(1, 10_000);
}
function depositNFT() internal {
raacNFt.approve(address(lendingPool), 1);
lendingPool.depositNFT(1);
}
function deposit(uint256 amount) internal {
reserveAsset.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
}
}

Tools Used

Manual review

Recommendations

Either call ReserveLibrary::updateReserveState() to refresh the indexes before returning the usage index, or call ReserveLibrary::getNormalizedDebt().

function getNormalizedDebt() external view returns (uint256) {
- return reserve.usageIndex;
+ return ReserveLibrary.getNormalizedDebt(reserve, rateData);
}
Updates

Lead Judging Commences

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

LendingPool::getNormalizedIncome() and getNormalizedDebt() returns stale data without updating state first, causing RToken calculations to use outdated values

Support

FAQs

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