Complex conditional logic for function access with AccessManaged

The current AccessManaged contract exposes only the restricted modifier to enforce access control on functions. However, this approach limits flexibility, as it doesn't allow for more complex conditional logic (e.g., "allow access only if these conditions are met").

Should all access control be implemented strictly through the restricted modifier? If so, developers are forced to create multiple function variants in order to apply access restrictions dynamically based on external conditions.

Alternatively, should access be checked directly within the function logic using hasRole() or similar methods? This would enable more granular control and conditional checks, rather than relying solely on the restricted modifier upfront.

Here a contract example with both approaches:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {AccessManaged} from 'src/dependencies/openzeppelin/AccessManaged.sol';

library Roles {
  uint64 public constant DEFAULT_ADMIN_ROLE = 0;
  uint64 public constant UPDATE_PARAM_ROLE = 1;
}

contract TargetContract is AccessManaged {
  uint256 public targetParam;

  error Unauthorized();

  constructor(address accessManager) AccessManaged(accessManager) {}

  function updateParamOptionA(uint256 newTargetParam) external {
    require(isUpdatable(newTargetParam) || hasUpdateParamRole(msg.sender), Unauthorized());
    targetParam = newTargetParam;
  }

  function updateParamOptionB_1(uint256 newTargetParam) external {
    require(isUpdatable(newTargetParam), Unauthorized());
    targetParam = newTargetParam;
  }

  function updateParamOptionB_2(uint256 newTargetParam) external restricted {
    targetParam = newTargetParam;
  }

  /// @dev permissionlessly updatable only if block.number is even, otherwise need role
  function isUpdatable(uint256 param) internal view returns (bool) {
    return param % 2 == 0;
  }

  function hasUpdateParamRole(address caller) internal view returns (bool) {
    (bool result, ) = IAccessManager(authority()).hasRole(Roles.UPDATE_PARAM_ROLE, caller);
    return result;
  }
}

Hello @miguelmtzinf

In addition to the restricted() modifier, the AccessManaged contract also provides some internal functions. This one you are looking for is _checkCanCall(address caller, bytes calldata data)

You could do

function updateParamOptionA(uint256 newTargetParam) external {
    if (!isUpdatable(newTargetParam)) { 
        // check restrictions set in the access manager
        _checkCanCall(_msgSender(), _msgData());
    }
    targetParam = newTargetParam;
  }

For reference, here is a token contract that uses an AccessManager implement KYC in the ERC20._update() function

Thanks for the insight! The checkCanCall function provides the flexibility we need to impose restrictions only under certain conditions. However, it currently reverts directly, and it feels like we’re missing an internal function that could instead return a boolean result.

If we were to have a function that returns a boolean result, we could do the following:

function updateParamOptionA(uint256 newTargetParam) external {
    if (isUpdatable(newTargetParam) || _checkCanCall(_msgSender(), _msgData())) {
        targetParam = newTargetParam;
    }
  }