// SPDX-License-Identifier: MIT // MyFun Contracts v0.1.4 (access/AccessControl.compact) pragma language_version >= 0.16.0; /** * @module AccessControl * @description An unshielded AccessControl library. * This module provides a role-based access control mechanism, where roles can be used to * represent a set of permissions. * * Roles are referred to by their `Bytes<32>` identifier. These should be exposed * in the top-level contract and be unique. One way to achieve this is by * using `export sealed ledger` hash digests that are initialized in the top-level contract: * * ```typescript * import CompactStandardLibrary; * import "./node_modules/@openzeppelin-compact/accessControl/src/AccessControl" prefix AccessControl_; * * export sealed ledger MY_ROLE: Bytes<32>; * * constructor() { * MY_ROLE = persistentHash>(pad(32, "MY_ROLE")); * } * ``` * * To restrict access to a circuit, use {assertOnlyRole}: * * ```typescript * circuit foo(): [] { * assertOnlyRole(MY_ROLE); * ... * } * ``` * * Roles can be granted and revoked dynamically via the {grantRole} and * {revokeRole} circuits. Each role has an associated admin role, and only * accounts that have a role's admin role can call {grantRole} and {revokeRole}. * * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means * that only accounts with this role will be able to grant or revoke other * roles. More complex role relationships can be created by using * {_setRoleAdmin}. To set a custom `DEFAULT_ADMIN_ROLE`, implement the `Initializable` * module and set `DEFAULT_ADMIN_ROLE` in the `initialize()` circuit. * * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to * grant and revoke this role. Extra precautions should be taken to secure * accounts that have been granted it. * * @notice Roles can only be granted to ZswapCoinPublicKeys * through the main role approval circuits (`grantRole` and `_grantRole`). * In other words, role approvals to contract addresses are disallowed through these * circuits. * This is because Compact currently does not support contract-to-contract calls which means * if a contract is granted a role, the contract cannot directly call the protected * circuit. * * @notice This module does offer an experimental circuit that allows roles to be granted * to contract addresses (`_unsafeGrantRole`). * Note that the circuit name is very explicit ("unsafe") with this experimental circuit. * Until contract-to-contract calls are supported, * there is no direct way for a contract to call protected circuits. * * @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls * are supported. * * @notice Missing Features and Improvements: * * - Role events * - An ERC165-like interface */ module AccessControl { import CompactStandardLibrary; import "../utils/Utils" prefix Utils_; /** * @description Mapping from a role identifier -> account -> its permissions. * @type {Bytes<32>} roleId - A hash representing a role identifier. * @type {Map, Boolean>} hasRole - A mapping from an account to a * Boolean determining if the account is approved for a role. * @type {Map} * @type {Map, Map, Boolean>} _operatorRoles  */ export ledger _operatorRoles: Map, Map, Boolean>>; /** * @description Mapping from a role identifier to an admin role identifier. * @type {Bytes<32>} roleId - A hash representing a role identifier. * @type {Bytes<32>} adminId - A hash representing an admin identifier. * @type {Map} * @type {Map, Bytes<32>>} _adminRoles  */ export ledger _adminRoles: Map, Bytes<32>>; export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; /** * @description Returns `true` if `account` has been granted `roleId`. * * @circuitInfo k=10, rows=487 * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - The account to query. * @return {Boolean} - Whether the account has the specified role.   */ export circuit hasRole(roleId: Bytes<32>, account: Either): Boolean { if ( _operatorRoles.member(disclose(roleId)) && _operatorRoles .lookup(roleId) .member(disclose(account)) ) { return _operatorRoles .lookup(roleId) .lookup(disclose(account)); } else { return false; } } /** * @description Reverts if `ownPublicKey()` is missing `roleId`. * * @circuitInfo k=10, rows=345 * * Requirements: * * - The caller must have `roleId`. * - The caller must not be a ContractAddress * * @param {Bytes<32>} roleId - The role identifier. * @return {[]} - Empty tuple. */ export circuit assertOnlyRole(roleId: Bytes<32>): [] { _checkRole(roleId, left(ownPublicKey())); } /** * @description Reverts if `account` is missing `roleId`. * * @circuitInfo k=10, rows=467 * * Requirements: * * - `account` must have `roleId`. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - The account to query. * @return {[]} - Empty tuple. */ export circuit _checkRole(roleId: Bytes<32>, account: Either): [] { assert(hasRole(roleId, account), "AccessControl: unauthorized account"); } /** * @description Returns the admin role that controls `roleId` or * a byte array with all zero bytes if `roleId` doesn't exist. See {grantRole} and {revokeRole}. * * To change a role’s admin use {_setRoleAdmin}. * * @circuitInfo k=10, rows=207 * * @param {Bytes<32>} roleId - The role identifier. * @return {Bytes<32>} roleAdmin - The admin role that controls `roleId`. */ export circuit getRoleAdmin(roleId: Bytes<32>): Bytes<32> { if (_adminRoles.member(disclose(roleId))) { return _adminRoles.lookup(disclose(roleId)); } return default>; } /** * @description Grants `roleId` to `account`. * * @circuitInfo k=10, rows=994 * * Requirements: * * - `account` must not be a ContractAddress. * - The caller must have `roleId`'s admin role. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @return {[]} - Empty tuple. */ export circuit grantRole(roleId: Bytes<32>, account: Either): [] { assertOnlyRole(getRoleAdmin(roleId)); _grantRole(roleId, account); } /** * @description Revokes `roleId` from `account`. * * @circuitInfo k=10, rows=827 * * Requirements: * * - The caller must have `roleId`'s admin role. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @return {[]} - Empty tuple. */ export circuit revokeRole(roleId: Bytes<32>, account: Either): [] { assertOnlyRole(getRoleAdmin(roleId)); _revokeRole(roleId, account); } /** * @description Revokes `roleId` from the calling account. * * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's * purpose is to provide a mechanism for accounts to lose their privileges * if they are compromised (such as when a trusted device is misplaced). * * @circuitInfo k=10, rows=640 * * Requirements: * * - The caller must be `callerConfirmation`. * - The caller must not be a `ContractAddress`. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. * @return {[]} - Empty tuple. */ export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { assert(callerConfirmation == left(ownPublicKey()), "AccessControl: bad confirmation"); _revokeRole(roleId, callerConfirmation); } /** * @description Sets `adminRole` as `roleId`'s admin role. * * @circuitInfo k=10, rows=209 * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} adminRole - The admin role identifier. * @return {[]} - Empty tuple. */ export circuit _setRoleAdmin(roleId: Bytes<32>, adminRole: Bytes<32>): [] { _adminRoles.insert(disclose(roleId), disclose(adminRole)); } /** * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. * * @circuitInfo k=10, rows=734 * * Requirements: * * - `account` must not be a ContractAddress. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @return {Boolean} roleGranted - A boolean indicating if `roleId` was granted. */ export circuit _grantRole(roleId: Bytes<32>, account: Either): Boolean { assert(!Utils_isContractAddress(account), "AccessControl: unsafe role approval"); return _unsafeGrantRole(roleId, account); } /** * @description Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. * Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. * * @circuitInfo k=10, rows=733 * * @notice External smart contracts cannot call the token contract at this time, so granting a role to an ContractAddress may * render a circuit permanently inaccessible. * * @param {Bytes<32>} roleId - The role identifier. * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. */ export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { if (hasRole(roleId, account)) { return false; } if (!_operatorRoles.member(disclose(roleId))) { _operatorRoles.insert( disclose(roleId), default, Boolean >> ); _operatorRoles .lookup(roleId) .insert(disclose(account), true); return true; } _operatorRoles.lookup(roleId).insert(disclose(account), true); return true; } /** * @description Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. * Internal circuit without access restriction. * * @circuitInfo k=10, rows=563 * * @param {Bytes<32>} roleId - The role identifier. * @param {Bytes<32>} adminRole - The admin role identifier. * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { if (!hasRole(roleId, account)) { return false; } _operatorRoles .lookup(roleId) .insert(disclose(account), false); return true; } }