Prepared by:
HALBORN
Last Updated 07/02/2025
Date of Engagement: June 24th, 2025 - June 25th, 2025
100% of all REPORTED Findings have been addressed
All findings
4
Critical
0
High
1
Medium
1
Low
2
Informational
0
Quex
engaged Halborn to perform a security assessment of their smart contracts from June 24th to June 25th, 2025. The assessment scope was limited to the smart contracts provided to the Halborn team. Commit hashes and additional details are available in the Scope section of this report.
The Halborn team dedicated two days to this engagement, with one full-time security engineer assigned to evaluate the security of the smart contracts.
The assigned security engineer is an expert in blockchain and smart contract security, possessing advanced skills in penetration testing, smart contract exploitation, and extensive knowledge of multiple blockchain protocols.
The objectives of this assessment were to:
Verify that the smart contract functions operate as intended.
Identify potential security vulnerabilities within the smart contracts.
In summary, Halborn identified several areas for improvement to reduce the likelihood and impact of potential risks, which were mostly addressed by the Quex team
. The primary recommendations were as follows:
Restrict internal flow logic to prohibit flow.consumer == address(this) to prevent unauthorized self-calls and manipulation of subscription funds.
Implement zero-address checks in the setOwner function to prevent permanent denial-of-service conditions through invalid ownership assignments.
Introduce per-consumer spending limits within the subscription model to mitigate griefing attacks via request spamming and prevent excessive balance reservation.
Ensure that ETH transfers to relayers in fulfillRequest and pushData are followed by proper return value checks to prevent silent failures and potential fund loss.
Halborn employed a combination of manual, semi-automated, and automated security testing to optimize efficiency, timeliness, practicality, and accuracy within the scope of this assessment. Manual testing was essential for uncovering logical, procedural, and implementation flaws, while automated techniques enhanced code coverage and quickly identified deviations from best security practices. The following phases and tools were utilized throughout the assessment:
Research into the architecture and purpose of the smart contracts.
Manual review and walkthrough of the smart contracts' code.
Manual assessment of key Solidity variables and functions to identify potential vulnerability classes.
Manual testing using custom scripts.
Static security analysis of the scoped contracts and imported functions using Slither
.
Local deployment and testing with Foundry & Hardhat
.
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Attack Origin (AO) | Arbitrary (AO:A) Specific (AO:S) | 1 0.2 |
Attack Cost (AC) | Low (AC:L) Medium (AC:M) High (AC:H) | 1 0.67 0.33 |
Attack Complexity (AX) | Low (AX:L) Medium (AX:M) High (AX:H) | 1 0.67 0.33 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Confidentiality (C) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Integrity (I) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Availability (A) | None (A:N) Low (A:L) Medium (A:M) High (A:H) Critical (A:C) | 0 0.25 0.5 0.75 1 |
Deposit (D) | None (D:N) Low (D:L) Medium (D:M) High (D:H) Critical (D:C) | 0 0.25 0.5 0.75 1 |
Yield (Y) | None (Y:N) Low (Y:L) Medium (Y:M) High (Y:H) Critical (Y:C) | 0 0.25 0.5 0.75 1 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
1
Medium
1
Low
2
Informational
0
Security analysis | Risk level | Remediation Date |
---|---|---|
Any External Account Can Manipulate Subscription Funds | High | Solved |
Unchecked Return Values in Relayer ETH Transfers | Medium | Solved |
Potential Subscription Ownership DoS | Low | Solved |
Subscription Balance Griefing via Request Spamming | Low | Risk Accepted - 06/25/2025 |
//
The DepositManagerFacet
allows any external account to bypass the quexOnly
modifier and execute restricted functions on any subscription without authorization as this check can be circumvented because the protocol allows unrestricted creation of "flows" that can make the diamond contract call itself.
modifier quexOnly() {
if (msg.sender != address(this)) {
revert IQuexActionRegistry.OnlyCallableInternally();
}
_;
}
Attack Scenario:
Malicious Flow Creation: An attacker calls FlowFacet.createFlow() with:
flow.consumer = address(diamond)
(makes diamond call itself)
flow.callback = DepositManagerFacet.reserve.selector
(targets restricted function)
Self-Call Trigger: The attacker calls QuexActionFacet.pushData() which executes:
bytes memory payload = abi.encodeWithSelector(flow.callback, flowId, message.dataItem, IdType.FlowId);
(bool success,) = flow.consumer.call{gas: flow.gasLimit}(payload);
Access Control Bypass: Since flow.consumer
equals the diamond address, the diamond makes an external call to itself. Inside the restricted function, msg.sender == address(diamond)
, satisfying the quexOnly
check.
All functions protected by quexOnly
can be called without authorization:
reserve(uint256 subscriptionId, uint256 amount
) - Lock subscription funds
release(uint256 subscriptionId, uint256 amount)
- Unlock reserved funds
fulfill(uint256 subscriptionId, uint256 reservedFee, uint256 actualFee)
- Manipulate balances
The following test function demonstrates the access control vulnerability:
function test_Exploit_DrainSubscription() public {
uint256 initialBalance = IDepositManager(address(diamond)).balance(subscriptionId);
assertEq(initialBalance, 10 ether);
DataItem memory fulfillData = DataItem({timestamp: block.timestamp, error: 10000 ether, value: bytes("")});
OracleMessage memory fulfillMsg = OracleMessage({actionId: actionId, dataItem: fulfillData, relayer: attacker});
ETHSignature memory fulfillSig = _signOracleMessage(fulfillMsg, TD_validInQuex_inOraclePool);
vm.prank(attacker);
testObject.pushData{value: quexFee}(fulfillMsg, fulfillSig, newFlowId, TD_validInQuex_inOraclePool.tdId);
}
Output:
The following output shows that access control can be bypassed to call reserve by calling pushData with custom arguments and a malicious flow.
Prevent flows where flow.consumer == address(this)
to block self-calls.
function createFlow(Flow memory flow) external returns (uint256 flowId) {
require(flow.consumer != address(this), "Self-calls forbidden");
// ... rest of function
}
SOLVED: The recommended mitigation has been implemented.
//
Both fulfillRequest
and pushData
functions in QuexActionFacet
performs relayer ETH transfers without checking the return values of low-level call()
operations.
payable(message.relayer).call{value: refund}("");
This can result in silent failures where transfers fail but transactions continue, leading to fund loss for relayer.
Verify the return value of the call()
and implement appropriate failure handling.
SOLVED: Funds are redirected to the treasury in case of failure.
//
The setOwner
function in DepositManagerFacet
lacks validation to prevent assigning the subscription owner to address(0)
.
This omission creates a permanent denial of service condition that renders the subscription completely unusable.
Implement validation in the setOwner
function to prevent assigning the zero address.
SOLVED: The recommended mitigation has been implemented.
//
The subscription model in DepositManagerFacet
allows whitelisted consumers to lock arbitrary amounts of a subscription's balance through requests. A malicious consumer can create multiple requests to reserve nearly the entire subscription balance, then abandon the requests, effectively denying the owner access to their funds even after the consumer is removed.
function reserveFunds(uint256 subscriptionId, QuexActionStorage.Request memory req)
private
returns (uint256 requestId)
{
uint256 totalFee = req.quexFee + req.maxRelayerRefund + req.oraclePoolFee;
DepositManagerFacet(address(this)).reserve(subscriptionId, totalFee);
}
It is recommended to add spending limits per consumer.
RISK ACCEPTED: The Quex team accepted the risk related to this finding with following comment - "This behavior is intentional and aligns with the design of the subscription model. Granting a consumer access to a subscription is a form of delegated trust. The subscription owner is expected to carefully evaluate whom they grant consumer rights..."
Halborn employed automated testing techniques to improve coverage of specific areas within the smart contracts under review. One of the primary tools used was Slither
, a static analysis framework for Solidity. After successfully compiling the smart contracts in the repository into their ABI and binary formats, Slither
was executed against the contracts. This tool performs static verification of mathematical relationships between Solidity variables to identify invalid or inconsistent usage of the contracts' APIs throughout the entire codebase.
The security team conducted a comprehensive review of the findings generated by Slither
. No significant issues were identified, as the findings were determined to be false positives.
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
Quex V1
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed