Critical Vulnerability In SignedDecimalMath.sol Prevent Division By Zero Exploits
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
- Math operations using
SignedDecimalMath.decimalDiv(a, b)
: The protocol relies on theSignedDecimalMath.decimalDiv()
function for performing division operations within financial calculations. - Unvalidated input
b
: The function accepts the denominatorb
from both trusted and untrusted callers without any internal validation to ensure it is non-zero. - Assumption of non-zero
b
: The protocol implicitly assumes that callers will not useb == 0
, but this assumption is not enforced through explicit checks.
External Preconditions
- Malicious or unaware users: A malicious user, an unaware administrator, or a faulty adapter can call functions with
feeRate == 0
orpercent == 0
. - Propagation of zero values: These zero values propagate into the
decimalDiv(a, 0)
call. - 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:
- Interaction with Vulnerable Logic: A user or attacker interacts with the
Router
,Executor
, or any other logic within the protocol that utilizes theSignedDecimalMath.decimalDiv()
function. - Zero Operand Supply: The attacker supplies or configures the second operand
(b)
of thedecimalDiv()
function to be0
. This can be done by setting parameters likefeeRate
or adapter percentages to zero. - Division by Zero Operation: The internal calculation within the
decimalDiv()
function performs the operation(a * ONE) / 0
. - 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.
- 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.