Skip to main content

Access Control

To decreasing the contract size we provide an optimized version of Access Control. This is similar to the linux file permission bitmask exposed in the chmod command line function.

Our implementation uses binary bitmask determine the access control roles for an address. This optimizes the gas cost and the size of the smart contract code itself.

This is described in EasyAccessControl.sol and used by an external AccessControl contract that is ERC2771-aware.

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.
  • Meta-transactions: 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, keeping business logic slim and reducing bytecode.

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 roles IDs within the access controls.

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

As 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

If you want to use the unused bits in the future to add new roles you can add something like

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

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 == 1100 // adding of bits, granting multiple roles

AnyNumber & WALLETS_ADMIN_ROLE > 0 // checking if AnyNumber contains 0100 bit, it can be used for checking the role

The contracts expose simple 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 notes

  • 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())).