Add AccessManager contracts (#4121)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
audit/2023-03
Francisco 2 years ago committed by GitHub
parent 3f610ebc25
commit fa112be682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .changeset/quiet-trainers-kick.md
  2. 10
      contracts/access/README.adoc
  3. 75
      contracts/access/manager/AccessManaged.sol
  4. 341
      contracts/access/manager/AccessManager.sol
  5. 54
      contracts/access/manager/AccessManagerAdapter.sol
  6. 13
      contracts/access/manager/IAuthority.sol
  7. 34
      contracts/mocks/AccessManagerMocks.sol
  8. 2
      hardhat.config.js
  9. 55
      test/access/manager/AccessManaged.test.js
  10. 506
      test/access/manager/AccessManager.test.js
  11. 1
      test/helpers/enums.js

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`AccessManager`: Added a new contract for managing access control of complex systems in a consolidated location.

@ -25,3 +25,13 @@ This directory provides ways to restrict who can access the functions of a contr
{{AccessControlEnumerable}}
{{AccessControlDefaultAdminRules}}
== AccessManager
{{IAuthority}}
{{AccessManager}}
{{AccessManaged}}
{{AccessManagerAdapter}}

@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../../utils/Context.sol";
import "./IAuthority.sol";
/**
* @dev This contract module makes available a {restricted} modifier. Functions decorated with this modifier will be
* permissioned according to an "authority": a contract like {AccessManager} that follows the {IAuthority} interface,
* implementing a policy that allows certain callers access to certain functions.
*
* IMPORTANT: The `restricted` modifier should never be used on `internal` functions, judiciously used in `public`
* functions, and ideally only used in `external` functions. See {restricted}.
*/
contract AccessManaged is Context {
event AuthorityUpdated(address indexed sender, IAuthority indexed newAuthority);
IAuthority private _authority;
/**
* @dev Restricts access to a function as defined by the connected Authority for this contract and the
* caller and selector of the function that entered the contract.
*
* [IMPORTANT]
* ====
* In general, this modifier should only be used on `external` functions. It is okay to use it on `public` functions
* that are used as external entry points and are not called internally. Unless you know what you're doing, it
* should never be used on `internal` functions. Failure to follow these rules can have critical security
* implications! This is because the permissions are determined by the function that entered the contract, i.e. the
* function at the bottom of the call stack, and not the function where the modifier is visible in the source code.
* ====
*/
modifier restricted() {
_checkCanCall(_msgSender(), msg.sig);
_;
}
/**
* @dev Initializes the contract connected to an initial authority.
*/
constructor(IAuthority initialAuthority) {
_setAuthority(initialAuthority);
}
/**
* @dev Returns the current authority.
*/
function authority() public view virtual returns (IAuthority) {
return _authority;
}
/**
* @dev Transfers control to a new authority. The caller must be the current authority.
*/
function setAuthority(IAuthority newAuthority) public virtual {
require(_msgSender() == address(_authority), "AccessManaged: not current authority");
_setAuthority(newAuthority);
}
/**
* @dev Transfers control to a new authority. Internal function with no access restriction.
*/
function _setAuthority(IAuthority newAuthority) internal virtual {
_authority = newAuthority;
emit AuthorityUpdated(_msgSender(), newAuthority);
}
/**
* @dev Reverts if the caller is not allowed to call the function identified by a selector.
*/
function _checkCanCall(address caller, bytes4 selector) internal view virtual {
require(_authority.canCall(caller, address(this), selector), "AccessManaged: authority rejected");
}
}

@ -0,0 +1,341 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "../AccessControl.sol";
import "../AccessControlDefaultAdminRules.sol";
import "./IAuthority.sol";
import "./AccessManaged.sol";
interface IAccessManager is IAuthority, IAccessControlDefaultAdminRules {
enum AccessMode {
Custom,
Closed,
Open
}
event GroupUpdated(uint8 indexed group, string name);
event GroupAllowed(address indexed target, bytes4 indexed selector, uint8 indexed group, bool allowed);
event AccessModeUpdated(address indexed target, AccessMode indexed mode);
function createGroup(uint8 group, string calldata name) external;
function updateGroupName(uint8 group, string calldata name) external;
function hasGroup(uint8 group) external view returns (bool);
function getUserGroups(address user) external view returns (bytes32 groups);
function grantGroup(uint8 group, address user) external;
function revokeGroup(uint8 group, address user) external;
function renounceGroup(uint8 group, address user) external;
function getFunctionAllowedGroups(address target, bytes4 selector) external view returns (bytes32 groups);
function setFunctionAllowedGroup(address target, bytes4[] calldata selectors, uint8 group, bool allowed) external;
function getContractMode(address target) external view returns (AccessMode);
function setContractModeCustom(address target) external;
function setContractModeOpen(address target) external;
function setContractModeClosed(address target) external;
function transferContractAuthority(address target, address newAuthority) external;
}
/**
* @dev AccessManager is a central contract to store the permissions of a system.
*
* The smart contracts under the control of an AccessManager instance will have a set of "restricted" functions, and the
* exact details of how access is restricted for each of those functions is configurable by the admins of the instance.
* These restrictions are expressed in terms of "groups".
*
* An AccessManager instance will define a set of groups. Each of them must be created before they can be granted, with
* a maximum of 255 created groups. Users can be added into any number of these groups. Each of them defines an
* AccessControl role, and may confer access to some of the restricted functions in the system, as configured by admins
* through the use of {setFunctionAllowedGroup}.
*
* Note that a function in a target contract may become permissioned in this way only when: 1) said contract is
* {AccessManaged} and is connected to this contract as its manager, and 2) said function is decorated with the
* `restricted` modifier.
*
* There is a special group defined by default named "public" which all accounts automatically have.
*
* Contracts can also be configured in two special modes: 1) the "open" mode, where all functions are allowed to the
* "public" group, and 2) the "closed" mode, where no function is allowed to any group.
*
* Since all the permissions of the managed system can be modified by the admins of this instance, it is expected that
* it will be highly secured (e.g., a multisig or a well-configured DAO). Additionally, {AccessControlDefaultAdminRules}
* is included to enforce security rules on this account.
*
* NOTE: Some of the functions in this contract, such as {getUserGroups}, return a `bytes32` bitmap to succintly
* represent a set of groups. In a bitmap, bit `n` (counting from the least significant bit) will be 1 if and only if
* the group with number `n` is in the set. For example, the hex value `0x05` represents the set of the two groups
* numbered 0 and 2 from its binary equivalence `0b101`
*/
contract AccessManager is IAccessManager, AccessControlDefaultAdminRules {
bytes32 _createdGroups;
// user -> groups
mapping(address => bytes32) private _userGroups;
// target -> selector -> groups
mapping(address => mapping(bytes4 => bytes32)) private _allowedGroups;
// target -> mode
mapping(address => AccessMode) private _contractMode;
uint8 private constant _GROUP_PUBLIC = type(uint8).max;
/**
* @dev Initializes an AccessManager with initial default admin and transfer delay.
*/
constructor(
uint48 initialDefaultAdminDelay,
address initialDefaultAdmin
) AccessControlDefaultAdminRules(initialDefaultAdminDelay, initialDefaultAdmin) {
_createGroup(_GROUP_PUBLIC, "public");
}
/**
* @dev Returns true if the caller can invoke on a target the function identified by a function selector.
* Entrypoint for {AccessManaged} contracts.
*/
function canCall(address caller, address target, bytes4 selector) public view virtual returns (bool) {
bytes32 allowedGroups = getFunctionAllowedGroups(target, selector);
bytes32 callerGroups = getUserGroups(caller);
return callerGroups & allowedGroups != 0;
}
/**
* @dev Creates a new group with a group number that can be chosen arbitrarily but must be unused, and gives it a
* human-readable name. The caller must be the default admin.
*
* Group numbers are not auto-incremented in order to avoid race conditions, but administrators can safely use
* sequential numbers.
*
* Emits {GroupUpdated}.
*/
function createGroup(uint8 group, string memory name) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
_createGroup(group, name);
}
/**
* @dev Updates an existing group's name. The caller must be the default admin.
*/
function updateGroupName(uint8 group, string memory name) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
require(group != _GROUP_PUBLIC, "AccessManager: built-in group");
require(hasGroup(group), "AccessManager: unknown group");
emit GroupUpdated(group, name);
}
/**
* @dev Returns true if the group has already been created via {createGroup}.
*/
function hasGroup(uint8 group) public view virtual returns (bool) {
return _getGroup(_createdGroups, group);
}
/**
* @dev Returns a bitmap of the groups the user has. See note on bitmaps above.
*/
function getUserGroups(address user) public view virtual returns (bytes32) {
return _userGroups[user] | _groupMask(_GROUP_PUBLIC);
}
/**
* @dev Grants a user a group.
*
* Emits {RoleGranted} with the role id of the group, if wasn't already held by the user.
*/
function grantGroup(uint8 group, address user) public virtual {
grantRole(_encodeGroupRole(group), user); // will check msg.sender
}
/**
* @dev Removes a group from a user.
*
* Emits {RoleRevoked} with the role id of the group, if previously held by the user.
*/
function revokeGroup(uint8 group, address user) public virtual {
revokeRole(_encodeGroupRole(group), user); // will check msg.sender
}
/**
* @dev Allows a user to renounce a group.
*
* Emits {RoleRevoked} with the role id of the group, if previously held by the user.
*/
function renounceGroup(uint8 group, address user) public virtual {
renounceRole(_encodeGroupRole(group), user); // will check msg.sender
}
/**
* @dev Returns a bitmap of the groups that are allowed to call a function of a target contract. If the target
* contract is in open or closed mode it will be reflected in the return value.
*/
function getFunctionAllowedGroups(address target, bytes4 selector) public view virtual returns (bytes32) {
AccessMode mode = getContractMode(target);
if (mode == AccessMode.Open) {
return _groupMask(_GROUP_PUBLIC);
} else if (mode == AccessMode.Closed) {
return 0;
} else {
return _allowedGroups[target][selector];
}
}
/**
* @dev Changes whether a group is allowed to call a function of a contract, according to the `allowed` argument.
* The caller must be the default admin.
*/
function setFunctionAllowedGroup(
address target,
bytes4[] calldata selectors,
uint8 group,
bool allowed
) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
for (uint256 i = 0; i < selectors.length; i++) {
bytes4 selector = selectors[i];
_allowedGroups[target][selector] = _withUpdatedGroup(_allowedGroups[target][selector], group, allowed);
emit GroupAllowed(target, selector, group, allowed);
}
}
/**
* @dev Returns the mode of the target contract, which may be custom (`0`), closed (`1`), or open (`2`).
*/
function getContractMode(address target) public view virtual returns (AccessMode) {
return _contractMode[target];
}
/**
* @dev Sets the target contract to be in custom restricted mode. All restricted functions in the target contract
* will follow the group-based restrictions defined by the AccessManager. The caller must be the default admin.
*/
function setContractModeCustom(address target) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
_setContractMode(target, AccessMode.Custom);
}
/**
* @dev Sets the target contract to be in "open" mode. All restricted functions in the target contract will become
* callable by anyone. The caller must be the default admin.
*/
function setContractModeOpen(address target) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
_setContractMode(target, AccessMode.Open);
}
/**
* @dev Sets the target contract to be in "closed" mode. All restricted functions in the target contract will be
* closed down and disallowed to all. The caller must be the default admin.
*/
function setContractModeClosed(address target) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
_setContractMode(target, AccessMode.Closed);
}
/**
* @dev Transfers a target contract onto a new authority. The caller must be the default admin.
*/
function transferContractAuthority(
address target,
address newAuthority
) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
AccessManaged(target).setAuthority(IAuthority(newAuthority));
}
/**
* @dev Creates a new group.
*
* Emits {GroupUpdated}.
*/
function _createGroup(uint8 group, string memory name) internal virtual {
require(!hasGroup(group), "AccessManager: existing group");
_createdGroups = _withUpdatedGroup(_createdGroups, group, true);
emit GroupUpdated(group, name);
}
/**
* @dev Augmented version of {AccessControl-_grantRole} that keeps track of user group bitmaps.
*/
function _grantRole(bytes32 role, address user) internal virtual override {
super._grantRole(role, user);
(bool isGroup, uint8 group) = _decodeGroupRole(role);
if (isGroup) {
require(hasGroup(group), "AccessManager: unknown group");
_userGroups[user] = _withUpdatedGroup(_userGroups[user], group, true);
}
}
/**
* @dev Augmented version of {AccessControl-_revokeRole} that keeps track of user group bitmaps.
*/
function _revokeRole(bytes32 role, address user) internal virtual override {
super._revokeRole(role, user);
(bool isGroup, uint8 group) = _decodeGroupRole(role);
if (isGroup) {
require(hasGroup(group), "AccessManager: unknown group");
require(group != _GROUP_PUBLIC, "AccessManager: irrevocable group");
_userGroups[user] = _withUpdatedGroup(_userGroups[user], group, false);
}
}
/**
* @dev Sets the restricted mode of a target contract.
*/
function _setContractMode(address target, AccessMode mode) internal virtual {
_contractMode[target] = mode;
emit AccessModeUpdated(target, mode);
}
/**
* @dev Returns the {AccessControl} role id that corresponds to a group.
*
* This role id starts with the ASCII characters `group:`, followed by zeroes, and ends with the single byte
* corresponding to the group number.
*/
function _encodeGroupRole(uint8 group) internal pure virtual returns (bytes32) {
return bytes32("group:") | bytes32(uint256(group));
}
/**
* @dev Decodes a role id into a group, if it is a role id of the kind returned by {_encodeGroupRole}.
*/
function _decodeGroupRole(bytes32 role) internal pure virtual returns (bool isGroup, uint8 group) {
bytes32 tagMask = ~bytes32(uint256(0xff));
bytes32 tag = role & tagMask;
isGroup = tag == bytes32("group:");
group = uint8(role[31]);
}
/**
* @dev Returns a bit mask where the only non-zero bit is the group number bit.
*/
function _groupMask(uint8 group) private pure returns (bytes32) {
return bytes32(1 << group);
}
/**
* @dev Returns the value of the group number bit in a bitmap.
*/
function _getGroup(bytes32 bitmap, uint8 group) private pure returns (bool) {
return bitmap & _groupMask(group) > 0;
}
/**
* @dev Returns a new group bitmap where a specific group was updated.
*/
function _withUpdatedGroup(bytes32 bitmap, uint8 group, bool value) private pure returns (bytes32) {
bytes32 mask = _groupMask(group);
if (value) {
return bitmap | mask;
} else {
return bitmap & ~mask;
}
}
}

@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./AccessManager.sol";
import "./AccessManaged.sol";
/**
* @dev This contract can be used to migrate existing {Ownable} or {AccessControl} contracts into an {AccessManager}
* system.
*
* Ownable contracts can have their ownership transferred to an instance of this adapter. AccessControl contracts can
* grant all roles to the adapter, while ideally revoking them from all other accounts. Subsequently, the permissions
* for those contracts can be managed centrally and with function granularity in the {AccessManager} instance the
* adapter is connected to.
*
* Permissioned interactions with thus migrated contracts must go through the adapter's {relay} function and will
* proceed if the function is allowed for the caller in the AccessManager instance.
*/
contract AccessManagerAdapter is AccessManaged {
bytes32 private constant _DEFAULT_ADMIN_ROLE = 0;
/**
* @dev Initializes an adapter connected to an AccessManager instance.
*/
constructor(AccessManager manager) AccessManaged(manager) {}
/**
* @dev Relays a function call to the target contract. The call will be relayed if the AccessManager allows the
* caller access to this function in the target contract, i.e. if the caller is in a team that is allowed for the
* function, or if the caller is the default admin for the AccessManager. The latter is meant to be used for
* ad hoc operations such as asset recovery.
*/
function relay(address target, bytes memory data) external payable {
bytes4 sig = bytes4(data);
AccessManager manager = AccessManager(address(authority()));
require(
manager.canCall(msg.sender, target, sig) || manager.hasRole(_DEFAULT_ADMIN_ROLE, msg.sender),
"AccessManagerAdapter: caller not allowed"
);
(bool ok, bytes memory result) = target.call{value: msg.value}(data);
assembly {
let result_pointer := add(32, result)
let result_size := mload(result)
switch ok
case true {
return(result_pointer, result_size)
}
default {
revert(result_pointer, result_size)
}
}
}
}

@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Standard interface for permissioning originally defined in Dappsys.
*/
interface IAuthority {
/**
* @dev Returns true if the caller can invoke on a target the function identified by a function selector.
*/
function canCall(address caller, address target, bytes4 selector) external view returns (bool allowed);
}

@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "../access/manager/IAuthority.sol";
import "../access/manager/AccessManaged.sol";
contract SimpleAuthority is IAuthority {
address _allowedCaller;
address _allowedTarget;
bytes4 _allowedSelector;
function setAllowed(address allowedCaller, address allowedTarget, bytes4 allowedSelector) public {
_allowedCaller = allowedCaller;
_allowedTarget = allowedTarget;
_allowedSelector = allowedSelector;
}
function canCall(address caller, address target, bytes4 selector) external view override returns (bool) {
return caller == _allowedCaller && target == _allowedTarget && selector == _allowedSelector;
}
}
abstract contract AccessManagedMock is AccessManaged {
event RestrictedRan();
function restrictedFunction() external restricted {
emit RestrictedRan();
}
function otherRestrictedFunction() external restricted {
emit RestrictedRan();
}
}

@ -3,7 +3,7 @@
// - COVERAGE: enable coverage report
// - ENABLE_GAS_REPORT: enable gas report
// - COMPILE_MODE: production modes enables optimizations (default: development)
// - COMPILE_VERSION: compiler version (default: 0.8.9)
// - COMPILE_VERSION: compiler version
// - COINMARKETCAP: coinmarkercat api key for USD value in gas report
const fs = require('fs');

@ -0,0 +1,55 @@
const {
expectEvent,
expectRevert,
constants: { ZERO_ADDRESS },
} = require('@openzeppelin/test-helpers');
const AccessManaged = artifacts.require('$AccessManagedMock');
const SimpleAuthority = artifacts.require('SimpleAuthority');
contract('AccessManaged', function (accounts) {
const [authority, other, user] = accounts;
it('construction', async function () {
const managed = await AccessManaged.new(authority);
expectEvent.inConstruction(managed, 'AuthorityUpdated', {
oldAuthority: ZERO_ADDRESS,
newAuthority: authority,
});
expect(await managed.authority()).to.equal(authority);
});
describe('setAuthority', function () {
it(`current authority can change managed's authority`, async function () {
const managed = await AccessManaged.new(authority);
const set = await managed.setAuthority(other, { from: authority });
expectEvent(set, 'AuthorityUpdated', {
sender: authority,
newAuthority: other,
});
expect(await managed.authority()).to.equal(other);
});
it(`other account cannot change managed's authority`, async function () {
const managed = await AccessManaged.new(authority);
await expectRevert(managed.setAuthority(other, { from: other }), 'AccessManaged: not current authority');
});
});
describe('restricted', function () {
const selector = web3.eth.abi.encodeFunctionSignature('restrictedFunction()');
it('allows if authority returns true', async function () {
const authority = await SimpleAuthority.new();
const managed = await AccessManaged.new(authority.address);
await authority.setAllowed(user, managed.address, selector);
const restricted = await managed.restrictedFunction({ from: user });
expectEvent(restricted, 'RestrictedRan');
});
it('reverts if authority returns false', async function () {
const authority = await SimpleAuthority.new();
const managed = await AccessManaged.new(authority.address);
await expectRevert(managed.restrictedFunction({ from: user }), 'AccessManaged: authority rejected');
});
});
});

@ -0,0 +1,506 @@
const {
expectEvent,
expectRevert,
time: { duration },
} = require('@openzeppelin/test-helpers');
const { AccessMode } = require('../../helpers/enums');
const AccessManager = artifacts.require('AccessManager');
const AccessManagerAdapter = artifacts.require('AccessManagerAdapter');
const AccessManaged = artifacts.require('$AccessManagedMock');
const Ownable = artifacts.require('$Ownable');
const AccessControl = artifacts.require('$AccessControl');
const groupUtils = {
mask: group => 1n << BigInt(group),
decodeBitmap: hexBitmap => {
const m = BigInt(hexBitmap);
const allGroups = new Array(256).fill().map((_, i) => i.toString());
return allGroups.filter(i => (m & groupUtils.mask(i)) !== 0n);
},
role: group => web3.utils.asciiToHex('group:').padEnd(64, '0') + group.toString(16).padStart(2, '0'),
};
const PUBLIC_GROUP = '255';
contract('AccessManager', function (accounts) {
const [admin, nonAdmin, user1, user2, otherAuthority] = accounts;
beforeEach('deploy', async function () {
this.delay = duration.days(1);
this.manager = await AccessManager.new(this.delay, admin);
});
it('configures default admin rules', async function () {
expect(await this.manager.defaultAdmin()).to.equal(admin);
expect(await this.manager.defaultAdminDelay()).to.be.bignumber.equal(this.delay);
});
describe('groups', function () {
const group = '0';
const name = 'dao';
const otherGroup = '1';
const otherName = 'council';
describe('public group', function () {
it('is created automatically', async function () {
await expectEvent.inConstruction(this.manager, 'GroupUpdated', {
group: PUBLIC_GROUP,
name: 'public',
});
});
it('includes all users automatically', async function () {
const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1));
expect(groups).to.include(PUBLIC_GROUP);
});
});
describe('creating', function () {
it('admin can create groups', async function () {
const created = await this.manager.createGroup(group, name, { from: admin });
expectEvent(created, 'GroupUpdated', { group, name });
expect(await this.manager.hasGroup(group)).to.equal(true);
expect(await this.manager.hasGroup(otherGroup)).to.equal(false);
});
it('non-admin cannot create groups', async function () {
await expectRevert(this.manager.createGroup(group, name, { from: nonAdmin }), 'missing role');
});
it('cannot recreate a group', async function () {
await this.manager.createGroup(group, name, { from: admin });
await expectRevert(this.manager.createGroup(group, name, { from: admin }), 'AccessManager: existing group');
});
});
describe('updating', function () {
beforeEach('create group', async function () {
await this.manager.createGroup(group, name, { from: admin });
});
it('admin can update group', async function () {
const updated = await this.manager.updateGroupName(group, otherName, { from: admin });
expectEvent(updated, 'GroupUpdated', { group, name: otherName });
});
it('non-admin cannot update group', async function () {
await expectRevert(this.manager.updateGroupName(group, name, { from: nonAdmin }), 'missing role');
});
it('cannot update built in group', async function () {
await expectRevert(
this.manager.updateGroupName(PUBLIC_GROUP, name, { from: admin }),
'AccessManager: built-in group',
);
});
it('cannot update nonexistent group', async function () {
await expectRevert(
this.manager.updateGroupName(otherGroup, name, { from: admin }),
'AccessManager: unknown group',
);
});
});
describe('granting', function () {
beforeEach('create group', async function () {
await this.manager.createGroup(group, name, { from: admin });
});
it('admin can grant group', async function () {
const granted = await this.manager.grantGroup(group, user1, { from: admin });
expectEvent(granted, 'RoleGranted', { account: user1, role: groupUtils.role(group) });
const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1));
expect(groups).to.include(group);
});
it('non-admin cannot grant group', async function () {
await expectRevert(this.manager.grantGroup(group, user1, { from: nonAdmin }), 'missing role');
});
it('cannot grant nonexistent group', async function () {
await expectRevert(this.manager.grantGroup(otherGroup, user1, { from: admin }), 'AccessManager: unknown group');
});
});
describe('revoking & renouncing', function () {
beforeEach('create and grant group', async function () {
await this.manager.createGroup(group, name, { from: admin });
await this.manager.grantGroup(group, user1, { from: admin });
});
it('admin can revoke group', async function () {
await this.manager.revokeGroup(group, user1, { from: admin });
const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1));
expect(groups).to.not.include(group);
});
it('non-admin cannot revoke group', async function () {
await expectRevert(this.manager.revokeGroup(group, user1, { from: nonAdmin }), 'missing role');
});
it('user can renounce group', async function () {
await this.manager.renounceGroup(group, user1, { from: user1 });
const groups = groupUtils.decodeBitmap(await this.manager.getUserGroups(user1));
expect(groups).to.not.include(group);
});
it(`user cannot renounce other user's groups`, async function () {
await expectRevert(
this.manager.renounceGroup(group, user1, { from: user2 }),
'can only renounce roles for self',
);
await expectRevert(
this.manager.renounceGroup(group, user2, { from: user1 }),
'can only renounce roles for self',
);
});
it('cannot revoke public group', async function () {
await expectRevert(
this.manager.revokeGroup(PUBLIC_GROUP, user1, { from: admin }),
'AccessManager: irrevocable group',
);
});
it('cannot revoke nonexistent group', async function () {
await expectRevert(
this.manager.revokeGroup(otherGroup, user1, { from: admin }),
'AccessManager: unknown group',
);
await expectRevert(
this.manager.renounceGroup(otherGroup, user1, { from: user1 }),
'AccessManager: unknown group',
);
});
});
describe('querying', function () {
it('returns expected groups', async function () {
const getGroups = () => this.manager.getUserGroups(user1);
// only public group initially
expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000000000');
await this.manager.createGroup('0', '0', { from: admin });
await this.manager.grantGroup('0', user1, { from: admin });
expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000000001');
await this.manager.createGroup('1', '1', { from: admin });
await this.manager.grantGroup('1', user1, { from: admin });
expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000000003');
await this.manager.createGroup('16', '16', { from: admin });
await this.manager.grantGroup('16', user1, { from: admin });
expect(await getGroups()).to.equal('0x8000000000000000000000000000000000000000000000000000000000010003');
});
});
});
describe('allowing', function () {
const group = '1';
const groupMember = user1;
const selector = web3.eth.abi.encodeFunctionSignature('restrictedFunction()');
const otherSelector = web3.eth.abi.encodeFunctionSignature('otherRestrictedFunction()');
beforeEach('deploying managed contract', async function () {
await this.manager.createGroup(group, '', { from: admin });
await this.manager.grantGroup(group, groupMember, { from: admin });
this.managed = await AccessManaged.new(this.manager.address);
});
it('non-admin cannot change allowed groups', async function () {
await expectRevert(
this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, true, { from: nonAdmin }),
'missing role',
);
});
it('single selector', async function () {
const receipt = await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, true, {
from: admin,
});
expectEvent(receipt, 'GroupAllowed', {
target: this.managed.address,
selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4
group,
allowed: true,
});
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]);
const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector);
expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([]);
const restricted = await this.managed.restrictedFunction({ from: groupMember });
expectEvent(restricted, 'RestrictedRan');
await expectRevert(
this.managed.otherRestrictedFunction({ from: groupMember }),
'AccessManaged: authority rejected',
);
});
it('multiple selectors', async function () {
const receipt = await this.manager.setFunctionAllowedGroup(
this.managed.address,
[selector, otherSelector],
group,
true,
{ from: admin },
);
expectEvent(receipt, 'GroupAllowed', {
target: this.managed.address,
selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4
group,
allowed: true,
});
expectEvent(receipt, 'GroupAllowed', {
target: this.managed.address,
selector: otherSelector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4
group,
allowed: true,
});
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]);
const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector);
expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([group]);
const restricted = await this.managed.restrictedFunction({ from: groupMember });
expectEvent(restricted, 'RestrictedRan');
await this.managed.otherRestrictedFunction({ from: groupMember });
expectEvent(restricted, 'RestrictedRan');
});
it('works on open target', async function () {
await this.manager.setContractModeOpen(this.managed.address, { from: admin });
await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin });
});
it('works on closed target', async function () {
await this.manager.setContractModeClosed(this.managed.address, { from: admin });
await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin });
});
});
describe('disallowing', function () {
const group = '1';
const groupMember = user1;
const selector = web3.eth.abi.encodeFunctionSignature('restrictedFunction()');
const otherSelector = web3.eth.abi.encodeFunctionSignature('otherRestrictedFunction()');
beforeEach('deploying managed contract', async function () {
await this.manager.createGroup(group, '', { from: admin });
await this.manager.grantGroup(group, groupMember, { from: admin });
this.managed = await AccessManaged.new(this.manager.address);
await this.manager.setFunctionAllowedGroup(this.managed.address, [selector, otherSelector], group, true, {
from: admin,
});
});
it('non-admin cannot change disallowed groups', async function () {
await expectRevert(
this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: nonAdmin }),
'missing role',
);
});
it('single selector', async function () {
const receipt = await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, {
from: admin,
});
expectEvent(receipt, 'GroupAllowed', {
target: this.managed.address,
selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4,
group,
allowed: false,
});
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([]);
const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector);
expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([group]);
await expectRevert(this.managed.restrictedFunction({ from: groupMember }), 'AccessManaged: authority rejected');
const otherRestricted = await this.managed.otherRestrictedFunction({ from: groupMember });
expectEvent(otherRestricted, 'RestrictedRan');
});
it('multiple selectors', async function () {
const receipt = await this.manager.setFunctionAllowedGroup(
this.managed.address,
[selector, otherSelector],
group,
false,
{ from: admin },
);
expectEvent(receipt, 'GroupAllowed', {
target: this.managed.address,
selector: selector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4
group,
allowed: false,
});
expectEvent(receipt, 'GroupAllowed', {
target: this.managed.address,
selector: otherSelector.padEnd(66, '0'), // there seems to be a bug in decoding the indexed bytes4
group,
allowed: false,
});
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([]);
const otherAllowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, otherSelector);
expect(groupUtils.decodeBitmap(otherAllowedGroups)).to.deep.equal([]);
await expectRevert(this.managed.restrictedFunction({ from: groupMember }), 'AccessManaged: authority rejected');
await expectRevert(
this.managed.otherRestrictedFunction({ from: groupMember }),
'AccessManaged: authority rejected',
);
});
it('works on open target', async function () {
await this.manager.setContractModeOpen(this.managed.address, { from: admin });
await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin });
});
it('works on closed target', async function () {
await this.manager.setContractModeClosed(this.managed.address, { from: admin });
await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, false, { from: admin });
});
});
describe('modes', function () {
const group = '1';
const selector = web3.eth.abi.encodeFunctionSignature('restrictedFunction()');
beforeEach('deploying managed contract', async function () {
this.managed = await AccessManaged.new(this.manager.address);
await this.manager.createGroup('1', 'a group', { from: admin });
await this.manager.setFunctionAllowedGroup(this.managed.address, [selector], group, true, { from: admin });
});
it('custom mode is default', async function () {
expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Custom);
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]);
});
it('open mode', async function () {
const receipt = await this.manager.setContractModeOpen(this.managed.address, { from: admin });
expectEvent(receipt, 'AccessModeUpdated', {
target: this.managed.address,
mode: AccessMode.Open,
});
expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Open);
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([PUBLIC_GROUP]);
});
it('closed mode', async function () {
const receipt = await this.manager.setContractModeClosed(this.managed.address, { from: admin });
expectEvent(receipt, 'AccessModeUpdated', {
target: this.managed.address,
mode: AccessMode.Closed,
});
expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Closed);
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([]);
});
it('mode cycle', async function () {
await this.manager.setContractModeOpen(this.managed.address, { from: admin });
await this.manager.setContractModeClosed(this.managed.address, { from: admin });
await this.manager.setContractModeCustom(this.managed.address, { from: admin });
expect(await this.manager.getContractMode(this.managed.address)).to.bignumber.equal(AccessMode.Custom);
const allowedGroups = await this.manager.getFunctionAllowedGroups(this.managed.address, selector);
expect(groupUtils.decodeBitmap(allowedGroups)).to.deep.equal([group]);
});
it('non-admin cannot change mode', async function () {
await expectRevert(this.manager.setContractModeCustom(this.managed.address), 'missing role');
await expectRevert(this.manager.setContractModeOpen(this.managed.address), 'missing role');
await expectRevert(this.manager.setContractModeClosed(this.managed.address), 'missing role');
});
});
describe('transfering authority', function () {
beforeEach('deploying managed contract', async function () {
this.managed = await AccessManaged.new(this.manager.address);
});
it('admin can transfer authority', async function () {
await this.manager.transferContractAuthority(this.managed.address, otherAuthority, { from: admin });
expect(await this.managed.authority()).to.equal(otherAuthority);
});
it('non-admin cannot transfer authority', async function () {
await expectRevert(
this.manager.transferContractAuthority(this.managed.address, otherAuthority, { from: nonAdmin }),
'missing role',
);
});
});
describe('adapter', function () {
const group = '0';
beforeEach('deploying adapter', async function () {
await this.manager.createGroup(group, 'a group', { from: admin });
await this.manager.grantGroup(group, user1, { from: admin });
this.adapter = await AccessManagerAdapter.new(this.manager.address);
});
it('with ownable', async function () {
const target = await Ownable.new();
await target.transferOwnership(this.adapter.address);
const { data } = await target.$_checkOwner.request();
const selector = data.slice(0, 10);
await expectRevert(
this.adapter.relay(target.address, data, { from: user1 }),
'AccessManagerAdapter: caller not allowed',
);
await this.manager.setFunctionAllowedGroup(target.address, [selector], group, true, { from: admin });
await this.adapter.relay(target.address, data, { from: user1 });
});
it('with access control', async function () {
const ROLE = web3.utils.soliditySha3('ROLE');
const target = await AccessControl.new();
await target.$_grantRole(ROLE, this.adapter.address);
const { data } = await target.$_checkRole.request(ROLE);
const selector = data.slice(0, 10);
await expectRevert(
this.adapter.relay(target.address, data, { from: user1 }),
'AccessManagerAdapter: caller not allowed',
);
await this.manager.setFunctionAllowedGroup(target.address, [selector], group, true, { from: admin });
await this.adapter.relay(target.address, data, { from: user1 });
});
it('transfer authority', async function () {
await this.manager.transferContractAuthority(this.adapter.address, otherAuthority, { from: admin });
expect(await this.adapter.authority()).to.equal(otherAuthority);
});
});
});

@ -9,4 +9,5 @@ module.exports = {
ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'),
VoteType: Enum('Against', 'For', 'Abstain'),
Rounding: Enum('Down', 'Up', 'Zero'),
AccessMode: Enum('Custom', 'Closed', 'Open'),
};

Loading…
Cancel
Save