From 247d75f857ea93d63a61db874d5be4fd17521f6b Mon Sep 17 00:00:00 2001 From: Daksh Miglani Date: Sun, 9 May 2021 21:00:29 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20new=20access=20con?= =?UTF-8?q?trol=20and=20use=20it=20in=20InstaMappings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/mapping/AccessControl.sol | 240 ++++++++++++++++++++++++++++ contracts/mapping/InstaMappings.sol | 37 ++--- test/instamappings.js | 27 ++++ 3 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 contracts/mapping/AccessControl.sol create mode 100644 test/instamappings.js diff --git a/contracts/mapping/AccessControl.sol b/contracts/mapping/AccessControl.sol new file mode 100644 index 00000000..9c8b8fb7 --- /dev/null +++ b/contracts/mapping/AccessControl.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +import "@openzeppelin/contracts/utils/EnumerableSet.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; + +/** + * @dev Contract module that allows children to implement role-based access + * control mechanisms. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ``` + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ``` + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. 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}. + * + * 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. + */ +abstract contract AccessControl is Context { + using EnumerableSet for EnumerableSet.AddressSet; + using Address for address; + + struct RoleData { + EnumerableSet.AddressSet members; + bytes32 adminRole; + } + + mapping(bytes32 => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + * + * _Available since v3.1._ + */ + event RoleAdminChanged( + bytes32 indexed role, + bytes32 indexed previousAdminRole, + bytes32 indexed newAdminRole + ); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {_setupRole}. + */ + event RoleGranted( + bytes32 indexed role, + address indexed account, + address indexed sender + ); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked( + bytes32 indexed role, + address indexed account, + address indexed sender + ); + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view returns (bool) { + return _roles[role].members.contains(account); + } + + /** + * @dev Returns the number of accounts that have `role`. Can be used + * together with {getRoleMember} to enumerate all bearers of a role. + */ + function getRoleMemberCount(bytes32 role) public view returns (uint256) { + return _roles[role].members.length(); + } + + /** + * @dev Returns one of the accounts that have `role`. `index` must be a + * value between 0 and {getRoleMemberCount}, non-inclusive. + * + * Role bearers are not sorted in any particular way, and their ordering may + * change at any point. + * + * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure + * you perform all queries on the same block. See the following + * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] + * for more information. + */ + function getRoleMember(bytes32 role, uint256 index) + public + view + returns (address) + { + return _roles[role].members.at(index); + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have `DEFAULT_ADMIN_ROLE` role. + */ + function grantRole(bytes32 role, address account) public virtual { + require( + hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), + "AccessControl: sender must be the master admin" + ); + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have `DEFAULT_ADMIN_ROLE` role. + */ + function revokeRole(bytes32 role, address account) public virtual { + require( + hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), + "AccessControl: sender must be the master admin" + ); + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function'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). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `account`. + */ + function renounceRole(bytes32 role, address account) public virtual { + require( + account == _msgSender(), + "AccessControl: can only renounce roles for self" + ); + + _revokeRole(role, account); + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. Note that unlike {grantRole}, this function doesn't perform any + * checks on the calling account. + * + * [WARNING] + * ==== + * This function should only be called from the constructor when setting + * up the initial roles for the system. + * + * Using this function in any other way is effectively circumventing the admin + * system imposed by {AccessControl}. + * ==== + */ + function _setupRole(bytes32 role, address account) internal virtual { + _grantRole(role, account); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { + emit RoleAdminChanged(role, _roles[role].adminRole, adminRole); + _roles[role].adminRole = adminRole; + } + + function _grantRole(bytes32 role, address account) private { + if (_roles[role].members.add(account)) { + emit RoleGranted(role, account, _msgSender()); + } + } + + function _revokeRole(bytes32 role, address account) private { + if (_roles[role].members.remove(account)) { + emit RoleRevoked(role, account, _msgSender()); + } + } +} diff --git a/contracts/mapping/InstaMappings.sol b/contracts/mapping/InstaMappings.sol index 145383ec..c0bb340f 100644 --- a/contracts/mapping/InstaMappings.sol +++ b/contracts/mapping/InstaMappings.sol @@ -1,46 +1,39 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; -import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +import {AccessControl} from "./AccessControl.sol"; + interface IndexInterface { function master() external view returns (address); } contract InstaMappings is AccessControl { - IndexInterface public constant instaIndex = IndexInterface(0x2971AdFa57b20E5a416aE5a708A8655A9c74f723); + IndexInterface public constant instaIndex = + IndexInterface(0x2971AdFa57b20E5a416aE5a708A8655A9c74f723); bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); - function setMaster() public { - require(msg.sender == address(this), "msg.sender is not this contract"); - uint256 adminCount = getRoleMemberCount(DEFAULT_ADMIN_ROLE); - require(adminCount == 1, "setMaster::Wrong-admin-count"); - address currentMaster = getRoleMember(DEFAULT_ADMIN_ROLE, 0); - address master = instaIndex.master(); - - if (currentMaster != master) { - grantRole(DEFAULT_ADMIN_ROLE, master); - revokeRole(DEFAULT_ADMIN_ROLE, currentMaster); - } - - adminCount = getRoleMemberCount(DEFAULT_ADMIN_ROLE); - require(adminCount == 1, "setMaster::Wrong-admin-count"); - require(hasRole(DEFAULT_ADMIN_ROLE, master), "setMaster::InstaIndex-master-not-set"); - } - constructor() { _setupRole(DEFAULT_ADMIN_ROLE, instaIndex.master()); _setRoleAdmin(DEFAULT_ADMIN_ROLE, ADMIN_ROLE); _setupRole(ADMIN_ROLE, address(this)); } - function getMappingContractRole(address mappingContract) public pure returns (bytes32 role){ + function getMappingContractRole(address mappingContract) + public + pure + returns (bytes32 role) + { assembly { role := mload(add(mappingContract, 32)) } } - function hasRole(address mappingAddr, address account) public view returns (bool) { + function hasRole(address mappingAddr, address account) + public + view + returns (bool) + { return super.hasRole(getMappingContractRole(mappingAddr), account); } @@ -55,4 +48,4 @@ contract InstaMappings is AccessControl { function renounceRole(address mappingAddr, address account) public { super.renounceRole(getMappingContractRole(mappingAddr), account); } -} \ No newline at end of file +} diff --git a/test/instamappings.js b/test/instamappings.js new file mode 100644 index 00000000..f2b5ff8b --- /dev/null +++ b/test/instamappings.js @@ -0,0 +1,27 @@ +const { ethers } = require("hardhat"); + +describe("Test InstaMapping contract", () => { + let signer, addressAdmin, fakeAccount; + let mappingContract; + const otherContractAddress = "0x514910771af9ca656af840dff83e8264ecf986ca"; + + beforeEach("deploy contract", async () => { + [signer, addressAdmin, fakeAccount] = await ethers.getSigners(); + + const mappingContractFactory = await ethers.getContractFactory("InstaMappings"); + mappingContract = await mappingContractFactory.deploy(); + + await mappingContract.deployed(); + }); + + it("should grant role", async () => { + const GRANT_ROLE_KEY = 'grantRole(address,address)' + const HAS_ROLE_KEY = 'hasRole(address,address)'; + await mappingContract[GRANT_ROLE_KEY](otherContractAddress, addressAdmin.address, { + gasLimit: 12000000 + }); + + expect(await mappingContract[HAS_ROLE_KEY](otherContractAddress, addressAdmin.address)).to.eq(true); + }); + +}); \ No newline at end of file