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 fromEasyAccessControl
andERC2771Context
. - Contracts like
RestrictedLockupToken
now reference a deployedAccessControl
instance instead of keeping roles internally. - Meta-transactions:
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, 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 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())
).