Skip to content

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

@rbxict

Description

@rbxict

Security Audit Report – BaseJumpRateModelV2 (Compound Jump Rate Model V2)
Auditor: ChatGPT (Immunefi)
Date: 2026‑03‑21


1. Executive Summary

The contract implements a configurable interest‑rate model used by Compound‑style money markets. Most of the logic follows the reference implementation and leverages Solidity 0.8.x built‑in overflow checks, so arithmetic safety is largely assured.

Two critical issues were identified that can cause Denial‑of‑Service (DoS) or unstable interest‑rate calculations:

# Severity Title Brief Description
1 Critical utilizationRate denominator under‑/overflow → potential revert When reserves > cash + borrows the expression cash + borrows - reserves underflows, causing a revert (or division‑by‑zero when the result is 0). This can freeze the market because the core borrowRate/supplyRate functions depend on utilizationRate.
2 High Missing validation of kink (and other rate parameters) → unrealistic or malicious rates kink is used as a utilization breakpoint (utilization <= kink). If set > BASE (1e18) or to 0, the piece‑wise interest‑rate formula can yield negative slopes or allow the “jump” multiplier to be applied to the entire utilization range, destabilising the market. No checks are performed in updateJumpRateModelInternal.

No other critical vulnerabilities (re‑entrancy, unchecked external calls, ownership takeover, integer overflow, etc.) were found.


2. Detailed Findings

2.1. utilizationRate denominator under/overflow

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);
}
  • Problem – The denominator cash + borrows - reserves can become negative (under‑flow) when reserves exceeds the sum of cash and borrows. In Solidity 0.8+, under‑flow triggers a runtime revert. Additionally, if the denominator evaluates to zero (e.g., cash + borrows == reserves), the division operation will revert with a division‑by‑zero error.

  • Impact

    • The function is called by the core interest‑rate calculations (getBorrowRate, getSupplyRate in the parent InterestRateModel).
    • A revert in utilizationRate propagates up, causing the market to fail to compute rates for that block.
    • This halts borrowing/lending actions, effectively a Denial‑of‑Service for the whole market.
    • An attacker (or an honest user) can trigger the condition by depositing an excessive amount of reserves (or by manipulating the accounting in a downstream contract that reports reserves).
  • Recommended Fix – Safeguard the denominator:

    function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
        if (borrows == 0) {
            return 0;
        }
    
        uint total = cash + borrows;
        if (total <= reserves) {
            // No utilization when all assets are held as reserves
            return 0;
        }
    
        return (borrows * BASE) / (total - reserves);
    }

    This guarantees a non‑zero, non‑negative denominator and returns a sensible utilization (0) when reserves dominate.

2.2. Missing validation of kink and other parameters

The contract allows the owner (usually a Timelock) to update the model parameters via updateJumpRateModel. The internal helper updateJumpRateModelInternal (code not shown) simply assigns the supplied values to storage variables:

function updateJumpRateModel(uint baseRatePerYear, uint multiplierPerYear,
                             uint jumpMultiplierPerYear, uint kink_) external {
    require(msg.sender == owner, "only the owner may call this function.");
    updateJumpRateModelInternal(baseRatePerYear, multiplierPerYear,
                                jumpMultiplierPerYear, kink_);
}
  • Problem – There are no constraints on:

    • kink_ being within the range 0 … BASE (i.e., 0 % to 100 % utilization).
    • baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear being non‑negative (they are uint, but could be set to extreme values).

    In the original Compound implementation, kink is required to be ≤ BASE; otherwise the piece‑wise logic that checks if (utilization <= kink) becomes meaningless and can result in the “jump” multiplier being applied to the whole utilization space or never applied at all.

  • Impact

    • A malicious or erroneous parameter update could set kink = 0 or kink > BASE, causing incorrect interest‑rate curves.
    • Extreme rates (e.g., absurdly high jumpMultiplierPerBlock) could force borrowers into unsustainable debt or freeze the market due to overflow when rates are used elsewhere (e.g., accrual calculations).
    • Even though the owner is expected to be a Timelock, the lack of validation makes the contract vulnerable to human error or malicious governance proposals.
  • Recommended Fix – Add explicit checks inside updateJumpRateModelInternal (or directly in updateJumpRateModel) :

    function _validateParams(
        uint baseRatePerYear,
        uint multiplierPerYear,
        uint jumpMultiplierPerYear,
        uint kink_
    ) internal pure {
        require(kink_ <= BASE, "kink must be <= BASE");
        // optional: enforce a minimum kink (e.g., > 0) if desired
        // optional: bound the multipliers to reasonable ranges to avoid overflow in downstream calculations
    }
    
    function updateJumpRateModelInternal(
        uint baseRatePerYear,
        uint multiplierPerYear,
        uint jumpMultiplierPerYear,
        uint kink_
    ) internal {
        _validateParams(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_);
        // existing conversion from per‑year to per‑block and storage assignments…
    }

    Adding these guards prevents accidental mis‑configuration and restricts the model to logical parameter space.


3. Overall Security Rating

Category Rating
Critical / High Severity Bugs 2 (both issues are in the critical/high range)
Overall Contract Security Needs Improvement – the contract is functional but must be patched before production deployment.

4. Recommendations & Remediation Checklist

Action
Fix utilizationRate – add total‑cash‑plus‑borrows check and guard against total <= reserves.
Validate kink – enforce 0 ≤ kink ≤ BASE.
Optionally bound rate multipliers – set reasonable upper limits (e.g., <= 10 * BASE) to protect downstream accrual calculations.
Add unit tests covering edge cases: (a) reserves > cash + borrows; (b) kink out of range; (c) extreme multiplier values.
Review InterestRateModel – ensure that any functions that consume baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock also perform safe arithmetic.
Document ownership model – clarify that owner must be a Timelock and that governance proposals should be validated before calling updateJumpRateModel.
Run static analysis (Slither, MythX) on the full contract suite to ensure no hidden re‑entrancy or low‑level call issues.

5. Conclusion

The BaseJumpRateModelV2 contract is largely well‑structured and benefits from Solidity 0.8’s built‑in safety. However, two pivotal bugs—denominator under/overflow in utilizationRate and lack of parameter validation for kink (and related rates)—pose a serious risk of denial‑of‑service and economic destabilisation.

Implementing the remediation steps above will bring the contract to a secure state suitable for production deployment.


End of Report


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