Core Contracts

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

Incorrect Utilization Rate Calculation in RAACMinter

Summary

The getUtilizationRate() function in the RAACMinter contract uses an incorrect value for calculating the total borrowed amount by using the usage index instead of the actual borrowed amount. However, due to the bounded nature of emission rate adjustments and control mechanisms in place, the impact is limited.

Vulnerability Details

The vulnerability exists in the getUtilizationRate() function:

function getUtilizationRate() internal view returns (uint256) {
@> uint256 totalBorrowed = lendingPool.getNormalizedDebt(); // Returns usage index (scaled by 1e27)
uint256 totalDeposits = stabilityPool.getTotalDeposits();
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}

The issue arises because:

  • getNormalizedDebt() returns the usage index which is scaled by 1e27 (RAY)

  • This value is used directly in calculations with totalDeposits which is in normal token decimals (1e18)

  • The resulting utilization rate will be massively inflated due to the scale difference

For example:

  • If actual borrowed amount is 100 tokens

  • Usage index might be 1.5e27 (1.5 RAY)

  • Total deposits is 200e18 tokens

  • Current calculation: (1.5e27 * 100) / 200e18 = 750,000%

  • Correct calculation should be: (100e18 * 100) / 200e18 = 50%

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

⚠️ Adjust the RAACMinter contract to include some console log statements:

+ import "forge-std/console2.sol";
contract RAACMinter is IRAACMinter, Ownable, ReentrancyGuard, Pausable, AccessControl {
function calculateNewEmissionRate() internal view returns (uint256) {
uint256 utilizationRate = getUtilizationRate();
uint256 adjustment = (emissionRate * adjustmentFactor) / 100;
+ console2.log("\nCalculateNewEmissionRate:");
+ console2.log("emissionRate", emissionRate);
+ console2.log("utilizationRate", utilizationRate);
+ console2.log("utilizationTarget", utilizationTarget);
+ console2.log("adjustment", adjustment);
if (utilizationRate > utilizationTarget) {
uint256 increasedRate = emissionRate + adjustment;
uint256 maxRate = increasedRate > benchmarkRate ? increasedRate : benchmarkRate;
return maxRate < maxEmissionRate ? maxRate : maxEmissionRate;
} else if (utilizationRate < utilizationTarget) {
uint256 decreasedRate = emissionRate > adjustment ? emissionRate - adjustment : 0;
uint256 minRate = decreasedRate < benchmarkRate ? decreasedRate : benchmarkRate;
return minRate > minEmissionRate ? minRate : minEmissionRate;
}
return emissionRate;
}
function getUtilizationRate() internal view returns (uint256) {
uint256 totalBorrowed = lendingPool.getNormalizedDebt();
uint256 totalDeposits = stabilityPool.getTotalDeposits();
+ console2.log("\nGetUtilizationRate:");
+ console2.log("totalBorrowed", totalBorrowed);
+ console2.log("totalDeposits", totalDeposits);
if (totalDeposits == 0) return 0;
return (totalBorrowed * 100) / totalDeposits;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RAACHousePrices} from "../../contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {RAACMinter, IRAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract FoundryTest is Test {
StabilityPool public stabilityPool;
LendingPool public lendingPool;
RAACMinter public raacMinter;
crvUSDToken public crvusd;
RToken public rToken;
DEToken public deToken;
RAACToken public raacToken;
RAACNFT public raacNFT;
DebtToken public debtToken;
RAACHousePrices public raacHousePrices;
address public owner;
address public user1;
address public user2;
address public user3;
address public borrower;
address public treasury;
uint256 public constant INITIAL_BALANCE = 1000e18;
uint256 public constant INITIAL_PRIME_RATE = 0.1e27;
uint256 public constant TOKEN_ID = 1;
uint256 public constant HOUSE_PRICE = 1000e18;
function setUp() public {
// Setup accounts
owner = address(this);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
borrower = makeAddr("borrower");
// Deploy base tokens
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
raacToken = new RAACToken(owner, 100, 50);
// Deploy price oracle and set oracle
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
// Deploy NFT
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
// Deploy pool tokens
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
// Deploy pools
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
stabilityPool = new StabilityPool(owner);
// this is needed otherwise lastEmissionUpdateTimestamp will underflow in the RAACMinter constructor
vm.warp(block.timestamp + 2 days);
// Deploy RAAC minter
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
// Setup cross-contract references
lendingPool.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
// Initialize Stability Pool
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
// Setup permissions
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
// Mint initial tokens and setup approvals
// also deposit crvUSD to the stability pool to get rTokens
_setupInitialBalancesAndAllowances();
}
function test_RaacMinterUtilizationRate() public {
uint256 depositAmount = 100e18;
vm.startPrank(user1);
// first deposit doesn't have any impact because totalDeposits = 0 at start
stabilityPool.deposit(depositAmount);
assertEq(deToken.balanceOf(user1), depositAmount);
assertEq(rToken.balanceOf(address(stabilityPool)), depositAmount);
vm.stopPrank();
// borrow from the LendingPool
_setupBorrower();
vm.warp(block.timestamp + 1 days);
vm.startPrank(user1);
// deposit again to print the CalculateNewEmissionRate and
// GetUtilizationRate console logs
stabilityPool.deposit(900e18);
vm.stopPrank();
}
function _setupBorrower() internal {
vm.startPrank(owner);
raacHousePrices.setHousePrice(TOKEN_ID, HOUSE_PRICE);
vm.stopPrank();
crvusd.mint(borrower, HOUSE_PRICE);
vm.startPrank(borrower);
crvusd.approve(address(raacNFT), HOUSE_PRICE);
raacNFT.mint(TOKEN_ID, HOUSE_PRICE);
// deposit NFT to the lending pool
raacNFT.approve(address(lendingPool), TOKEN_ID);
lendingPool.depositNFT(TOKEN_ID);
// Borrow
lendingPool.borrow(HOUSE_PRICE);
vm.stopPrank();
}
function _setupInitialBalancesAndAllowances() internal {
// Mint crvUSD to users
crvusd.mint(user1, INITIAL_BALANCE);
crvusd.mint(user2, INITIAL_BALANCE);
crvusd.mint(user3, INITIAL_BALANCE);
// Setup approvals for users
vm.startPrank(user1);
crvusd.approve(address(lendingPool), type(uint256).max);
lendingPool.deposit(INITIAL_BALANCE);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.stopPrank();
vm.startPrank(user2);
crvusd.approve(address(lendingPool), type(uint256).max);
lendingPool.deposit(INITIAL_BALANCE);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.stopPrank();
vm.startPrank(user3);
crvusd.approve(address(lendingPool), type(uint256).max);
lendingPool.deposit(INITIAL_BALANCE);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.stopPrank();
}
}
GetUtilizationRate:
totalBorrowed 1000000000000000000000000000 // 1e27 = usageIndex and not borrowed amount
totalDeposits 0
CalculateNewEmissionRate:
emissionRate 138888888888888888
utilizationRate 0
utilizationTarget 70
adjustment 6944444444444444
GetUtilizationRate:
totalBorrowed 1000000000000000000000000000
totalDeposits 100000000000000000000
CalculateNewEmissionRate:
emissionRate 131944444444444444
utilizationRate 1000000000 // in %
utilizationTarget 70
adjustment 6597222222222222

Impact

While the calculation is incorrect, several factors limit the impact:

  1. The utilization rate is only used for threshold comparison against utilizationTarget (70%)

  2. Emission rate adjustments are bounded by:

    1. Hard limits via minEmissionRate and maxEmissionRate

    2. adjustmentFactor limiting changes to 5% per update

  3. Emission updates are time-locked via emissionUpdateInterval

It's still rated as medium because the utilizationRate will always be higher than the utilizationTarget because of the inflated value so the new emission rate will always increase to the max allowed value.

Tools Used

  • Manual Review

Recommendations

Replace the current implementation with the correct total borrowed amount.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACMinter::getUtilizationRate incorrectly mixes stability pool deposits with lending pool debt index instead of using proper lending pool metrics

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

RAACMinter::getUtilizationRate incorrectly mixes stability pool deposits with lending pool debt index instead of using proper lending pool metrics

Support

FAQs

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