Compare commits
2 Commits
chore/remo
...
audit/2023
| Author | SHA1 | Date | |
|---|---|---|---|
| bf5786aae0 | |||
| 8a92fb82ea |
5
.changeset/brave-lobsters-punch.md
Normal file
5
.changeset/brave-lobsters-punch.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': major
|
||||
---
|
||||
|
||||
`Governor`: Refactored internals to implement common queuing logic in the core module of the Governor. Added `queue` and `_queueOperations` functions that act at different levels. Modules that implement queuing via timelocks are expected to override `_queueOperations` to implement the timelock-specific logic. Added `_executeOperations` as the equivalent for execution.
|
||||
5
.changeset/wild-beds-visit.md
Normal file
5
.changeset/wild-beds-visit.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': major
|
||||
---
|
||||
|
||||
`GovernorStorage`: Added a new governor extension that stores the proposal details in storage, with an interface that operates on `proposalId`, as well as proposal enumerability. This replaces the old `GovernorCompatibilityBravo` module.
|
||||
99
contracts/access/manager/AccessManaged.sol
Normal file
99
contracts/access/manager/AccessManaged.sol
Normal file
@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IAuthority, safeCanCall} from "./IAuthority.sol";
|
||||
import {IManaged} from "./IManaged.sol";
|
||||
import {Context} from "../../utils/Context.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 to access 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}.
|
||||
*/
|
||||
abstract contract AccessManaged is Context, IManaged {
|
||||
address private _authority;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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.
|
||||
* ====
|
||||
*
|
||||
* [NOTE]
|
||||
* ====
|
||||
* 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.
|
||||
* ====
|
||||
*/
|
||||
modifier restricted() {
|
||||
_checkCanCall(_msgSender(), address(this), msg.sig);
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the current authority.
|
||||
*/
|
||||
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(address newAuthority) public virtual {
|
||||
address caller = _msgSender();
|
||||
if (caller != authority()) {
|
||||
revert AccessManagedUnauthorized(caller);
|
||||
}
|
||||
if (newAuthority.code.length == 0) {
|
||||
revert AccessManagedInvalidAuthority(newAuthority);
|
||||
}
|
||||
_setAuthority(newAuthority);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Transfers control to a new authority. Internal function with no access restriction.
|
||||
*/
|
||||
function _setAuthority(address newAuthority) internal virtual {
|
||||
_authority = newAuthority;
|
||||
emit AuthorityUpdated(newAuthority);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Reverts if the caller is not allowed to call the function identified by a selector.
|
||||
*/
|
||||
function _checkCanCall(address caller, address target, bytes4 selector) internal view virtual {
|
||||
(bool allowed, uint32 delay) = safeCanCall(authority(), caller, target, selector);
|
||||
if (!allowed) {
|
||||
if (delay > 0) {
|
||||
revert AccessManagedRequiredDelay(caller, delay);
|
||||
} else {
|
||||
revert AccessManagedUnauthorized(caller);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
616
contracts/access/manager/AccessManager.sol
Normal file
616
contracts/access/manager/AccessManager.sol
Normal file
@ -0,0 +1,616 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IAccessManager} from "./IAccessManager.sol";
|
||||
import {IManaged} from "./IManaged.sol";
|
||||
import {IAuthority} from "./IAuthority.sol";
|
||||
import {AccessManagedAdapter} from "./utils/AccessManagedAdapter.sol";
|
||||
import {Address} from "../../utils/Address.sol";
|
||||
import {Context} from "../../utils/Context.sol";
|
||||
import {Multicall} from "../../utils/Multicall.sol";
|
||||
import {Time} from "../../utils/types/Time.sol";
|
||||
|
||||
/**
|
||||
* @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. Accounts can be added into any number of these groups. Each of
|
||||
* them defines a 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 where functions are mapped to groups are said to be in a "custom" mode, but 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
|
||||
* they will be highly secured (e.g., a multisig or a well-configured DAO).
|
||||
*
|
||||
* NOTE: This contract implements a form of the {IAuthority} interface, but {canCall} has additional return data so it
|
||||
* doesn't inherit `IAuthority`. It is however compatible with the `IAuthority` interface since the first 32 bytes of
|
||||
* the return data are a boolean as expected by that interface.
|
||||
*/
|
||||
contract AccessManager is Context, Multicall, IAccessManager {
|
||||
using Time for *;
|
||||
|
||||
uint256 public constant ADMIN_GROUP = type(uint256).min; // 0
|
||||
uint256 public constant PUBLIC_GROUP = type(uint256).max; // 2**256-1
|
||||
|
||||
mapping(address target => AccessMode mode) private _contractMode;
|
||||
mapping(address target => mapping(bytes4 selector => uint256 groupId)) private _allowedGroups;
|
||||
mapping(uint256 groupId => Group) private _groups;
|
||||
mapping(bytes32 operationId => uint48 schedule) private _schedules;
|
||||
|
||||
// This should be transcient storage when supported by the EVM.
|
||||
bytes32 private _relayIdentifier;
|
||||
|
||||
/**
|
||||
* @dev Check that the caller has a given permission level (`groupId`). Note that this does NOT consider execution
|
||||
* delays that may be associated to that group.
|
||||
*/
|
||||
modifier onlyGroup(uint256 groupId) {
|
||||
address msgsender = _msgSender();
|
||||
if (!hasGroup(groupId, msgsender)) {
|
||||
revert AccessControlUnauthorizedAccount(msgsender, groupId);
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialAdmin) {
|
||||
// admin is active immediately and without any execution delay.
|
||||
_grantGroup(ADMIN_GROUP, initialAdmin, 0, 0);
|
||||
}
|
||||
|
||||
// =================================================== GETTERS ====================================================
|
||||
/**
|
||||
* @dev Check if an address (`caller`) is authorised to call a given function on a given contract directly (with
|
||||
* no restriction). Additionally, it returns the delay needed to perform the call indirectly through the {schedule}
|
||||
* & {relay} workflow.
|
||||
*
|
||||
* This function is usually called by the targeted contract to control immediate execution of restricted functions.
|
||||
* Therefore we only return true is the call can be performed without any delay. If the call is subject to a delay,
|
||||
* then the function should return false, and the caller should schedule the operation for future execution.
|
||||
*
|
||||
* We may be able to hash the operation, and check if the call was scheduled, but we would not be able to cleanup
|
||||
* the schedule, leaving the possibility of multiple executions. Maybe this function should not be view?
|
||||
*
|
||||
* NOTE: The IAuthority interface does not include the `uint32` delay. This is an extension of that interface that
|
||||
* is backward compatible. Some contract may thus ignore the second return argument. In that case they will fail
|
||||
* to identify the indirect workflow, and will consider call that require a delay to be forbidden.
|
||||
*/
|
||||
function canCall(address caller, address target, bytes4 selector) public view virtual returns (bool, uint32) {
|
||||
AccessMode mode = getContractMode(target);
|
||||
if (mode == AccessMode.Open) {
|
||||
return (true, 0);
|
||||
} else if (mode == AccessMode.Closed) {
|
||||
return (false, 0);
|
||||
} else if (caller == address(this)) {
|
||||
// Caller is AccessManager => call was relayed. In that case the relay already checked permissions. We
|
||||
// verify that the call "identifier", which is set during the relay call, is correct.
|
||||
return (_relayIdentifier == keccak256(abi.encodePacked(target, selector)), 0);
|
||||
} else {
|
||||
uint256 groupId = getFunctionAllowedGroup(target, selector);
|
||||
bool inGroup = hasGroup(groupId, caller);
|
||||
uint32 executeDelay = inGroup ? getAccess(groupId, caller).delay.get() : 0;
|
||||
return (inGroup && executeDelay == 0, executeDelay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the mode under which a contract is operating.
|
||||
*/
|
||||
function getContractMode(address target) public view virtual returns (AccessMode) {
|
||||
return _contractMode[target];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the permission level (group) required to call a function. This only applies for contract that are
|
||||
* operating under the `Custom` mode.
|
||||
*/
|
||||
function getFunctionAllowedGroup(address target, bytes4 selector) public view virtual returns (uint256) {
|
||||
return _allowedGroups[target][selector];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the id of the group that acts as an admin for given group.
|
||||
*
|
||||
* The admin permission is required to grant the group, revoke the group and update the execution delay to execute
|
||||
* an operation that is restricted to this group.
|
||||
*/
|
||||
function getGroupAdmin(uint256 groupId) public view virtual returns (uint256) {
|
||||
return _groups[groupId].admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the group that acts as a guardian for a given group.
|
||||
*
|
||||
* The guardian permission allows canceling operations that have been scheduled under the group.
|
||||
*/
|
||||
function getGroupGuardian(uint256 groupId) public view virtual returns (uint256) {
|
||||
return _groups[groupId].guardian;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the group current grant delay, that value may change at any point, without an event emitted, following
|
||||
* a call to {setGrantDelay}. Changes to this value, including effect timepoint are notified by the
|
||||
* {GroupGrantDelayChanged} event.
|
||||
*/
|
||||
function getGroupGrantDelay(uint256 groupId) public view virtual returns (uint32) {
|
||||
return _groups[groupId].delay.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the access details for a given account in a given group. These details include the timepoint at which
|
||||
* membership becomes active, and the delay applied to all operation by this user that require this permission
|
||||
* level.
|
||||
*/
|
||||
function getAccess(uint256 groupId, address account) public view virtual returns (Access memory) {
|
||||
return _groups[groupId].members[account];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if a given account currently had the permission level corresponding to a given group. Note that this
|
||||
* permission might be associated with a delay. {getAccess} can provide more details.
|
||||
*/
|
||||
function hasGroup(uint256 groupId, address account) public view virtual returns (bool) {
|
||||
return groupId == PUBLIC_GROUP || getAccess(groupId, account).since.isSetAndPast(Time.timestamp());
|
||||
}
|
||||
|
||||
// =============================================== GROUP MANAGEMENT ===============================================
|
||||
/**
|
||||
* @dev Give a label to a group, for improved group discoverabily by UIs.
|
||||
*
|
||||
* Emits a {GroupLabel} event.
|
||||
*/
|
||||
function labelGroup(uint256 groupId, string calldata label) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
emit GroupLabel(groupId, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Give permission to an account to execute function restricted to a group. Optionally, a delay can be
|
||||
* enforced for any function call, byt this user, that require this level of permission. This call is only
|
||||
* effective after a grant delay that is specific to the group being granted.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be in the group's admins
|
||||
*
|
||||
* Emits a {GroupGranted} event
|
||||
*/
|
||||
function grantGroup(
|
||||
uint256 groupId,
|
||||
address account,
|
||||
uint32 executionDelay
|
||||
) public virtual onlyGroup(getGroupAdmin(groupId)) {
|
||||
_grantGroup(groupId, account, getGroupGrantDelay(groupId), executionDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Remove an account for a group, with immediate effect.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be in the group's admins
|
||||
*
|
||||
* Emits a {GroupRevoked} event
|
||||
*/
|
||||
function revokeGroup(uint256 groupId, address account) public virtual onlyGroup(getGroupAdmin(groupId)) {
|
||||
_revokeGroup(groupId, account);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Renounce group permissions for the calling account, with immediate effect.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be `callerConfirmation`.
|
||||
*
|
||||
* Emits a {GroupRevoked} event
|
||||
*/
|
||||
function renounceGroup(uint256 groupId, address callerConfirmation) public virtual {
|
||||
if (callerConfirmation != _msgSender()) {
|
||||
revert AccessManagerBadConfirmation();
|
||||
}
|
||||
_revokeGroup(groupId, callerConfirmation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set the execution delay for a given account in a given group. This update is not immediate and follows the
|
||||
* delay rules. For example, If a user currently has a delay of 3 hours, and this is called to reduce that delay to
|
||||
* 1 hour, the new delay will take some time to take effect, enforcing that any operation executed in the 3 hours
|
||||
* that follows this update was indeed scheduled before this update.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be in the group's admins
|
||||
*
|
||||
* Emits a {GroupExecutionDelayUpdate} event
|
||||
*/
|
||||
function setExecuteDelay(
|
||||
uint256 groupId,
|
||||
address account,
|
||||
uint32 newDelay
|
||||
) public virtual onlyGroup(getGroupAdmin(groupId)) {
|
||||
_setExecuteDelay(groupId, account, newDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change admin group for a given group.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {GroupAdminChanged} event
|
||||
*/
|
||||
function setGroupAdmin(uint256 groupId, uint256 admin) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
_setGroupAdmin(groupId, admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change guardian group for a given group.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {GroupGuardianChanged} event
|
||||
*/
|
||||
function setGroupGuardian(uint256 groupId, uint256 guardian) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
_setGroupGuardian(groupId, guardian);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update the .
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {GroupGrantDelayChanged} event
|
||||
*/
|
||||
function setGrantDelay(uint256 groupId, uint32 newDelay) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
_setGrantDelay(groupId, newDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {grantGroup} without access control.
|
||||
*
|
||||
* Emits a {GroupGranted} event
|
||||
*/
|
||||
function _grantGroup(uint256 groupId, address account, uint32 grantDelay, uint32 executionDelay) internal virtual {
|
||||
if (groupId == PUBLIC_GROUP) {
|
||||
revert AccessManagerLockedGroup(groupId);
|
||||
} else if (_groups[groupId].members[account].since != 0) {
|
||||
revert AccessManagerAcountAlreadyInGroup(groupId, account);
|
||||
}
|
||||
|
||||
uint48 since = Time.timestamp() + grantDelay;
|
||||
_groups[groupId].members[account] = Access({since: since, delay: executionDelay.toDelay()});
|
||||
|
||||
emit GroupGranted(groupId, account, since, executionDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {revokeGroup} without access control. This logic is also used by {renounceGroup}.
|
||||
*
|
||||
* Emits a {GroupRevoked} event
|
||||
*/
|
||||
function _revokeGroup(uint256 groupId, address account) internal virtual {
|
||||
if (groupId == PUBLIC_GROUP) {
|
||||
revert AccessManagerLockedGroup(groupId);
|
||||
} else if (_groups[groupId].members[account].since == 0) {
|
||||
revert AccessManagerAcountNotInGroup(groupId, account);
|
||||
}
|
||||
|
||||
delete _groups[groupId].members[account];
|
||||
|
||||
emit GroupRevoked(groupId, account);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {setExecuteDelay} without access control.
|
||||
*
|
||||
* Emits a {GroupExecutionDelayUpdate} event
|
||||
*/
|
||||
function _setExecuteDelay(uint256 groupId, address account, uint32 newDuration) internal virtual {
|
||||
if (groupId == PUBLIC_GROUP) {
|
||||
revert AccessManagerLockedGroup(groupId);
|
||||
} else if (_groups[groupId].members[account].since == 0) {
|
||||
revert AccessManagerAcountNotInGroup(groupId, account);
|
||||
}
|
||||
|
||||
Time.Delay newDelay = _groups[groupId].members[account].delay.update(newDuration, 0); // TODO: minsetback ?
|
||||
_groups[groupId].members[account].delay = newDelay;
|
||||
|
||||
(, , uint48 effectPoint) = newDelay.split();
|
||||
emit GroupExecutionDelayUpdate(groupId, account, newDuration, effectPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {setGroupAdmin} without access control.
|
||||
*
|
||||
* Emits a {GroupAdminChanged} event
|
||||
*/
|
||||
function _setGroupAdmin(uint256 groupId, uint256 admin) internal virtual {
|
||||
if (groupId == ADMIN_GROUP || groupId == PUBLIC_GROUP) {
|
||||
revert AccessManagerLockedGroup(groupId);
|
||||
}
|
||||
|
||||
_groups[groupId].admin = admin;
|
||||
|
||||
emit GroupAdminChanged(groupId, admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {setGroupGuardian} without access control.
|
||||
*
|
||||
* Emits a {GroupGuardianChanged} event
|
||||
*/
|
||||
function _setGroupGuardian(uint256 groupId, uint256 guardian) internal virtual {
|
||||
if (groupId == ADMIN_GROUP || groupId == PUBLIC_GROUP) {
|
||||
revert AccessManagerLockedGroup(groupId);
|
||||
}
|
||||
|
||||
_groups[groupId].guardian = guardian;
|
||||
|
||||
emit GroupGuardianChanged(groupId, guardian);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {setGrantDelay} without access control.
|
||||
*
|
||||
* Emits a {GroupGrantDelayChanged} event
|
||||
*/
|
||||
function _setGrantDelay(uint256 groupId, uint32 newDelay) internal virtual {
|
||||
if (groupId == PUBLIC_GROUP) {
|
||||
revert AccessManagerLockedGroup(groupId);
|
||||
}
|
||||
|
||||
Time.Delay updated = _groups[groupId].delay.update(newDelay, 0); // TODO: minsetback ?
|
||||
_groups[groupId].delay = updated;
|
||||
|
||||
(, , uint48 effect) = updated.split();
|
||||
emit GroupGrantDelayChanged(groupId, newDelay, effect);
|
||||
}
|
||||
|
||||
// ============================================= FUNCTION MANAGEMENT ==============================================
|
||||
/**
|
||||
* @dev Set the level of permission (`group`) required to call functions identified by the `selectors` in the
|
||||
* `target` contract.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {FunctionAllowedGroupUpdated} event per selector
|
||||
*/
|
||||
function setFunctionAllowedGroup(
|
||||
address target,
|
||||
bytes4[] calldata selectors,
|
||||
uint256 groupId
|
||||
) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
// todo set delay or document risks
|
||||
for (uint256 i = 0; i < selectors.length; ++i) {
|
||||
_setFunctionAllowedGroup(target, selectors[i], groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal version of {setFunctionAllowedGroup} without access control.
|
||||
*
|
||||
* Emits a {FunctionAllowedGroupUpdated} event
|
||||
*/
|
||||
function _setFunctionAllowedGroup(address target, bytes4 selector, uint256 groupId) internal virtual {
|
||||
_allowedGroups[target][selector] = groupId;
|
||||
emit FunctionAllowedGroupUpdated(target, selector, groupId);
|
||||
}
|
||||
|
||||
// =============================================== MODE MANAGEMENT ================================================
|
||||
/**
|
||||
* @dev Set the operating mode of a contract to Custom. This enables the group mechanism for per-function access
|
||||
* restriction and delay enforcement.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {AccessModeUpdated} event.
|
||||
*/
|
||||
function setContractModeCustom(address target) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
// todo set delay or document risks
|
||||
_setContractMode(target, AccessMode.Custom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set the operating mode of a contract to Open. This allows anyone to call any `restricted()` function with
|
||||
* no delay.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {AccessModeUpdated} event.
|
||||
*/
|
||||
function setContractModeOpen(address target) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
// todo set delay or document risks
|
||||
_setContractMode(target, AccessMode.Open);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set the operating mode of a contract to Close. This prevents anyone from calling any `restricted()`
|
||||
* function.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*
|
||||
* Emits a {AccessModeUpdated} event.
|
||||
*/
|
||||
function setContractModeClosed(address target) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
// todo set delay or document risks
|
||||
_setContractMode(target, AccessMode.Closed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set the operating mode of a contract. This is an internal setter with no access restrictions.
|
||||
*
|
||||
* Emits a {AccessModeUpdated} event.
|
||||
*/
|
||||
function _setContractMode(address target, AccessMode mode) internal virtual {
|
||||
_contractMode[target] = mode;
|
||||
emit AccessModeUpdated(target, mode);
|
||||
}
|
||||
|
||||
// ============================================== DELAYED OPERATIONS ==============================================
|
||||
/**
|
||||
* @dev Return the timepoint at which a scheduled operation will be ready for execution. This returns 0 if the
|
||||
* operation is not yet scheduled, was executed or was canceled.
|
||||
*/
|
||||
function getSchedule(bytes32 id) public view virtual returns (uint48) {
|
||||
return _schedules[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Schedule a delayed operation, and return the operation identifier.
|
||||
*
|
||||
* Emits a {Scheduled} event.
|
||||
*/
|
||||
function schedule(address target, bytes calldata data) public virtual returns (bytes32) {
|
||||
address caller = _msgSender();
|
||||
bytes4 selector = bytes4(data[0:4]);
|
||||
|
||||
// Fetch restriction to that apply to the caller on the targeted function
|
||||
(bool allowed, uint32 setback) = canCall(caller, target, selector);
|
||||
|
||||
// If caller is not authorised, revert
|
||||
if (!allowed && setback == 0) {
|
||||
revert AccessManagerUnauthorizedCall(caller, target, selector);
|
||||
}
|
||||
|
||||
// If caller is authorised, schedule operation
|
||||
bytes32 operationId = _hashOperation(caller, target, data);
|
||||
if (_schedules[operationId] != 0) {
|
||||
revert AccessManagerAlreadyScheduled(operationId);
|
||||
}
|
||||
_schedules[operationId] = Time.timestamp() + setback;
|
||||
|
||||
emit Scheduled(operationId, caller, target, data);
|
||||
return operationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Execute a function that is delay restricted, provided it was properly scheduled beforehand, or the
|
||||
* execution delay is 0.
|
||||
*
|
||||
* Emits a {Executed} event if the call was scheduled. Unscheduled call (with no delay) do not emit that event.
|
||||
*/
|
||||
function relay(address target, bytes calldata data) public payable virtual {
|
||||
relayViaAdapter(target, data, address(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Execute a function that is delay restricted in the same way as {relay} but through an
|
||||
* {AccessManagedAdapter}.
|
||||
*/
|
||||
function relayViaAdapter(address target, bytes calldata data, address adapter) public payable virtual {
|
||||
address caller = _msgSender();
|
||||
bytes4 selector = bytes4(data[0:4]);
|
||||
|
||||
// Fetch restriction to that apply to the caller on the targeted function
|
||||
(bool allowed, uint32 setback) = canCall(caller, target, selector);
|
||||
|
||||
// If caller is not authorised, revert
|
||||
if (!allowed && setback == 0) {
|
||||
revert AccessManagerUnauthorizedCall(caller, target, selector);
|
||||
}
|
||||
|
||||
// If caller is authorised, check operation was scheduled early enough
|
||||
bytes32 operationId = _hashOperation(caller, target, data);
|
||||
uint48 timepoint = _schedules[operationId];
|
||||
if (setback != 0) {
|
||||
if (timepoint == 0) {
|
||||
revert AccessManagerNotScheduled(operationId);
|
||||
} else if (timepoint > Time.timestamp()) {
|
||||
revert AccessManagerNotReady(operationId);
|
||||
}
|
||||
}
|
||||
if (timepoint != 0) {
|
||||
delete _schedules[operationId];
|
||||
emit Executed(operationId);
|
||||
}
|
||||
|
||||
// Mark the target and selector as authorised
|
||||
bytes32 relayIdentifierBefore = _relayIdentifier;
|
||||
_relayIdentifier = keccak256(abi.encodePacked(target, selector));
|
||||
|
||||
if (adapter != address(0)) {
|
||||
// Perform call through adapter
|
||||
AccessManagedAdapter(adapter).relay{value: msg.value}(target, data);
|
||||
} else {
|
||||
// Perform call directly
|
||||
Address.functionCallWithValue(target, data, msg.value);
|
||||
}
|
||||
|
||||
// Reset relay identifier
|
||||
_relayIdentifier = relayIdentifierBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Cancel a scheduled (delayed) operation.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be the proposer, or a guardian of the targeted function
|
||||
*
|
||||
* Emits a {Canceled} event.
|
||||
*/
|
||||
function cancel(address caller, address target, bytes calldata data) public virtual {
|
||||
address msgsender = _msgSender();
|
||||
bytes4 selector = bytes4(data[0:4]);
|
||||
|
||||
bytes32 operationId = _hashOperation(caller, target, data);
|
||||
if (_schedules[operationId] == 0) {
|
||||
revert AccessManagerNotScheduled(operationId);
|
||||
} else if (
|
||||
caller != msgsender &&
|
||||
!hasGroup(ADMIN_GROUP, msgsender) &&
|
||||
!hasGroup(getGroupGuardian(getFunctionAllowedGroup(target, selector)), msgsender)
|
||||
) {
|
||||
// calls can only be canceled by the account that scheduled them, a global admin, or by a guardian of the required group.
|
||||
revert AccessManagerCannotCancel(msgsender, caller, target, selector);
|
||||
}
|
||||
|
||||
delete _schedules[operationId];
|
||||
emit Canceled(operationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Hashing function for delayed operations
|
||||
*/
|
||||
function _hashOperation(address caller, address target, bytes calldata data) private pure returns (bytes32) {
|
||||
return keccak256(abi.encode(caller, target, data));
|
||||
}
|
||||
|
||||
// ==================================================== OTHERS ====================================================
|
||||
/**
|
||||
* @dev Change the AccessManager instance used by a contract that correctly uses this instance.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the caller must be a global admin
|
||||
*/
|
||||
function updateAuthority(IManaged target, address newAuthority) public virtual onlyGroup(ADMIN_GROUP) {
|
||||
// todo set delay or document risks
|
||||
target.setAuthority(newAuthority);
|
||||
}
|
||||
}
|
||||
120
contracts/access/manager/IAccessManager.sol
Normal file
120
contracts/access/manager/IAccessManager.sol
Normal file
@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IManaged} from "./IManaged.sol";
|
||||
import {Time} from "../../utils/types/Time.sol";
|
||||
|
||||
interface IAccessManager {
|
||||
enum AccessMode {
|
||||
Custom,
|
||||
Closed,
|
||||
Open
|
||||
}
|
||||
|
||||
// Structure that stores the details for a group/account pair. This structures fit into a single slot.
|
||||
struct Access {
|
||||
// Timepoint at which the user gets the permission. If this is either 0, or in the future, the group permission
|
||||
// are not available. Should be checked using {Time-isSetAndPast}
|
||||
uint48 since;
|
||||
// delay for execution. Only applies to restricted() / relay() calls. This does not restrict access to
|
||||
// functions that use the `onlyGroup` modifier.
|
||||
Time.Delay delay;
|
||||
}
|
||||
|
||||
// Structure that stores the details of a group, including:
|
||||
// - the members of the group
|
||||
// - the admin group (that can grant or revoke permissions)
|
||||
// - the guardian group (that can cancel operations targeting functions that need this group
|
||||
// - the grand delay
|
||||
struct Group {
|
||||
mapping(address user => Access access) members;
|
||||
uint256 admin;
|
||||
uint256 guardian;
|
||||
Time.Delay delay; // delay for granting
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev A delay operation was schedule.
|
||||
*/
|
||||
event Scheduled(bytes32 operationId, address caller, address target, bytes data);
|
||||
|
||||
/**
|
||||
* @dev A scheduled operation was executed.
|
||||
*/
|
||||
event Executed(bytes32 operationId);
|
||||
|
||||
/**
|
||||
* @dev A scheduled operation was canceled.
|
||||
*/
|
||||
event Canceled(bytes32 operationId);
|
||||
|
||||
event GroupLabel(uint256 indexed groupId, string label);
|
||||
event GroupGranted(uint256 indexed groupId, address indexed account, uint48 since, uint32 delay);
|
||||
event GroupRevoked(uint256 indexed groupId, address indexed account);
|
||||
event GroupExecutionDelayUpdate(uint256 indexed groupId, address indexed account, uint32 delay, uint48 from);
|
||||
event GroupAdminChanged(uint256 indexed groupId, uint256 indexed admin);
|
||||
event GroupGuardianChanged(uint256 indexed groupId, uint256 indexed guardian);
|
||||
event GroupGrantDelayChanged(uint256 indexed groupId, uint32 delay, uint48 from);
|
||||
event AccessModeUpdated(address indexed target, AccessMode mode);
|
||||
event FunctionAllowedGroupUpdated(address indexed target, bytes4 selector, uint256 indexed groupId);
|
||||
|
||||
error AccessManagerAlreadyScheduled(bytes32 operationId);
|
||||
error AccessManagerNotScheduled(bytes32 operationId);
|
||||
error AccessManagerNotReady(bytes32 operationId);
|
||||
error AccessManagerLockedGroup(uint256 groupId);
|
||||
error AccessManagerAcountAlreadyInGroup(uint256 groupId, address account);
|
||||
error AccessManagerAcountNotInGroup(uint256 groupId, address account);
|
||||
error AccessManagerBadConfirmation();
|
||||
error AccessControlUnauthorizedAccount(address msgsender, uint256 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 getContractMode(address target) external view returns (AccessMode);
|
||||
|
||||
function getFunctionAllowedGroup(address target, bytes4 selector) external view returns (uint256);
|
||||
|
||||
function getGroupAdmin(uint256 group) external view returns (uint256);
|
||||
|
||||
function getGroupGuardian(uint256 group) external view returns (uint256);
|
||||
|
||||
function getGroupGrantDelay(uint256 groupId) external view returns (uint32);
|
||||
|
||||
function getAccess(uint256 group, address account) external view returns (Access memory);
|
||||
|
||||
function hasGroup(uint256 group, address account) external view returns (bool);
|
||||
|
||||
function grantGroup(uint256 group, address account, uint32 executionDelay) external;
|
||||
|
||||
function revokeGroup(uint256 group, address account) external;
|
||||
|
||||
function renounceGroup(uint256 group, address callerConfirmation) external;
|
||||
|
||||
function setExecuteDelay(uint256 group, address account, uint32 newDelay) external;
|
||||
|
||||
function setGroupAdmin(uint256 group, uint256 admin) external;
|
||||
|
||||
function setGroupGuardian(uint256 group, uint256 guardian) external;
|
||||
|
||||
function setGrantDelay(uint256 group, uint32 newDelay) external;
|
||||
|
||||
function setContractModeCustom(address target) external;
|
||||
|
||||
function setContractModeOpen(address target) external;
|
||||
|
||||
function setContractModeClosed(address target) external;
|
||||
|
||||
function schedule(address target, bytes calldata data) external returns (bytes32);
|
||||
|
||||
function cancel(address caller, address target, bytes calldata data) external;
|
||||
|
||||
function relay(address target, bytes calldata data) external payable;
|
||||
|
||||
function updateAuthority(IManaged target, address newAuthority) external;
|
||||
}
|
||||
37
contracts/access/manager/IAuthority.sol
Normal file
37
contracts/access/manager/IAuthority.sol
Normal file
@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 safeCanCall(
|
||||
address authority,
|
||||
address caller,
|
||||
address target,
|
||||
bytes4 selector
|
||||
) 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);
|
||||
}
|
||||
15
contracts/access/manager/IManaged.sol
Normal file
15
contracts/access/manager/IManaged.sol
Normal file
@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
interface IManaged {
|
||||
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;
|
||||
}
|
||||
42
contracts/access/manager/utils/AccessManagedAdapter.sol
Normal file
42
contracts/access/manager/utils/AccessManagedAdapter.sol
Normal file
@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {AccessManaged} from "../AccessManaged.sol";
|
||||
import {Address} from "../../../utils/Address.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 AccessManagedAdapter is AccessManaged {
|
||||
error AccessManagedAdapterUnauthorizedSelfRelay();
|
||||
|
||||
/**
|
||||
* @dev Initializes an adapter connected to an AccessManager instance.
|
||||
*/
|
||||
constructor(address initialAuthority) AccessManaged(initialAuthority) {}
|
||||
|
||||
/**
|
||||
* @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 a member of the group that is
|
||||
* allowed for the function.
|
||||
*/
|
||||
function relay(address target, bytes calldata data) external payable {
|
||||
if (target == address(this)) {
|
||||
revert AccessManagedAdapterUnauthorizedSelfRelay();
|
||||
}
|
||||
|
||||
_checkCanCall(_msgSender(), target, bytes4(data[0:4]));
|
||||
|
||||
Address.functionCallWithValue(target, data, msg.value);
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,6 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
"ExtendedBallot(uint256 proposalId,uint8 support,address voter,uint256 nonce,string reason,bytes params)"
|
||||
);
|
||||
|
||||
// solhint-disable var-name-mixedcase
|
||||
struct ProposalCore {
|
||||
address proposer;
|
||||
uint48 voteStart;
|
||||
@ -42,12 +41,21 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
bool executed;
|
||||
bool canceled;
|
||||
}
|
||||
// solhint-enable var-name-mixedcase
|
||||
|
||||
struct ProposalExtra {
|
||||
uint48 eta;
|
||||
}
|
||||
|
||||
// Each object in this should fit into a single slot so it can be cached efficiently
|
||||
struct ProposalFull {
|
||||
ProposalCore core;
|
||||
ProposalExtra extra;
|
||||
}
|
||||
|
||||
bytes32 private constant _ALL_PROPOSAL_STATES_BITMAP = bytes32((2 ** (uint8(type(ProposalState).max) + 1)) - 1);
|
||||
string private _name;
|
||||
|
||||
mapping(uint256 => ProposalCore) private _proposals;
|
||||
mapping(uint256 => ProposalFull) private _proposals;
|
||||
|
||||
// This queue keeps track of the governor operating on itself. Calls to functions protected by the
|
||||
// {onlyGovernance} modifier needs to be whitelisted in this queue. Whitelisting is set in {_beforeExecute},
|
||||
@ -90,27 +98,8 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
|
||||
bytes4 governorCancelId = this.cancel.selector ^ this.proposalProposer.selector;
|
||||
|
||||
bytes4 governorParamsId = this.castVoteWithReasonAndParams.selector ^
|
||||
this.castVoteWithReasonAndParamsBySig.selector ^
|
||||
this.getVotesWithParams.selector;
|
||||
|
||||
// The original interface id in v4.3.
|
||||
bytes4 governor43Id = type(IGovernor).interfaceId ^
|
||||
type(IERC6372).interfaceId ^
|
||||
governorCancelId ^
|
||||
governorParamsId;
|
||||
|
||||
// An updated interface id in v4.6, with params added.
|
||||
bytes4 governor46Id = type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ^ governorCancelId;
|
||||
|
||||
// For the updated interface id in v4.9, we use governorCancelId directly.
|
||||
|
||||
return
|
||||
interfaceId == governor43Id ||
|
||||
interfaceId == governor46Id ||
|
||||
interfaceId == governorCancelId ||
|
||||
interfaceId == type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ||
|
||||
interfaceId == type(IERC1155Receiver).interfaceId ||
|
||||
super.supportsInterface(interfaceId);
|
||||
}
|
||||
@ -157,13 +146,11 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
function state(uint256 proposalId) public view virtual override returns (ProposalState) {
|
||||
// ProposalCore is just one slot. We can load it from storage to memory with a single sload and use memory
|
||||
// object as a cache. This avoid duplicating expensive sloads.
|
||||
ProposalCore memory proposal = _proposals[proposalId];
|
||||
ProposalCore memory core = _proposals[proposalId].core;
|
||||
|
||||
if (proposal.executed) {
|
||||
if (core.executed) {
|
||||
return ProposalState.Executed;
|
||||
}
|
||||
|
||||
if (proposal.canceled) {
|
||||
} else if (core.canceled) {
|
||||
return ProposalState.Canceled;
|
||||
}
|
||||
|
||||
@ -183,19 +170,19 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
|
||||
if (deadline >= currentTimepoint) {
|
||||
return ProposalState.Active;
|
||||
}
|
||||
|
||||
if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) {
|
||||
} else if (!_quorumReached(proposalId) || !_voteSucceeded(proposalId)) {
|
||||
return ProposalState.Defeated;
|
||||
} else if (proposalEta(proposalId) == 0) {
|
||||
return ProposalState.Succeeded;
|
||||
} else {
|
||||
return ProposalState.Defeated;
|
||||
return ProposalState.Queued;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
|
||||
* @dev See {IGovernor-proposalThreshold}.
|
||||
*/
|
||||
function proposalThreshold() public view virtual returns (uint256) {
|
||||
function proposalThreshold() public view virtual override returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -203,21 +190,28 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
* @dev See {IGovernor-proposalSnapshot}.
|
||||
*/
|
||||
function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) {
|
||||
return _proposals[proposalId].voteStart;
|
||||
return _proposals[proposalId].core.voteStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-proposalDeadline}.
|
||||
*/
|
||||
function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) {
|
||||
return _proposals[proposalId].voteStart + _proposals[proposalId].voteDuration;
|
||||
return _proposals[proposalId].core.voteStart + _proposals[proposalId].core.voteDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the account that created a given proposal.
|
||||
* @dev See {IGovernor-proposalProposer}.
|
||||
*/
|
||||
function proposalProposer(uint256 proposalId) public view virtual override returns (address) {
|
||||
return _proposals[proposalId].proposer;
|
||||
return _proposals[proposalId].core.proposer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-proposalEta}.
|
||||
*/
|
||||
function proposalEta(uint256 proposalId) public view virtual override returns (uint256) {
|
||||
return _proposals[proposalId].extra.eta;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,32 +278,47 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
string memory description
|
||||
) public virtual override returns (uint256) {
|
||||
address proposer = _msgSender();
|
||||
require(_isValidDescriptionForProposer(proposer, description), "Governor: proposer restricted");
|
||||
|
||||
uint256 currentTimepoint = clock();
|
||||
|
||||
// Avoid stack too deep
|
||||
{
|
||||
uint256 proposerVotes = getVotes(proposer, currentTimepoint - 1);
|
||||
uint256 votesThreshold = proposalThreshold();
|
||||
if (proposerVotes < votesThreshold) {
|
||||
revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold);
|
||||
}
|
||||
// check description restriction
|
||||
if (!_isValidDescriptionForProposer(proposer, description)) {
|
||||
revert GovernorRestrictedProposer(proposer);
|
||||
}
|
||||
|
||||
// check proposal threshold
|
||||
uint256 proposerVotes = getVotes(proposer, clock() - 1);
|
||||
uint256 votesThreshold = proposalThreshold();
|
||||
if (proposerVotes < votesThreshold) {
|
||||
revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold);
|
||||
}
|
||||
|
||||
return _propose(targets, values, calldatas, description, proposer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal propose mechanism. Can be overridden to add more logic on proposal creation.
|
||||
*
|
||||
* Emits a {IGovernor-ProposalCreated} event.
|
||||
*/
|
||||
function _propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description,
|
||||
address proposer
|
||||
) internal virtual returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description)));
|
||||
|
||||
if (targets.length != values.length || targets.length != calldatas.length || targets.length == 0) {
|
||||
revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length);
|
||||
}
|
||||
if (_proposals[proposalId].voteStart != 0) {
|
||||
if (_proposals[proposalId].core.voteStart != 0) {
|
||||
revert GovernorUnexpectedProposalState(proposalId, state(proposalId), bytes32(0));
|
||||
}
|
||||
|
||||
uint256 snapshot = currentTimepoint + votingDelay();
|
||||
uint256 snapshot = clock() + votingDelay();
|
||||
uint256 duration = votingPeriod();
|
||||
|
||||
_proposals[proposalId] = ProposalCore({
|
||||
_proposals[proposalId].core = ProposalCore({
|
||||
proposer: proposer,
|
||||
voteStart: SafeCast.toUint48(snapshot),
|
||||
voteDuration: SafeCast.toUint32(duration),
|
||||
@ -332,6 +341,54 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-queue}.
|
||||
*/
|
||||
function queue(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual override returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
|
||||
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Succeeded));
|
||||
|
||||
uint48 eta = _queueOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
|
||||
if (eta != 0) {
|
||||
_proposals[proposalId].extra.eta = eta;
|
||||
emit ProposalQueued(proposalId, eta);
|
||||
} else {
|
||||
revert GovernorQueueNotImplemented();
|
||||
}
|
||||
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal queuing mechanism. Can be overridden (without a super call) to modify the way queuing is
|
||||
* performed (for example adding a vault/timelock).
|
||||
*
|
||||
* This is empty by default, and must be overridden to implement queuing.
|
||||
*
|
||||
* This function returns a timestamp that describes the expected eta for execution. If the returned value is 0
|
||||
* (which is the default value), the core will consider queueing did not succeed, and the public {queue} function
|
||||
* will revert.
|
||||
*
|
||||
* NOTE: Calling this function directly will NOT check the current state of the proposal, or emit the
|
||||
* `ProposalQueued` event. Queuing a proposal should be done using {queue} or {_queue}.
|
||||
*/
|
||||
function _queueOperations(
|
||||
uint256 /*proposalId*/,
|
||||
address[] memory /*targets*/,
|
||||
uint256[] memory /*values*/,
|
||||
bytes[] memory /*calldatas*/,
|
||||
bytes32 /*descriptionHash*/
|
||||
) internal virtual returns (uint48) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-execute}.
|
||||
*/
|
||||
@ -343,49 +400,43 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
) public payable virtual override returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
|
||||
ProposalState currentState = state(proposalId);
|
||||
if (currentState != ProposalState.Succeeded && currentState != ProposalState.Queued) {
|
||||
revert GovernorUnexpectedProposalState(
|
||||
proposalId,
|
||||
currentState,
|
||||
_encodeStateBitmap(ProposalState.Succeeded) | _encodeStateBitmap(ProposalState.Queued)
|
||||
);
|
||||
_validateStateBitmap(
|
||||
proposalId,
|
||||
_encodeStateBitmap(ProposalState.Succeeded) | _encodeStateBitmap(ProposalState.Queued)
|
||||
);
|
||||
|
||||
// mark as executed before calls to avoid reentrancy
|
||||
_proposals[proposalId].core.executed = true;
|
||||
|
||||
// before execute: register governance call in queue.
|
||||
if (_executor() != address(this)) {
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
if (targets[i] == address(this)) {
|
||||
_governanceCall.pushBack(keccak256(calldatas[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_executeOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
|
||||
// after execute: cleanup governance call queue.
|
||||
if (_executor() != address(this) && !_governanceCall.empty()) {
|
||||
_governanceCall.clear();
|
||||
}
|
||||
_proposals[proposalId].executed = true;
|
||||
|
||||
emit ProposalExecuted(proposalId);
|
||||
|
||||
_beforeExecute(proposalId, targets, values, calldatas, descriptionHash);
|
||||
_execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||
_afterExecute(proposalId, targets, values, calldatas, descriptionHash);
|
||||
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-cancel}.
|
||||
* @dev Internal execution mechanism. Can be overridden (without a super call) to modify the way execution is
|
||||
* performed (for example adding a vault/timelock).
|
||||
*
|
||||
* NOTE: Calling this function directly will NOT check the current state of the proposal, set the executed flag to
|
||||
* true or emit the `ProposalExecuted` event. Executing a proposal should be done using {execute} or {_execute}.
|
||||
*/
|
||||
function cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual override returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
ProposalState currentState = state(proposalId);
|
||||
if (currentState != ProposalState.Pending) {
|
||||
revert GovernorUnexpectedProposalState(proposalId, currentState, _encodeStateBitmap(ProposalState.Pending));
|
||||
}
|
||||
if (_msgSender() != proposalProposer(proposalId)) {
|
||||
revert GovernorOnlyProposer(_msgSender());
|
||||
}
|
||||
return _cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal execution mechanism. Can be overridden to implement different execution mechanism
|
||||
*/
|
||||
function _execute(
|
||||
function _executeOperations(
|
||||
uint256 /* proposalId */,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
@ -399,44 +450,31 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Hook before execution is triggered.
|
||||
* @dev See {IGovernor-cancel}.
|
||||
*/
|
||||
function _beforeExecute(
|
||||
uint256 /* proposalId */,
|
||||
function cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory /* values */,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 /*descriptionHash*/
|
||||
) internal virtual {
|
||||
if (_executor() != address(this)) {
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
if (targets[i] == address(this)) {
|
||||
_governanceCall.pushBack(keccak256(calldatas[i]));
|
||||
}
|
||||
}
|
||||
bytes32 descriptionHash
|
||||
) public virtual override returns (uint256) {
|
||||
// The proposalId will be recomputed in the `_cancel` call further down. However we need the value before we
|
||||
// do the internal call, because we need to check the proposal state BEFORE the internal `_cancel` call
|
||||
// changes it. The `hashProposal` duplication has a cost that is limited, and that we accept.
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
|
||||
// public cancel restrictions (on top of existing _cancel restrictions).
|
||||
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Pending));
|
||||
if (_msgSender() != proposalProposer(proposalId)) {
|
||||
revert GovernorOnlyProposer(_msgSender());
|
||||
}
|
||||
|
||||
return _cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Hook after execution is triggered.
|
||||
*/
|
||||
function _afterExecute(
|
||||
uint256 /* proposalId */,
|
||||
address[] memory /* targets */,
|
||||
uint256[] memory /* values */,
|
||||
bytes[] memory /* calldatas */,
|
||||
bytes32 /*descriptionHash*/
|
||||
) internal virtual {
|
||||
if (_executor() != address(this)) {
|
||||
if (!_governanceCall.empty()) {
|
||||
_governanceCall.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as
|
||||
* canceled to allow distinguishing it from executed proposals.
|
||||
* @dev Internal cancel mechanism with minimal restrictions. A proposal can be cancelled in any state other than
|
||||
* Canceled, Expired, or Executed. Once cancelled a proposal can't be re-submitted.
|
||||
*
|
||||
* Emits a {IGovernor-ProposalCanceled} event.
|
||||
*/
|
||||
@ -448,20 +486,15 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
) internal virtual returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
|
||||
ProposalState currentState = state(proposalId);
|
||||
|
||||
bytes32 forbiddenStates = _encodeStateBitmap(ProposalState.Canceled) |
|
||||
_encodeStateBitmap(ProposalState.Expired) |
|
||||
_encodeStateBitmap(ProposalState.Executed);
|
||||
if (forbiddenStates & _encodeStateBitmap(currentState) != 0) {
|
||||
revert GovernorUnexpectedProposalState(
|
||||
proposalId,
|
||||
currentState,
|
||||
_ALL_PROPOSAL_STATES_BITMAP ^ forbiddenStates
|
||||
);
|
||||
}
|
||||
_proposals[proposalId].canceled = true;
|
||||
_validateStateBitmap(
|
||||
proposalId,
|
||||
_ALL_PROPOSAL_STATES_BITMAP ^
|
||||
_encodeStateBitmap(ProposalState.Canceled) ^
|
||||
_encodeStateBitmap(ProposalState.Expired) ^
|
||||
_encodeStateBitmap(ProposalState.Executed)
|
||||
);
|
||||
|
||||
_proposals[proposalId].core.canceled = true;
|
||||
emit ProposalCanceled(proposalId);
|
||||
|
||||
return proposalId;
|
||||
@ -695,6 +728,20 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
return bytes32(1 << uint8(proposalState));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check that the current state of a proposal matches the requirements described by the `allowedStates` bitmap.
|
||||
* This bitmap should be built using `_encodeStateBitmap`.
|
||||
*
|
||||
* If requirements are not met, reverts with a {GovernorUnexpectedProposalState} error.
|
||||
*/
|
||||
function _validateStateBitmap(uint256 proposalId, bytes32 allowedStates) private view returns (ProposalState) {
|
||||
ProposalState currentState = state(proposalId);
|
||||
if (_encodeStateBitmap(currentState) & allowedStates == bytes32(0)) {
|
||||
revert GovernorUnexpectedProposalState(proposalId, currentState, allowedStates);
|
||||
}
|
||||
return currentState;
|
||||
}
|
||||
|
||||
/*
|
||||
* @dev Check if the proposer is authorized to submit a proposal with the given description.
|
||||
*
|
||||
@ -779,4 +826,30 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc IGovernor
|
||||
*/
|
||||
function clock() public view virtual returns (uint48);
|
||||
|
||||
/**
|
||||
* @inheritdoc IGovernor
|
||||
*/
|
||||
// solhint-disable-next-line func-name-mixedcase
|
||||
function CLOCK_MODE() public view virtual returns (string memory);
|
||||
|
||||
/**
|
||||
* @inheritdoc IGovernor
|
||||
*/
|
||||
function votingDelay() public view virtual returns (uint256);
|
||||
|
||||
/**
|
||||
* @inheritdoc IGovernor
|
||||
*/
|
||||
function votingPeriod() public view virtual returns (uint256);
|
||||
|
||||
/**
|
||||
* @inheritdoc IGovernor
|
||||
*/
|
||||
function quorum(uint256 timepoint) public view virtual returns (uint256);
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import {IERC6372} from "../interfaces/IERC6372.sol";
|
||||
/**
|
||||
* @dev Interface of the {Governor} core.
|
||||
*/
|
||||
abstract contract IGovernor is IERC165, IERC6372 {
|
||||
interface IGovernor is IERC165, IERC6372 {
|
||||
enum ProposalState {
|
||||
Pending,
|
||||
Active,
|
||||
@ -69,15 +69,35 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
error GovernorInvalidVotingPeriod(uint256 votingPeriod);
|
||||
|
||||
/**
|
||||
* @dev The `proposer` does not have the required votes to operate on a proposal.
|
||||
* @dev The `proposer` does not have the required votes to create a proposal.
|
||||
*/
|
||||
error GovernorInsufficientProposerVotes(address proposer, uint256 votes, uint256 threshold);
|
||||
|
||||
/**
|
||||
* @dev The `proposer` is not allowed to create a proposal.
|
||||
*/
|
||||
error GovernorRestrictedProposer(address proposer);
|
||||
|
||||
/**
|
||||
* @dev The vote type used is not valid for the corresponding counting module.
|
||||
*/
|
||||
error GovernorInvalidVoteType();
|
||||
|
||||
/**
|
||||
* @dev Queue operation is not implemented for this governor. Execute should be called directly.
|
||||
*/
|
||||
error GovernorQueueNotImplemented();
|
||||
|
||||
/**
|
||||
* @dev The proposal hasn't been queued yet.
|
||||
*/
|
||||
error GovernorNotQueuedProposal(uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev The proposal has already been queued.
|
||||
*/
|
||||
error GovernorAlreadyQueuedProposal(uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev The provided signature is not valid for the expected `voter`.
|
||||
* If the `voter` is a contract, the signature is not valid using {IERC1271-isValidSignature}.
|
||||
@ -100,15 +120,20 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a proposal is canceled.
|
||||
* @dev Emitted when a proposal is queued.
|
||||
*/
|
||||
event ProposalCanceled(uint256 proposalId);
|
||||
event ProposalQueued(uint256 proposalId, uint256 eta);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a proposal is executed.
|
||||
*/
|
||||
event ProposalExecuted(uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a proposal is canceled.
|
||||
*/
|
||||
event ProposalCanceled(uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev Emitted when a vote is cast without params.
|
||||
*
|
||||
@ -135,26 +160,26 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
* @notice module:core
|
||||
* @dev Name of the governor instance (used in building the ERC712 domain separator).
|
||||
*/
|
||||
function name() public view virtual returns (string memory);
|
||||
function name() external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev Version of the governor instance (used in building the ERC712 domain separator). Default: "1"
|
||||
*/
|
||||
function version() public view virtual returns (string memory);
|
||||
function version() external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev See {IERC6372}
|
||||
*/
|
||||
function clock() public view virtual returns (uint48);
|
||||
function clock() external view returns (uint48);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev See EIP-6372.
|
||||
* @dev See {IERC6372}
|
||||
*/
|
||||
// solhint-disable-next-line func-name-mixedcase
|
||||
function CLOCK_MODE() public view virtual returns (string memory);
|
||||
function CLOCK_MODE() external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice module:voting
|
||||
@ -179,7 +204,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
* JavaScript class.
|
||||
*/
|
||||
// solhint-disable-next-line func-name-mixedcase
|
||||
function COUNTING_MODE() public view virtual returns (string memory);
|
||||
function COUNTING_MODE() external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
@ -190,13 +215,19 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public pure virtual returns (uint256);
|
||||
) external pure returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev Current state of a proposal, following Compound's convention
|
||||
*/
|
||||
function state(uint256 proposalId) public view virtual returns (ProposalState);
|
||||
function state(uint256 proposalId) external view returns (ProposalState);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev The number of votes required in order for a voter to become a proposer.
|
||||
*/
|
||||
function proposalThreshold() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
@ -204,20 +235,28 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
* snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the
|
||||
* following block.
|
||||
*/
|
||||
function proposalSnapshot(uint256 proposalId) public view virtual returns (uint256);
|
||||
function proposalSnapshot(uint256 proposalId) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev Timepoint at which votes close. If using block number, votes close at the end of this block, so it is
|
||||
* possible to cast a vote during this block.
|
||||
*/
|
||||
function proposalDeadline(uint256 proposalId) public view virtual returns (uint256);
|
||||
function proposalDeadline(uint256 proposalId) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev The account that created a proposal.
|
||||
*/
|
||||
function proposalProposer(uint256 proposalId) public view virtual returns (address);
|
||||
function proposalProposer(uint256 proposalId) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice module:core
|
||||
* @dev The time when a queued proposal becomes executable ("ETA"). Unlike {proposalSnapshot} and
|
||||
* {proposalDeadline}, this doesn't use the governor clock, and instead relies on the executor's clock which may be
|
||||
* different. In most cases this will be a timestamp.
|
||||
*/
|
||||
function proposalEta(uint256 proposalId) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:user-config
|
||||
@ -230,7 +269,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
* NOTE: While this interface returns a uint256, timepoints are stored as uint48 following the ERC-6372 clock type.
|
||||
* Consequently this value must fit in a uint48 (when added to the current clock). See {IERC6372-clock}.
|
||||
*/
|
||||
function votingDelay() public view virtual returns (uint256);
|
||||
function votingDelay() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:user-config
|
||||
@ -244,7 +283,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
* proposals that have already been submitted. The type used to save it is a uint32. Consequently, while this
|
||||
* interface returns a uint256, the value it returns should fit in a uint32.
|
||||
*/
|
||||
function votingPeriod() public view virtual returns (uint256);
|
||||
function votingPeriod() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:user-config
|
||||
@ -253,7 +292,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
* NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This allows to scale the
|
||||
* quorum depending on values such as the totalSupply of a token at this timepoint (see {ERC20Votes}).
|
||||
*/
|
||||
function quorum(uint256 timepoint) public view virtual returns (uint256);
|
||||
function quorum(uint256 timepoint) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:reputation
|
||||
@ -262,7 +301,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
* Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or
|
||||
* multiple), {ERC20Votes} tokens.
|
||||
*/
|
||||
function getVotes(address account, uint256 timepoint) public view virtual returns (uint256);
|
||||
function getVotes(address account, uint256 timepoint) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:reputation
|
||||
@ -272,13 +311,13 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
address account,
|
||||
uint256 timepoint,
|
||||
bytes memory params
|
||||
) public view virtual returns (uint256);
|
||||
) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice module:voting
|
||||
* @dev Returns whether `account` has cast a vote on `proposalId`.
|
||||
*/
|
||||
function hasVoted(uint256 proposalId, address account) public view virtual returns (bool);
|
||||
function hasVoted(uint256 proposalId, address account) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Create a new proposal. Vote start after a delay specified by {IGovernor-votingDelay} and lasts for a
|
||||
@ -291,22 +330,37 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) public virtual returns (uint256 proposalId);
|
||||
) external returns (uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev Queue a proposal. Some governors require this step to be performed before execution can happen. If queuing
|
||||
* is not necessary, this function may revert.
|
||||
* Queuing a proposal requires the quorum to be reached, the vote to be successful, and the deadline to be reached.
|
||||
*
|
||||
* Emits a {ProposalQueued} event.
|
||||
*/
|
||||
function queue(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) external returns (uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev Execute a successful proposal. This requires the quorum to be reached, the vote to be successful, and the
|
||||
* deadline to be reached.
|
||||
* deadline to be reached. Depending on the governor it might also be required that the proposal was queued and
|
||||
* that some delay passed.
|
||||
*
|
||||
* Emits a {ProposalExecuted} event.
|
||||
*
|
||||
* Note: some module can modify the requirements for execution, for example by adding an additional timelock.
|
||||
* NOTE: Some modules can modify the requirements for execution, for example by adding an additional timelock.
|
||||
*/
|
||||
function execute(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public payable virtual returns (uint256 proposalId);
|
||||
) external payable returns (uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending state, i.e.
|
||||
@ -319,14 +373,14 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual returns (uint256 proposalId);
|
||||
) external returns (uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev Cast a vote
|
||||
*
|
||||
* Emits a {VoteCast} event.
|
||||
*/
|
||||
function castVote(uint256 proposalId, uint8 support) public virtual returns (uint256 balance);
|
||||
function castVote(uint256 proposalId, uint8 support) external returns (uint256 balance);
|
||||
|
||||
/**
|
||||
* @dev Cast a vote with a reason
|
||||
@ -337,7 +391,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
uint256 proposalId,
|
||||
uint8 support,
|
||||
string calldata reason
|
||||
) public virtual returns (uint256 balance);
|
||||
) external returns (uint256 balance);
|
||||
|
||||
/**
|
||||
* @dev Cast a vote with a reason and additional encoded parameters
|
||||
@ -349,7 +403,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
uint8 support,
|
||||
string calldata reason,
|
||||
bytes memory params
|
||||
) public virtual returns (uint256 balance);
|
||||
) external returns (uint256 balance);
|
||||
|
||||
/**
|
||||
* @dev Cast a vote using the voter's signature, including ERC-1271 signature support.
|
||||
@ -361,7 +415,7 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
uint8 support,
|
||||
address voter,
|
||||
bytes memory signature
|
||||
) public virtual returns (uint256 balance);
|
||||
) external returns (uint256 balance);
|
||||
|
||||
/**
|
||||
* @dev Cast a vote with a reason and additional encoded parameters using the voter's signature,
|
||||
@ -376,5 +430,5 @@ abstract contract IGovernor is IERC165, IERC6372 {
|
||||
string calldata reason,
|
||||
bytes memory params,
|
||||
bytes memory signature
|
||||
) public virtual returns (uint256 balance);
|
||||
) external returns (uint256 balance);
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ Timelock extensions add a delay for governance decisions to be executed. The wor
|
||||
|
||||
Other extensions can customize the behavior or interface in multiple ways.
|
||||
|
||||
* {GovernorCompatibilityBravo}: Extends the interface to be fully `GovernorBravo`-compatible. Note that events are compatible regardless of whether this extension is included or not.
|
||||
* {GovernorStorage}: Stores the proposal details onchain and provides enumerability of the proposals. This can be useful for some L2 chains where storage is cheap compared to calldata.
|
||||
|
||||
* {GovernorSettings}: Manages some of the settings (voting delay, voting period duration, and proposal threshold) in a way that can be updated through a governance proposal, without requiring an upgrade.
|
||||
|
||||
@ -74,7 +74,7 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
|
||||
|
||||
{{GovernorPreventLateQuorum}}
|
||||
|
||||
{{GovernorCompatibilityBravo}}
|
||||
{{GovernorStorage}}
|
||||
|
||||
== Utils
|
||||
|
||||
|
||||
@ -1,333 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/GovernorCompatibilityBravo.sol)
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {SafeCast} from "../../utils/math/SafeCast.sol";
|
||||
import {IGovernorTimelock} from "../extensions/IGovernorTimelock.sol";
|
||||
import {IGovernor, Governor} from "../Governor.sol";
|
||||
import {IGovernorCompatibilityBravo} from "./IGovernorCompatibilityBravo.sol";
|
||||
|
||||
/**
|
||||
* @dev Compatibility layer that implements GovernorBravo compatibility on top of {Governor}.
|
||||
*
|
||||
* This compatibility layer includes a voting system and requires a {IGovernorTimelock} compatible module to be added
|
||||
* through inheritance. It does not include token bindings, nor does it include any variable upgrade patterns.
|
||||
*
|
||||
* NOTE: When using this module, you may need to enable the Solidity optimizer to avoid hitting the contract size limit.
|
||||
*/
|
||||
abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorCompatibilityBravo, Governor {
|
||||
enum VoteType {
|
||||
Against,
|
||||
For,
|
||||
Abstain
|
||||
}
|
||||
|
||||
struct ProposalDetails {
|
||||
address[] targets;
|
||||
uint256[] values;
|
||||
string[] signatures;
|
||||
bytes[] calldatas;
|
||||
uint256 forVotes;
|
||||
uint256 againstVotes;
|
||||
uint256 abstainVotes;
|
||||
mapping(address => Receipt) receipts;
|
||||
bytes32 descriptionHash;
|
||||
}
|
||||
|
||||
mapping(uint256 => ProposalDetails) private _proposalDetails;
|
||||
|
||||
// solhint-disable-next-line func-name-mixedcase
|
||||
function COUNTING_MODE() public pure virtual override returns (string memory) {
|
||||
return "support=bravo&quorum=bravo";
|
||||
}
|
||||
|
||||
// ============================================== Proposal lifecycle ==============================================
|
||||
/**
|
||||
* @dev See {IGovernor-propose}.
|
||||
*/
|
||||
function propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) public virtual override(IGovernor, Governor) returns (uint256) {
|
||||
// Stores the proposal details (if not already present) and executes the propose logic from the core.
|
||||
_storeProposal(targets, values, new string[](calldatas.length), calldatas, description);
|
||||
return super.propose(targets, values, calldatas, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernorCompatibilityBravo-propose}.
|
||||
*/
|
||||
function propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
string[] memory signatures,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) public virtual override returns (uint256) {
|
||||
if (signatures.length != calldatas.length) {
|
||||
revert GovernorInvalidSignaturesLength(signatures.length, calldatas.length);
|
||||
}
|
||||
// Stores the full proposal and fallback to the public (possibly overridden) propose. The fallback is done
|
||||
// after the full proposal is stored, so the store operation included in the fallback will be skipped. Here we
|
||||
// call `propose` and not `super.propose` to make sure if a child contract override `propose`, whatever code
|
||||
// is added there is also executed when calling this alternative interface.
|
||||
_storeProposal(targets, values, signatures, calldatas, description);
|
||||
return propose(targets, values, _encodeCalldata(signatures, calldatas), description);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernorCompatibilityBravo-queue}.
|
||||
*/
|
||||
function queue(uint256 proposalId) public virtual override {
|
||||
(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) = _getProposalParameters(proposalId);
|
||||
|
||||
queue(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernorCompatibilityBravo-execute}.
|
||||
*/
|
||||
function execute(uint256 proposalId) public payable virtual override {
|
||||
(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) = _getProposalParameters(proposalId);
|
||||
|
||||
execute(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Cancel a proposal with GovernorBravo logic.
|
||||
*/
|
||||
function cancel(uint256 proposalId) public virtual override {
|
||||
(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) = _getProposalParameters(proposalId);
|
||||
|
||||
cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Cancel a proposal with GovernorBravo logic. At any moment a proposal can be cancelled, either by the
|
||||
* proposer, or by third parties if the proposer's voting power has dropped below the proposal threshold.
|
||||
*/
|
||||
function cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual override(IGovernor, Governor) returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
address proposer = proposalProposer(proposalId);
|
||||
|
||||
uint256 proposerVotes = getVotes(proposer, clock() - 1);
|
||||
uint256 votesThreshold = proposalThreshold();
|
||||
if (_msgSender() != proposer && proposerVotes >= votesThreshold) {
|
||||
revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold);
|
||||
}
|
||||
|
||||
return _cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Encodes calldatas with optional function signature.
|
||||
*/
|
||||
function _encodeCalldata(
|
||||
string[] memory signatures,
|
||||
bytes[] memory calldatas
|
||||
) private pure returns (bytes[] memory) {
|
||||
bytes[] memory fullcalldatas = new bytes[](calldatas.length);
|
||||
for (uint256 i = 0; i < fullcalldatas.length; ++i) {
|
||||
fullcalldatas[i] = bytes(signatures[i]).length == 0
|
||||
? calldatas[i]
|
||||
: bytes.concat(abi.encodeWithSignature(signatures[i]), calldatas[i]);
|
||||
}
|
||||
|
||||
return fullcalldatas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Retrieve proposal parameters by id, with fully encoded calldatas.
|
||||
*/
|
||||
function _getProposalParameters(
|
||||
uint256 proposalId
|
||||
)
|
||||
private
|
||||
view
|
||||
returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
|
||||
{
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
return (
|
||||
details.targets,
|
||||
details.values,
|
||||
_encodeCalldata(details.signatures, details.calldatas),
|
||||
details.descriptionHash
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Store proposal metadata (if not already present) for later lookup.
|
||||
*/
|
||||
function _storeProposal(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
string[] memory signatures,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) private {
|
||||
bytes32 descriptionHash = keccak256(bytes(description));
|
||||
uint256 proposalId = hashProposal(targets, values, _encodeCalldata(signatures, calldatas), descriptionHash);
|
||||
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
if (details.descriptionHash == bytes32(0)) {
|
||||
details.targets = targets;
|
||||
details.values = values;
|
||||
details.signatures = signatures;
|
||||
details.calldatas = calldatas;
|
||||
details.descriptionHash = descriptionHash;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================== Views =====================================================
|
||||
/**
|
||||
* @dev See {IGovernorCompatibilityBravo-proposals}.
|
||||
*/
|
||||
function proposals(
|
||||
uint256 proposalId
|
||||
)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override
|
||||
returns (
|
||||
uint256 id,
|
||||
address proposer,
|
||||
uint256 eta,
|
||||
uint256 startBlock,
|
||||
uint256 endBlock,
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
uint256 abstainVotes,
|
||||
bool canceled,
|
||||
bool executed
|
||||
)
|
||||
{
|
||||
id = proposalId;
|
||||
proposer = proposalProposer(proposalId);
|
||||
eta = proposalEta(proposalId);
|
||||
startBlock = proposalSnapshot(proposalId);
|
||||
endBlock = proposalDeadline(proposalId);
|
||||
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
forVotes = details.forVotes;
|
||||
againstVotes = details.againstVotes;
|
||||
abstainVotes = details.abstainVotes;
|
||||
|
||||
ProposalState currentState = state(proposalId);
|
||||
canceled = currentState == ProposalState.Canceled;
|
||||
executed = currentState == ProposalState.Executed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernorCompatibilityBravo-getActions}.
|
||||
*/
|
||||
function getActions(
|
||||
uint256 proposalId
|
||||
)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override
|
||||
returns (
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
string[] memory signatures,
|
||||
bytes[] memory calldatas
|
||||
)
|
||||
{
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
return (details.targets, details.values, details.signatures, details.calldatas);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernorCompatibilityBravo-getReceipt}.
|
||||
*/
|
||||
function getReceipt(uint256 proposalId, address voter) public view virtual override returns (Receipt memory) {
|
||||
return _proposalDetails[proposalId].receipts[voter];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernorCompatibilityBravo-quorumVotes}.
|
||||
*/
|
||||
function quorumVotes() public view virtual override returns (uint256) {
|
||||
return quorum(clock() - 1);
|
||||
}
|
||||
|
||||
// ==================================================== Voting ====================================================
|
||||
/**
|
||||
* @dev See {IGovernor-hasVoted}.
|
||||
*/
|
||||
function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
|
||||
return _proposalDetails[proposalId].receipts[account].hasVoted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {Governor-_quorumReached}. In this module, only forVotes count toward the quorum.
|
||||
*/
|
||||
function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
return quorum(proposalSnapshot(proposalId)) <= details.forVotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be strictly over the againstVotes.
|
||||
*/
|
||||
function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
return details.forVotes > details.againstVotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {Governor-_countVote}. In this module, the support follows Governor Bravo.
|
||||
*/
|
||||
function _countVote(
|
||||
uint256 proposalId,
|
||||
address account,
|
||||
uint8 support,
|
||||
uint256 weight,
|
||||
bytes memory // params
|
||||
) internal virtual override {
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
Receipt storage receipt = details.receipts[account];
|
||||
|
||||
if (receipt.hasVoted) {
|
||||
revert GovernorAlreadyCastVote(account);
|
||||
}
|
||||
receipt.hasVoted = true;
|
||||
receipt.support = support;
|
||||
receipt.votes = SafeCast.toUint96(weight);
|
||||
|
||||
if (support == uint8(VoteType.Against)) {
|
||||
details.againstVotes += weight;
|
||||
} else if (support == uint8(VoteType.For)) {
|
||||
details.forVotes += weight;
|
||||
} else if (support == uint8(VoteType.Abstain)) {
|
||||
details.abstainVotes += weight;
|
||||
} else {
|
||||
revert GovernorInvalidVoteType();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/IGovernorCompatibilityBravo.sol)
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IGovernor} from "../IGovernor.sol";
|
||||
|
||||
/**
|
||||
* @dev Interface extension that adds missing functions to the {Governor} core to provide `GovernorBravo` compatibility.
|
||||
*/
|
||||
abstract contract IGovernorCompatibilityBravo is IGovernor {
|
||||
/**
|
||||
* @dev Mismatch between the parameters length for a proposal call.
|
||||
*/
|
||||
error GovernorInvalidSignaturesLength(uint256 signatures, uint256 calldatas);
|
||||
|
||||
/**
|
||||
* @dev Proposal structure from Compound Governor Bravo. Not actually used by the compatibility layer, as
|
||||
* {{proposal}} returns a very different structure.
|
||||
*/
|
||||
struct Proposal {
|
||||
uint256 id;
|
||||
address proposer;
|
||||
uint256 eta;
|
||||
address[] targets;
|
||||
uint256[] values;
|
||||
string[] signatures;
|
||||
bytes[] calldatas;
|
||||
uint256 startBlock;
|
||||
uint256 endBlock;
|
||||
uint256 forVotes;
|
||||
uint256 againstVotes;
|
||||
uint256 abstainVotes;
|
||||
bool canceled;
|
||||
bool executed;
|
||||
mapping(address => Receipt) receipts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Receipt structure from Compound Governor Bravo
|
||||
*/
|
||||
struct Receipt {
|
||||
bool hasVoted;
|
||||
uint8 support;
|
||||
uint96 votes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface.
|
||||
*/
|
||||
function quorumVotes() public view virtual returns (uint256);
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface: _"The official record of all proposals ever proposed"_.
|
||||
*/
|
||||
function proposals(
|
||||
uint256
|
||||
)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
returns (
|
||||
uint256 id,
|
||||
address proposer,
|
||||
uint256 eta,
|
||||
uint256 startBlock,
|
||||
uint256 endBlock,
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
uint256 abstainVotes,
|
||||
bool canceled,
|
||||
bool executed
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface: _"Function used to propose a new proposal"_.
|
||||
*/
|
||||
function propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
string[] memory signatures,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) public virtual returns (uint256);
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface: _"Queues a proposal of state succeeded"_.
|
||||
*/
|
||||
function queue(uint256 proposalId) public virtual;
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface: _"Executes a queued proposal if eta has passed"_.
|
||||
*/
|
||||
function execute(uint256 proposalId) public payable virtual;
|
||||
|
||||
/**
|
||||
* @dev Cancels a proposal only if the sender is the proposer or the proposer delegates' voting power dropped below the proposal threshold.
|
||||
*/
|
||||
function cancel(uint256 proposalId) public virtual;
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface: _"Gets actions of a proposal"_.
|
||||
*/
|
||||
function getActions(
|
||||
uint256 proposalId
|
||||
)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
returns (
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
string[] memory signatures,
|
||||
bytes[] memory calldatas
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Part of the Governor Bravo's interface: _"Gets the receipt for a voter on a given proposal"_.
|
||||
*/
|
||||
function getReceipt(uint256 proposalId, address voter) public view virtual returns (Receipt memory);
|
||||
}
|
||||
109
contracts/governance/extensions/GovernorStorage.sol
Normal file
109
contracts/governance/extensions/GovernorStorage.sol
Normal file
@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Governor} from "../Governor.sol";
|
||||
|
||||
/**
|
||||
* @dev Extension of {Governor} that implements storage of proposal details. This modules also provides primitives for
|
||||
* the enumerability of proposals.
|
||||
*
|
||||
* Use cases for this module include:
|
||||
* - UIs that explore the proposal state without relying on event indexing.
|
||||
* - Using only the proposalId as an argument in the {Governor-queue} and {Governor-execute} functions for L2 chains where storage is cheap compared to calldata.
|
||||
*/
|
||||
abstract contract GovernorStorage is Governor {
|
||||
struct ProposalDetails {
|
||||
address[] targets;
|
||||
uint256[] values;
|
||||
bytes[] calldatas;
|
||||
bytes32 descriptionHash;
|
||||
}
|
||||
|
||||
uint256[] private _proposalIds;
|
||||
mapping(uint256 => ProposalDetails) private _proposalDetails;
|
||||
|
||||
/**
|
||||
* @dev Hook into the proposing mechanism
|
||||
*/
|
||||
function _propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description,
|
||||
address proposer
|
||||
) internal virtual override returns (uint256) {
|
||||
uint256 proposalId = super._propose(targets, values, calldatas, description, proposer);
|
||||
|
||||
// store
|
||||
_proposalIds.push(proposalId);
|
||||
_proposalDetails[proposalId] = ProposalDetails({
|
||||
targets: targets,
|
||||
values: values,
|
||||
calldatas: calldatas,
|
||||
descriptionHash: keccak256(bytes(description))
|
||||
});
|
||||
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Version of {IGovernorTimelock-queue} with only `proposalId` as an argument.
|
||||
*/
|
||||
function queue(uint256 proposalId) public virtual {
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
queue(details.targets, details.values, details.calldatas, details.descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Version of {IGovernor-execute} with only `proposalId` as an argument.
|
||||
*/
|
||||
function execute(uint256 proposalId) public payable virtual {
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
execute(details.targets, details.values, details.calldatas, details.descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev ProposalId version of {IGovernor-cancel}.
|
||||
*/
|
||||
function cancel(uint256 proposalId) public virtual {
|
||||
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||
cancel(details.targets, details.values, details.calldatas, details.descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the number of stored proposals.
|
||||
*/
|
||||
function proposalCount() public view virtual returns (uint256) {
|
||||
return _proposalIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the details of a proposalId. Reverts if `proposalId` is not a known proposal.
|
||||
*/
|
||||
function proposalDetails(
|
||||
uint256 proposalId
|
||||
) public view virtual returns (address[] memory, uint256[] memory, bytes[] memory, bytes32) {
|
||||
ProposalDetails memory details = _proposalDetails[proposalId];
|
||||
if (details.descriptionHash == 0) {
|
||||
revert GovernorNonexistentProposal(proposalId);
|
||||
}
|
||||
return (details.targets, details.values, details.calldatas, details.descriptionHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the details (including the proposalId) of a proposal given its sequential index.
|
||||
*/
|
||||
function proposalDetailsAt(
|
||||
uint256 index
|
||||
) public view virtual returns (uint256, address[] memory, uint256[] memory, bytes[] memory, bytes32) {
|
||||
uint256 proposalId = _proposalIds[index];
|
||||
(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) = proposalDetails(proposalId);
|
||||
return (proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
}
|
||||
211
contracts/governance/extensions/GovernorTimelock.sol
Normal file
211
contracts/governance/extensions/GovernorTimelock.sol
Normal file
@ -0,0 +1,211 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IGovernorTimelock} from "./IGovernorTimelock.sol";
|
||||
import {IGovernor, Governor} from "../Governor.sol";
|
||||
import {IManaged} from "../../access/manager/IManaged.sol";
|
||||
import {IAuthority, safeCanCall} from "../../access/manager/IAuthority.sol";
|
||||
import {IAccessManager} from "../../access/manager/IAccessManager.sol";
|
||||
import {Address} from "../../utils/Address.sol";
|
||||
import {Math} from "../../utils/math/Math.sol";
|
||||
|
||||
/**
|
||||
* @dev TODO
|
||||
*
|
||||
* _Available since v5.0._
|
||||
*/
|
||||
abstract contract GovernorTimelock is IGovernorTimelock, Governor {
|
||||
struct ExecutionDetail {
|
||||
address authority;
|
||||
uint32 delay;
|
||||
}
|
||||
|
||||
mapping(uint256 => ExecutionDetail[]) private _executionDetails;
|
||||
mapping(uint256 => uint256) private _proposalEta;
|
||||
|
||||
/**
|
||||
* @dev Overridden version of the {Governor-state} function with added support for the `Queued` and `Expired` state.
|
||||
*/
|
||||
function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) {
|
||||
ProposalState currentState = super.state(proposalId);
|
||||
|
||||
if (currentState == ProposalState.Succeeded && proposalEta(proposalId) != 0) {
|
||||
return ProposalState.Queued;
|
||||
} else {
|
||||
return currentState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Public accessor to check the eta of a queued proposal.
|
||||
*/
|
||||
function proposalEta(uint256 proposalId) public view virtual override returns (uint256) {
|
||||
return _proposalEta[proposalId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Public accessor to check the execution details.
|
||||
*/
|
||||
function proposalExecutionDetails(uint256 proposalId) public view returns (ExecutionDetail[] memory) {
|
||||
return _executionDetails[proposalId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-propose}
|
||||
*/
|
||||
function propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) public virtual override(IGovernor, Governor) returns (uint256) {
|
||||
uint256 proposalId = super.propose(targets, values, calldatas, description);
|
||||
|
||||
ExecutionDetail[] storage details = _executionDetails[proposalId];
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
details.push(_detectExecutionDetails(targets[i], bytes4(calldatas[i])));
|
||||
}
|
||||
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Function to queue a proposal to the timelock.
|
||||
*
|
||||
* NOTE: execution delay is estimated based on the delay information retrieved in {proposal}. This value may be
|
||||
* off if the delay were updated during the vote.
|
||||
*/
|
||||
function queue(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual override returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
|
||||
ProposalState currentState = state(proposalId);
|
||||
if (currentState != ProposalState.Succeeded) {
|
||||
revert GovernorUnexpectedProposalState(
|
||||
proposalId,
|
||||
currentState,
|
||||
_encodeStateBitmap(ProposalState.Succeeded)
|
||||
);
|
||||
}
|
||||
|
||||
ExecutionDetail[] storage details = _executionDetails[proposalId];
|
||||
ExecutionDetail memory detail;
|
||||
uint32 setback = 0;
|
||||
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
detail = details[i];
|
||||
if (detail.authority != address(0)) {
|
||||
IAccessManager(detail.authority).schedule(targets[i], calldatas[i]);
|
||||
}
|
||||
setback = uint32(Math.max(setback, detail.delay)); // cast is safe, both parameters are uint32
|
||||
}
|
||||
|
||||
uint256 eta = block.timestamp + setback;
|
||||
_proposalEta[proposalId] = eta;
|
||||
|
||||
return eta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-_execute}
|
||||
*/
|
||||
function _execute(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 /*descriptionHash*/
|
||||
) internal virtual override {
|
||||
ExecutionDetail[] storage details = _executionDetails[proposalId];
|
||||
ExecutionDetail memory detail;
|
||||
|
||||
// TODO: enforce ETA (might include some _defaultDelaySeconds that are not enforced by any authority)
|
||||
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
detail = details[i];
|
||||
if (detail.authority != address(0)) {
|
||||
IAccessManager(detail.authority).relay{value: values[i]}(targets[i], calldatas[i]);
|
||||
} else {
|
||||
(bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]);
|
||||
Address.verifyCallResult(success, returndata);
|
||||
}
|
||||
}
|
||||
|
||||
delete _executionDetails[proposalId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IGovernor-_cancel}
|
||||
*/
|
||||
function _cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal virtual override returns (uint256) {
|
||||
uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash);
|
||||
|
||||
ExecutionDetail[] storage details = _executionDetails[proposalId];
|
||||
ExecutionDetail memory detail;
|
||||
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
detail = details[i];
|
||||
if (detail.authority != address(0)) {
|
||||
IAccessManager(detail.authority).cancel(address(this), targets[i], calldatas[i]);
|
||||
}
|
||||
}
|
||||
|
||||
delete _executionDetails[proposalId];
|
||||
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Default delay to apply to function calls that are not (scheduled and) executed through an AccessManager.
|
||||
*
|
||||
* NOTE: execution delays are processed by the AccessManager contracts. We expect these to always be in seconds.
|
||||
* Therefore, the default delay that is enforced for calls that don't go through an access manager is also in
|
||||
* seconds, regardless of the Governor's clock mode.
|
||||
*/
|
||||
function _defaultDelaySeconds() internal view virtual returns (uint32) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if the execution of a call needs to be performed through an AccessManager and what delay should be
|
||||
* applied to this call.
|
||||
*
|
||||
* Returns { manager: address(0), delay: _defaultDelaySeconds() } if:
|
||||
* - target does not have code
|
||||
* - target does not implement IManaged
|
||||
* - calling canCall on the target's manager returns a 0 delay
|
||||
* - calling canCall on the target's manager reverts
|
||||
* Otherwise (calling canCall on the target's manager returns a non 0 delay), return the address of the
|
||||
* AccessManager to use, and the delay for this call.
|
||||
*/
|
||||
function _detectExecutionDetails(address target, bytes4 selector) private view returns (ExecutionDetail memory) {
|
||||
bool success;
|
||||
bytes memory returndata;
|
||||
|
||||
// Get authority
|
||||
(success, returndata) = target.staticcall(abi.encodeCall(IManaged.authority, ()));
|
||||
if (success && returndata.length >= 0x20) {
|
||||
address authority = abi.decode(returndata, (address));
|
||||
|
||||
// Check if governor can call, and try to detect a delay
|
||||
(bool authorized, uint32 delay) = safeCanCall(authority, address(this), target, selector);
|
||||
|
||||
// If direct call is not authorized, and delayed call is possible
|
||||
if (!authorized && delay > 0) {
|
||||
return ExecutionDetail({authority: authority, delay: delay});
|
||||
}
|
||||
}
|
||||
|
||||
return ExecutionDetail({authority: address(0), delay: _defaultDelaySeconds()});
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,11 @@
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IGovernorTimelock} from "./IGovernorTimelock.sol";
|
||||
import {IGovernor, Governor} from "../Governor.sol";
|
||||
import {ICompoundTimelock} from "../../vendor/compound/ICompoundTimelock.sol";
|
||||
import {IERC165} from "../../interfaces/IERC165.sol";
|
||||
import {Address} from "../../utils/Address.sol";
|
||||
import {SafeCast} from "../../utils/math/SafeCast.sol";
|
||||
|
||||
/**
|
||||
* @dev Extension of {Governor} that binds the execution process to a Compound Timelock. This adds a delay, enforced by
|
||||
@ -19,11 +19,9 @@ import {Address} from "../../utils/Address.sol";
|
||||
* the assets and permissions must be attached to the {TimelockController}. Any asset sent to the {Governor} will be
|
||||
* inaccessible.
|
||||
*/
|
||||
abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
|
||||
abstract contract GovernorTimelockCompound is Governor {
|
||||
ICompoundTimelock private _timelock;
|
||||
|
||||
mapping(uint256 => uint256) private _proposalTimelocks;
|
||||
|
||||
/**
|
||||
* @dev Emitted when the timelock controller used for proposal execution is modified.
|
||||
*/
|
||||
@ -37,68 +35,36 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
* @dev Overridden version of the {Governor-state} function with added support for the `Expired` state.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, Governor) returns (bool) {
|
||||
return interfaceId == type(IGovernorTimelock).interfaceId || super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Overridden version of the {Governor-state} function with added support for the `Queued` and `Expired` state.
|
||||
*/
|
||||
function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) {
|
||||
function state(uint256 proposalId) public view virtual override returns (ProposalState) {
|
||||
ProposalState currentState = super.state(proposalId);
|
||||
|
||||
if (currentState != ProposalState.Succeeded) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
uint256 eta = proposalEta(proposalId);
|
||||
if (eta == 0) {
|
||||
return currentState;
|
||||
} else if (block.timestamp >= eta + _timelock.GRACE_PERIOD()) {
|
||||
return ProposalState.Expired;
|
||||
} else {
|
||||
return ProposalState.Queued;
|
||||
}
|
||||
return
|
||||
(currentState == ProposalState.Queued &&
|
||||
block.timestamp >= proposalEta(proposalId) + _timelock.GRACE_PERIOD())
|
||||
? ProposalState.Expired
|
||||
: currentState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Public accessor to check the address of the timelock
|
||||
*/
|
||||
function timelock() public view virtual override returns (address) {
|
||||
function timelock() public view virtual returns (address) {
|
||||
return address(_timelock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Public accessor to check the eta of a queued proposal
|
||||
*/
|
||||
function proposalEta(uint256 proposalId) public view virtual override returns (uint256) {
|
||||
return _proposalTimelocks[proposalId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Function to queue a proposal to the timelock.
|
||||
*/
|
||||
function queue(
|
||||
function _queueOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual override returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
|
||||
ProposalState currentState = state(proposalId);
|
||||
if (currentState != ProposalState.Succeeded) {
|
||||
revert GovernorUnexpectedProposalState(
|
||||
proposalId,
|
||||
currentState,
|
||||
_encodeStateBitmap(ProposalState.Succeeded)
|
||||
);
|
||||
}
|
||||
|
||||
uint256 eta = block.timestamp + _timelock.delay();
|
||||
_proposalTimelocks[proposalId] = eta;
|
||||
bytes32 /*descriptionHash*/
|
||||
) internal virtual override returns (uint48) {
|
||||
uint48 eta = SafeCast.toUint48(block.timestamp + _timelock.delay());
|
||||
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
if (_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta)))) {
|
||||
@ -107,15 +73,14 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
|
||||
_timelock.queueTransaction(targets[i], values[i], "", calldatas[i], eta);
|
||||
}
|
||||
|
||||
emit ProposalQueued(proposalId, eta);
|
||||
|
||||
return proposalId;
|
||||
return eta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Overridden execute function that run the already queued proposal through the timelock.
|
||||
* @dev Overridden version of the {Governor-_executeOperations} function that run the already queued proposal through
|
||||
* the timelock.
|
||||
*/
|
||||
function _execute(
|
||||
function _executeOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
@ -146,8 +111,6 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
|
||||
|
||||
uint256 eta = proposalEta(proposalId);
|
||||
if (eta > 0) {
|
||||
// update state first
|
||||
delete _proposalTimelocks[proposalId];
|
||||
// do external call later
|
||||
for (uint256 i = 0; i < targets.length; ++i) {
|
||||
_timelock.cancelTransaction(targets[i], values[i], "", calldatas[i], eta);
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IGovernorTimelock} from "./IGovernorTimelock.sol";
|
||||
import {IGovernor, Governor} from "../Governor.sol";
|
||||
import {TimelockController} from "../TimelockController.sol";
|
||||
import {IERC165} from "../../interfaces/IERC165.sol";
|
||||
import {SafeCast} from "../../utils/math/SafeCast.sol";
|
||||
|
||||
/**
|
||||
* @dev Extension of {Governor} that binds the execution process to an instance of {TimelockController}. This adds a
|
||||
@ -22,7 +22,7 @@ import {IERC165} from "../../interfaces/IERC165.sol";
|
||||
* available to them through the timelock, and 2) approved governance proposals can be blocked by them, effectively
|
||||
* executing a Denial of Service attack. This risk will be mitigated in a future release.
|
||||
*/
|
||||
abstract contract GovernorTimelockControl is IGovernorTimelock, Governor {
|
||||
abstract contract GovernorTimelockControl is Governor {
|
||||
TimelockController private _timelock;
|
||||
mapping(uint256 => bytes32) private _timelockIds;
|
||||
|
||||
@ -39,27 +39,17 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
* @dev Overridden version of the {Governor-state} function that considers the status reported by the timelock.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, Governor) returns (bool) {
|
||||
return interfaceId == type(IGovernorTimelock).interfaceId || super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Overridden version of the {Governor-state} function with added support for the `Queued` state.
|
||||
*/
|
||||
function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) {
|
||||
function state(uint256 proposalId) public view virtual override returns (ProposalState) {
|
||||
ProposalState currentState = super.state(proposalId);
|
||||
|
||||
if (currentState != ProposalState.Succeeded) {
|
||||
if (currentState != ProposalState.Queued) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
// core tracks execution, so we just have to check if successful proposal have been queued.
|
||||
bytes32 queueid = _timelockIds[proposalId];
|
||||
if (queueid == bytes32(0)) {
|
||||
return currentState;
|
||||
} else if (_timelock.isOperationPending(queueid)) {
|
||||
if (_timelock.isOperationPending(queueid)) {
|
||||
return ProposalState.Queued;
|
||||
} else if (_timelock.isOperationDone(queueid)) {
|
||||
// This can happen if the proposal is executed directly on the timelock.
|
||||
@ -73,52 +63,34 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor {
|
||||
/**
|
||||
* @dev Public accessor to check the address of the timelock
|
||||
*/
|
||||
function timelock() public view virtual override returns (address) {
|
||||
function timelock() public view virtual returns (address) {
|
||||
return address(_timelock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Public accessor to check the eta of a queued proposal
|
||||
*/
|
||||
function proposalEta(uint256 proposalId) public view virtual override returns (uint256) {
|
||||
uint256 eta = _timelock.getTimestamp(_timelockIds[proposalId]);
|
||||
return eta == 1 ? 0 : eta; // _DONE_TIMESTAMP (1) should be replaced with a 0 value
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Function to queue a proposal to the timelock.
|
||||
*/
|
||||
function queue(
|
||||
function _queueOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual override returns (uint256) {
|
||||
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
|
||||
|
||||
ProposalState currentState = state(proposalId);
|
||||
if (currentState != ProposalState.Succeeded) {
|
||||
revert GovernorUnexpectedProposalState(
|
||||
proposalId,
|
||||
currentState,
|
||||
_encodeStateBitmap(ProposalState.Succeeded)
|
||||
);
|
||||
}
|
||||
|
||||
) internal virtual override returns (uint48) {
|
||||
uint256 delay = _timelock.getMinDelay();
|
||||
|
||||
bytes32 salt = _timelockSalt(descriptionHash);
|
||||
_timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, salt);
|
||||
_timelock.scheduleBatch(targets, values, calldatas, 0, salt, delay);
|
||||
|
||||
emit ProposalQueued(proposalId, block.timestamp + delay);
|
||||
|
||||
return proposalId;
|
||||
return SafeCast.toUint48(block.timestamp + delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Overridden execute function that run the already queued proposal through the timelock.
|
||||
* @dev Overridden version of the {Governor-_executeOperations} function that runs the already queued proposal through
|
||||
* the timelock.
|
||||
*/
|
||||
function _execute(
|
||||
function _executeOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
@ -145,8 +117,8 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor {
|
||||
bytes32 descriptionHash
|
||||
) internal virtual override returns (uint256) {
|
||||
uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash);
|
||||
bytes32 timelockId = _timelockIds[proposalId];
|
||||
|
||||
bytes32 timelockId = _timelockIds[proposalId];
|
||||
if (timelockId != 0) {
|
||||
// cancel
|
||||
_timelock.cancel(timelockId);
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts v4.4.1 (governance/extensions/IGovernorTimelock.sol)
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IGovernor} from "../IGovernor.sol";
|
||||
|
||||
/**
|
||||
* @dev Extension of the {IGovernor} for timelock supporting modules.
|
||||
*/
|
||||
abstract contract IGovernorTimelock is IGovernor {
|
||||
/**
|
||||
* @dev The proposal hasn't been queued yet.
|
||||
*/
|
||||
error GovernorNotQueuedProposal(uint256 proposalId);
|
||||
|
||||
/**
|
||||
* @dev The proposal has already been queued.
|
||||
*/
|
||||
error GovernorAlreadyQueuedProposal(uint256 proposalId);
|
||||
|
||||
event ProposalQueued(uint256 proposalId, uint256 eta);
|
||||
|
||||
function timelock() public view virtual returns (address);
|
||||
|
||||
function proposalEta(uint256 proposalId) public view virtual returns (uint256);
|
||||
|
||||
function queue(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public virtual returns (uint256 proposalId);
|
||||
}
|
||||
18
contracts/mocks/AccessManagedTarget.sol
Normal file
18
contracts/mocks/AccessManagedTarget.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IGovernor, Governor} from "../../../governance/Governor.sol";
|
||||
import {GovernorCompatibilityBravo} from "../../../governance/compatibility/GovernorCompatibilityBravo.sol";
|
||||
import {GovernorCountingSimple} from "../../../governance/extensions/GovernorCountingSimple.sol";
|
||||
import {GovernorVotes} from "../../../governance/extensions/GovernorVotes.sol";
|
||||
import {GovernorVotesQuorumFraction} from "../../../governance/extensions/GovernorVotesQuorumFraction.sol";
|
||||
import {GovernorTimelockControl} from "../../../governance/extensions/GovernorTimelockControl.sol";
|
||||
@ -12,7 +12,7 @@ import {IERC165} from "../../../interfaces/IERC165.sol";
|
||||
|
||||
contract MyGovernor is
|
||||
Governor,
|
||||
GovernorCompatibilityBravo,
|
||||
GovernorCountingSimple,
|
||||
GovernorVotes,
|
||||
GovernorVotesQuorumFraction,
|
||||
GovernorTimelockControl
|
||||
@ -36,38 +36,28 @@ contract MyGovernor is
|
||||
|
||||
// The functions below are overrides required by Solidity.
|
||||
|
||||
function state(
|
||||
uint256 proposalId
|
||||
) public view override(Governor, IGovernor, GovernorTimelockControl) returns (ProposalState) {
|
||||
function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
|
||||
return super.state(proposalId);
|
||||
}
|
||||
|
||||
function propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) {
|
||||
return super.propose(targets, values, calldatas, description);
|
||||
}
|
||||
|
||||
function cancel(
|
||||
function _queueOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) {
|
||||
return super.cancel(targets, values, calldatas, descriptionHash);
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
|
||||
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _execute(
|
||||
function _executeOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) {
|
||||
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _cancel(
|
||||
@ -82,10 +72,4 @@ contract MyGovernor is
|
||||
function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
|
||||
return super._executor();
|
||||
}
|
||||
|
||||
function supportsInterface(
|
||||
bytes4 interfaceId
|
||||
) public view override(Governor, IERC165, GovernorTimelockControl) returns (bool) {
|
||||
return super.supportsInterface(interfaceId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IGovernor, Governor} from "../../governance/Governor.sol";
|
||||
import {GovernorCompatibilityBravo} from "../../governance/compatibility/GovernorCompatibilityBravo.sol";
|
||||
import {IGovernorTimelock, GovernorTimelockCompound} from "../../governance/extensions/GovernorTimelockCompound.sol";
|
||||
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
|
||||
import {GovernorVotes} from "../../governance/extensions/GovernorVotes.sol";
|
||||
import {IERC165} from "../../interfaces/IERC165.sol";
|
||||
|
||||
abstract contract GovernorCompatibilityBravoMock is
|
||||
GovernorCompatibilityBravo,
|
||||
GovernorSettings,
|
||||
GovernorTimelockCompound,
|
||||
GovernorVotes
|
||||
{
|
||||
function quorum(uint256) public pure override returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function supportsInterface(
|
||||
bytes4 interfaceId
|
||||
) public view override(IERC165, Governor, GovernorTimelockCompound) returns (bool) {
|
||||
return super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
function state(
|
||||
uint256 proposalId
|
||||
) public view override(IGovernor, Governor, GovernorTimelockCompound) returns (ProposalState) {
|
||||
return super.state(proposalId);
|
||||
}
|
||||
|
||||
function proposalEta(
|
||||
uint256 proposalId
|
||||
) public view override(IGovernorTimelock, GovernorTimelockCompound) returns (uint256) {
|
||||
return super.proposalEta(proposalId);
|
||||
}
|
||||
|
||||
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
|
||||
return super.proposalThreshold();
|
||||
}
|
||||
|
||||
function propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description
|
||||
) public override(IGovernor, Governor, GovernorCompatibilityBravo) returns (uint256) {
|
||||
return super.propose(targets, values, calldatas, description);
|
||||
}
|
||||
|
||||
function queue(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 salt
|
||||
) public override(IGovernorTimelock, GovernorTimelockCompound) returns (uint256) {
|
||||
return super.queue(targets, values, calldatas, salt);
|
||||
}
|
||||
|
||||
function execute(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 salt
|
||||
) public payable override(IGovernor, Governor) returns (uint256) {
|
||||
return super.execute(targets, values, calldatas, salt);
|
||||
}
|
||||
|
||||
function cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) {
|
||||
return super.cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _execute(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockCompound) {
|
||||
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 salt
|
||||
) internal override(Governor, GovernorTimelockCompound) returns (uint256 proposalId) {
|
||||
return super._cancel(targets, values, calldatas, salt);
|
||||
}
|
||||
|
||||
function _executor() internal view override(Governor, GovernorTimelockCompound) returns (address) {
|
||||
return super._executor();
|
||||
}
|
||||
}
|
||||
73
contracts/mocks/governance/GovernorStorageMock.sol
Normal file
73
contracts/mocks/governance/GovernorStorageMock.sol
Normal file
@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {IGovernor, Governor} from "../../governance/Governor.sol";
|
||||
import {GovernorTimelockControl} from "../../governance/extensions/GovernorTimelockControl.sol";
|
||||
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
|
||||
import {GovernorCountingSimple} from "../../governance/extensions/GovernorCountingSimple.sol";
|
||||
import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
|
||||
import {GovernorStorage} from "../../governance/extensions/GovernorStorage.sol";
|
||||
|
||||
abstract contract GovernorStorageMock is
|
||||
GovernorSettings,
|
||||
GovernorTimelockControl,
|
||||
GovernorVotesQuorumFraction,
|
||||
GovernorCountingSimple,
|
||||
GovernorStorage
|
||||
{
|
||||
function quorum(uint256 blockNumber) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) {
|
||||
return super.quorum(blockNumber);
|
||||
}
|
||||
|
||||
function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
|
||||
return super.state(proposalId);
|
||||
}
|
||||
|
||||
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
|
||||
return super.proposalThreshold();
|
||||
}
|
||||
|
||||
function _propose(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
string memory description,
|
||||
address proposer
|
||||
) internal virtual override(Governor, GovernorStorage) returns (uint256) {
|
||||
return super._propose(targets, values, calldatas, description, proposer);
|
||||
}
|
||||
|
||||
function _queueOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
|
||||
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _executeOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) {
|
||||
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
|
||||
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
|
||||
return super._executor();
|
||||
}
|
||||
}
|
||||
@ -14,15 +14,7 @@ abstract contract GovernorTimelockCompoundMock is
|
||||
GovernorVotesQuorumFraction,
|
||||
GovernorCountingSimple
|
||||
{
|
||||
function supportsInterface(
|
||||
bytes4 interfaceId
|
||||
) public view override(Governor, GovernorTimelockCompound) returns (bool) {
|
||||
return super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
function quorum(
|
||||
uint256 blockNumber
|
||||
) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) {
|
||||
function quorum(uint256 blockNumber) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) {
|
||||
return super.quorum(blockNumber);
|
||||
}
|
||||
|
||||
@ -36,23 +28,33 @@ abstract contract GovernorTimelockCompoundMock is
|
||||
return super.proposalThreshold();
|
||||
}
|
||||
|
||||
function _execute(
|
||||
function _queueOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockCompound) returns (uint48) {
|
||||
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _executeOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockCompound) {
|
||||
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 salt
|
||||
) internal override(Governor, GovernorTimelockCompound) returns (uint256 proposalId) {
|
||||
return super._cancel(targets, values, calldatas, salt);
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockCompound) returns (uint256) {
|
||||
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _executor() internal view override(Governor, GovernorTimelockCompound) returns (address) {
|
||||
|
||||
@ -14,15 +14,7 @@ abstract contract GovernorTimelockControlMock is
|
||||
GovernorVotesQuorumFraction,
|
||||
GovernorCountingSimple
|
||||
{
|
||||
function supportsInterface(
|
||||
bytes4 interfaceId
|
||||
) public view override(Governor, GovernorTimelockControl) returns (bool) {
|
||||
return super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
function quorum(
|
||||
uint256 blockNumber
|
||||
) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) {
|
||||
function quorum(uint256 blockNumber) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) {
|
||||
return super.quorum(blockNumber);
|
||||
}
|
||||
|
||||
@ -34,14 +26,24 @@ abstract contract GovernorTimelockControlMock is
|
||||
return super.proposalThreshold();
|
||||
}
|
||||
|
||||
function _execute(
|
||||
function _queueOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
|
||||
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _executeOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) {
|
||||
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
function _cancel(
|
||||
@ -49,7 +51,7 @@ abstract contract GovernorTimelockControlMock is
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint256 proposalId) {
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
|
||||
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
|
||||
|
||||
131
contracts/utils/types/Time.sol
Normal file
131
contracts/utils/types/Time.sol
Normal file
@ -0,0 +1,131 @@
|
||||
// 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 {updateAt} 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 the Delay will be at a given timepoint
|
||||
*/
|
||||
function getAt(Delay self, uint48 timepoint) internal pure returns (uint32) {
|
||||
(uint32 oldValue, uint32 newValue, uint48 effect) = self.split();
|
||||
return (effect == 0 || effect > timepoint) ? oldValue : newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the current value.
|
||||
*/
|
||||
function get(Delay self) internal view returns (uint32) {
|
||||
return self.getAt(timestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the pending value, and effect timepoint. If the effect timepoint is 0, then the pending value should
|
||||
* not be considered.
|
||||
*/
|
||||
function getPending(Delay self) internal pure returns (uint32, uint48) {
|
||||
(, uint32 newValue, uint48 effect) = self.split();
|
||||
return (newValue, effect);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update a Delay object so that a new duration takes effect at a given timepoint.
|
||||
*/
|
||||
function updateAt(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 update(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.updateAt(newValue, timestamp() + setback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Split a delay into its components: oldValue, newValue and effect (transition timepoint).
|
||||
*/
|
||||
function split(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));
|
||||
}
|
||||
}
|
||||
@ -18,11 +18,11 @@ OpenZeppelin’s Governor system was designed with a concern for compatibility w
|
||||
|
||||
The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only.
|
||||
|
||||
=== Governor & GovernorCompatibilityBravo
|
||||
=== Governor & GovernorStorage
|
||||
|
||||
An OpenZeppelin Governor contract is by default not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha are Bravo are likewise not available. It’s possible to opt in to a higher level of compatibility by inheriting from the GovernorCompatibilityBravo module, which covers the proposal lifecycle functions such as `propose` and `execute`.
|
||||
An OpenZeppelin Governor contract is not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha are Bravo are likewise not available. It’s possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased the storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends.
|
||||
|
||||
Note that even with the use of this module, there will still be differences in the way that `proposalId`s are calculated. Governor uses the hash of the proposal parameters with the purpose of keeping its data off-chain by event indexing, while the original Bravo implementation uses sequential `proposalId`s. Due to this and other differences, several of the functions from GovernorBravo are not included in the compatibility module.
|
||||
Note that even with the use of this module, one important difference with Compound's GovernorBravo is the way that `proposalId`s are calculated. Governor uses the hash of the proposal parameters with the purpose of keeping its data off-chain by event indexing, while the original Bravo implementation uses sequential `proposalId`s.
|
||||
|
||||
=== GovernorTimelockControl & GovernorTimelockCompound
|
||||
|
||||
@ -201,13 +201,14 @@ The Governor will automatically detect the clock mode used by the token and adap
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Governor} from "@openzeppelin/contracts/governance/Governor.sol";
|
||||
import {GovernorCompatibilityBravo} from "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol";
|
||||
import {GovernorCountingSimple} from "@openzeppelin/contracts/governance/compatibility/GovernorCountingSimple.sol";
|
||||
import {GovernorVotes} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
|
||||
import {GovernorVotesQuorumFraction} from "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
|
||||
import {GovernorTimelockControl} from "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
|
||||
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
|
||||
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
||||
|
||||
contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
|
||||
contract MyGovernor is Governor, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
|
||||
constructor(IVotes _token, TimelockController _timelock)
|
||||
Governor("MyGovernor")
|
||||
GovernorVotes(_token)
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -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"
|
||||
|
||||
927
test/access/manager/AccessManager.test.js
Normal file
927
test/access/manager/AccessManager.test.js
Normal file
@ -0,0 +1,927 @@
|
||||
const { expectEvent, constants, time } = require('@openzeppelin/test-helpers');
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
const { AccessMode } = require('../../helpers/enums');
|
||||
const { selector } = require('../../helpers/methods');
|
||||
const { clockFromReceipt } = require('../../helpers/time');
|
||||
|
||||
const AccessManager = artifacts.require('$AccessManager');
|
||||
const AccessManagedTarget = artifacts.require('$AccessManagedTarget');
|
||||
|
||||
const GROUPS = {
|
||||
ADMIN: web3.utils.toBN(0),
|
||||
SOME_ADMIN: web3.utils.toBN(17),
|
||||
SOME: web3.utils.toBN(42),
|
||||
PUBLIC: constants.MAX_UINT256,
|
||||
};
|
||||
Object.assign(GROUPS, Object.fromEntries(Object.entries(GROUPS).map(([key, value]) => [value, key])));
|
||||
|
||||
const executeDelay = web3.utils.toBN(10);
|
||||
const grantDelay = web3.utils.toBN(10);
|
||||
|
||||
const MAX_UINT = n => web3.utils.toBN(1).shln(n).subn(1);
|
||||
|
||||
const split = delay => ({
|
||||
oldValue: web3.utils.toBN(delay).shrn(0).and(MAX_UINT(32)).toString(),
|
||||
newValue: web3.utils.toBN(delay).shrn(32).and(MAX_UINT(32)).toString(),
|
||||
effect: web3.utils.toBN(delay).shrn(64).and(MAX_UINT(48)).toString(),
|
||||
});
|
||||
|
||||
contract('AccessManager', function (accounts) {
|
||||
const [admin, manager, member, user, other] = accounts;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.manager = await AccessManager.new(admin);
|
||||
this.target = await AccessManagedTarget.new(this.manager.address);
|
||||
|
||||
// add member to group
|
||||
await this.manager.$_setGroupAdmin(GROUPS.SOME, GROUPS.SOME_ADMIN);
|
||||
await this.manager.$_setGroupGuardian(GROUPS.SOME, GROUPS.SOME_ADMIN);
|
||||
await this.manager.$_grantGroup(GROUPS.SOME_ADMIN, manager, 0, 0);
|
||||
await this.manager.$_grantGroup(GROUPS.SOME, member, 0, 0);
|
||||
|
||||
// helpers for indirect calls
|
||||
this.call = [this.target.address, selector('fnRestricted()')];
|
||||
this.opId = web3.utils.keccak256(
|
||||
web3.eth.abi.encodeParameters(['address', 'address', 'bytes'], [user, ...this.call]),
|
||||
);
|
||||
this.schedule = (opts = {}) => this.manager.schedule(...this.call, { from: user, ...opts });
|
||||
this.relay = (opts = {}) => this.manager.relay(...this.call, { from: user, ...opts });
|
||||
this.cancel = (opts = {}) => this.manager.cancel(user, ...this.call, { from: user, ...opts });
|
||||
});
|
||||
|
||||
it('groups are correctly initialized', async function () {
|
||||
// group admin
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME_ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN);
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.PUBLIC)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
// group guardian
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME_ADMIN)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN);
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.PUBLIC)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
// group members
|
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, admin)).to.be.equal(true);
|
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, manager)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, member)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.ADMIN, user)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, admin)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, manager)).to.be.equal(true);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, member)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME_ADMIN, user)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, admin)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, manager)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.equal(true);
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.equal(false);
|
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, admin)).to.be.equal(true);
|
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, manager)).to.be.equal(true);
|
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, member)).to.be.equal(true);
|
||||
expect(await this.manager.hasGroup(GROUPS.PUBLIC, user)).to.be.equal(true);
|
||||
});
|
||||
|
||||
describe('Groups management', function () {
|
||||
describe('label group', function () {
|
||||
it('admin can emit a label event', async function () {
|
||||
expectEvent(await this.manager.labelGroup(GROUPS.SOME, 'Some label', { from: admin }), 'GroupLabel', {
|
||||
groupId: GROUPS.SOME,
|
||||
label: 'Some label',
|
||||
});
|
||||
});
|
||||
|
||||
it('admin can re-emit a label event', async function () {
|
||||
await this.manager.labelGroup(GROUPS.SOME, 'Some label', { from: admin });
|
||||
|
||||
expectEvent(await this.manager.labelGroup(GROUPS.SOME, 'Updated label', { from: admin }), 'GroupLabel', {
|
||||
groupId: GROUPS.SOME,
|
||||
label: 'Updated label',
|
||||
});
|
||||
});
|
||||
|
||||
it('emitting a label is restricted', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.labelGroup(GROUPS.SOME, 'Invalid label', { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[other, GROUPS.ADMIN],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('grand group', function () {
|
||||
describe('without a grant delay', function () {
|
||||
it('without an execute delay', async function () {
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager });
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
expectEvent(receipt, 'GroupGranted', { groupId: GROUPS.SOME, account: user, since: timestamp, delay: '0' });
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.true;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user);
|
||||
expect(delay).to.be.bignumber.equal('0');
|
||||
expect(since).to.be.bignumber.equal(timestamp);
|
||||
});
|
||||
|
||||
it('with an execute delay', async function () {
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, executeDelay, { from: manager });
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
expectEvent(receipt, 'GroupGranted', {
|
||||
groupId: GROUPS.SOME,
|
||||
account: user,
|
||||
since: timestamp,
|
||||
delay: executeDelay,
|
||||
});
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.true;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user);
|
||||
expect(delay).to.be.bignumber.equal(executeDelay);
|
||||
expect(since).to.be.bignumber.equal(timestamp);
|
||||
});
|
||||
|
||||
it('to a user that is already in the group', async function () {
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.true;
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.manager.grantGroup(GROUPS.SOME, member, 0, { from: manager }),
|
||||
'AccessManagerAcountAlreadyInGroup',
|
||||
[GROUPS.SOME, member],
|
||||
);
|
||||
});
|
||||
|
||||
it('to a user that is scheduled for joining the group', async function () {
|
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 10, 0); // grant delay 10
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager }),
|
||||
'AccessManagerAcountAlreadyInGroup',
|
||||
[GROUPS.SOME, user],
|
||||
);
|
||||
});
|
||||
|
||||
it('grant group is restricted', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.grantGroup(GROUPS.SOME, user, 0, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[other, GROUPS.SOME_ADMIN],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a grant delay', function () {
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setGrantDelay(GROUPS.SOME, grantDelay);
|
||||
});
|
||||
|
||||
it('granted group is not active immediatly', async function () {
|
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager });
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
expectEvent(receipt, 'GroupGranted', {
|
||||
groupId: GROUPS.SOME,
|
||||
account: user,
|
||||
since: timestamp.add(grantDelay),
|
||||
delay: '0',
|
||||
});
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user);
|
||||
expect(delay).to.be.bignumber.equal('0');
|
||||
expect(since).to.be.bignumber.equal(timestamp.add(grantDelay));
|
||||
});
|
||||
|
||||
it('granted group is active after the delay', async function () {
|
||||
const { receipt } = await this.manager.grantGroup(GROUPS.SOME, user, 0, { from: manager });
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
expectEvent(receipt, 'GroupGranted', {
|
||||
groupId: GROUPS.SOME,
|
||||
account: user,
|
||||
since: timestamp.add(grantDelay),
|
||||
delay: '0',
|
||||
});
|
||||
|
||||
await time.increase(grantDelay);
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.true;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user);
|
||||
expect(delay).to.be.bignumber.equal('0');
|
||||
expect(since).to.be.bignumber.equal(timestamp.add(grantDelay));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('revoke group', function () {
|
||||
it('from a user that is already in the group', async function () {
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.true;
|
||||
|
||||
const { receipt } = await this.manager.revokeGroup(GROUPS.SOME, member, { from: manager });
|
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: member });
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.false;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user);
|
||||
expect(delay).to.be.bignumber.equal('0');
|
||||
expect(since).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('from a user that is scheduled for joining the group', async function () {
|
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 10, 0); // grant delay 10
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
const { receipt } = await this.manager.revokeGroup(GROUPS.SOME, user, { from: manager });
|
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: user });
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user);
|
||||
expect(delay).to.be.bignumber.equal('0');
|
||||
expect(since).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('from a user that is not in the group', async function () {
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.manager.revokeGroup(GROUPS.SOME, user, { from: manager }),
|
||||
'AccessManagerAcountNotInGroup',
|
||||
[GROUPS.SOME, user],
|
||||
);
|
||||
});
|
||||
|
||||
it('revoke group is restricted', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.revokeGroup(GROUPS.SOME, member, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[other, GROUPS.SOME_ADMIN],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renounce group', function () {
|
||||
it('for a user that is already in the group', async function () {
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.true;
|
||||
|
||||
const { receipt } = await this.manager.renounceGroup(GROUPS.SOME, member, { from: member });
|
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: member });
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, member)).to.be.false;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, member);
|
||||
expect(delay).to.be.bignumber.equal('0');
|
||||
expect(since).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('for a user that is schedule for joining the group', async function () {
|
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 10, 0); // grant delay 10
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
const { receipt } = await this.manager.renounceGroup(GROUPS.SOME, user, { from: user });
|
||||
expectEvent(receipt, 'GroupRevoked', { groupId: GROUPS.SOME, account: user });
|
||||
|
||||
expect(await this.manager.hasGroup(GROUPS.SOME, user)).to.be.false;
|
||||
|
||||
const { delay, since } = await this.manager.getAccess(GROUPS.SOME, user);
|
||||
expect(delay).to.be.bignumber.equal('0');
|
||||
expect(since).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('for a user that is not in the group', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.renounceGroup(GROUPS.SOME, user, { from: user }),
|
||||
'AccessManagerAcountNotInGroup',
|
||||
[GROUPS.SOME, user],
|
||||
);
|
||||
});
|
||||
|
||||
it('bad user confirmation', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.renounceGroup(GROUPS.SOME, member, { from: user }),
|
||||
'AccessManagerBadConfirmation',
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change group admin', function () {
|
||||
it("admin can set any group's admin", async function () {
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN);
|
||||
|
||||
const { receipt } = await this.manager.setGroupAdmin(GROUPS.SOME, GROUPS.ADMIN, { from: admin });
|
||||
expectEvent(receipt, 'GroupAdminChanged', { groupId: GROUPS.SOME, admin: GROUPS.ADMIN });
|
||||
|
||||
expect(await this.manager.getGroupAdmin(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
});
|
||||
|
||||
it("seeting a group's admin is restricted", async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.setGroupAdmin(GROUPS.SOME, GROUPS.SOME, { from: manager }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[manager, GROUPS.ADMIN],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change group guardian', function () {
|
||||
it("admin can set any group's admin", async function () {
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.SOME_ADMIN);
|
||||
|
||||
const { receipt } = await this.manager.setGroupGuardian(GROUPS.SOME, GROUPS.ADMIN, { from: admin });
|
||||
expectEvent(receipt, 'GroupGuardianChanged', { groupId: GROUPS.SOME, guardian: GROUPS.ADMIN });
|
||||
|
||||
expect(await this.manager.getGroupGuardian(GROUPS.SOME)).to.be.bignumber.equal(GROUPS.ADMIN);
|
||||
});
|
||||
|
||||
it("setting a group's admin is restricted", async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.setGroupGuardian(GROUPS.SOME, GROUPS.SOME, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[other, GROUPS.ADMIN],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change execution delay', function () {
|
||||
it('increassing the delay has immediate effect', async function () {
|
||||
const oldDelay = web3.utils.toBN(10);
|
||||
const newDelay = web3.utils.toBN(100);
|
||||
|
||||
const { receipt: receipt1 } = await this.manager.$_setExecuteDelay(GROUPS.SOME, member, oldDelay);
|
||||
const timestamp1 = await clockFromReceipt.timestamp(receipt1).then(web3.utils.toBN);
|
||||
|
||||
const delayBefore = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay));
|
||||
expect(delayBefore.oldValue).to.be.bignumber.equal('0');
|
||||
expect(delayBefore.newValue).to.be.bignumber.equal(oldDelay);
|
||||
expect(delayBefore.effect).to.be.bignumber.equal(timestamp1);
|
||||
|
||||
const { receipt: receipt2 } = await this.manager.setExecuteDelay(GROUPS.SOME, member, newDelay, {
|
||||
from: manager,
|
||||
});
|
||||
const timestamp2 = await clockFromReceipt.timestamp(receipt2).then(web3.utils.toBN);
|
||||
|
||||
expectEvent(receipt2, 'GroupExecutionDelayUpdate', {
|
||||
groupId: GROUPS.SOME,
|
||||
account: member,
|
||||
delay: newDelay,
|
||||
from: timestamp2,
|
||||
});
|
||||
|
||||
// immediate effect
|
||||
const delayAfter = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay));
|
||||
expect(delayAfter.oldValue).to.be.bignumber.equal(oldDelay);
|
||||
expect(delayAfter.newValue).to.be.bignumber.equal(newDelay);
|
||||
expect(delayAfter.effect).to.be.bignumber.equal(timestamp2);
|
||||
});
|
||||
|
||||
it('decreassing the delay takes time', async function () {
|
||||
const oldDelay = web3.utils.toBN(100);
|
||||
const newDelay = web3.utils.toBN(10);
|
||||
|
||||
const { receipt: receipt1 } = await this.manager.$_setExecuteDelay(GROUPS.SOME, member, oldDelay);
|
||||
const timestamp1 = await clockFromReceipt.timestamp(receipt1).then(web3.utils.toBN);
|
||||
|
||||
const delayBefore = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay));
|
||||
expect(delayBefore.oldValue).to.be.bignumber.equal('0');
|
||||
expect(delayBefore.newValue).to.be.bignumber.equal(oldDelay);
|
||||
expect(delayBefore.effect).to.be.bignumber.equal(timestamp1);
|
||||
|
||||
const { receipt: receipt2 } = await this.manager.setExecuteDelay(GROUPS.SOME, member, newDelay, {
|
||||
from: manager,
|
||||
});
|
||||
const timestamp2 = await clockFromReceipt.timestamp(receipt2).then(web3.utils.toBN);
|
||||
|
||||
expectEvent(receipt2, 'GroupExecutionDelayUpdate', {
|
||||
groupId: GROUPS.SOME,
|
||||
account: member,
|
||||
delay: newDelay,
|
||||
from: timestamp2.add(oldDelay).sub(newDelay),
|
||||
});
|
||||
|
||||
// delayed effect
|
||||
const delayAfter = await this.manager.getAccess(GROUPS.SOME, member).then(([, delay]) => split(delay));
|
||||
|
||||
expect(delayAfter.oldValue).to.be.bignumber.equal(oldDelay);
|
||||
expect(delayAfter.newValue).to.be.bignumber.equal(newDelay);
|
||||
expect(delayAfter.effect).to.be.bignumber.equal(timestamp2.add(oldDelay).sub(newDelay));
|
||||
});
|
||||
|
||||
it('cannot set the delay of a non member', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.setExecuteDelay(GROUPS.SOME, other, executeDelay, { from: manager }),
|
||||
'AccessManagerAcountNotInGroup',
|
||||
[GROUPS.SOME, other],
|
||||
);
|
||||
});
|
||||
|
||||
it('can set a user execution delay during the grant delay', async function () {
|
||||
await this.manager.$_grantGroup(GROUPS.SOME, other, 10, 0);
|
||||
|
||||
const { receipt } = await this.manager.setExecuteDelay(GROUPS.SOME, other, executeDelay, { from: manager });
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
|
||||
expectEvent(receipt, 'GroupExecutionDelayUpdate', {
|
||||
groupId: GROUPS.SOME,
|
||||
account: other,
|
||||
delay: executeDelay,
|
||||
from: timestamp,
|
||||
});
|
||||
});
|
||||
|
||||
it('changing the execution delay is restricted', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.setExecuteDelay(GROUPS.SOME, member, executeDelay, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[GROUPS.SOME_ADMIN, other],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change grant delay', function () {
|
||||
it('increassing the delay has immediate effect', async function () {
|
||||
const oldDelay = web3.utils.toBN(10);
|
||||
const newDelay = web3.utils.toBN(100);
|
||||
await this.manager.$_setGrantDelay(GROUPS.SOME, oldDelay);
|
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(oldDelay);
|
||||
|
||||
const { receipt } = await this.manager.setGrantDelay(GROUPS.SOME, newDelay, { from: admin });
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
|
||||
expectEvent(receipt, 'GroupGrantDelayChanged', { groupId: GROUPS.SOME, delay: newDelay, from: timestamp });
|
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(newDelay);
|
||||
});
|
||||
|
||||
it('increassing the delay has delay effect', async function () {
|
||||
const oldDelay = web3.utils.toBN(100);
|
||||
const newDelay = web3.utils.toBN(10);
|
||||
await this.manager.$_setGrantDelay(GROUPS.SOME, oldDelay);
|
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(oldDelay);
|
||||
|
||||
const { receipt } = await this.manager.setGrantDelay(GROUPS.SOME, newDelay, { from: admin });
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
|
||||
expectEvent(receipt, 'GroupGrantDelayChanged', {
|
||||
groupId: GROUPS.SOME,
|
||||
delay: newDelay,
|
||||
from: timestamp.add(oldDelay).sub(newDelay),
|
||||
});
|
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(oldDelay);
|
||||
|
||||
await time.increase(oldDelay.sub(newDelay));
|
||||
|
||||
expect(await this.manager.getGroupGrantDelay(GROUPS.SOME)).to.be.bignumber.equal(newDelay);
|
||||
});
|
||||
|
||||
it('changing the grant delay is restricted', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.setGrantDelay(GROUPS.SOME, grantDelay, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[GROUPS.ADMIN, other],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mode management', function () {
|
||||
for (const [modeName, mode] of Object.entries(AccessMode)) {
|
||||
describe(`setContractMode${modeName}`, function () {
|
||||
it('set the mode and emits an event', async function () {
|
||||
// set the target to another mode, so we can check the effects
|
||||
await this.manager.$_setContractMode(
|
||||
this.target.address,
|
||||
Object.values(AccessMode).find(m => m != mode),
|
||||
);
|
||||
|
||||
expect(await this.manager.getContractMode(this.target.address)).to.not.be.bignumber.equal(mode);
|
||||
|
||||
expectEvent(
|
||||
await this.manager[`setContractMode${modeName}`](this.target.address, { from: admin }),
|
||||
'AccessModeUpdated',
|
||||
{ target: this.target.address, mode },
|
||||
);
|
||||
|
||||
expect(await this.manager.getContractMode(this.target.address)).to.be.bignumber.equal(mode);
|
||||
});
|
||||
|
||||
it('is restricted', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager[`setContractMode${modeName}`](this.target.address, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[other, GROUPS.ADMIN],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Change function permissions', function () {
|
||||
const sigs = ['someFunction()', 'someOtherFunction(uint256)', 'oneMoreFunction(address,uint8)'].map(selector);
|
||||
|
||||
it('admin can set function allowed group', async function () {
|
||||
for (const sig of sigs) {
|
||||
expect(await this.manager.getFunctionAllowedGroup(this.target.address, sig)).to.be.bignumber.equal(
|
||||
GROUPS.ADMIN,
|
||||
);
|
||||
}
|
||||
|
||||
const { receipt: receipt1 } = await this.manager.setFunctionAllowedGroup(this.target.address, sigs, GROUPS.SOME, {
|
||||
from: admin,
|
||||
});
|
||||
|
||||
for (const sig of sigs) {
|
||||
expectEvent(receipt1, 'FunctionAllowedGroupUpdated', {
|
||||
target: this.target.address,
|
||||
selector: sig,
|
||||
groupId: GROUPS.SOME,
|
||||
});
|
||||
expect(await this.manager.getFunctionAllowedGroup(this.target.address, sig)).to.be.bignumber.equal(GROUPS.SOME);
|
||||
}
|
||||
|
||||
const { receipt: receipt2 } = await this.manager.setFunctionAllowedGroup(
|
||||
this.target.address,
|
||||
[sigs[1]],
|
||||
GROUPS.SOME_ADMIN,
|
||||
{ from: admin },
|
||||
);
|
||||
expectEvent(receipt2, 'FunctionAllowedGroupUpdated', {
|
||||
target: this.target.address,
|
||||
selector: sigs[1],
|
||||
groupId: GROUPS.SOME_ADMIN,
|
||||
});
|
||||
|
||||
for (const sig of sigs) {
|
||||
expect(await this.manager.getFunctionAllowedGroup(this.target.address, sig)).to.be.bignumber.equal(
|
||||
sig == sigs[1] ? GROUPS.SOME_ADMIN : GROUPS.SOME,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('changing function permissions is restricted', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.setFunctionAllowedGroup(this.target.address, sigs, GROUPS.SOME, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[other, GROUPS.ADMIN],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Calling restricted & unrestricted functions', function () {
|
||||
const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [ai, bi].flat())));
|
||||
|
||||
for (const [callerOpt, targetOpt] of product(
|
||||
[
|
||||
{ groups: [] },
|
||||
{ groups: [GROUPS.SOME] },
|
||||
{ groups: [GROUPS.SOME], delay: executeDelay },
|
||||
{ groups: [GROUPS.SOME, GROUPS.PUBLIC], delay: executeDelay },
|
||||
],
|
||||
[
|
||||
{ mode: AccessMode.Open },
|
||||
{ mode: AccessMode.Closed },
|
||||
{ mode: AccessMode.Custom, group: GROUPS.ADMIN },
|
||||
{ mode: AccessMode.Custom, group: GROUPS.SOME },
|
||||
{ mode: AccessMode.Custom, group: GROUPS.PUBLIC },
|
||||
],
|
||||
)) {
|
||||
const public =
|
||||
targetOpt.mode == AccessMode.Open || (targetOpt.mode == AccessMode.Custom && targetOpt.group == GROUPS.PUBLIC);
|
||||
|
||||
// can we call with a delay ?
|
||||
const indirectSuccess =
|
||||
public || (targetOpt.mode == AccessMode.Custom && callerOpt.groups?.includes(targetOpt.group));
|
||||
|
||||
// can we call without a delay ?
|
||||
const directSuccess =
|
||||
public ||
|
||||
(targetOpt.mode == AccessMode.Custom && callerOpt.groups?.includes(targetOpt.group) && !callerOpt.delay);
|
||||
|
||||
const description = [
|
||||
'Caller in groups',
|
||||
'[' + (callerOpt.groups ?? []).map(groupId => GROUPS[groupId]).join(', ') + ']',
|
||||
callerOpt.delay ? 'with a delay' : 'without a delay',
|
||||
'+',
|
||||
'contract in mode',
|
||||
Object.keys(AccessMode)[targetOpt.mode.toNumber()],
|
||||
targetOpt.mode == AccessMode.Custom ? `(${GROUPS[targetOpt.group]})` : '',
|
||||
].join(' ');
|
||||
|
||||
describe(description, function () {
|
||||
beforeEach(async function () {
|
||||
// setup
|
||||
await Promise.all([
|
||||
this.manager.$_setContractMode(this.target.address, targetOpt.mode),
|
||||
targetOpt.group &&
|
||||
this.manager.$_setFunctionAllowedGroup(this.target.address, selector('fnRestricted()'), targetOpt.group),
|
||||
targetOpt.group &&
|
||||
this.manager.$_setFunctionAllowedGroup(
|
||||
this.target.address,
|
||||
selector('fnUnrestricted()'),
|
||||
targetOpt.group,
|
||||
),
|
||||
...(callerOpt.groups ?? [])
|
||||
.filter(groupId => groupId != GROUPS.PUBLIC)
|
||||
.map(groupId => this.manager.$_grantGroup(groupId, user, 0, callerOpt.delay ?? 0)),
|
||||
]);
|
||||
|
||||
// post setup checks
|
||||
expect(await this.manager.getContractMode(this.target.address)).to.be.bignumber.equal(targetOpt.mode);
|
||||
if (targetOpt.group) {
|
||||
expect(
|
||||
await this.manager.getFunctionAllowedGroup(this.target.address, selector('fnRestricted()')),
|
||||
).to.be.bignumber.equal(targetOpt.group);
|
||||
expect(
|
||||
await this.manager.getFunctionAllowedGroup(this.target.address, selector('fnUnrestricted()')),
|
||||
).to.be.bignumber.equal(targetOpt.group);
|
||||
}
|
||||
for (const groupId of callerOpt.groups ?? []) {
|
||||
const access = await this.manager.getAccess(groupId, user);
|
||||
if (groupId == GROUPS.PUBLIC) {
|
||||
expect(access.since).to.be.bignumber.eq('0');
|
||||
expect(access.delay).to.be.bignumber.eq('0');
|
||||
} else {
|
||||
expect(access.since).to.be.bignumber.gt('0');
|
||||
expect(access.delay).to.be.bignumber.eq(String(callerOpt.delay ?? 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('canCall', async function () {
|
||||
const result = await this.manager.canCall(user, this.target.address, selector('fnRestricted()'));
|
||||
expect(result[0]).to.be.equal(directSuccess);
|
||||
expect(result[1]).to.be.bignumber.equal(!directSuccess && indirectSuccess ? callerOpt.delay ?? '0' : '0');
|
||||
});
|
||||
|
||||
it('Calling a non restricted function never revert', async function () {
|
||||
expectEvent(await this.target.fnUnrestricted({ from: user }), 'CalledUnrestricted', {
|
||||
caller: user,
|
||||
});
|
||||
});
|
||||
|
||||
it(`Calling a restricted function directly should ${directSuccess ? 'succeed' : 'revert'}`, async function () {
|
||||
const promise = this.target.fnRestricted({ from: user });
|
||||
|
||||
if (directSuccess) {
|
||||
expectEvent(await promise, 'CalledRestricted', { caller: user });
|
||||
} else {
|
||||
await expectRevertCustomError(promise, 'AccessManagedUnauthorized', [user]);
|
||||
}
|
||||
});
|
||||
|
||||
it('Calling indirectly: only relay', async function () {
|
||||
// relay without schedule
|
||||
if (directSuccess) {
|
||||
const { receipt, tx } = await this.relay();
|
||||
expectEvent.notEmitted(receipt, 'Executed', { operationId: this.opId });
|
||||
expectEvent.inTransaction(tx, this.target, 'Calledrestricted', { caller: this.manager.address });
|
||||
} else if (indirectSuccess) {
|
||||
await expectRevertCustomError(this.relay(), 'AccessManagerNotScheduled', [this.opId]);
|
||||
} else {
|
||||
await expectRevertCustomError(this.relay(), 'AccessManagerUnauthorizedCall', [user, ...this.call]);
|
||||
}
|
||||
});
|
||||
|
||||
it('Calling indirectly: schedule and relay', async function () {
|
||||
if (directSuccess || indirectSuccess) {
|
||||
const { receipt } = await this.schedule();
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
|
||||
expectEvent(receipt, 'Scheduled', {
|
||||
operationId: this.opId,
|
||||
caller: user,
|
||||
target: this.call[0],
|
||||
data: this.call[1],
|
||||
});
|
||||
|
||||
// if can call directly, delay should be 0. Otherwize, the delay should be applied
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(
|
||||
timestamp.add(directSuccess ? web3.utils.toBN(0) : callerOpt.delay),
|
||||
);
|
||||
|
||||
// execute without wait
|
||||
if (directSuccess) {
|
||||
const { receipt, tx } = await this.relay();
|
||||
expectEvent(receipt, 'Executed', { operationId: this.opId });
|
||||
expectEvent.inTransaction(tx, this.target, 'Calledrestricted', { caller: this.manager.address });
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0');
|
||||
} else if (indirectSuccess) {
|
||||
await expectRevertCustomError(this.relay(), 'AccessManagerNotReady', [this.opId]);
|
||||
} else {
|
||||
await expectRevertCustomError(this.relay(), 'AccessManagerUnauthorizedCall', [user, ...this.call]);
|
||||
}
|
||||
} else {
|
||||
await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]);
|
||||
}
|
||||
});
|
||||
|
||||
it('Calling indirectly: schedule wait and relay', async function () {
|
||||
if (directSuccess || indirectSuccess) {
|
||||
const { receipt } = await this.schedule();
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
|
||||
expectEvent(receipt, 'Scheduled', {
|
||||
operationId: this.opId,
|
||||
caller: user,
|
||||
target: this.call[0],
|
||||
data: this.call[1],
|
||||
});
|
||||
|
||||
// if can call directly, delay should be 0. Otherwize, the delay should be applied
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(
|
||||
timestamp.add(directSuccess ? web3.utils.toBN(0) : callerOpt.delay),
|
||||
);
|
||||
|
||||
// wait
|
||||
await time.increase(callerOpt.delay ?? 0);
|
||||
|
||||
// execute without wait
|
||||
if (directSuccess || indirectSuccess) {
|
||||
const { receipt, tx } = await this.relay();
|
||||
expectEvent(receipt, 'Executed', { operationId: this.opId });
|
||||
expectEvent.inTransaction(tx, this.target, 'Calledrestricted', { caller: this.manager.address });
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0');
|
||||
} else {
|
||||
await expectRevertCustomError(this.relay(), 'AccessManagerUnauthorizedCall', [user, ...this.call]);
|
||||
}
|
||||
} else {
|
||||
await expectRevertCustomError(this.schedule(), 'AccessManagerUnauthorizedCall', [user, ...this.call]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Indirect execution corner-cases', async function () {
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setFunctionAllowedGroup(...this.call, GROUPS.SOME);
|
||||
await this.manager.$_grantGroup(GROUPS.SOME, user, 0, executeDelay);
|
||||
});
|
||||
|
||||
it('Checking canCall when caller is the manager depend on the _relayIdentifier', async function () {
|
||||
expect(await this.manager.getContractMode(this.target.address)).to.be.bignumber.equal(AccessMode.Custom);
|
||||
|
||||
const result = await this.manager.canCall(this.manager.address, this.target.address, '0x00000000');
|
||||
expect(result[0]).to.be.false;
|
||||
expect(result[1]).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('Cannot execute earlier', async function () {
|
||||
const { receipt } = await this.schedule();
|
||||
const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN);
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal(timestamp.add(executeDelay));
|
||||
|
||||
// we need to set the clock 2 seconds before the value, because the increaseTo "consumes" the timestamp
|
||||
// and the next transaction will be one after that (see check bellow)
|
||||
await time.increaseTo(timestamp.add(executeDelay).subn(2));
|
||||
|
||||
// too early
|
||||
await expectRevertCustomError(this.relay(), 'AccessManagerNotReady', [this.opId]);
|
||||
|
||||
// the revert happened one second before the execution delay expired
|
||||
expect(await time.latest()).to.be.bignumber.equal(timestamp.add(executeDelay).subn(1));
|
||||
|
||||
// ok
|
||||
await this.relay();
|
||||
|
||||
// the success happened when the delay was reached (earliest possible)
|
||||
expect(await time.latest()).to.be.bignumber.equal(timestamp.add(executeDelay));
|
||||
});
|
||||
|
||||
it('Cannot schedule an already scheduled operation', async function () {
|
||||
const { receipt } = await this.schedule();
|
||||
expectEvent(receipt, 'Scheduled', {
|
||||
operationId: this.opId,
|
||||
caller: user,
|
||||
target: this.call[0],
|
||||
data: this.call[1],
|
||||
});
|
||||
|
||||
await expectRevertCustomError(this.schedule(), 'AccessManagerAlreadyScheduled', [this.opId]);
|
||||
});
|
||||
|
||||
it('Cannot cancel an operation that is not scheduled', async function () {
|
||||
await expectRevertCustomError(this.cancel(), 'AccessManagerNotScheduled', [this.opId]);
|
||||
});
|
||||
|
||||
it('Cannot cancel an operation that is not already relayed', async function () {
|
||||
await this.schedule();
|
||||
await time.increase(executeDelay);
|
||||
await this.relay();
|
||||
|
||||
await expectRevertCustomError(this.cancel(), 'AccessManagerNotScheduled', [this.opId]);
|
||||
});
|
||||
|
||||
it('Scheduler can cancel', async function () {
|
||||
await this.schedule();
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0');
|
||||
|
||||
expectEvent(await this.cancel({ from: manager }), 'Canceled', { operationId: this.opId });
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('Guardian can cancel', async function () {
|
||||
await this.schedule();
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0');
|
||||
|
||||
expectEvent(await this.cancel({ from: manager }), 'Canceled', { operationId: this.opId });
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('Cancel is restricted', async function () {
|
||||
await this.schedule();
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0');
|
||||
|
||||
await expectRevertCustomError(this.cancel({ from: other }), 'AccessManagerCannotCancel', [
|
||||
other,
|
||||
user,
|
||||
...this.call,
|
||||
]);
|
||||
|
||||
expect(await this.manager.getSchedule(this.opId)).to.not.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('Can re-schedule after execution', async function () {
|
||||
await this.schedule();
|
||||
await time.increase(executeDelay);
|
||||
await this.relay();
|
||||
|
||||
// reschedule
|
||||
const { receipt } = await this.schedule();
|
||||
expectEvent(receipt, 'Scheduled', {
|
||||
operationId: this.opId,
|
||||
caller: user,
|
||||
target: this.call[0],
|
||||
data: this.call[1],
|
||||
});
|
||||
});
|
||||
|
||||
it('Can re-schedule after cancel', async function () {
|
||||
await this.schedule();
|
||||
await this.cancel();
|
||||
|
||||
// reschedule
|
||||
const { receipt } = await this.schedule();
|
||||
expectEvent(receipt, 'Scheduled', {
|
||||
operationId: this.opId,
|
||||
caller: user,
|
||||
target: this.call[0],
|
||||
data: this.call[1],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('authority update', function () {
|
||||
beforeEach(async function () {
|
||||
this.newManager = await AccessManager.new(admin);
|
||||
});
|
||||
|
||||
it('admin can change authority', async function () {
|
||||
expect(await this.target.authority()).to.be.equal(this.manager.address);
|
||||
|
||||
const { tx } = await this.manager.updateAuthority(this.target.address, this.newManager.address, { from: admin });
|
||||
expectEvent.inTransaction(tx, this.target, 'AuthorityUpdated', { authority: this.newManager.address });
|
||||
|
||||
expect(await this.target.authority()).to.be.equal(this.newManager.address);
|
||||
});
|
||||
|
||||
it('cannot set an address without code as the authority', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.updateAuthority(this.target.address, user, { from: admin }),
|
||||
'AccessManagedInvalidAuthority',
|
||||
[user],
|
||||
);
|
||||
});
|
||||
|
||||
it('updateAuthority is restricted on manager', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.manager.updateAuthority(this.target.address, this.newManager.address, { from: other }),
|
||||
'AccessControlUnauthorizedAccount',
|
||||
[other, GROUPS.ADMIN],
|
||||
);
|
||||
});
|
||||
|
||||
it('setAuthority is restricted on AccessManaged', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.target.setAuthority(this.newManager.address, { from: admin }),
|
||||
'AccessManagedUnauthorized',
|
||||
[admin],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
158
test/access/manager/utils/AccessManagedAdapter.test.js
Normal file
158
test/access/manager/utils/AccessManagedAdapter.test.js
Normal file
@ -0,0 +1,158 @@
|
||||
const { constants, time } = require('@openzeppelin/test-helpers');
|
||||
const { expectRevertCustomError } = require('../../../helpers/customError');
|
||||
const { AccessMode } = require('../../../helpers/enums');
|
||||
const { selector } = require('../../../helpers/methods');
|
||||
|
||||
const AccessManager = artifacts.require('$AccessManager');
|
||||
const AccessManagedAdapter = artifacts.require('AccessManagedAdapter');
|
||||
const Ownable = artifacts.require('$Ownable');
|
||||
|
||||
const groupId = web3.utils.toBN(1);
|
||||
|
||||
contract('AccessManagedAdapter', function (accounts) {
|
||||
const [admin, user, other] = accounts;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.manager = await AccessManager.new(admin);
|
||||
this.adapter = await AccessManagedAdapter.new(this.manager.address);
|
||||
this.ownable = await Ownable.new(this.adapter.address);
|
||||
|
||||
// add user to group
|
||||
await this.manager.$_grantGroup(groupId, user, 0, 0);
|
||||
});
|
||||
|
||||
it('initial state', async function () {
|
||||
expect(await this.adapter.authority()).to.be.equal(this.manager.address);
|
||||
expect(await this.ownable.owner()).to.be.equal(this.adapter.address);
|
||||
});
|
||||
|
||||
describe('Contract is Closed', function () {
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setContractMode(this.ownable.address, AccessMode.Closed);
|
||||
});
|
||||
|
||||
it('directly call: reverts', async function () {
|
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]);
|
||||
});
|
||||
|
||||
it('relayed call (with group): reverts', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user }),
|
||||
'AccessManagedUnauthorized',
|
||||
[user],
|
||||
);
|
||||
});
|
||||
|
||||
it('relayed call (without group): reverts', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other }),
|
||||
'AccessManagedUnauthorized',
|
||||
[other],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Contract is Open', function () {
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setContractMode(this.ownable.address, AccessMode.Open);
|
||||
});
|
||||
|
||||
it('directly call: reverts', async function () {
|
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]);
|
||||
});
|
||||
|
||||
it('relayed call (with group): success', async function () {
|
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user });
|
||||
});
|
||||
|
||||
it('relayed call (without group): success', async function () {
|
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Contract is in Custom mode', function () {
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setContractMode(this.ownable.address, AccessMode.Custom);
|
||||
});
|
||||
|
||||
describe('function is open to specific group', function () {
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setFunctionAllowedGroup(this.ownable.address, selector('$_checkOwner()'), groupId);
|
||||
});
|
||||
|
||||
it('directly call: reverts', async function () {
|
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]);
|
||||
});
|
||||
|
||||
it('relayed call (with group): success', async function () {
|
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user });
|
||||
});
|
||||
|
||||
it('relayed call (without group): reverts', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other }),
|
||||
'AccessManagedUnauthorized',
|
||||
[other],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('function is open to public group', function () {
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setFunctionAllowedGroup(
|
||||
this.ownable.address,
|
||||
selector('$_checkOwner()'),
|
||||
constants.MAX_UINT256,
|
||||
);
|
||||
});
|
||||
|
||||
it('directly call: reverts', async function () {
|
||||
await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]);
|
||||
});
|
||||
|
||||
it('relayed call (with group): success', async function () {
|
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user });
|
||||
});
|
||||
|
||||
it('relayed call (without group): success', async function () {
|
||||
await this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: other });
|
||||
});
|
||||
});
|
||||
|
||||
describe('function is available with execution delay', function () {
|
||||
const delay = 10;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.manager.$_setExecuteDelay(groupId, user, delay);
|
||||
await this.manager.$_setFunctionAllowedGroup(this.ownable.address, selector('$_checkOwner()'), groupId);
|
||||
});
|
||||
|
||||
it('unscheduled call reverts', async function () {
|
||||
await expectRevertCustomError(
|
||||
this.adapter.relay(this.ownable.address, selector('$_checkOwner()'), { from: user }),
|
||||
'AccessManagedRequiredDelay',
|
||||
[user, delay],
|
||||
);
|
||||
});
|
||||
|
||||
it('scheduled call succeeds', async function () {
|
||||
await this.manager.schedule(this.ownable.address, selector('$_checkOwner()'), { from: user });
|
||||
await time.increase(delay);
|
||||
await this.manager.relayViaAdapter(this.ownable.address, selector('$_checkOwner()'), this.adapter.address, {
|
||||
from: user,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('bubble revert reasons', async function () {
|
||||
const { address } = await Ownable.new(admin);
|
||||
await this.manager.$_setContractMode(address, AccessMode.Open);
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.adapter.relay(address, selector('$_checkOwner()')),
|
||||
'OwnableUnauthorizedAccount',
|
||||
[this.adapter.address],
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -79,7 +79,7 @@ contract('Governor', function (accounts) {
|
||||
);
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams', 'GovernorCancel']);
|
||||
shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor']);
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('deployment check', async function () {
|
||||
@ -305,6 +305,17 @@ contract('Governor', function (accounts) {
|
||||
ZERO_BYTES32,
|
||||
]);
|
||||
});
|
||||
|
||||
it('if proposer has below threshold votes', async function () {
|
||||
const votes = web3.utils.toWei('10');
|
||||
const threshold = web3.utils.toWei('1000');
|
||||
await this.mock.$_setProposalThreshold(threshold);
|
||||
await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorInsufficientProposerVotes', [
|
||||
voter1,
|
||||
votes,
|
||||
threshold,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on vote', function () {
|
||||
@ -427,6 +438,16 @@ contract('Governor', function (accounts) {
|
||||
});
|
||||
});
|
||||
|
||||
describe('on queue', function () {
|
||||
it('always', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await expectRevertCustomError(this.helper.queue(), 'GovernorQueueNotImplemented', []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on execute', function () {
|
||||
it('if proposal does not exist', async function () {
|
||||
await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]);
|
||||
@ -826,7 +847,9 @@ contract('Governor', function (accounts) {
|
||||
});
|
||||
|
||||
it('someone else cannot propose', async function () {
|
||||
await expectRevert(this.helper.propose({ from: voter1 }), 'Governor: proposer restricted');
|
||||
await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorRestrictedProposer', [
|
||||
voter1,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,297 +0,0 @@
|
||||
const { expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { computeCreateAddress } = require('../../helpers/create');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper } = require('../../helpers/governance');
|
||||
const { clockFromReceipt } = require('../../helpers/time');
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
|
||||
const Timelock = artifacts.require('CompTimelock');
|
||||
const Governor = artifacts.require('$GovernorCompatibilityBravoMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const { shouldBehaveLikeEIP6372 } = require('../utils/EIP6372.behavior');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorCompatibilityBravo', function (accounts) {
|
||||
const [owner, proposer, voter1, voter2, voter3, voter4, other] = accounts;
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const proposalThreshold = web3.utils.toWei('10');
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
const votes = {
|
||||
[owner]: tokenSupply,
|
||||
[proposer]: proposalThreshold,
|
||||
[voter1]: web3.utils.toWei('10'),
|
||||
[voter2]: web3.utils.toWei('7'),
|
||||
[voter3]: web3.utils.toWei('5'),
|
||||
[voter4]: web3.utils.toWei('2'),
|
||||
[other]: 0,
|
||||
};
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
const [deployer] = await web3.eth.getAccounts();
|
||||
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName, version);
|
||||
|
||||
// Need to predict governance address to set it as timelock admin with a delayed transfer
|
||||
const nonce = await web3.eth.getTransactionCount(deployer);
|
||||
const predictGovernor = computeCreateAddress(deployer, nonce + 1);
|
||||
|
||||
this.timelock = await Timelock.new(predictGovernor, 2 * 86400);
|
||||
this.mock = await Governor.new(
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
proposalThreshold,
|
||||
this.timelock.address,
|
||||
this.token.address,
|
||||
);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: proposer, value: votes[proposer] }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: votes[voter1] }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: votes[voter2] }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: votes[voter3] }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: votes[voter4] }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
signature: 'mockFunction()',
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
});
|
||||
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('deployment check', async function () {
|
||||
expect(await this.mock.name()).to.be.equal(name);
|
||||
expect(await this.mock.token()).to.be.equal(this.token.address);
|
||||
expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay);
|
||||
expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
|
||||
expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
|
||||
expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0');
|
||||
expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo');
|
||||
});
|
||||
|
||||
it('nominal workflow', async function () {
|
||||
// Before
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value);
|
||||
expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
|
||||
|
||||
// Run proposal
|
||||
const txPropose = await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
// After
|
||||
expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
|
||||
expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
|
||||
expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0');
|
||||
expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
|
||||
|
||||
const proposal = await this.mock.proposals(this.proposal.id);
|
||||
expect(proposal.id).to.be.bignumber.equal(this.proposal.id);
|
||||
expect(proposal.proposer).to.be.equal(proposer);
|
||||
expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id));
|
||||
expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id));
|
||||
expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id));
|
||||
expect(proposal.canceled).to.be.equal(false);
|
||||
expect(proposal.executed).to.be.equal(true);
|
||||
|
||||
const action = await this.mock.getActions(this.proposal.id);
|
||||
expect(action.targets).to.be.deep.equal(this.proposal.targets);
|
||||
// expect(action.values).to.be.deep.equal(this.proposal.values);
|
||||
expect(action.signatures).to.be.deep.equal(this.proposal.signatures);
|
||||
expect(action.calldatas).to.be.deep.equal(this.proposal.data);
|
||||
|
||||
const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1);
|
||||
expect(voteReceipt1.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For);
|
||||
expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10'));
|
||||
|
||||
const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2);
|
||||
expect(voteReceipt2.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For);
|
||||
expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7'));
|
||||
|
||||
const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3);
|
||||
expect(voteReceipt3.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against);
|
||||
expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5'));
|
||||
|
||||
const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4);
|
||||
expect(voteReceipt4.hasVoted).to.be.equal(true);
|
||||
expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain);
|
||||
expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2'));
|
||||
|
||||
expectEvent(txPropose, 'ProposalCreated', {
|
||||
proposalId: this.proposal.id,
|
||||
proposer,
|
||||
targets: this.proposal.targets,
|
||||
// values: this.proposal.values,
|
||||
signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail
|
||||
calldatas: this.proposal.fulldata,
|
||||
voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay),
|
||||
voteEnd: web3.utils
|
||||
.toBN(await clockFromReceipt[mode](txPropose.receipt))
|
||||
.add(votingDelay)
|
||||
.add(votingPeriod),
|
||||
description: this.proposal.description,
|
||||
});
|
||||
expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
it('double voting is forbidden', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await expectRevertCustomError(
|
||||
this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
|
||||
'GovernorAlreadyCastVote',
|
||||
[voter1],
|
||||
);
|
||||
});
|
||||
|
||||
it('with function selector and arguments', async function () {
|
||||
const target = this.receiver.address;
|
||||
this.helper.setProposal(
|
||||
[
|
||||
{ target, data: this.receiver.contract.methods.mockFunction().encodeABI() },
|
||||
{ target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() },
|
||||
{ target, signature: 'mockFunctionNonPayable()' },
|
||||
{
|
||||
target,
|
||||
signature: 'mockFunctionWithArgs(uint256,uint256)',
|
||||
data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
|
||||
await this.helper.waitForDeadline();
|
||||
await this.helper.queue();
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.helper.execute();
|
||||
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', {
|
||||
a: '17',
|
||||
b: '42',
|
||||
});
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', {
|
||||
a: '18',
|
||||
b: '43',
|
||||
});
|
||||
});
|
||||
|
||||
it('with inconsistent array size for selector and arguments', async function () {
|
||||
const target = this.receiver.address;
|
||||
const signatures = ['mockFunction()']; // One signature
|
||||
const data = ['0x', this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI()]; // Two data entries
|
||||
this.helper.setProposal(
|
||||
{
|
||||
targets: [target, target],
|
||||
values: [0, 0],
|
||||
signatures,
|
||||
data,
|
||||
},
|
||||
'<proposal description>',
|
||||
);
|
||||
|
||||
await expectRevertCustomError(this.helper.propose({ from: proposer }), 'GovernorInvalidSignaturesLength', [
|
||||
signatures.length,
|
||||
data.length,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('should revert', function () {
|
||||
describe('on propose', function () {
|
||||
it('if proposal does not meet proposalThreshold', async function () {
|
||||
await expectRevertCustomError(this.helper.propose({ from: other }), 'GovernorInsufficientProposerVotes', [
|
||||
other,
|
||||
votes[other],
|
||||
proposalThreshold,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on vote', function () {
|
||||
it('if vote type is invalid', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.waitForSnapshot();
|
||||
await expectRevertCustomError(
|
||||
this.helper.vote({ support: 5 }, { from: voter1 }),
|
||||
'GovernorInvalidVoteType',
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', function () {
|
||||
it('proposer can cancel', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.helper.cancel('external', { from: proposer });
|
||||
});
|
||||
|
||||
it('anyone can cancel if proposer drop below threshold', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer });
|
||||
await this.helper.cancel('external');
|
||||
});
|
||||
|
||||
it('cannot cancel is proposer is still above threshold', async function () {
|
||||
await this.helper.propose({ from: proposer });
|
||||
await expectRevertCustomError(this.helper.cancel('external'), 'GovernorInsufficientProposerVotes', [
|
||||
proposer,
|
||||
votes[proposer],
|
||||
proposalThreshold,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
150
test/governance/extensions/GovernorStorage.test.js
Normal file
150
test/governance/extensions/GovernorStorage.test.js
Normal file
@ -0,0 +1,150 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper, timelockSalt } = require('../../helpers/governance');
|
||||
|
||||
const Timelock = artifacts.require('TimelockController');
|
||||
const Governor = artifacts.require('$GovernorStorageMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
|
||||
const TOKENS = [
|
||||
{ Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
|
||||
{ Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
|
||||
];
|
||||
|
||||
contract('GovernorStorage', function (accounts) {
|
||||
const [owner, voter1, voter2, voter3, voter4] = accounts;
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE');
|
||||
const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE');
|
||||
const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE');
|
||||
|
||||
const name = 'OZ-Governor';
|
||||
const version = '1';
|
||||
const tokenName = 'MockToken';
|
||||
const tokenSymbol = 'MTKN';
|
||||
const tokenSupply = web3.utils.toWei('100');
|
||||
const votingDelay = web3.utils.toBN(4);
|
||||
const votingPeriod = web3.utils.toBN(16);
|
||||
const value = web3.utils.toWei('1');
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
beforeEach(async function () {
|
||||
const [deployer] = await web3.eth.getAccounts();
|
||||
|
||||
this.token = await Token.new(tokenName, tokenSymbol, tokenName, version);
|
||||
this.timelock = await Timelock.new(3600, [], [], deployer);
|
||||
this.mock = await Governor.new(
|
||||
name,
|
||||
votingDelay,
|
||||
votingPeriod,
|
||||
0,
|
||||
this.timelock.address,
|
||||
this.token.address,
|
||||
0,
|
||||
);
|
||||
this.receiver = await CallReceiver.new();
|
||||
|
||||
this.helper = new GovernorHelper(this.mock, mode);
|
||||
|
||||
await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
|
||||
|
||||
// normal setup: governor is proposer, everyone is executor, timelock is its own admin
|
||||
await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address);
|
||||
await this.timelock.grantRole(PROPOSER_ROLE, owner);
|
||||
await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address);
|
||||
await this.timelock.grantRole(CANCELLER_ROLE, owner);
|
||||
await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS);
|
||||
await this.timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer);
|
||||
|
||||
await this.token.$_mint(owner, tokenSupply);
|
||||
await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
|
||||
await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
|
||||
|
||||
// default proposal
|
||||
this.proposal = this.helper.setProposal(
|
||||
[
|
||||
{
|
||||
target: this.receiver.address,
|
||||
value,
|
||||
data: this.receiver.contract.methods.mockFunction().encodeABI(),
|
||||
},
|
||||
],
|
||||
'<proposal description>',
|
||||
);
|
||||
this.proposal.timelockid = await this.timelock.hashOperationBatch(
|
||||
...this.proposal.shortProposal.slice(0, 3),
|
||||
'0x0',
|
||||
timelockSalt(this.mock.address, this.proposal.shortProposal[3]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('proposal indexing', function () {
|
||||
it('before propose', async function () {
|
||||
expect(await this.mock.proposalCount()).to.be.bignumber.equal('0');
|
||||
|
||||
// panic code 0x32 (out-of-bound)
|
||||
await expectRevert.unspecified(this.mock.proposalDetailsAt(0));
|
||||
|
||||
await expectRevertCustomError(this.mock.proposalDetails(this.proposal.id), 'GovernorNonexistentProposal', [
|
||||
this.proposal.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('after propose', async function () {
|
||||
await this.helper.propose();
|
||||
|
||||
expect(await this.mock.proposalCount()).to.be.bignumber.equal('1');
|
||||
|
||||
const proposalDetailsAt0 = await this.mock.proposalDetailsAt(0);
|
||||
expect(proposalDetailsAt0[0]).to.be.bignumber.equal(this.proposal.id);
|
||||
expect(proposalDetailsAt0[1]).to.be.deep.equal(this.proposal.targets);
|
||||
expect(proposalDetailsAt0[2].map(x => x.toString())).to.be.deep.equal(this.proposal.values);
|
||||
expect(proposalDetailsAt0[3]).to.be.deep.equal(this.proposal.fulldata);
|
||||
expect(proposalDetailsAt0[4]).to.be.equal(this.proposal.descriptionHash);
|
||||
|
||||
const proposalDetailsForId = await this.mock.proposalDetails(this.proposal.id);
|
||||
expect(proposalDetailsForId[0]).to.be.deep.equal(this.proposal.targets);
|
||||
expect(proposalDetailsForId[1].map(x => x.toString())).to.be.deep.equal(this.proposal.values);
|
||||
expect(proposalDetailsForId[2]).to.be.deep.equal(this.proposal.fulldata);
|
||||
expect(proposalDetailsForId[3]).to.be.equal(this.proposal.descriptionHash);
|
||||
});
|
||||
});
|
||||
|
||||
it('queue and execute by id', async function () {
|
||||
await this.helper.propose();
|
||||
await this.helper.waitForSnapshot();
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
|
||||
await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
|
||||
await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
|
||||
await this.helper.waitForDeadline();
|
||||
const txQueue = await this.mock.queue(this.proposal.id);
|
||||
await this.helper.waitForEta();
|
||||
const txExecute = await this.mock.execute(this.proposal.id);
|
||||
|
||||
expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid });
|
||||
await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', {
|
||||
id: this.proposal.timelockid,
|
||||
});
|
||||
|
||||
expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid });
|
||||
await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
|
||||
});
|
||||
|
||||
it('cancel by id', async function () {
|
||||
await this.helper.propose();
|
||||
const txCancel = await this.mock.cancel(this.proposal.id);
|
||||
expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id });
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -6,8 +6,6 @@ const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/govern
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
const { computeCreateAddress } = require('../../helpers/create');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const Timelock = artifacts.require('CompTimelock');
|
||||
const Governor = artifacts.require('$GovernorTimelockCompoundMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
@ -77,8 +75,6 @@ contract('GovernorTimelockCompound', function (accounts) {
|
||||
);
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
|
||||
|
||||
it("doesn't accept ether transfers", async function () {
|
||||
await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
|
||||
});
|
||||
|
||||
@ -2,11 +2,9 @@ const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/te
|
||||
const { expect } = require('chai');
|
||||
|
||||
const Enums = require('../../helpers/enums');
|
||||
const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance');
|
||||
const { GovernorHelper, proposalStatesToBitMap, timelockSalt } = require('../../helpers/governance');
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
|
||||
const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
|
||||
|
||||
const Timelock = artifacts.require('TimelockController');
|
||||
const Governor = artifacts.require('$GovernorTimelockControlMock');
|
||||
const CallReceiver = artifacts.require('CallReceiverMock');
|
||||
@ -37,9 +35,6 @@ contract('GovernorTimelockControl', function (accounts) {
|
||||
|
||||
for (const { mode, Token } of TOKENS) {
|
||||
describe(`using ${Token._json.contractName}`, function () {
|
||||
const timelockSalt = (address, descriptionHash) =>
|
||||
'0x' + web3.utils.toBN(address).shln(96).xor(web3.utils.toBN(descriptionHash)).toString(16, 64);
|
||||
|
||||
beforeEach(async function () {
|
||||
const [deployer] = await web3.eth.getAccounts();
|
||||
|
||||
@ -97,8 +92,6 @@ contract('GovernorTimelockControl', function (accounts) {
|
||||
);
|
||||
});
|
||||
|
||||
shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
|
||||
|
||||
it("doesn't accept ether transfers", async function () {
|
||||
await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
|
||||
});
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
const { rlp } = require('ethereumjs-util');
|
||||
const RLP = require('rlp');
|
||||
|
||||
function computeCreateAddress(deployer, nonce) {
|
||||
return web3.utils.toChecksumAddress(web3.utils.sha3(rlp.encode([deployer.address ?? deployer, nonce])).slice(-40));
|
||||
return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([deployer.address ?? deployer, nonce])).slice(-40));
|
||||
}
|
||||
|
||||
function computeCreate2Address(saltHex, bytecode, deployer) {
|
||||
return web3.utils.toChecksumAddress(
|
||||
web3.utils
|
||||
.sha3(
|
||||
'0x' +
|
||||
['ff', deployer.address ?? deployer, saltHex, web3.utils.soliditySha3(bytecode)]
|
||||
.map(x => x.replace(/0x/, ''))
|
||||
.join(''),
|
||||
`0x${['ff', deployer.address ?? deployer, saltHex, web3.utils.soliditySha3(bytecode)]
|
||||
.map(x => x.replace(/0x/, ''))
|
||||
.join('')}`,
|
||||
)
|
||||
.slice(-40),
|
||||
);
|
||||
|
||||
@ -8,4 +8,5 @@ module.exports = {
|
||||
VoteType: Enum('Against', 'For', 'Abstain'),
|
||||
Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'),
|
||||
OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'),
|
||||
AccessMode: Enum('Custom', 'Closed', 'Open'),
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
const { web3 } = require('hardhat');
|
||||
const { forward } = require('../helpers/time');
|
||||
const { ProposalState } = require('./enums');
|
||||
|
||||
@ -15,6 +16,9 @@ function concatOpts(args, opts = null) {
|
||||
return opts ? args.concat(opts) : args;
|
||||
}
|
||||
|
||||
const timelockSalt = (address, descriptionHash) =>
|
||||
'0x' + web3.utils.toBN(address).shln(96).xor(web3.utils.toBN(descriptionHash)).toString(16, 64);
|
||||
|
||||
class GovernorHelper {
|
||||
constructor(governor, mode = 'blocknumber') {
|
||||
this.governor = governor;
|
||||
@ -245,4 +249,5 @@ function proposalStatesToBitMap(proposalStates, options = {}) {
|
||||
module.exports = {
|
||||
GovernorHelper,
|
||||
proposalStatesToBitMap,
|
||||
timelockSalt,
|
||||
};
|
||||
|
||||
5
test/helpers/methods.js
Normal file
5
test/helpers/methods.js
Normal file
@ -0,0 +1,5 @@
|
||||
const { soliditySha3 } = require('web3-utils');
|
||||
|
||||
module.exports = {
|
||||
selector: signature => soliditySha3(signature).substring(0, 10),
|
||||
};
|
||||
@ -1,7 +1,6 @@
|
||||
const { expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { computeCreate2Address } = require('../helpers/create');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { computeCreate2Address } = require('../helpers/create');
|
||||
const { expectRevertCustomError } = require('../helpers/customError');
|
||||
|
||||
const shouldBehaveLikeClone = require('./Clones.behaviour');
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const { balance, ether, expectEvent, expectRevert, send } = require('@openzeppelin/test-helpers');
|
||||
const { computeCreate2Address } = require('../helpers/create');
|
||||
const { expect } = require('chai');
|
||||
const { computeCreate2Address } = require('../helpers/create');
|
||||
const { expectRevertCustomError } = require('../helpers/customError');
|
||||
|
||||
const Create2 = artifacts.require('$Create2');
|
||||
|
||||
@ -56,27 +56,11 @@ const INTERFACES = {
|
||||
'COUNTING_MODE()',
|
||||
'hashProposal(address[],uint256[],bytes[],bytes32)',
|
||||
'state(uint256)',
|
||||
'proposalThreshold()',
|
||||
'proposalSnapshot(uint256)',
|
||||
'proposalDeadline(uint256)',
|
||||
'votingDelay()',
|
||||
'votingPeriod()',
|
||||
'quorum(uint256)',
|
||||
'getVotes(address,uint256)',
|
||||
'hasVoted(uint256,address)',
|
||||
'propose(address[],uint256[],bytes[],string)',
|
||||
'execute(address[],uint256[],bytes[],bytes32)',
|
||||
'castVote(uint256,uint8)',
|
||||
'castVoteWithReason(uint256,uint8,string)',
|
||||
'castVoteBySig(uint256,uint8,address,bytes)',
|
||||
],
|
||||
GovernorWithParams: [
|
||||
'name()',
|
||||
'version()',
|
||||
'COUNTING_MODE()',
|
||||
'hashProposal(address[],uint256[],bytes[],bytes32)',
|
||||
'state(uint256)',
|
||||
'proposalSnapshot(uint256)',
|
||||
'proposalDeadline(uint256)',
|
||||
'proposalProposer(uint256)',
|
||||
'proposalEta(uint256)',
|
||||
'votingDelay()',
|
||||
'votingPeriod()',
|
||||
'quorum(uint256)',
|
||||
@ -84,15 +68,15 @@ const INTERFACES = {
|
||||
'getVotesWithParams(address,uint256,bytes)',
|
||||
'hasVoted(uint256,address)',
|
||||
'propose(address[],uint256[],bytes[],string)',
|
||||
'queue(address[],uint256[],bytes[],bytes32)',
|
||||
'execute(address[],uint256[],bytes[],bytes32)',
|
||||
'cancel(address[],uint256[],bytes[],bytes32)',
|
||||
'castVote(uint256,uint8)',
|
||||
'castVoteWithReason(uint256,uint8,string)',
|
||||
'castVoteWithReasonAndParams(uint256,uint8,string,bytes)',
|
||||
'castVoteBySig(uint256,uint8,address,bytes)',
|
||||
'castVoteWithReasonAndParamsBySig(uint256,uint8,address,string,bytes,bytes)',
|
||||
],
|
||||
GovernorCancel: ['proposalProposer(uint256)', 'cancel(address[],uint256[],bytes[],bytes32)'],
|
||||
GovernorTimelock: ['timelock()', 'proposalEta(uint256)', 'queue(address[],uint256[],bytes[],bytes32)'],
|
||||
ERC2981: ['royaltyInfo(uint256,uint256)'],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user