Skip to content

Latest commit

 

History

History
510 lines (409 loc) · 16.5 KB

File metadata and controls

510 lines (409 loc) · 16.5 KB

RuntimeRBAC Contract Integration

The RuntimeRBAC class provides type-safe access to Bloxchain RuntimeRBAC contracts with dynamic role-based access control and flexible permission management.

🎯 Overview

RuntimeRBAC extends BaseStateMachine with advanced role management:

  • Dynamic role creation and management via batch configuration
  • Flexible permission system with function-level access control
  • Meta-transaction support for role operations
  • Event-driven role updates for external monitoring

Note: Function schema registration has been moved to GuardController for better architectural separation. RuntimeRBAC focuses on role and permission management, while GuardController handles execution control and function schema management.

🚀 Quick Start

import { RuntimeRBAC } from '@bloxchain/sdk/typescript'
import { createPublicClient, createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'

// Initialize clients
const publicClient = createPublicClient({
  chain: mainnet,
  transport: http()
})

const walletClient = createWalletClient({
  account: privateKeyToAccount('0x...'),
  chain: mainnet,
  transport: http()
})

// Create RuntimeRBAC instance
const runtimeRBAC = new RuntimeRBAC(
  publicClient,
  walletClient,
  '0x...', // contract address
  mainnet
)

📖 Core Features

1. Role Management

Get Role Information

const role = await runtimeRBAC.getRole('0x...') // role hash
console.log('Role info:', {
  name: role.roleName,
  hash: role.roleHash,
  maxWallets: role.maxWallets,
  walletCount: role.walletCount,
  isProtected: role.isProtected
})

Check Account Role

const hasRole = await runtimeRBAC.hasRole(
  '0x...', // role hash
  '0x...'  // account address
)
console.log('Account has role:', hasRole)

Get Authorized Wallets in Role

const wallets = await runtimeRBAC.getAuthorizedWallets('0x...') // role hash
console.log('Authorized wallets in role:', wallets)

Get Roles for Wallet

const roles = await runtimeRBAC.getWalletRoles('0x...') // wallet address
console.log('Roles for wallet:', roles)
// Returns array of role hashes assigned to the wallet
// Uses reverse index for efficient O(n) lookup where n = wallet's role count

Get Supported Roles

const roles = await runtimeRBAC.getSupportedRoles()
console.log('Supported roles:', roles)

2. Function Schema Management

Get Function Schema

const schema = await runtimeRBAC.getFunctionSchema('0xa9059cbb') // function selector
console.log('Function schema:', {
  signature: schema.functionSignature,
  selector: schema.functionSelector,
  operationType: schema.operationType,
  operationName: schema.operationName,
  supportedActionsBitmap: schema.supportedActionsBitmap,
  isProtected: schema.isProtected,
  handlerForSelectors: schema.handlerForSelectors
})
// Supported actions as array: EngineBlox.convertBitmapToActions(schema.supportedActionsBitmap)

Get Supported Functions

const functions = await runtimeRBAC.getSupportedFunctions()
console.log('Supported functions:', functions)

3. Permission Management

Get Active Role Permissions

const permissions = await runtimeRBAC.getActiveRolePermissions('0x...') // role hash
permissions.forEach(permission => {
  console.log('Permission:', {
    functionSelector: permission.functionSelector,
    grantedActionsBitmap: permission.grantedActionsBitmap,
    handlerForSelectors: permission.handlerForSelectors
  })
})

Check Action Permission

const hasPermission = await runtimeRBAC.hasActionPermission(
  '0x...', // account address
  '0xa9059cbb', // function selector
  TxAction.EXECUTE_TIME_DELAY_REQUEST
)
console.log('Has action permission:', hasPermission)

Handler vs execution selectors (how EngineBlox enforces wiring)

Role permissions store a handlerForSelectors array on each FunctionPermission. On-chain behavior is split as follows (see contracts/core/lib/EngineBlox.sol):

  1. Grant time (addFunctionToRole): _validateHandlerForSelectors checks that every entry in the permission’s handlerForSelectors is allowed by the function schema for that functionSelector when the schema has enforceHandlerRelations (strict mode). This does not re-run on every hasActionPermission read.

  2. Runtime permission (hasActionPermission / roleHasActionPermission): Only whether the wallet’s roles include the functionSelector and the TxAction bitmap. The stored handlerForSelectors list on the role is not consulted again on each call.

  3. Meta / dual-selector paths (_validateExecutionAndHandlerPermissions): Requires hasActionPermission for both executionSelector and handlerSelector. If the handler function schema has enforceHandlerRelations, the engine also requires executionSelector to appear in functions[handlerSelector].handlerForSelectors — a global handler→execution graph on the schema, independent of which role row granted access.

  4. Flexible schemas: If enforceHandlerRelations is false for a schema, that global pairing check is skipped by design. The @custom:security OPERATIONAL MODES discussion (strict vs flexible handler wiring) lives on EngineBlox.registerFunction in contracts/core/lib/EngineBlox.sol—that is the schema-registration entry point (this repository does not expose a separate registerFunctionSchema symbol). When wiring roles, whitelists, and meta-tx flows in production, follow the action ordering and batch constraints in contracts/core/access/lib/definitions/RuntimeRBACDefinitions.sol (role-config / meta) and contracts/core/execution/lib/definitions/GuardControllerDefinitions.sol (guard-config / macros) so schemas, grants, and calls stay consistent.

🔄 Batch Configuration Workflow

RuntimeRBAC uses batch configuration for all role and function management operations. This allows multiple changes to be applied atomically via meta-transactions.

Role Configuration Actions

The batch system supports the following action types:

enum RoleConfigActionType {
  CREATE_ROLE,
  REMOVE_ROLE,
  ADD_WALLET,
  REVOKE_WALLET,
  ADD_FUNCTION_TO_ROLE,
  REMOVE_FUNCTION_FROM_ROLE
}

Create Role via Batch

import { encodeAbiParameters } from 'viem'

// Define function permissions for the role
const functionPermissions = [
  {
    functionSelector: '0xa9059cbb', // transfer(address,uint256)
    grantedActionsBitmap: 0b000000111, // EXECUTE_TIME_DELAY_REQUEST, APPROVE, CANCEL
    handlerForSelectors: ['0x00000000'] // bytes4(0) for execution selector
  }
]

// Create batch action
const createRoleAction = {
  actionType: 'CREATE_ROLE',
  data: encodeAbiParameters(
    ['string', 'uint256', 'tuple[]'],
    [
      'TreasuryManager',
      5, // maxWallets
      functionPermissions
    ]
  )
}

// Create meta-transaction for batch
const metaTxParams = await runtimeRBAC.createMetaTxParams(
  contractAddress,
  '0x...', // roleConfigBatchRequestAndApprove selector
  TxAction.SIGN_META_REQUEST_AND_APPROVE,
  24n * 60n * 60n, // 24 hour deadline
  BigInt('50000000000'), // max gas price
  ownerAddress
)

const metaTx = await runtimeRBAC.generateUnsignedMetaTransactionForNew(
  ownerAddress,
  contractAddress,
  0n, // value
  0n, // gas limit
  keccak256('ROLE_CONFIG_BATCH'), // operation type
  '0x...', // executeRoleConfigBatch selector
  encodeAbiParameters(
    ['tuple[]'],
    [[createRoleAction]]
  ),
  metaTxParams
)

// Sign the meta-transaction
const signature = await walletClient.signMessage({
  message: { raw: metaTx.message },
  account: ownerAddress
})

// Execute via broadcaster
const txHash = await runtimeRBAC.roleConfigBatchRequestAndApprove(
  { ...metaTx, signature },
  { from: broadcasterAddress }
)

Add Wallet to Role via Batch

const addWalletAction = {
  actionType: RoleConfigActionType.ADD_WALLET,
  data: encodeAbiParameters(
    ['bytes32', 'address'],
    [roleHash, walletAddress]
  )
}

// Create and execute batch (similar to create role example)

Add Function Permission to Role via Batch

Note: The function schema must be registered via GuardController before adding permissions.

const addFunctionToRoleAction = {
  actionType: RoleConfigActionType.ADD_FUNCTION_TO_ROLE,
  data: encodeAbiParameters(
    ['bytes32', 'tuple'],
    [
      roleHash,
      {
        functionSelector: '0xa9059cbb',
        grantedActionsBitmap: 0b000000111,
        handlerForSelectors: ['0x00000000']
      }
    ]
  )
}

// Create and execute batch (similar to create role example)

📡 Event Monitoring

Listen for Role Configuration Events

Contracts emit ComponentEvent(bytes4 functionSelector, bytes data). For RBAC config, filter by executeRoleConfigBatch selector and decode data as (RoleConfigActionType, bytes32 roleHash, bytes4 functionSelector, address wallet). See contract API and NatSpec.

const unwatch = publicClient.watchContractEvent({
  address: contractAddress,
  abi: runtimeRBAC.abi,
  eventName: 'ComponentEvent',
  onLogs: (logs) => {
    logs.forEach(log => {
      if (log.args.functionSelector === executeRoleConfigBatchSelector) {
        const decoded = decodeAbiParameters(/* ... */, log.args.data)
        console.log('Role config:', decoded)
      }
    })
  }
})
unwatch()

🛡️ Security Features

1. Role Protection

Protected roles (OWNER_ROLE, BROADCASTER_ROLE, RECOVERY_ROLE) cannot be removed:

const role = await runtimeRBAC.getRole(roleHash)
if (role.isProtected) {
  console.log('This role is protected and cannot be removed')
}

2. Wallet Limits

Roles have maximum wallet limits:

const role = await runtimeRBAC.getRole(roleHash)
const wallets = await runtimeRBAC.getAuthorizedWallets(roleHash)

if (wallets.length >= role.maxWallets) {
  throw new Error('Role has reached maximum wallet limit')
}

3. Function-Level Permissions

Fine-grained permission control with action-level permissions:

// Check specific action permission
const canRequest = await runtimeRBAC.hasActionPermission(
  account,
  '0xa9059cbb', // transfer function selector
  TxAction.EXECUTE_TIME_DELAY_REQUEST
)

if (!canRequest) {
  throw new Error('Account does not have request permission')
}

4. Function Schema Protection

Protected function schemas cannot be unregistered:

const schema = await runtimeRBAC.getFunctionSchema(functionSelector)
if (schema.isProtected) {
  console.log('This function schema is protected and cannot be unregistered')
}

🔧 Advanced Usage

Batch Role Operations

// Create multiple roles in a single batch
const actions = [
  {
    actionType: 'CREATE_ROLE',
    data: encodeAbiParameters(
      ['string', 'uint256', 'tuple[]'],
      ['ADMIN_ROLE', 3, adminPermissions]
    )
  },
  {
    actionType: 'CREATE_ROLE',
    data: encodeAbiParameters(
      ['string', 'uint256', 'tuple[]'],
      ['MODERATOR_ROLE', 10, moderatorPermissions]
    )
  },
  {
    actionType: 'ADD_WALLET',
    data: encodeAbiParameters(
      ['bytes32', 'address'],
      [adminRoleHash, adminAddress]
    )
  }
]

// Execute batch via meta-transaction

Function Registration Workflow

Note: Function schema registration is now handled by GuardController. Use GuardController's guardConfigBatchRequestAndApprove with the REGISTER_FUNCTION action.

import { GuardController, GuardConfigActionType } from '@bloxchain/sdk/typescript'

// Step 1: Register function schema via GuardController
const registerAction = {
  actionType: GuardConfigActionType.REGISTER_FUNCTION,
  data: encodeAbiParameters(
    ['string', 'string', 'uint8[]'],
    [
      'withdraw(address,uint256)',
      'WITHDRAW_OPERATION',
      [TxAction.EXECUTE_TIME_DELAY_REQUEST, TxAction.EXECUTE_TIME_DELAY_APPROVE]
    ]
  )
}

// Execute via GuardController
await guardController.guardConfigBatchRequestAndApprove(metaTx, { from: broadcasterAddress })

// Step 2: Add function permission to role via RuntimeRBAC
const addPermissionAction = {
  actionType: RoleConfigActionType.ADD_FUNCTION_TO_ROLE,
  data: encodeAbiParameters(
    ['bytes32', 'tuple'],
    [
      roleHash,
      {
        functionSelector: '0x...', // withdraw selector
        grantedActionsBitmap: 0b000000011,
        handlerForSelectors: ['0x00000000']
      }
    ]
  )
}

// Execute via RuntimeRBAC
await runtimeRBAC.roleConfigBatchRequestAndApprove(metaTx, { from: broadcasterAddress })

🧪 Testing

Unit Testing

import { describe, it, expect } from 'vitest'

describe('RuntimeRBAC', () => {
  it('should return correct role information', async () => {
    const role = await runtimeRBAC.getRole(roleHash)
    expect(role.roleHash).toBe(roleHash)
    expect(role.maxWallets).toBeGreaterThan(0)
  })

  it('should check role membership', async () => {
    const hasRole = await runtimeRBAC.hasRole(roleHash, account)
    expect(typeof hasRole).toBe('boolean')
  })

  it('should get roles for wallet', async () => {
    const walletRoles = await runtimeRBAC.getWalletRoles(account)
    expect(Array.isArray(walletRoles)).toBe(true)
  })

  it('should get wallets in role', async () => {
    const wallets = await runtimeRBAC.getAuthorizedWallets(roleHash)
    expect(Array.isArray(wallets)).toBe(true)
  })
})

Integration Testing

describe('RuntimeRBAC Integration', () => {
  it('should complete role creation workflow', async () => {
    // Create role via batch
    const createAction = {
      actionType: 'CREATE_ROLE',
      data: encodeAbiParameters(
        ['string', 'uint256', 'tuple[]'],
        ['TEST_ROLE', 5, []]
      )
    }

    // Execute batch via meta-transaction
    const txHash = await runtimeRBAC.roleConfigBatchRequestAndApprove(
      metaTx,
      { from: broadcasterAddress }
    )

    // Verify role exists
    const role = await runtimeRBAC.getRole(roleHash)
    expect(role.roleName).toBe('TEST_ROLE')
  })
})

🚨 Common Issues

Issue: "Role is protected"

Solution: Protected roles (OWNER_ROLE, BROADCASTER_ROLE, RECOVERY_ROLE) cannot be removed. Use a different role or create a new one.

Issue: "Role has reached maximum wallet limit"

Solution: Increase the role's wallet limit or revoke roles from other accounts before adding new ones.

Issue: "Function schema not found"

Solution: Register the function schema via GuardController before adding it to a role. Use GuardController's guardConfigBatchRequestAndApprove with REGISTER_FUNCTION action.

Issue: "Insufficient permissions"

Solution: Ensure the account has the required role and function permissions. Check with hasActionPermission.

Issue: "Invalid role hash"

Solution: Use the correct role hash. Generate it using keccak256(abi.encodePacked(roleName)).

Issue: "Handler selector mismatch"

Solution: Ensure handlerForSelectors array in function permission matches the function schema's handlerForSelectors array. Use bytes4(0) for execution selectors.

Issue: ResourceAlreadyExists when adding a function to a role

Solution: Re-adding the same selector with RoleConfigActionType.ADD_FUNCTION_TO_ROLE hits the same ResourceAlreadyExists revert as a direct addFunctionToRole would when the selector is already on the role. To change the stored permission bitmap or handlerForSelectors, include RoleConfigActionType.REMOVE_FUNCTION_FROM_ROLE in a roleConfigBatchRequestAndApproveexecuteRoleConfigBatch flow first, then ADD_FUNCTION_TO_ROLE with the new values—the engine applies removal through that batch path; there is no supported separate public removeFunctionFromRole entrypoint for integrators. Note: protected schemas cannot be removed from roles (CannotModifyProtected), so grants of protected selectors are effectively permanent unless the role itself is removed.

📚 Related Documentation