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 fromEasyAccessControl
andERC2771Context
- Contracts like
RestrictedLockupToken
now reference a deployedAccessControl
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, preservingmsg.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 externalaccessControl
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
Role | Bit Position | Purpose | Key Functions |
---|---|---|---|
Contract Admin | 000001 (1) | System configuration and emergency controls | Deploy contracts, set max rates, pause operations |
Reserve Admin | 000010 (2) | Token supply management | mint() , burn() , forceTransferBetween() |
Wallets Admin | 000100 (4) | Address compliance and group management | AML/KYC, transfer groups, holder management |
Transfer Admin | 001000 (8) | Transfer rules and payment operations | Group transfers, interest payments, dividend funding |
Soft Burn Admin | 010000 (16) | Allowance-based burning | softBurn() - burns tokens using allowance mechanism |
Mint Admin | 100000 (32) | Alternative minting authority | mintTokenType() - specialized minting operations |
Soft Burn vs Reserve Admin Burn
Function | Role Required | Mechanism | Use Case |
---|---|---|---|
burn() | Reserve Admin | Direct authority | Administrative burns, error corrections |
softBurn() | Soft Burn Admin | Requires allowance | DeFi integrations, automated burning protocols |
Mint Admin vs Reserve Admin
Function | Role Required | Purpose | Flexibility |
---|---|---|---|
mint() | Reserve Admin | Standard minting | Auto token type determination |
mintTokenType() | Mint Admin OR Reserve Admin | Specialized minting | Explicit 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 Integer | Admin Roles | Common Use Case |
---|---|---|
1 | Contract Admin | System configuration only |
2 | Reserve Admin | Token issuer with full control |
16 | Soft Burn Admin | DeFi protocol integration |
32 | Mint Admin | Specialized minting service |
18 | Reserve Admin + Soft Burn Admin | Comprehensive token management |
34 | Reserve Admin + Mint Admin | Full minting flexibility |
63 | All Roles | Development/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 addressreserveAdmin_
(address): Initial reserve administrator addresstransferAdmin_
(address): Initial transfer administrator addresstrustedForwarder_
(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 torole
(uint8): The role bitmask to grant
Requirements:
- Caller must have CONTRACT_ADMIN_ROLE
addr
must not be zero addressrole
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 fromrole
(uint8): The role bitmask to revoke
Requirements:
- Caller must have CONTRACT_ADMIN_ROLE
addr
must not be zero addressrole
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 toroles_
(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 fromroles_
(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 checkrole
(uint8): The role bitmask to check for
Returns:
bool
: True if the address has the role, false otherwise
Requirements:
addr
must not be zero addressrole
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 changegrantee
(address, indexed): The address that received or lost the rolerole
(uint8): The role that was changedstatus
(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.