Particle CS
Access Control

Assessed Bloxchain's role-based access control system, covering time-delay and meta-transaction execution paths and the RuntimeRBAC permission model.
report id
0828
category
Infra & Wallets
language
Solidity
final report
May 28, 2026
lines of code
3800
Content

Download audit report

1

Executive Summary

This document presents the results of the security review conducted by Nethermind Security for ParticleCS's Bloxchain protocol contracts which is an enterprise-grade security framework for building smart contracts.

The core thesis is that a single compromised key should never be sufficient to immediately do damage. The protocol enforces this architecturally through two parallel workflows that integrators can mix freely per operation:

  • Time-delay workflow — two on-chain transactions over a configured time window (request → wait → approve). The window provides an intervention opportunity for the rightful party to cancel.
  • Meta-transaction workflow — one on-chain transaction with two distinct roles: the signer produces an EIP-712 signature off-chain, and the executor submits it on-chain. The signer/executor split is enforced architecturally via the ConflictingMetaTxPermissions invariant — a single role cannot hold both SIGN_META_* and EXECUTE_META_* permissions for the same selector.

The reference identity model uses three protected roles that are bootstrapped at deployment and cannot be removed: Owner, Broadcaster, and Recovery. On top of these, integrators may use the RuntimeRBAC component to define and manage arbitrary additional roles dynamically.

The audit comprises 3800 lines of Solidity code. The audit was performed using (a) manual analysis of the codebase, and (b) automated analysis tools.

Audit assessment
Audit TypeSecurity Review
Initial ReportMay 15, 2026
Final ReportMay 28, 2026
Initial Commit8bbef3ab46
Final Commitcf91f1a2b9
Documentation AssessmentHigh
Test Suite AssessmentHigh
Total findings
5
Fixed
5
Acknowledged
0
Mitigated
0
Unresolved
0

Findings by severity

  • Critical0
  • High0
  • Medium0
  • Low2
  • Informational3
  • Best Practices0

4

System Overview

The design rests on three principles that together define the protocol's security posture: a single source of mutation, mandatory two-signature authorization, and defense in depth via redundant gates.

Single source of mutation. All mutable state lives in one struct (SecureOperationState) instantiated once per deployed contract. Only the EngineBlox library may mutate it, which isolates the protocol's invariants to a single audit surface.

Two-signature mandatory. Every state-changing operation must be authorized by at least two distinct parties — either across time (request now, approve later, with a window to intervene) or across roles (signer ≠ executor for meta-tx). This is enforced architecturally rather than by convention.

Defense in depth via redundant gates. Where one check could be sufficient, the protocol layers two or more checks that look at the same property from different angles: identity vs. role membership, permission grant vs. handler/execution wiring, and storage status vs. structural invariants. A single compromised layer cannot bypass the others.

4.1 Component layering

The framework is organized as three tiers — a stateless library at the top, a single base contract that owns the protocol's storage, and three composable components that each contribute a slice of the public surface. Sections 4.1.1 through 4.1.3 describe each tier, and Section 4.1.4 explains how a concrete deployable contract is assembled from them.

4.1.1 EngineBlox

EngineBlox is a Solidity library, it owns no storage of its own. Its public functions are linked into integrating contracts via DELEGATECALL, so when EngineBlox runs, address(this) and storage context belong to the integrating contract. The library mutates whatever SecureOperationState storage reference the caller passes in.

4.1.2 BaseStateMachine

BaseStateMachine is the only contract that declares _secureState. All component contracts inherit from it, directly or transitively. This guarantees a single _secureState slot per deployed contract — regardless of how many components are composed.

4.1.3 Components

Each component (SecureOwnable, RuntimeRBAC, GuardController) adds its own public surface — the user-facing operations for ownership transfer, role config, and external execution — plus its own definitions library that declares its function schemas and role permission grants.

4.1.4 Concrete deployable contracts

A concrete deployable contract inherits whichever components it needs. A wallet-style contract typically composes all three. A vault, an ERC20, or a payment scheduler may inherit only SecureOwnable. A clone factory may inherit only BaseStateMachine. The composition is per-deployment; the framework does not prescribe a single pattern.

4.2 Transaction lifecycle

Every operation in the protocol is represented as a TxRecord indexed by a monotonically-increasing txId, and each record carries a single TxStatus enum value that tracks its progression through the lifecycle:

The protocol exposes two parallel workflow patterns that integrators may mix per operation. Both perform the same PENDING → EXECUTING → terminal state (COMPLETED, FAILED, or CANCELLED) progression; they differ in how authorization is supplied and whether a wait window is enforced.

4.2.1 Time-delay flow

This pattern uses two on-chain transactions over a configured window. An initiator with EXECUTE_TIME_DELAY_REQUEST permission creates a PENDING record with releaseTime = block.timestamp + timeLockPeriodSec. After the window elapses, an approver with EXECUTE_TIME_DELAY_APPROVE permission moves the record through EXECUTING to a terminal state (COMPLETED, FAILED, or CANCELLED). A separate cancel path with EXECUTE_TIME_DELAY_CANCEL permission can short-circuit to CANCELLED at any time during the window.

4.2.2 Meta-transaction flow

This pattern uses one on-chain transaction with two distinct roles. A signer with SIGN_META_* permission produces an EIP-712 typed-data signature off-chain over the full MetaTransaction struct. An executor with EXECUTE_META_* permission then submits the signed message on-chain. EngineBlox runs a full integrity battery (signature length, chainId, deadline, gasprice, nonce, handlerContract bind, signer-permission dual check, ECDSA recovery) before incrementing the signer's nonce and moving the record through EXECUTING to a terminal state within the same call.

A fused “request-and-approve” meta-tx variant is used for operations the protocol designers chose not to gate with a time-delay — recovery rotation, time-lock change, role-config batch, and guard-config batch. The two-signature property is preserved, but the time-lock observation window is omitted.

4.2.3 External execution

The EXECUTING transition is the only moment the protocol calls arbitrary external code. Returndata copying is bounded to 32 KiB, defending against returnbomb griefing. The target address must clear a per-function target whitelist (functionTargetWhitelist[executionSelector]); an empty whitelist means deny, with no implicit wildcard. A separate system macro selector set permits target == address(this) only for specific intentionally-self-call execution selectors (e.g., executeTransferOwnership). The whitelist is checked at request time and re-checked before payment-leg external calls inside executeAttachedPayment.

5

Risk Rating Methodology

The risk rating methodology used by Nethermind Security follows the principles established by the OWASP Foundation. The severity of each finding is determined by two factors: Likelihood and Impact.

Likelihood measures how likely the finding is to be uncovered and exploited by an attacker. This factor will be one of the following values:

  1. High: the issue is trivial to exploit and has no specific conditions that need to be met;
  2. Medium: the issue is moderately complex and may have some conditions that need to be met;
  3. Low: the issue is very complex and requires very specific conditions to be met.

When defining the likelihood of a finding, other factors are also considered. These can include but are not limited to motive, opportunity, exploit accessibility, ease of discovery, and ease of exploit.

Impact is a measure of the damage that may be caused if an attacker exploits the finding. This factor will be one of the following values:

  1. High: the issue can cause significant damage, such as loss of funds or the protocol entering an unrecoverable state;
  2. Medium: the issue can cause moderate damage, such as impacts that only affect a small group of users or only a particular part of the protocol;
  3. Low: the issue can cause little to no damage, such as bugs that are easily recoverable or cause unexpected interactions that cause minor inconveniences.

When defining the impact of a finding, other factors are also considered. These can include but are not limited to data and state integrity, loss of availability, financial loss, and reputation damage. After defining the likelihood and impact of an issue, the severity is determined according to the table below.

Impact / LikelihoodLowMediumHigh
HighMediumHighCritical
MediumLowMediumHigh
LowInfo / Best PracticesLowMedium
UndeterminedUndeterminedUndeterminedUndetermined

To address issues that do not fit a High, Medium or Low severity, Nethermind Security also uses three more finding severities: Informational, Best Practices, and Undetermined.

  1. Informational findings do not pose any risk to the application, but they carry some information that the audit team intends to pass to the client formally;
  2. Best Practice findings are used when some piece of code does not conform with smart contract development best practices;
  3. Undetermined findings are used when we cannot predict the impact or likelihood of the issue.

6

Issues

6.1 Removing a target from the whitelist can cause transactions in the PENDING state to become stuck

FileFix commitTypeStatus
contracts/core/lib/EngineBlox.sol72d67daLowFixed

A pending transaction in EngineBlox can leave the PENDING status only through the approval paths or through the cancellation paths. Both ways verifies that the target for the transaction is whitelisted:

function _cancelTransaction(SecureOperationState storage self, uint256 txId) private {
// @audit-issue This call blocks cancellation when the target is no longer whitelisted.
_validateTargetWhitelist(
self,
self.txRecords[txId].params.executionSelector,
self.txRecords[txId].params.target
);

self.txRecords[txId].status = TxStatus.CANCELLED;

removePendingTx(self, txId);

logTxEvent(self, txId, self.txRecords[txId].params.executionSelector);
}

This can cause a scenario where a transaction is stuck in PENDING state:

  1. A requester calls txRequest(...) with target T, while T is whitelisted for executionSelector. The record is stored with status = PENDING;
  2. The whitelist administrator removes T from self.functionTargetWhitelist[executionSelector];
  3. Any attempt to call txDelayedApproval(...) or _txApprovalWithMetaTx(...) reverts inside _completeTransaction(...) on the whitelist check, after consuming gas for the external call;
  4. Any attempt to call txCancellation(...) or txCancellationWithMetaTx(...) reverts inside _cancelTransaction(...) on the same whitelist check, before any state transition can occur;

The record is stuck in TxStatus.PENDING indefinitely. The only way to unblock it is for an administrator to re-add T to the whitelist.

Recommendation

Consider removing the call to _validateTargetWhitelist(...) from _cancelTransaction(...). Cancellation does not interact with the target and does not depend on the target being currently authorized.

Resolution

Removed _validateTargetWhitelist(...) from _cancelTransaction(...). Fixed in commit 72d67d.

6.2 Target whitelist is checked after the external call has executed

FileFix commitTypeStatus
contracts/core/lib/EngineBlox.sol72d67daLowFixed

Target whitelisting is enforced through _validateTargetWhitelist(...), which checks that target is registered for executionSelector in self.functionTargetWhitelist. The goal is to restrict every approved transaction to a destination that was authorized before.

When txDelayedApproval(...) is called, the functions perform the external call first and after that they call _completeTransaction(...), which is where the whitelist is checked:

function txDelayedApproval(SecureOperationState storage self, uint256 txId, bytes4 handlerSelector)
public
returns (TxRecord memory)
{
// ...
self.txRecords[txId].status = TxStatus.EXECUTING;

// INTERACT: External call after state update
(bool success, bytes memory result) = executeTransaction(self, self.txRecords[txId]);

// @audit-issue Whitelist is enforced only after the external call has run.
_completeTransaction(self, txId, success, result);
// ...
}
function _completeTransaction(
SecureOperationState storage self,
uint256 txId,
bool success,
bytes memory result
) private {
// @audit-issue This check belongs before `executeTransaction(...)`, not after it.
_validateTargetWhitelist(
self,
self.txRecords[txId].params.executionSelector,
self.txRecords[txId].params.target
);
// ...
}

The same pattern repeats in _txApprovalWithMetaTx(...), which also calls executeTransaction(...) before _completeTransaction(...).

Two consequences follow from this ordering.

First, gas is wasted on the unhappy path. If the target is no longer whitelisted at approval time, for example because an admin removed it between request and approval, the engine still flips status to EXECUTING, forwards the configured gas to the target, and lets the target execute arbitrary code before _completeTransaction(...) reverts the entire transaction on the late whitelist check. The caller pays for all of that work even though the call should never have been issued.

Second, the late check breaks checks-effects-interactions pattern.

Recommendation

Consider moving _validateTargetWhitelist(...) so that the whitelist is enforced before any external call is made.

Resolution

The finding was valid as a gas-efficiency and CEI-ordering issue. _validateTargetWhitelist(...) was moved out of _completeTransaction(...) and is enforced before any external call on every approval path (txDelayedApproval(...) and _txApprovalWithMetaTx(...). Fixed in commit 72d67d.

6.3 In-memory writes to record.status and record.result in executeTransaction(...) have no effect

FileFix commitTypeStatus
contracts/core/lib/EngineBlox.sol72d67daInfoFixed

The executeTransaction(...) helper performs the low-level call associated with an approved transaction. The helper accepts the transaction record as TxRecord memory record, which is a memory copy of self.txRecords[txId] taken at the call site.

After performing the external call, the helper transitions the record status and store the returned data depending on the call outcome:

function executeTransaction(SecureOperationState storage self, TxRecord memory record)
private
returns (bool, bytes memory)
{
// ...
if (success) {
// @audit-issue Writing to a memory copy has no effect on the persisted record.
record.status = TxStatus.COMPLETED;
record.result = result;

if (record.payment.recipient != address(0)) {
executeAttachedPayment(self, record);
}
} else {
// @audit-issue Writing to a memory copy has no effect on the persisted record.
record.status = TxStatus.FAILED;
record.result = result;
}

return (success, result);
}

Because record is a memory parameter rather than a storage reference, both branches mutate a local copy that is discarded once the function returns. The persisted status and result are written only later by _completeTransaction(...), which the callers invoke after executeTransaction(...) returns. The intermediate memory writes are dead code. They are not consumed by the caller, the nested executeAttachedPayment(...) re-reads status from storage via _validateTxStatus(self, record.txId, TxStatus.EXECUTING) instead of from record.status, and any external reader of self.txRecords[txId] observes the values set by _completeTransaction(...).

Recommendation

Consider removing the assignments to record.status and record.result from executeTransaction(...), since they have no effect.

Resolution

Removed dead record.status and record.result assignments. Fixed in commit 72d67d.

6.4 validateDeadline(...) and validateMetaTxDeadline(...) behave differently when block.timestamp == deadline

The protocol uses two different helpers to decide whether the deadline field of a meta-transaction is still valid. They are intended to express the same predicate but they treat the case block.timestamp == deadline differently.

function validateDeadline(uint256 deadline) internal view {
// @audit-issue Strict comparison rejects `block.timestamp == deadline`.
if (deadline <= block.timestamp) revert DeadlineInPast(deadline, block.timestamp);
}
function validateMetaTxDeadline(uint256 deadline) internal view {
// @audit-issue Non-strict comparison accepts `block.timestamp == deadline`.
if (block.timestamp > deadline) revert MetaTxExpired(deadline, block.timestamp);
}

validateDeadline(...) is used in generateMetaTransaction(...), which is the helper that builds the MetaTransaction payload that an off-chain signer is expected to sign. validateMetaTxDeadline(...) is invoked from the meta-transaction verification path that runs inside verifySignature(...) and is reached from txApprovalWithMetaTx(...), txCancellationWithMetaTx(...), and the request-and-approve meta-tx variants.

This produces an inconsistency at the exact second equal to deadline. A meta-transaction whose deadline matches the current block timestamp at verification time is still executed normally, because validateMetaTxDeadline(...) admits the boundary. The same deadline, however, cannot be used to construct a fresh MetaTransaction payload through generateMetaTransaction(...), because validateDeadline(...) rejects it.

Recommendation

Consider aligning the two helpers on the same boundary semantics.

Resolution

Removed validateDeadline(...) and unified on validateMetaTxDeadline(...). Fixed in commit 72d67d.

6.5 validateTransactionExists(...) does not bound txId against txCounter

The validateTransactionExists(...) helper in SharedValidation is named so that callers can rely on it to confirm that a given txId corresponds to a real transaction record. Its current implementation only rejects the zero value:

function validateTransactionExists(uint256 txId) internal pure {
if (txId == 0) revert ResourceNotFound(bytes32(uint256(txId)));
}

In EngineBlox, transaction identifiers are minted sequentially. Each new record receives txId = self.txCounter + 1 inside createTxRecord(...), and self.txCounter is then incremented in txRequest(...), so any allocated identifier lies in the inclusive range [1, self.txCounter]. Identifiers strictly greater than self.txCounter are not valid attached to any existing record, however the function accepts them as if they did.

Recommendation

Consider modifying validateTransactionExists(...) so that it also rejects any txId greater than the current self.txCounter.

Resolution

Added txCounter parameter and reject txId > txCounter. Fixed in commit 72d67d.

7

Documentation Evaluation

Software documentation refers to the written or visual information that describes the functionality, architecture, design, and implementation of software. It provides a comprehensive overview of the software system and helps users, developers, and stakeholders understand how the software works, how to use it, and how to maintain it. Software documentation can take different forms, such as user manuals, system manuals, technical specifications, requirements documents, design documents, and code comments. Software documentation is critical in software development, enabling effective communication between developers, testers, users, and other stakeholders. It helps to ensure that everyone involved in the development process has a shared understanding of the software system and its functionality. Moreover, software documentation can improve software maintenance by providing a clear and complete understanding of the software system, making it easier for developers to maintain, modify, and update the software over time. Smart contracts can use various types of software documentation. Some of the most common types include:

  • Technical whitepaper: A technical whitepaper is a comprehensive document describing the smart contract’s design and technical details. It includes information about the purpose of the contract, its architecture, its components, and how they interact with each other;
  • User manual: A user manual is a document that provides information about how to use the smart contract. It includes step-by-step instructions on how to perform various tasks and explains the different features and functionalities of the contract;
  • Code documentation: Code documentation is a document that provides details about the code of the smart contract. It includes information about the functions, variables, and classes used in the code, as well as explanations of how they work;
  • API documentation: API documentation is a document that provides information about the API (Application Programming Interface) of the smart contract. It includes details about the methods, parameters, and responses that can be used to interact with the contract;
  • Testing documentation: Testing documentation is a document that provides information about how the smart contract was tested. It includes details about the test cases that were used, the results of the tests, and any issues that were identified during testing;
  • Audit documentation: Audit documentation includes reports, notes, and other materials related to the security audit of the smart contract. This type of documentation is critical in ensuring that the smart contract is secure and free from vulnerabilities.

These types of documentation are essential for smart contract development and maintenance. They help ensure that the contract is properly designed, implemented, and tested, and they provide a reference for developers who need to modify or maintain the contract in the future.

8

Remarks & Test Suite

The Particle CS team provided documentation detailing the full Bloxchain protocol architecture. Additionally, the team provided a walkthrough of the entire protocol including all the main flows with expected state changes. Furthermore, the team addressed all questions and concerns raised by the Nethermind Security team, providing valuable insights and a comprehensive understanding of the project's technical aspects.

Test suite
336Tests passed0Failed

The complete test output is in the full report.

As auditors, we recommend that any changes or updates made to the audited codebase undergo a re-audit or security review to address potential vulnerabilities or risks introduced by the modifications. By conducting a re-audit or security review of the modified codebase, you can significantly enhance the overall security of your system and reduce the likelihood of exploitation. However, we do not possess the authority or right to impose obligations or restrictions on our clients regarding codebase updates, modifications, or subsequent audits. Accordingly, the decision to seek a re-audit or security review lies solely with you.

This report is based on the scope of materials and documentation provided by you to Nethermind in order that Nethermind could conduct the security review outlined in 1. Executive Summary and 2. Audited Files. The results set out in this report may not be complete nor inclusive of all vulnerabilities. Nethermind has provided the review and this report on an as-is, where-is, and as-available basis. You agree that your access and/or use, including but not limited to any associated services, products, protocols, platforms, content, and materials, will be at your sole risk. Blockchain technology remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond the programming language, or other programming aspects that could present security risks. This report does not indicate the endorsement of any particular project or team, nor guarantee its security. No third party should rely on this report in any way, including for the purpose of making any decisions to buy or sell a product, service or any other asset. To the fullest extent permitted by law, Nethermind disclaims any liability in connection with this report, its content, and any related services and products and your use thereof, including, without limitation, the implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Nethermind does not warrant, endorse, guarantee, or assume responsibility for any product or service advertised or offered by a third party through the product, any open source or third-party software, code, libraries, materials, or information linked to, called by, referenced by or accessible through the report, its content, and the related services and products, any hyperlinked websites, any websites or mobile applications appearing on any advertising, and Nethermind will not be a party to or in any way be responsible for monitoring any transaction between you and any third-party providers of products or services. As with the purchase or use of a product or service through any medium or in any environment, you should use your best judgment and exercise caution where appropriate. FOR AVOIDANCE OF DOUBT, THE REPORT, ITS CONTENT, ACCESS, AND/OR USAGE THEREOF, INCLUDING ANY ASSOCIATED SERVICES OR MATERIALS, SHALL NOT BE CONSIDERED OR RELIED UPON AS ANY FORM OF FINANCIAL, INVESTMENT, TAX, LEGAL, REGULATORY, OR OTHER ADVICE.