Access Manager (#4416)

Co-authored-by: Ernesto García <ernestognw@gmail.com>
Co-authored-by: Francisco Giordano <fg@frang.io>
This commit is contained in:
Hadrien Croubois
2023-08-07 06:57:10 +02:00
committed by GitHub
parent 1169bb1e51
commit 9bb8008c23
15 changed files with 2184 additions and 878 deletions

View File

@ -1,9 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma solidity ^0.8.20;
import "../../utils/Context.sol";
import "./IAuthority.sol";
import {IAuthority} from "./IAuthority.sol";
import {AuthorityUtils} from "./AuthorityUtils.sol";
import {IAccessManager} from "./IAccessManager.sol";
import {IAccessManaged} from "./IAccessManaged.sol";
import {Context} from "../../utils/Context.sol";
/**
* @dev This contract module makes available a {restricted} modifier. Functions decorated with this modifier will be
@ -13,10 +16,17 @@ import "./IAuthority.sol";
* 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);
abstract contract AccessManaged is Context, IAccessManaged {
address private _authority;
IAuthority private _authority;
bool private _consumingSchedule;
/**
* @dev Initializes the contract connected to an initial authority.
*/
constructor(address initialAuthority) {
_setAuthority(initialAuthority);
}
/**
* @dev Restricts access to a function as defined by the connected Authority for this contract and the
@ -24,9 +34,9 @@ contract AccessManaged is Context {
*
* [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
* 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.
* ====
@ -35,53 +45,76 @@ contract AccessManaged is Context {
* ====
* Selector collisions are mitigated by scoping permissions per contract, but some edge cases must be considered:
*
* * If the https://docs.soliditylang.org/en/latest/contracts.html#receive-ether-function[`receive()`] function is restricted,
* any other function with a `0x00000000` selector will share permissions with `receive()`.
* * Similarly, if there's no `receive()` function but a `fallback()` instead, the fallback might be called with empty `calldata`,
* sharing the `0x00000000` selector permissions as well.
* * For any other selector, if the restricted function is set on an upgradeable contract, an upgrade may remove the restricted
* function and replace it with a new method whose selector replaces the last one, keeping the previous permissions.
* * If the https://docs.soliditylang.org/en/v0.8.20/contracts.html#receive-ether-function[`receive()`] function
* is restricted, any other function with a `0x00000000` selector will share permissions with `receive()`.
* * Similarly, if there's no `receive()` function but a `fallback()` instead, the fallback might be called with
* empty `calldata`, sharing the `0x00000000` selector permissions as well.
* * For any other selector, if the restricted function is set on an upgradeable contract, an upgrade may remove
* the restricted function and replace it with a new method whose selector replaces the last one, keeping the
* previous permissions.
* ====
*/
modifier restricted() {
_checkCanCall(_msgSender(), msg.sig);
_checkCanCall(_msgSender(), _msgData());
_;
}
/**
* @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) {
function authority() public view virtual returns (address) {
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");
function setAuthority(address newAuthority) public virtual {
address caller = _msgSender();
if (caller != authority()) {
revert AccessManagedUnauthorized(caller);
}
if (newAuthority.code.length == 0) {
revert AccessManagedInvalidAuthority(newAuthority);
}
_setAuthority(newAuthority);
}
/**
* @dev Returns true only in the context of a delayed restricted call, at the moment that the scheduled operation is
* being consumed. Prevents denial of service for delayed restricted calls in the case that the contract performs
* attacker controlled calls.
*/
function isConsumingScheduledOp() public view returns (bool) {
return _consumingSchedule;
}
/**
* @dev Transfers control to a new authority. Internal function with no access restriction.
*/
function _setAuthority(IAuthority newAuthority) internal virtual {
function _setAuthority(address newAuthority) internal virtual {
_authority = newAuthority;
emit AuthorityUpdated(_msgSender(), newAuthority);
emit AuthorityUpdated(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");
function _checkCanCall(address caller, bytes calldata data) internal virtual {
(bool allowed, uint32 delay) = AuthorityUtils.canCallWithDelay(
authority(),
caller,
address(this),
bytes4(data)
);
if (!allowed) {
if (delay > 0) {
_consumingSchedule = true;
IAccessManager(authority()).consumeScheduledOp(caller, data);
_consumingSchedule = false;
} else {
revert AccessManagedUnauthorized(caller);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +0,0 @@
// 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)
}
}
}
}

View File

@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IAuthority} from "./IAuthority.sol";
library AuthorityUtils {
/**
* @dev Since `AccessManager` implements an extended IAuthority interface, invoking `canCall` with backwards compatibility
* for the preexisting `IAuthority` interface requires special care to avoid reverting on insufficient return data.
* This helper function takes care of invoking `canCall` in a backwards compatible way without reverting.
*/
function canCallWithDelay(
address authority,
address caller,
address target,
bytes4 selector
) internal view returns (bool allowed, uint32 delay) {
(bool success, bytes memory data) = authority.staticcall(
abi.encodeCall(IAuthority.canCall, (caller, target, selector))
);
if (success) {
if (data.length >= 0x40) {
(allowed, delay) = abi.decode(data, (bool, uint32));
} else if (data.length >= 0x20) {
allowed = abi.decode(data, (bool));
}
}
return (allowed, delay);
}
}

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IAccessManaged {
event AuthorityUpdated(address authority);
error AccessManagedUnauthorized(address caller);
error AccessManagedRequiredDelay(address caller, uint32 delay);
error AccessManagedInvalidAuthority(address authority);
function authority() external view returns (address);
function setAuthority(address) external;
function isConsumingScheduledOp() external view returns (bool);
}

View File

@ -0,0 +1,110 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IAccessManaged} from "./IAccessManaged.sol";
import {Time} from "../../utils/types/Time.sol";
interface IAccessManager {
/**
* @dev A delayed operation was scheduled.
*/
event OperationScheduled(bytes32 indexed operationId, uint48 schedule, address caller, address target, bytes data);
/**
* @dev A scheduled operation was executed.
*/
event OperationExecuted(bytes32 indexed operationId, uint48 schedule);
/**
* @dev A scheduled operation was canceled.
*/
event OperationCanceled(bytes32 indexed operationId, uint48 schedule);
event GroupLabel(uint64 indexed groupId, string label);
event GroupGranted(uint64 indexed groupId, address indexed account, uint48 since, uint32 delay);
event GroupRevoked(uint64 indexed groupId, address indexed account);
event GroupExecutionDelayUpdated(uint64 indexed groupId, address indexed account, uint32 delay, uint48 from);
event GroupAdminChanged(uint64 indexed groupId, uint64 indexed admin);
event GroupGuardianChanged(uint64 indexed groupId, uint64 indexed guardian);
event GroupGrantDelayChanged(uint64 indexed groupId, uint32 delay, uint48 from);
event ContractFamilyUpdated(address indexed target, uint64 indexed familyId);
event ContractClosed(address indexed target, bool closed);
event FamilyFunctionGroupUpdated(uint64 indexed familyId, bytes4 selector, uint64 indexed groupId);
event FamilyAdminDelayUpdated(uint64 indexed familyId, uint32 delay, uint48 from);
error AccessManagerAlreadyScheduled(bytes32 operationId);
error AccessManagerNotScheduled(bytes32 operationId);
error AccessManagerNotReady(bytes32 operationId);
error AccessManagerExpired(bytes32 operationId);
error AccessManagerLockedGroup(uint64 groupId);
error AccessManagerInvalidFamily(uint64 familyId);
error AccessManagerAccountAlreadyInGroup(uint64 groupId, address account);
error AccessManagerAccountNotInGroup(uint64 groupId, address account);
error AccessManagerBadConfirmation();
error AccessManagerUnauthorizedAccount(address msgsender, uint64 groupId);
error AccessManagerUnauthorizedCall(address caller, address target, bytes4 selector);
error AccessManagerCannotCancel(address msgsender, address caller, address target, bytes4 selector);
function canCall(
address caller,
address target,
bytes4 selector
) external view returns (bool allowed, uint32 delay);
function expiration() external returns (uint32);
function getContractFamily(address target) external view returns (uint64 familyId, bool closed);
function getFamilyFunctionGroup(uint64 familyId, bytes4 selector) external view returns (uint64);
function getFamilyAdminDelay(uint64 familyId) external view returns (uint32);
function getGroupAdmin(uint64 groupId) external view returns (uint64);
function getGroupGuardian(uint64 groupId) external view returns (uint64);
function getGroupGrantDelay(uint64 groupId) external view returns (uint32);
function getAccess(uint64 groupId, address account) external view returns (uint48, uint32, uint32, uint48);
function hasGroup(uint64 groupId, address account) external view returns (bool, uint32);
function labelGroup(uint64 groupId, string calldata label) external;
function grantGroup(uint64 groupId, address account, uint32 executionDelay) external;
function revokeGroup(uint64 groupId, address account) external;
function renounceGroup(uint64 groupId, address callerConfirmation) external;
function setExecuteDelay(uint64 groupId, address account, uint32 newDelay) external;
function setGroupAdmin(uint64 groupId, uint64 admin) external;
function setGroupGuardian(uint64 groupId, uint64 guardian) external;
function setGrantDelay(uint64 groupId, uint32 newDelay) external;
function setFamilyFunctionGroup(uint64 familyId, bytes4[] calldata selectors, uint64 groupId) external;
function setFamilyAdminDelay(uint64 familyId, uint32 newDelay) external;
function setContractFamily(address target, uint64 familyId) external;
function setContractClosed(address target, bool closed) external;
function getSchedule(bytes32 id) external returns (uint48);
function schedule(address target, bytes calldata data, uint48 when) external returns (bytes32);
function relay(address target, bytes calldata data) external payable;
function cancel(address caller, address target, bytes calldata data) external;
function consumeScheduledOp(address caller, bytes calldata data) external;
function updateAuthority(address target, address newAuthority) external;
}

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma solidity ^0.8.20;
/**
* @dev Standard interface for permissioning originally defined in Dappsys.

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {AccessManaged} from "../access/manager/AccessManaged.sol";
abstract contract AccessManagedTarget is AccessManaged {
event CalledRestricted(address caller);
event CalledUnrestricted(address caller);
function fnRestricted() public restricted {
emit CalledRestricted(msg.sender);
}
function fnUnrestricted() public {
emit CalledUnrestricted(msg.sender);
}
}

View File

@ -1,34 +0,0 @@
// 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();
}
}

View File

@ -0,0 +1,139 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Math} from "../math/Math.sol";
import {SafeCast} from "../math/SafeCast.sol";
/**
* @dev This library provides helpers for manipulating time-related objects.
*
* It uses the following types:
* - `uint48` for timepoints
* - `uint32` for durations
*
* While the library doesn't provide specific types for timepoints and duration, it does provide:
* - a `Delay` type to represent duration that can be programmed to change value automatically at a given point
* - additional helper functions
*/
library Time {
using Time for *;
/**
* @dev Get the block timestamp as a Timepoint.
*/
function timestamp() internal view returns (uint48) {
return SafeCast.toUint48(block.timestamp);
}
/**
* @dev Get the block number as a Timepoint.
*/
function blockNumber() internal view returns (uint48) {
return SafeCast.toUint48(block.number);
}
/**
* @dev Check if a timepoint is set, and in the past.
*/
function isSetAndPast(uint48 timepoint, uint48 ref) internal pure returns (bool) {
return timepoint != 0 && timepoint <= ref;
}
// ==================================================== Delay =====================================================
/**
* @dev A `Delay` is a uint32 duration that can be programmed to change value automatically at a given point in the
* future. The "effect" timepoint describes when the transitions happens from the "old" value to the "new" value.
* This allows updating the delay applied to some operation while keeping so guarantees.
*
* In particular, the {update} function guarantees that is the delay is reduced, the old delay still applies for
* some time. For example if the delay is currently 7 days to do an upgrade, the admin should not be able to set
* the delay to 0 and upgrade immediately. If the admin wants to reduce the delay, the old delay (7 days) should
* still apply for some time.
*
*
* The `Delay` type is 128 bits long, and packs the following:
* [000:031] uint32 for the current value (duration)
* [032:063] uint32 for the pending value (duration)
* [064:111] uint48 for the effect date (timepoint)
*
* NOTE: The {get} and {update} function operate using timestamps. Block number based delays should use the
* {getAt} and {withUpdateAt} variants of these functions.
*/
type Delay is uint112;
/**
* @dev Wrap a duration into a Delay to add the one-step "update in the future" feature
*/
function toDelay(uint32 duration) internal pure returns (Delay) {
return Delay.wrap(duration);
}
/**
* @dev Get the value at a given timepoint plus the pending value and effect timepoint if there is a scheduled
* change after this timepoint. If the effect timepoint is 0, then the pending value should not be considered.
*/
function getFullAt(Delay self, uint48 timepoint) internal pure returns (uint32, uint32, uint48) {
(uint32 oldValue, uint32 newValue, uint48 effect) = self.unpack();
return effect.isSetAndPast(timepoint) ? (newValue, 0, 0) : (oldValue, newValue, effect);
}
/**
* @dev Get the current value plus the pending value and effect timepoint if there is a scheduled change. If the
* effect timepoint is 0, then the pending value should not be considered.
*/
function getFull(Delay self) internal view returns (uint32, uint32, uint48) {
return self.getFullAt(timestamp());
}
/**
* @dev Get the value the Delay will be at a given timepoint.
*/
function getAt(Delay self, uint48 timepoint) internal pure returns (uint32) {
(uint32 delay, , ) = getFullAt(self, timepoint);
return delay;
}
/**
* @dev Get the current value.
*/
function get(Delay self) internal view returns (uint32) {
return self.getAt(timestamp());
}
/**
* @dev Update a Delay object so that a new duration takes effect at a given timepoint.
*/
function withUpdateAt(Delay self, uint32 newValue, uint48 effect) internal view returns (Delay) {
return pack(self.get(), newValue, effect);
}
/**
* @dev Update a Delay object so that it takes a new duration after at a timepoint that is automatically computed
* to enforce the old delay at the moment of the update.
*/
function withUpdate(Delay self, uint32 newValue, uint32 minSetback) internal view returns (Delay) {
uint32 value = self.get();
uint32 setback = uint32(Math.max(minSetback, value > newValue ? value - newValue : 0));
return self.withUpdateAt(newValue, timestamp() + setback);
}
/**
* @dev Split a delay into its components: oldValue, newValue and effect (transition timepoint).
*/
function unpack(Delay self) internal pure returns (uint32, uint32, uint48) {
uint112 raw = Delay.unwrap(self);
return (
uint32(raw), // oldValue
uint32(raw >> 32), // newValue
uint48(raw >> 64) // effect
);
}
/**
* @dev pack the components into a Delay object.
*/
function pack(uint32 oldValue, uint32 newValue, uint48 effect) internal pure returns (Delay) {
return Delay.wrap(uint112(oldValue) | (uint112(newValue) << 32) | (uint112(effect) << 64));
}
}

14
package-lock.json generated
View File

@ -45,7 +45,7 @@
"solhint": "^3.3.6",
"solhint-plugin-openzeppelin": "file:scripts/solhint-custom",
"solidity-ast": "^0.4.25",
"solidity-coverage": "^0.8.0",
"solidity-coverage": "^0.8.4",
"solidity-docgen": "^0.6.0-beta.29",
"undici": "^5.22.1",
"web3": "^1.3.0",
@ -12796,9 +12796,9 @@
}
},
"node_modules/solidity-coverage/node_modules/@solidity-parser/parser": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz",
"integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==",
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.1.tgz",
"integrity": "sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==",
"dev": true,
"dependencies": {
"antlr4ts": "^0.5.0-alpha.4"
@ -25272,9 +25272,9 @@
},
"dependencies": {
"@solidity-parser/parser": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz",
"integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==",
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.1.tgz",
"integrity": "sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==",
"dev": true,
"requires": {
"antlr4ts": "^0.5.0-alpha.4"

View File

@ -1,55 +0,0 @@
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');
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,5 @@ module.exports = {
ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'),
VoteType: Enum('Against', 'For', 'Abstain'),
Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'),
AccessMode: Enum('Custom', 'Closed', 'Open'),
OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'),
};

5
test/helpers/methods.js Normal file
View File

@ -0,0 +1,5 @@
const { soliditySha3 } = require('web3-utils');
module.exports = {
selector: signature => soliditySha3(signature).substring(0, 10),
};