Critical Vulnerability In SignedDecimalMath.sol Prevent Division By Zero Exploits

by Jeany 82 views
Iklan Headers

This article delves into a significant vulnerability discovered within the SignedDecimalMath.sol library, a crucial component for fixed-point arithmetic operations in the DeBank protocol. The vulnerability lies in the absence of a division-by-zero check within the decimalDiv() function, potentially leading to critical failures and denial-of-service scenarios.

Summary of the Vulnerability

The SignedDecimalMath library provides essential mathematical utilities for fixed-point arithmetic operations. The decimalDiv() function is used for both uint256 and int256 datatypes, but it lacks validation for the denominator b. If b is equal to zero, a division-by-zero panic occurs, causing the entire transaction to revert. This is a significant risk because these math functions are used in sensitive logic, such as fee and token splitting in components like Router and Executor. These components accept user-controlled or admin-configured parameters like feeRate and percent. If any of these parameters are zero or are manipulated to become zero, the protocol will revert unexpectedly, leading to a denial-of-service for legitimate users.

Root Cause Analysis

The functions decimalDiv(uint256 a, uint256 b) and decimalDiv(int256 a, int256 b) in SignedDecimalMath.sol perform a scaled division, as shown below:

return (a * ONE) / b; // For uint256
return (a * SignedONE) / b; // For int256

Neither version includes a check to ensure that b is not zero. This is problematic because zero can be passed into this function from various public or external entry points, including fee rate settings, swap percentages, and adapter logic. For example, in Router.sol, the feeAmount is calculated using amount.decimalMul(feeRate). If feeRate is zero, the result will be zero. Subsequently, SignedDecimalMath.decimalDiv(a, feeRate) is called, which will result in a division-by-zero panic if feeRate remains zero.

This makes the protocol highly susceptible to transaction reversions. If decimalDiv() is used in complex call paths, such as multi-path swaps, it could render the router or executor unusable. The lack of input validation on the denominator b in decimalDiv() is the root cause of this vulnerability. This oversight allows for the potential of a division-by-zero error, which can halt critical operations within the protocol.

Detailed Explanation of the Root Cause

The root cause of the vulnerability is the missing check for a zero denominator in the decimalDiv() functions within the SignedDecimalMath library. These functions are designed to perform scaled division for both unsigned and signed 256-bit integers. The absence of this check means that if the denominator b is zero, the EVM will throw a division-by-zero error, causing the transaction to revert.

This issue is particularly critical because the decimalDiv() function is used in key financial calculations within the protocol, such as fee calculations and token splitting. These calculations often involve parameters that are either user-controlled or admin-configured, like fee rates and percentages. If a malicious user or an unaware admin sets these parameters to zero, or manipulates them to become zero, it can trigger the vulnerability and halt the protocol's operations.

The vulnerability is further compounded by the fact that the decimalDiv() function is used in several critical components of the protocol, including the Router and Executor contracts. These contracts are responsible for handling swaps and multi-path executions, respectively. If the decimalDiv() function fails in these components, it can lead to a complete breakdown of the protocol's core functionalities.

To further illustrate the issue, consider the following example from Router.sol:

feeAmount = amount.decimalMul(feeRate); // if feeRate = 0, then 0
uint256 share = SignedDecimalMath.decimalDiv(a, feeRate); // division by 0 panic

In this scenario, if the feeRate is set to zero, the feeAmount will also be zero. When the decimalDiv() function is called with feeRate as the denominator, it will result in a division-by-zero panic, causing the transaction to revert. This highlights the critical nature of the vulnerability and the potential for denial-of-service attacks.

Preconditions for the Vulnerability

Internal Preconditions

  1. Math operations using SignedDecimalMath.decimalDiv(a, b): The protocol relies on the SignedDecimalMath.decimalDiv() function for performing division operations within financial calculations.
  2. Unvalidated input b: The function accepts the denominator b from both trusted and untrusted callers without any internal validation to ensure it is non-zero.
  3. Assumption of non-zero b: The protocol implicitly assumes that callers will not use b == 0, but this assumption is not enforced through explicit checks.

External Preconditions

  1. Malicious or unaware users: A malicious user, an unaware administrator, or a faulty adapter can call functions with feeRate == 0 or percent == 0.
  2. Propagation of zero values: These zero values propagate into the decimalDiv(a, 0) call.
  3. Lack of upstream checks: There are no checks enforced either upstream or within the library itself to prevent the division by zero.

For instance, the following scenarios can trigger the vulnerability:

Router.swap(..., feeRate = 0, ...)
Executor.executeMultiPath(... adapter.percent = 0 ...)

Attack Path Scenario

The attack path for exploiting this vulnerability is straightforward and can be initiated by either a malicious user or through misconfiguration of the system. The sequence of events leading to the exploit is as follows:

  1. Interaction with Vulnerable Logic: A user or attacker interacts with the Router, Executor, or any other logic within the protocol that utilizes the SignedDecimalMath.decimalDiv() function.
  2. Zero Operand Supply: The attacker supplies or configures the second operand (b) of the decimalDiv() function to be 0. This can be done by setting parameters like feeRate or adapter percentages to zero.
  3. Division by Zero Operation: The internal calculation within the decimalDiv() function performs the operation (a * ONE) / 0.
  4. EVM Panic and Reversion: The Ethereum Virtual Machine (EVM) encounters the division by zero error and throws a panic. This causes the entire transaction to revert, preventing any further state changes.
  5. Denial of Service: The transaction reversion halts all swap execution and any dependent logic, resulting in a denial-of-service. This can lead to failed trades, failed adapter delegations, or even a complete halt of the protocol's core functionalities.

To illustrate this, consider a scenario where a user attempts to execute a swap through the Router contract. If the feeRate is set to zero, the decimalDiv() function will be called with zero as the denominator, causing the transaction to revert. This not only prevents the user's swap from being executed but also disrupts any subsequent operations that depend on the successful execution of the swap.

Similarly, in the Executor contract, if the adapter.percent is set to zero, the decimalDiv() function will again be called with zero as the denominator, leading to a transaction reversion and a denial-of-service. This can severely impact the protocol's ability to execute multi-path swaps and other complex operations.

Impact of the Vulnerability

The impact of this vulnerability is significant, as it can lead to a denial-of-service, preventing users from interacting with the protocol and disrupting its core functionalities. When the denominator b is zero in decimalDiv(), any logic depending on this function will fail. This includes swaps, routing, fee calculations, and adapter logic.

For example, if a user attempts to complete a swap but their adapter or route percent is zero, the transaction will revert, and the swap will fail. Similarly, the protocol will be unable to charge or route fees correctly, disrupting the trade logic and potentially leading to financial losses.

In a broader context, if the vulnerability is exploited, the protocol's reputation could be severely damaged, leading to a loss of user trust and a decline in adoption. The inability to process transactions and manage fees can also result in significant financial losses for both the protocol and its users.

PoC (Proof of Concept)

Here's a Solidity code snippet demonstrating the vulnerability, which can be tested using Foundry:

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.25;

import "forge-std/Test.sol";
import "../src/library/SignedDecimalMath.sol";

contract DecimalMathPoCTest is Test {
    function testFail_UintDecimalDivByZero() public {
        uint256 a = 10 ether;
        uint256 b = 0;
        // Should revert due to division by zero
        uint256 result = SignedDecimalMath.decimalDiv(a, b);
    }

    function testFail_IntDecimalDivByZero() public {
        int256 a = 10 ether;
        int256 b = 0;
        // Should revert due to division by zero
        int256 result = SignedDecimalMath.decimalDiv(a, b);
    }
}

To run the test, use the following command:

forge test --match-path test/DecimalMathPoCTest.t.sol -vvvv

The expected output will show that both test cases fail due to a division-by-zero panic:

[FAIL. Reason: panic: division by zero] testFail_UintDecimalDivByZero()
[FAIL. Reason: panic: division by zero] testFail_IntDecimalDivByZero()

Mitigation Strategies

The most effective mitigation strategy is to add explicit validation within the decimalDiv() functions to prevent division by zero. This can be achieved by adding a require statement that checks if the denominator b is zero and reverts the transaction with a meaningful error message if it is. This ensures that any accidental or malicious attempts to divide by zero will be caught and handled gracefully, preventing a catastrophic failure of the protocol.

Here’s the suggested code fix:

function decimalDiv(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b != 0, "SignedDecimalMath: division by zero");
    return (a * ONE) / b;
}

function decimalDiv(int256 a, int256 b) internal pure returns (int256) {
    require(b != 0, "SignedDecimalMath: division by zero");
    return (a * SignedONE) / b;
}

By implementing this fix, any misuse of the function will result in a controlled revert with a clear error message, rather than a raw EVM panic. This not only enhances the stability and reliability of the protocol but also makes it easier to debug and maintain.

Conclusion

The absence of a division-by-zero check in the decimalDiv() function within the SignedDecimalMath library poses a significant vulnerability to the DeBank protocol. This oversight can lead to transaction reversions, denial-of-service attacks, and potential financial losses. By implementing the proposed mitigation strategy, which involves adding explicit validation for a non-zero denominator, the protocol can safeguard against these risks and ensure its continued stability and reliability. Addressing this vulnerability is crucial for maintaining the integrity of the DeBank protocol and protecting its users from potential exploits.

By addressing this vulnerability with the recommended mitigation, the DeBank protocol can ensure a more robust and reliable system, safeguarding user transactions and maintaining the integrity of the platform's core functionalities. Regular audits and proactive security measures are vital to the long-term success and trust in decentralized financial systems.