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
parent
3f610ebc25
commit
fa112be682
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`AccessManager`: Added a new contract for managing access control of complex systems in a consolidated location. |
@ -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(); |
||||
} |
||||
} |
@ -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); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue