Skip to content

Latest commit

 

History

History
160 lines (121 loc) · 6.19 KB

073.md

File metadata and controls

160 lines (121 loc) · 6.19 KB

Glamorous Plum Baboon

High

Unchecked Arithmetic in Borrow Cap Validation

Summary

The ValidationLogic:validateBorrow function contains an unchecked arithmetic operation when calculating vars.totalDebt. This can lead to potential overflows, allowing malicious actors to bypass borrow cap validations. Utilizing unchecked arithmetic without proper safeguards introduces risks of unexpected behavior in the smart contract.


Description

In the ValidationLogic:validateBorrow function, the following operation is performed:

vars.totalDebt = vars.totalSupplyVariableDebt + params.amount;
unchecked {
    require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BORROW_CAP_EXCEEDED);
}

The addition of vars.totalSupplyVariableDebt and params.amount is done outside of a checked arithmetic block, meaning it is susceptible to overflow. If vars.totalSupplyVariableDebt or params.amount is maliciously manipulated to near-maximum values, an overflow could occur, resulting in a calculated vars.totalDebt that is much smaller than the actual value. This would allow malicious borrowers to bypass the borrow cap check (vars.totalDebt <= vars.borrowCap * vars.assetUnit).


Root Cause

The unchecked arithmetic operation occurs at this line:

vars.totalDebt = vars.totalSupplyVariableDebt + params.amount;

Impact

  • Severity: High
  • Risk: Exploitable overflow in the calculation of vars.totalDebt could lead to a failure in enforcing the borrow cap, compromising the protocol’s risk management mechanisms.
  • Consequences: An attacker can borrow beyond the set borrow cap, which can destabilize the protocol, drain liquidity pools, and harm users’ funds.

Proof of Concept (PoC)

Assume:

  • vars.totalSupplyVariableDebt = 2^256 - 1 (maximum uint256 value)
  • params.amount = 1

Calculation without overflow check:

vars.totalDebt = vars.totalSupplyVariableDebt + params.amount;
// vars.totalDebt overflows to 0

The borrow cap validation:

require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BORROW_CAP_EXCEEDED);
// This validation passes since vars.totalDebt is now 0 due to overflow

This enables an attacker to bypass the borrow cap and borrow unrestricted amounts.


Foundry Test Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Test.sol";

contract ValidateBorrowTest is Test {
    // Mock of Errors used in the contract
    string constant BORROW_CAP_EXCEEDED = "BORROW_CAP_EXCEEDED";

    // Struct to simulate the function's variables
    struct Vars {
        uint256 totalSupplyVariableDebt;
        uint256 totalDebt;
        uint256 borrowCap;
        uint256 assetUnit;
    }

    // Function simulating validateBorrow
    function validateBorrow(Vars memory vars, uint256 amount) public pure {
        vars.totalDebt = vars.totalSupplyVariableDebt + amount;
        unchecked {
            require(
                vars.totalDebt <= vars.borrowCap * vars.assetUnit,
                BORROW_CAP_EXCEEDED
            );
        }
    }

    function testUncheckedArithmeticOverflow() public {
        // Arrange
        Vars memory vars;
        vars.totalSupplyVariableDebt = type(uint256).max; // Max uint256 value
        vars.borrowCap = 10; // Example borrow cap
        vars.assetUnit = 1 ether; // Example asset unit

        uint256 paramsAmount = 1; // Value to cause overflow

        // Act & Assert
        vm.expectRevert(bytes(BORROW_CAP_EXCEEDED)); // Expect the borrow cap to be bypassed
        validateBorrow(vars, paramsAmount); // Trigger the vulnerability
    }
}

Explanation of the Code

  1. Setup the Variables:

    • vars.totalSupplyVariableDebt is set to type(uint256).max, which is the maximum value a uint256 can hold (2^256 - 1).
    • params.amount is set to 1, which will cause an overflow when added to vars.totalSupplyVariableDebt.
  2. Simulate the Vulnerability:

    • The function validateBorrow performs unchecked arithmetic using unchecked block, causing the overflow.
    • When the overflow occurs, vars.totalDebt wraps around to 0.
  3. Check the Behavior:

    • The test expects a revert with the error BORROW_CAP_EXCEEDED. However, due to the overflow, the require condition incorrectly evaluates as true, and no revert occurs.
    • This demonstrates how the overflow bypasses the borrow cap validation.
  4. Use vm.expectRevert:

    • This ensures that the test verifies if the borrow cap validation is bypassed.

Run the Test

forge test --match-path test/ValidateBorrowTest.sol

Observe the results. If the vulnerability exists, the test will fail to revert, proving the exploit.


Fix Verification

After applying the recommended mitigation (using checked arithmetic), rerun the test. The test should pass, as the borrow cap validation will no longer be bypassed due to overflow.


Recommended Mitigation

To prevent overflow, the arithmetic operation should use checked arithmetic provided by libraries such as OpenZeppelin’s SafeMath or the built-in overflow checks introduced in Solidity 0.8. Specifically:

  1. Use Checked Arithmetic: Replace the arithmetic operation with checked arithmetic:

    vars.totalDebt = vars.totalSupplyVariableDebt + params.amount;
    require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BORROW_CAP_EXCEEDED);
  2. Explicit Safeguards: Alternatively, if unchecked arithmetic is required for gas optimization, ensure that inputs (vars.totalSupplyVariableDebt and params.amount) are validated beforehand to ensure they cannot approach values that would cause an overflow:

    require(vars.totalSupplyVariableDebt + params.amount >= vars.totalSupplyVariableDebt, "Arithmetic overflow detected");
    vars.totalDebt = vars.totalSupplyVariableDebt + params.amount;

By incorporating these measures, the borrow cap validation will reliably enforce the intended constraints and prevent overflow vulnerabilities.