Skip to main content

Access Control

Overview

The contract implements an access control system for the Security Token and Identity Registry. Bitmask role integers are used to represent the roles and permissions for an address.

Architecture

  • Externalized access control to a standalone AccessControl contract that inherits from EasyAccessControl and ERC2771Context
  • Contracts like RestrictedLockupToken now reference a deployed AccessControl instance instead of keeping roles internally
  • Identity Registry contract, having a separate list of admins, inherits from EasyAccessControl directly
  • AccessControl accepts the ERC-2771 trusted forwarder in its constructor, preserving msg.sender through _msgSender()
  • Role checks in token/other contracts call the external accessControl for authorization

Usage

AccessControl constructor

constructor(
address contractAdmin_,
address reserveAdmin_,
address transferAdmin_,
address trustedForwarder_
)

Pass the ERC-2771 forwarder address as the 4th parameter in deployments and tests.

How it works

We use a uint8 binary representation of a number, such as 01010101 to represent the role IDs within the access controls.

Roles are defined by a specific bit position in the bit storage representation.

For example, decimal 1 is 00000001 in binary mode, 2 is 00000010, 3 is 00000011, 4 is 00000100, 7 is 00000111, etc.

We describe the roles in use as:

uint8 constant CONTRACT_ADMIN_ROLE = 1; // 0001
uint8 constant RESERVE_ADMIN_ROLE = 2; // 0010
uint8 constant WALLETS_ADMIN_ROLE = 4; // 0100
uint8 constant TRANSFER_ADMIN_ROLE = 8; // 1000
uint8 constant SOFT_BURN_ADMIN_ROLE = 16; // 0001 0000
uint8 constant MINT_ADMIN_ROLE = 32; // 0010 0000

New roles can be defined by adding a new bit position to the bit storage representation.

uint8 constant NEW_ROLE = 64; // 0100 0000
uint8 constant SECOND_ROLE = 128; // 1000 0000

You can grant multiple roles by adding the role number values together to get the correct bitmask representation like this:

uint8 constant WALLET_AND_TRANSFER_ADMIN_ROLE = 12; // 0001100

or

uint8 constant WALLET_AND_TRANSFER_ADMIN_ROLE = WALLETS_ADMIN_ROLE | TRANSFER_ADMIN_ROLE; // 0001100

For manipulating binary numbers you can use binary operators &, | and ^.

Example:

uint8 constant CONTRACT_ADMIN_ROLE = 1; // 0001
uint8 constant RESERVE_ADMIN_ROLE = 2; // 0010
uint8 constant WALLETS_ADMIN_ROLE = 4; // 0100
uint8 constant TRANSFER_ADMIN_ROLE = 8; // 1000

WALLETS_ADMIN_ROLE | TRANSFER_ADMIN_ROLE == 12 // 1100 in binary, granting multiple roles

AnyNumber & WALLETS_ADMIN_ROLE > 0 // checking if AnyNumber contains 0100 bit, used for role checking

The contracts expose methods to manipulate Access Controls and check the roles via the external AccessControl. Note that granting new and revoking existing roles must be done in separate transactions.

Batched functions also allow for multiple roles to be granted or revoked in a single transaction.

// EasyAccessControl (internal logic)
function batchGrantRoles(address[] calldata addresses, uint8[] calldata roles) public onlyContractAdmin
function grantRole(address addr, uint8 role) public validRole(role) validAddress(addr) onlyContractAdmin
function batchRevokeRoles(address[] calldata addresses, uint8[] calldata roles) public onlyContractAdmin
function revokeRole(address addr, uint8 role) public validRole(role) validAddress(addr) onlyContractAdmin
function hasRole(address addr, uint8 role) public view validRole(role) validAddress(addr) returns (bool)

// AccessControl (external, ERC2771-aware) extends EasyAccessControl, same API, constructor also seeds roles:
// AccessControl(contractAdmin, reserveAdmin, transferAdmin, trustedForwarder)

Integration

  • When deploying RestrictedLockupToken, pass the external accessControl address via the constructor params object.
  • In JavaScript tests and scripts, ensure AccessControl.deploy is called with 4 params (including the forwarder), e.g.:
const AccessControl = await ethers.getContractFactory('AccessControl')
const accessControl = await AccessControl.deploy(
contractAdmin.address,
reserveAdmin.address,
transferAdmin.address,
forwarder.address
)
  • Token/extension methods perform role checks against accessControl (e.g., accessControl.hasRole(msg.sender, accessControl.RESERVE_ADMIN_ROLE())).

Roles Matrix

Admin permissions are defined with the following binary values. Roles are defined as the OR of a set of admin roles:

  • Contract Admin: 000001 (1)
  • Reserve Admin: 000010 (2)
  • Wallets Admin: 000100 (4)
  • Transfer Admin: 001000 (8)
  • Soft Burn Admin: 010000 (16)
  • Mint Admin: 100000 (32)

Core Admin Roles

RoleBit PositionPurposeKey Functions
Contract Admin000001 (1)System configuration and emergency controlsDeploy contracts, set max rates, pause operations
Reserve Admin000010 (2)Token supply managementmint(), burn(), forceTransferBetween()
Wallets Admin000100 (4)Address compliance and group managementAML/KYC, transfer groups, holder management
Transfer Admin001000 (8)Transfer rules and payment operationsGroup transfers, interest payments, dividend funding
Soft Burn Admin010000 (16)Allowance-based burningsoftBurn() - burns tokens using allowance mechanism
Mint Admin100000 (32)Alternative minting authoritymintTokenType() - specialized minting operations

Soft Burn vs Reserve Admin Burn

FunctionRole RequiredMechanismUse Case
burn()Reserve AdminDirect authorityAdministrative burns, error corrections
softBurn()Soft Burn AdminRequires allowanceDeFi integrations, automated burning protocols

Mint Admin vs Reserve Admin

FunctionRole RequiredPurposeFlexibility
mint()Reserve AdminStandard mintingAuto token type determination
mintTokenType()Mint Admin OR Reserve AdminSpecialized mintingExplicit token type control

Key Differences:

  • Reserve Admin: Full authority over token supply, suitable for issuer operations
  • Soft Burn Admin: Limited to allowance-based burns, suitable for automated protocols
  • Mint Admin: Can mint tokens but without broader reserve management powers

Example Role Combinations

Role IntegerAdmin RolesCommon Use Case
1Contract AdminSystem configuration only
2Reserve AdminToken issuer with full control
16Soft Burn AdminDeFi protocol integration
32Mint AdminSpecialized minting service
18Reserve Admin + Soft Burn AdminComprehensive token management
34Reserve Admin + Mint AdminFull minting flexibility
63All RolesDevelopment/testing environment

API Reference

Constructor

constructor(address contractAdmin_, address reserveAdmin_, address transferAdmin_, address trustedForwarder_)

Initializes the AccessControl contract with initial admin roles and trusted forwarder.

Parameters:

  • contractAdmin_ (address): Initial contract administrator address
  • reserveAdmin_ (address): Initial reserve administrator address
  • transferAdmin_ (address): Initial transfer administrator address
  • trustedForwarder_ (address): ERC-2771 trusted forwarder address for meta-transactions

Requirements:

  • All addresses must be non-zero
  • Sets up initial role assignments for the three core admin types

Role Constants

CONTRACT_ADMIN_ROLE() → uint8

Returns the contract admin role constant.

Returns:

  • uint8: The contract admin role value (1)

Usage: System configuration and emergency controls


RESERVE_ADMIN_ROLE() → uint8

Returns the reserve admin role constant.

Returns:

  • uint8: The reserve admin role value (2)

Usage: Token supply management (mint, burn, force transfers)


WALLETS_ADMIN_ROLE() → uint8

Returns the wallets admin role constant.

Returns:

  • uint8: The wallets admin role value (4)

Usage: Address compliance and group management


TRANSFER_ADMIN_ROLE() → uint8

Returns the transfer admin role constant.

Returns:

  • uint8: The transfer admin role value (8)

Usage: Transfer rules and payment operations


SOFT_BURN_ADMIN_ROLE() → uint8

Returns the soft burn admin role constant.

Returns:

  • uint8: The soft burn admin role value (16)

Usage: Allowance-based token burning


MINT_ADMIN_ROLE() → uint8

Returns the mint admin role constant.

Returns:

  • uint8: The mint admin role value (32)

Usage: Alternative minting authority with explicit token type control


Role Management Functions

grantRole(address addr, uint8 role)

Grants a specific role to an address.

Parameters:

  • addr (address): The address to grant the role to
  • role (uint8): The role bitmask to grant

Requirements:

  • Caller must have CONTRACT_ADMIN_ROLE
  • addr must not be zero address
  • role must be a valid role value
  • Address must not already have the role

Emits: RoleChange(msg.sender, addr, role, true)


revokeRole(address addr, uint8 role)

Revokes a specific role from an address.

Parameters:

  • addr (address): The address to revoke the role from
  • role (uint8): The role bitmask to revoke

Requirements:

  • Caller must have CONTRACT_ADMIN_ROLE
  • addr must not be zero address
  • role must be a valid role value
  • Cannot revoke CONTRACT_ADMIN_ROLE if it would leave no contract admins
  • Address must currently have the role

Emits: RoleChange(msg.sender, addr, role, false)


batchGrantRoles(address[] addresses, uint8[] roles_)

Grants multiple roles to multiple addresses in a single transaction.

Parameters:

  • addresses (address[]): Array of addresses to grant roles to
  • roles_ (uint8[]): Array of role bitmasks to grant (must match addresses length)

Requirements:

  • Caller must have CONTRACT_ADMIN_ROLE
  • Arrays must be the same length
  • All addresses must be non-zero
  • All roles must be valid
  • Addresses must not already have their respective roles

Emits: RoleChange event for each successful role grant


batchRevokeRoles(address[] addresses, uint8[] roles_)

Revokes multiple roles from multiple addresses in a single transaction.

Parameters:

  • addresses (address[]): Array of addresses to revoke roles from
  • roles_ (uint8[]): Array of role bitmasks to revoke (must match addresses length)

Requirements:

  • Caller must have CONTRACT_ADMIN_ROLE
  • Arrays must be the same length
  • All addresses must be non-zero
  • All roles must be valid
  • Cannot revoke CONTRACT_ADMIN_ROLE if it would leave no contract admins
  • Addresses must currently have their respective roles

Emits: RoleChange event for each successful role revocation


Query Functions

hasRole(address addr, uint8 role) → bool

Checks if an address has a specific role.

Parameters:

  • addr (address): The address to check
  • role (uint8): The role bitmask to check for

Returns:

  • bool: True if the address has the role, false otherwise

Requirements:

  • addr must not be zero address
  • role must be a valid role value

roles(address addr) → uint8

Returns the complete role bitmask for an address.

Parameters:

  • addr (address): The address to query roles for

Returns:

  • uint8: The complete role bitmask for the address

contractAdminCount() → uint8

Returns the current number of contract administrators.

Returns:

  • uint8: The count of addresses with CONTRACT_ADMIN_ROLE

Usage: Used to prevent revoking the last contract admin


ERC-2771 Functions

isTrustedForwarder(address forwarder) → bool

Checks if an address is the trusted forwarder for meta-transactions.

Parameters:

  • forwarder (address): The address to check

Returns:

  • bool: True if the address is the trusted forwarder

trustedForwarder() → address

Returns the current trusted forwarder address.

Returns:

  • address: The trusted forwarder address set in constructor

Events

RoleChange(address indexed grantor, address indexed grantee, uint8 role, bool indexed status)

Emitted when a role is granted or revoked.

Parameters:

  • grantor (address, indexed): The address that performed the role change
  • grantee (address, indexed): The address that received or lost the role
  • role (uint8): The role that was changed
  • status (bool, indexed): True for grant, false for revoke

Usage: Track all role changes for auditing and monitoring


Custom Errors

EasyAccessControl_AlreadyHasRole()

Thrown when attempting to grant a role to an address that already has it.


EasyAccessControl_ArraysMustBeSameLength()

Thrown when batch operations receive arrays of different lengths.


EasyAccessControl_AtLeastOneContractAdminRequired()

Thrown when attempting to revoke the last CONTRACT_ADMIN_ROLE.


EasyAccessControl_CannotRevokeRole()

Thrown when attempting to revoke a role that cannot be revoked.


EasyAccessControl_DoesNotHaveAdminRole(address addr)

Thrown when an address lacks required admin privileges.

Parameters:

  • addr (address): The address that lacks admin role

EasyAccessControl_DoesNotHaveContractAdminRole(address addr)

Thrown when an address lacks CONTRACT_ADMIN_ROLE for a restricted operation.

Parameters:

  • addr (address): The address that lacks contract admin role

EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(address addr)

Thrown when an address lacks either CONTRACT_ADMIN_ROLE or TRANSFER_ADMIN_ROLE.

Parameters:

  • addr (address): The address that lacks required roles

EasyAccessControl_DoesNotHaveReserveAdminRole(address addr)

Thrown when an address lacks RESERVE_ADMIN_ROLE for a restricted operation.

Parameters:

  • addr (address): The address that lacks reserve admin role

EasyAccessControl_DoesNotHaveTransferAdminRole(address addr)

Thrown when an address lacks TRANSFER_ADMIN_ROLE for a restricted operation.

Parameters:

  • addr (address): The address that lacks transfer admin role

EasyAccessControl_DoesNotHaveWalletsAdminRole(address addr)

Thrown when an address lacks WALLETS_ADMIN_ROLE for a restricted operation.

Parameters:

  • addr (address): The address that lacks wallets admin role

EasyAccessControl_InvalidRole()

Thrown when an invalid role value is provided.


EasyAccessControl_InvalidZeroAddress()

Thrown when a zero address is provided where a valid address is required.