Assessment reports>Hyperbeat Pay>Critical findings>The owner can self-assign the operator role
Category: Business Logic

The owner can self-assign the operator role

Critical Impact
Critical Severity
High Likelihood

Description

There is an inconsistency in how ManagementAccount resolves the operator address. The contract has an operator() function that returns the operator from the factory, but the _checkOperator() and _checkOwnerOrOperator() functions use role-based access control instead:

function operator() external view returns (address) {
    return IManagementAccountFactory(_factory).operator();
}

function _checkOperator() internal view {
!   if (!hasRole(OPERATOR_ROLE, msg.sender)) revert ManagementAccountErrors.NotOperator(msg.sender);
}

function _checkOwnerOrOperator() internal view {
!   if (!hasRole(OWNER_ROLE, msg.sender) && !hasRole(OPERATOR_ROLE, msg.sender)) {
        revert ManagementAccountErrors.NotOperator(msg.sender);
     }
}

The operator is designed to have privileged access to critical functions including settle(), executeModeChange(), approveWithdrawal(), approveServiceAction(), and approveAuthorizationRevocation(). However, during initialization, the ManagementAccount owner is granted DEFAULT_ADMIN_ROLE, which allows them to freely control role assignments. This enables the owner to self-assign OPERATOR_ROLE and execute these critical operator-only functions or revoke the OPERATOR_ROLE from the legitimate factory operator entirely.

Impact

Users can approve their own withdrawal requests for critical tokens, bypass mode-change controls, and approve service actions without authorization. This fundamentally breaks the trust model where only the legitimate operator should execute such operations.

Additionally, if the factory updates its operator address, existing ManagementAccount instances will remain unsynchronized and continue using the stale operator assigned during initialization. The new factory operator cannot act on previously deployed accounts since they will not have the OPERATOR_ROLE assigned, requiring manual intervention from the admin to update the operator role.

Recommendations

Remove OPERATOR_ROLE assignments from initialize() and change _checkOperator() and _checkOwnerOrOperator() to check directly against the factory's operator address:

function _checkOperator() internal view {
-   if (!hasRole(OPERATOR_ROLE, msg.sender)) revert ManagementAccountErrors.NotOperator(msg.sender);
+   if (msg.sender != operator()) revert ManagementAccountErrors.NotOperator(msg.sender);
}
[...]
function _checkOwnerOrOperator() internal view {
-   if (!hasRole(OWNER_ROLE, msg.sender) && !hasRole(OPERATOR_ROLE, msg.sender)) {
+   if (!hasRole(OWNER_ROLE, msg.sender) && msg.sender != operator()) {
        revert ManagementAccountErrors.NotOperator(msg.sender);
    }
}

Remediation

This issue has been acknowledged by Hyperbeat, and a fix was implemented in commit efe2310e.

Zellic © 2025Back to top ↑