Skip to content

[SECURITY DISCLOSURE] Critical Vulnerability found in BaseJumpRateModelV2.sol #297

@rbxict

Description

@rbxict

Bug‑Report – BaseJumpRateModelV2 (Compound‑style Jump Rate Model V2)
Prepared for: Client security audit team
Prepared by: Senior Web3 Security Auditor
Date: 2026‑03‑21


Executive Summary

The contract BaseJumpRateModelV2 is a fairly standard interest‑rate model used by Compound‑style money markets. The source code (Solidity ^0.8.10) leverages built‑in overflow checks, so arithmetic‑related exploits are largely mitigated.

Nevertheless, two high‑severity issues have been identified:

# Severity Category Short Description
1 HIGH Denial‑of‑Service (DoS) – Division‑by‑Zero utilizationRate() can revert when cash + borrows - reserves == 0 while borrows > 0. An attacker controlling reserves can force the market to hit this edge case, causing a revert in every call that depends on the interest‑rate calculation (e.g., borrow, repay, accrueInterest).
2 HIGH Owner Initialization – Zero‑Address Owner The constructor does not validate owner_. If the contract is deployed with owner_ == address(0), the updateJumpRateModel() function becomes permanently inaccessible, freezing the model’s parameters. This is a classic “owner‑lock” DoS that can be triggered unintentionally or by a malicious deployer.

Both issues are critical to the economic correctness of the protocol because they can halt interest‑rate computation, breaking the core lending/borrowing workflow.

No other critical or high‑severity vulnerabilities were observed in the provided snippet (e.g., re‑entrancy, unchecked external calls, arithmetic overflow, uninitialized storage, or privileged state changes).


Detailed Findings

1. Denial‑of‑Service via Division‑by‑Zero in utilizationRate()

Location

function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
    // Utilization rate is 0 when there are no borrows
    if (borrows == 0) {
        return 0;
    }

    return borrows * BASE / (cash + borrows - reserves);
}

Root Cause
The denominator cash + borrows - reserves is not protected against becoming zero. If an attacker (or a malicious governance action) sets reserves equal to cash + borrows, the division yields a zero divisor, causing the EVM to revert with a division by zero error.

Impact

  • Every market function that queries the borrow rate (e.g., borrowRatePerBlock(), accrueInterest()) internally calls utilizationRate().
  • A revert aborts the whole transaction, preventing borrowers from taking new loans, lenders from withdrawing, and the protocol from updating interest accruals.
  • This constitutes a Denial‑of‑Service that can be triggered by an attacker who can influence the reserves variable (e.g., via an admin call that adjusts reserves, or by donating a large amount of reserves).

Recommended Mitigation
Add a safeguard that ensures the denominator is never zero, for example:

function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
    if (borrows == 0) {
        return 0;
    }

    uint denominator = cash + borrows - reserves;
    require(denominator > 0, "Utilization denominator is zero");
    return borrows * BASE / denominator;
}

Alternatively, cap reserves to a maximum of cash + borrows - 1 in any function that modifies reserves.


2. Owner Can Be Initialized to the Zero Address

Location

constructor(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_, address owner_) internal {
    owner = owner_;
    updateJumpRateModelInternal(baseRatePerYear,  multiplierPerYear, jumpMultiplierPerYear, kink_);
}

Root Cause
The constructor does not validate that owner_ is a non‑zero address. If the contract is deployed with owner_ == address(0), the owner variable becomes 0x0. Consequently, the require(msg.sender == owner, ...) check in updateJumpRateModel() will always fail, making the function permanently inaccessible.

Impact

  • The model’s parameters (base rate, multiplier, jump multiplier, kink) become immutable after deployment, regardless of any intended governance upgrades.
  • If a legitimate upgrade path (e.g., a Timelock contract) is expected, the protocol is effectively bricked.
  • From a security perspective, a malicious deployer could deliberately set owner = address(0) to lock the parameters, causing a Denial‑of‑Service for the market.

Recommended Mitigation
Validate the owner address in the constructor:

constructor(uint baseRatePerYear, uint multiplierPerYear, uint jumpMultiplierPerYear, uint kink_, address owner_) internal {
    require(owner_ != address(0), "Owner cannot be zero address");
    owner = owner_;
    updateJumpRateModelInternal(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_);
}

Optionally, expose a transferOwnership(address newOwner) function (with proper timelock restrictions) to allow recovery in exceptional cases.


Additional Observations (No Critical Issues)

Observation Details
Arithmetic Safety Solidity ^0.8.x already reverts on overflow/underflow, so explicit SafeMath is unnecessary.
Access Control Only the owner (expected to be a Timelock) can call updateJumpRateModel(). No other privileged functions are present.
Events NewInterestParams is declared but never emitted in the provided snippet. Ensure that updateJumpRateModelInternal emits this event for transparency.
Public Variables owner, multiplierPerBlock, baseRatePerBlock, jumpMultiplierPerBlock, and kink are declared public, which is acceptable for read‑only access.
Immutable Constants BASE and blocksPerYear are correctly defined as constant.
Potential Gas Optimisation The utilizationRate function could be marked internal if only used within the contract, but this is a minor efficiency point.

Verdict

SECURITY STATUS: The contract contains high‑severity denial‑of‑service vectors (division‑by‑zero and zero‑address owner). These must be addressed before deployment in a production environment.


Action Items

  1. Implement denominator safeguard in utilizationRate() (see mitigation above).
  2. Add owner non‑zero validation in the constructor.
  3. Add unit tests that explicitly attempt the edge cases:
    • reserves = cash + borrows → expect revert with meaningful error.
    • Deploy contract with owner = address(0) → expect revert on construction.
  4. Re‑audit the updated contract, especially the updateJumpRateModelInternal implementation (not provided) to confirm no hidden state‑change bugs.

Once the above fixes are applied and verified, the contract can be considered secure for its intended purpose.


RECOMMENDATION: Immediate patch required. Bug Bounty Payout Address (ERC20): 0xe744f6791a685b0A0cC316ED44375B69361c837F

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions