Formal verification of AccessManager (#4611)
Co-authored-by: Ernesto García <ernestognw@gmail.com> Co-authored-by: Francisco Giordano <fg@frang.io>
This commit is contained in:
@ -17,7 +17,7 @@ $(DST): FORCE
|
||||
@cp -r $(SRC) $@
|
||||
|
||||
# Update a solidity file in the $DST directory using the corresponding patch
|
||||
$(DST)/%.sol: FORCE
|
||||
$(DST)/%.sol: FORCE | $(DST)
|
||||
@echo Applying patch to $@
|
||||
@patch -p0 -d $(DST) < $(patsubst $(DST)_%,$(DIFF)/%.patch,$(subst /,_,$@))
|
||||
|
||||
@ -31,7 +31,7 @@ $(DIFF): FORCE
|
||||
@mkdir $@
|
||||
|
||||
# Create the patch file by comparing the source and the destination
|
||||
$(DIFF)/%.patch: FORCE
|
||||
$(DIFF)/%.patch: FORCE | $(DIFF)
|
||||
@echo Generating patch $@
|
||||
@diff -ruN \
|
||||
$(patsubst $(DIFF)/%.patch,$(SRC)/%,$(subst _,/,$@)) \
|
||||
|
||||
98
certora/diff/access_manager_AccessManager.sol.patch
Normal file
98
certora/diff/access_manager_AccessManager.sol.patch
Normal file
@ -0,0 +1,98 @@
|
||||
--- access/manager/AccessManager.sol 2023-10-04 11:20:52.802378968 +0200
|
||||
+++ access/manager/AccessManager.sol 2023-10-04 14:49:43.126279234 +0200
|
||||
@@ -6,7 +6,6 @@
|
||||
import {IAccessManaged} from "./IAccessManaged.sol";
|
||||
import {Address} from "../../utils/Address.sol";
|
||||
import {Context} from "../../utils/Context.sol";
|
||||
-import {Multicall} from "../../utils/Multicall.sol";
|
||||
import {Math} from "../../utils/math/Math.sol";
|
||||
import {Time} from "../../utils/types/Time.sol";
|
||||
|
||||
@@ -48,7 +47,8 @@
|
||||
* mindful of the danger associated with functions such as {{Ownable-renounceOwnership}} or
|
||||
* {{AccessControl-renounceRole}}.
|
||||
*/
|
||||
-contract AccessManager is Context, Multicall, IAccessManager {
|
||||
+// NOTE: The FV version of this contract doesn't include Multicall because CVL HAVOCs on any `delegatecall`.
|
||||
+contract AccessManager is Context, IAccessManager {
|
||||
using Time for *;
|
||||
|
||||
// Structure that stores the details for a target contract.
|
||||
@@ -93,7 +93,7 @@
|
||||
mapping(bytes32 operationId => Schedule) private _schedules;
|
||||
|
||||
// This should be transient storage when supported by the EVM.
|
||||
- bytes32 private _executionId;
|
||||
+ bytes32 internal _executionId; // private → internal for FV
|
||||
|
||||
/**
|
||||
* @dev Check that the caller is authorized to perform the operation, following the restrictions encoded in
|
||||
@@ -185,6 +185,11 @@
|
||||
return _targets[target].adminDelay.get();
|
||||
}
|
||||
|
||||
+ // Exposed for FV
|
||||
+ function _getTargetAdminDelayFull(address target) internal view virtual returns (uint32, uint32, uint48) {
|
||||
+ return _targets[target].adminDelay.getFull();
|
||||
+ }
|
||||
+
|
||||
/**
|
||||
* @dev Get the id of the role that acts as an admin for given role.
|
||||
*
|
||||
@@ -213,6 +218,11 @@
|
||||
return _roles[roleId].grantDelay.get();
|
||||
}
|
||||
|
||||
+ // Exposed for FV
|
||||
+ function _getRoleGrantDelayFull(uint64 roleId) internal view virtual returns (uint32, uint32, uint48) {
|
||||
+ return _roles[roleId].grantDelay.getFull();
|
||||
+ }
|
||||
+
|
||||
/**
|
||||
* @dev Get the access details for a given account for a given role. These details include the timepoint at which
|
||||
* membership becomes active, and the delay applied to all operation by this user that requires this permission
|
||||
@@ -749,7 +759,7 @@
|
||||
/**
|
||||
* @dev Hashing function for execute protection
|
||||
*/
|
||||
- function _hashExecutionId(address target, bytes4 selector) private pure returns (bytes32) {
|
||||
+ function _hashExecutionId(address target, bytes4 selector) internal pure returns (bytes32) { // private → internal for FV
|
||||
return keccak256(abi.encode(target, selector));
|
||||
}
|
||||
|
||||
@@ -769,7 +779,7 @@
|
||||
/**
|
||||
* @dev Check if the current call is authorized according to admin logic.
|
||||
*/
|
||||
- function _checkAuthorized() private {
|
||||
+ function _checkAuthorized() internal virtual { // private → internal virtual for FV
|
||||
address caller = _msgSender();
|
||||
(bool immediate, uint32 delay) = _canCallSelf(caller, _msgData());
|
||||
if (!immediate) {
|
||||
@@ -792,7 +802,7 @@
|
||||
*/
|
||||
function _getAdminRestrictions(
|
||||
bytes calldata data
|
||||
- ) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
|
||||
+ ) internal view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) { // private → internal for FV
|
||||
if (data.length < 4) {
|
||||
return (false, 0, 0);
|
||||
}
|
||||
@@ -847,7 +857,7 @@
|
||||
address caller,
|
||||
address target,
|
||||
bytes calldata data
|
||||
- ) private view returns (bool immediate, uint32 delay) {
|
||||
+ ) internal view returns (bool immediate, uint32 delay) { // private → internal for FV
|
||||
if (target == address(this)) {
|
||||
return _canCallSelf(caller, data);
|
||||
} else {
|
||||
@@ -901,7 +911,7 @@
|
||||
/**
|
||||
* @dev Extracts the selector from calldata. Panics if data is not at least 4 bytes
|
||||
*/
|
||||
- function _checkSelector(bytes calldata data) private pure returns (bytes4) {
|
||||
+ function _checkSelector(bytes calldata data) internal pure returns (bytes4) { // private → internal for FV
|
||||
return bytes4(data[0:4]);
|
||||
}
|
||||
}
|
||||
36
certora/harnesses/AccessManagedHarness.sol
Normal file
36
certora/harnesses/AccessManagedHarness.sol
Normal file
@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "../patched/access/manager/IAccessManager.sol";
|
||||
import "../patched/access/manager/AccessManaged.sol";
|
||||
|
||||
contract AccessManagedHarness is AccessManaged {
|
||||
bytes internal SOME_FUNCTION_CALLDATA = abi.encodeCall(this.someFunction, ());
|
||||
|
||||
constructor(address initialAuthority) AccessManaged(initialAuthority) {}
|
||||
|
||||
function someFunction() public restricted() {
|
||||
// Sanity for FV: the msg.data when calling this function should be the same as the data used when checking
|
||||
// the schedule. This is a reformulation of `msg.data == SOME_FUNCTION_CALLDATA` that focuses on the operation
|
||||
// hash for this call.
|
||||
require(
|
||||
IAccessManager(authority()).hashOperation(_msgSender(), address(this), msg.data)
|
||||
==
|
||||
IAccessManager(authority()).hashOperation(_msgSender(), address(this), SOME_FUNCTION_CALLDATA)
|
||||
);
|
||||
}
|
||||
|
||||
function authority_canCall_immediate(address caller) public view returns (bool result) {
|
||||
(result,) = AuthorityUtils.canCallWithDelay(authority(), caller, address(this), this.someFunction.selector);
|
||||
}
|
||||
|
||||
function authority_canCall_delay(address caller) public view returns (uint32 result) {
|
||||
(,result) = AuthorityUtils.canCallWithDelay(authority(), caller, address(this), this.someFunction.selector);
|
||||
}
|
||||
|
||||
function authority_getSchedule(address caller) public view returns (uint48) {
|
||||
IAccessManager manager = IAccessManager(authority());
|
||||
return manager.getSchedule(manager.hashOperation(caller, address(this), SOME_FUNCTION_CALLDATA));
|
||||
}
|
||||
}
|
||||
116
certora/harnesses/AccessManagerHarness.sol
Normal file
116
certora/harnesses/AccessManagerHarness.sol
Normal file
@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "../patched/access/manager/AccessManager.sol";
|
||||
|
||||
contract AccessManagerHarness is AccessManager {
|
||||
// override with a storage slot that can basically take any value.
|
||||
uint32 private _minSetback;
|
||||
|
||||
constructor(address initialAdmin) AccessManager(initialAdmin) {}
|
||||
|
||||
// FV
|
||||
function minSetback() public view override returns (uint32) {
|
||||
return _minSetback;
|
||||
}
|
||||
|
||||
function canCall_immediate(address caller, address target, bytes4 selector) external view returns (bool result) {
|
||||
(result,) = canCall(caller, target, selector);
|
||||
}
|
||||
|
||||
function canCall_delay(address caller, address target, bytes4 selector) external view returns (uint32 result) {
|
||||
(,result) = canCall(caller, target, selector);
|
||||
}
|
||||
|
||||
function canCallExtended(address caller, address target, bytes calldata data) external view returns (bool, uint32) {
|
||||
return _canCallExtended(caller, target, data);
|
||||
}
|
||||
|
||||
function canCallExtended_immediate(address caller, address target, bytes calldata data) external view returns (bool result) {
|
||||
(result,) = _canCallExtended(caller, target, data);
|
||||
}
|
||||
|
||||
function canCallExtended_delay(address caller, address target, bytes calldata data) external view returns (uint32 result) {
|
||||
(,result) = _canCallExtended(caller, target, data);
|
||||
}
|
||||
|
||||
function getAdminRestrictions_restricted(bytes calldata data) external view returns (bool result) {
|
||||
(result,,) = _getAdminRestrictions(data);
|
||||
}
|
||||
|
||||
function getAdminRestrictions_roleAdminId(bytes calldata data) external view returns (uint64 result) {
|
||||
(,result,) = _getAdminRestrictions(data);
|
||||
}
|
||||
|
||||
function getAdminRestrictions_executionDelay(bytes calldata data) external view returns (uint32 result) {
|
||||
(,,result) = _getAdminRestrictions(data);
|
||||
}
|
||||
|
||||
function hasRole_isMember(uint64 roleId, address account) external view returns (bool result) {
|
||||
(result,) = hasRole(roleId, account);
|
||||
}
|
||||
|
||||
function hasRole_executionDelay(uint64 roleId, address account) external view returns (uint32 result) {
|
||||
(,result) = hasRole(roleId, account);
|
||||
}
|
||||
|
||||
function getAccess_since(uint64 roleId, address account) external view returns (uint48 result) {
|
||||
(result,,,) = getAccess(roleId, account);
|
||||
}
|
||||
|
||||
function getAccess_currentDelay(uint64 roleId, address account) external view returns (uint32 result) {
|
||||
(,result,,) = getAccess(roleId, account);
|
||||
}
|
||||
|
||||
function getAccess_pendingDelay(uint64 roleId, address account) external view returns (uint32 result) {
|
||||
(,,result,) = getAccess(roleId, account);
|
||||
}
|
||||
|
||||
function getAccess_effect(uint64 roleId, address account) external view returns (uint48 result) {
|
||||
(,,,result) = getAccess(roleId, account);
|
||||
}
|
||||
|
||||
function getTargetAdminDelay_after(address target) public view virtual returns (uint32 result) {
|
||||
(,result,) = _getTargetAdminDelayFull(target);
|
||||
}
|
||||
|
||||
function getTargetAdminDelay_effect(address target) public view virtual returns (uint48 result) {
|
||||
(,,result) = _getTargetAdminDelayFull(target);
|
||||
}
|
||||
|
||||
function getRoleGrantDelay_after(uint64 roleId) public view virtual returns (uint32 result) {
|
||||
(,result,) = _getRoleGrantDelayFull(roleId);
|
||||
}
|
||||
|
||||
function getRoleGrantDelay_effect(uint64 roleId) public view virtual returns (uint48 result) {
|
||||
(,,result) = _getRoleGrantDelayFull(roleId);
|
||||
}
|
||||
|
||||
function hashExecutionId(address target, bytes4 selector) external pure returns (bytes32) {
|
||||
return _hashExecutionId(target, selector);
|
||||
}
|
||||
|
||||
function executionId() external view returns (bytes32) {
|
||||
return _executionId;
|
||||
}
|
||||
|
||||
// Pad with zeros (and don't revert) if data is too short.
|
||||
function getSelector(bytes calldata data) external pure returns (bytes4) {
|
||||
return bytes4(data);
|
||||
}
|
||||
|
||||
function getFirstArgumentAsAddress(bytes calldata data) external pure returns (address) {
|
||||
return abi.decode(data[0x04:0x24], (address));
|
||||
}
|
||||
|
||||
function getFirstArgumentAsUint64(bytes calldata data) external pure returns (uint64) {
|
||||
return abi.decode(data[0x04:0x24], (uint64));
|
||||
}
|
||||
|
||||
function _checkAuthorized() internal override {
|
||||
// We need this hack otherwise certora will assume _checkSelector(_msgData()) can return anything :/
|
||||
require(msg.sig == _checkSelector(_msgData()));
|
||||
super._checkAuthorized();
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,25 @@
|
||||
"contract": "AccessControlDefaultAdminRulesHarness",
|
||||
"files": ["certora/harnesses/AccessControlDefaultAdminRulesHarness.sol"]
|
||||
},
|
||||
{
|
||||
"spec": "AccessManager",
|
||||
"contract": "AccessManagerHarness",
|
||||
"files": ["certora/harnesses/AccessManagerHarness.sol"],
|
||||
"options": ["--optimistic_hashing", "--optimistic_loop"]
|
||||
},
|
||||
{
|
||||
"spec": "AccessManaged",
|
||||
"contract": "AccessManagedHarness",
|
||||
"files": [
|
||||
"certora/harnesses/AccessManagedHarness.sol",
|
||||
"certora/harnesses/AccessManagerHarness.sol"
|
||||
],
|
||||
"options": [
|
||||
"--optimistic_hashing",
|
||||
"--optimistic_loop",
|
||||
"--link AccessManagedHarness:_authority=AccessManagerHarness"
|
||||
]
|
||||
},
|
||||
{
|
||||
"spec": "DoubleEndedQueue",
|
||||
"contract": "DoubleEndedQueueHarness",
|
||||
|
||||
34
certora/specs/AccessManaged.spec
Normal file
34
certora/specs/AccessManaged.spec
Normal file
@ -0,0 +1,34 @@
|
||||
import "helpers/helpers.spec";
|
||||
import "methods/IAccessManaged.spec";
|
||||
|
||||
methods {
|
||||
// FV
|
||||
function someFunction() external;
|
||||
function authority_canCall_immediate(address) external returns (bool);
|
||||
function authority_canCall_delay(address) external returns (uint32);
|
||||
function authority_getSchedule(address) external returns (uint48);
|
||||
}
|
||||
|
||||
invariant isConsumingScheduledOpClean()
|
||||
isConsumingScheduledOp() == to_bytes4(0);
|
||||
|
||||
rule callRestrictedFunction(env e) {
|
||||
bool immediate = authority_canCall_immediate(e, e.msg.sender);
|
||||
uint32 delay = authority_canCall_delay(e, e.msg.sender);
|
||||
uint48 scheduleBefore = authority_getSchedule(e, e.msg.sender);
|
||||
|
||||
someFunction@withrevert(e);
|
||||
bool success = !lastReverted;
|
||||
|
||||
uint48 scheduleAfter = authority_getSchedule(e, e.msg.sender);
|
||||
|
||||
// can only call if immediate, or (with delay) by consuming a scheduled op
|
||||
assert success => (
|
||||
immediate ||
|
||||
(
|
||||
delay > 0 &&
|
||||
isSetAndPast(e, scheduleBefore) &&
|
||||
scheduleAfter == 0
|
||||
)
|
||||
);
|
||||
}
|
||||
826
certora/specs/AccessManager.spec
Normal file
826
certora/specs/AccessManager.spec
Normal file
@ -0,0 +1,826 @@
|
||||
import "helpers/helpers.spec";
|
||||
import "methods/IAccessManager.spec";
|
||||
|
||||
methods {
|
||||
// FV
|
||||
function canCall_immediate(address,address,bytes4) external returns (bool);
|
||||
function canCall_delay(address,address,bytes4) external returns (uint32);
|
||||
function canCallExtended(address,address,bytes) external returns (bool,uint32);
|
||||
function canCallExtended_immediate(address,address,bytes) external returns (bool);
|
||||
function canCallExtended_delay(address,address,bytes) external returns (uint32);
|
||||
function getAdminRestrictions_restricted(bytes) external returns (bool);
|
||||
function getAdminRestrictions_roleAdminId(bytes) external returns (uint64);
|
||||
function getAdminRestrictions_executionDelay(bytes) external returns (uint32);
|
||||
function hasRole_isMember(uint64,address) external returns (bool);
|
||||
function hasRole_executionDelay(uint64,address) external returns (uint32);
|
||||
function getAccess_since(uint64,address) external returns (uint48);
|
||||
function getAccess_currentDelay(uint64,address) external returns (uint32);
|
||||
function getAccess_pendingDelay(uint64,address) external returns (uint32);
|
||||
function getAccess_effect(uint64,address) external returns (uint48);
|
||||
function getTargetAdminDelay_after(address target) external returns (uint32);
|
||||
function getTargetAdminDelay_effect(address target) external returns (uint48);
|
||||
function getRoleGrantDelay_after(uint64 roleId) external returns (uint32);
|
||||
function getRoleGrantDelay_effect(uint64 roleId) external returns (uint48);
|
||||
function hashExecutionId(address,bytes4) external returns (bytes32) envfree;
|
||||
function executionId() external returns (bytes32) envfree;
|
||||
function getSelector(bytes) external returns (bytes4) envfree;
|
||||
function getFirstArgumentAsAddress(bytes) external returns (address) envfree;
|
||||
function getFirstArgumentAsUint64(bytes) external returns (uint64) envfree;
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Helpers │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
definition isOnlyAuthorized(bytes4 selector) returns bool =
|
||||
selector == to_bytes4(sig:labelRole(uint64,string).selector ) ||
|
||||
selector == to_bytes4(sig:setRoleAdmin(uint64,uint64).selector ) ||
|
||||
selector == to_bytes4(sig:setRoleGuardian(uint64,uint64).selector ) ||
|
||||
selector == to_bytes4(sig:setGrantDelay(uint64,uint32).selector ) ||
|
||||
selector == to_bytes4(sig:setTargetAdminDelay(address,uint32).selector ) ||
|
||||
selector == to_bytes4(sig:updateAuthority(address,address).selector ) ||
|
||||
selector == to_bytes4(sig:setTargetClosed(address,bool).selector ) ||
|
||||
selector == to_bytes4(sig:setTargetFunctionRole(address,bytes4[],uint64).selector) ||
|
||||
selector == to_bytes4(sig:grantRole(uint64,address,uint32).selector ) ||
|
||||
selector == to_bytes4(sig:revokeRole(uint64,address).selector );
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Invariant: executionId must be clean when not in the middle of a call │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
invariant cleanExecutionId()
|
||||
executionId() == to_bytes32(0);
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Invariant: public role │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
invariant publicRole(env e, address account)
|
||||
hasRole_isMember(e, PUBLIC_ROLE(), account) &&
|
||||
hasRole_executionDelay(e, PUBLIC_ROLE(), account) == 0 &&
|
||||
getAccess_since(e, PUBLIC_ROLE(), account) == 0 &&
|
||||
getAccess_currentDelay(e, PUBLIC_ROLE(), account) == 0 &&
|
||||
getAccess_pendingDelay(e, PUBLIC_ROLE(), account) == 0 &&
|
||||
getAccess_effect(e, PUBLIC_ROLE(), account) == 0;
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Invariant: hasRole is consistent with getAccess │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
invariant hasRoleGetAccessConsistency(env e, uint64 roleId, address account)
|
||||
hasRole_isMember(e, roleId, account) == (roleId == PUBLIC_ROLE() || isSetAndPast(e, getAccess_since(e, roleId, account))) &&
|
||||
hasRole_executionDelay(e, roleId, account) == getAccess_currentDelay(e, roleId, account);
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Functions: canCall, canCallExtended, getAccess, hasRole, isTargetClosed and getTargetFunctionRole do NOT revert │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule noRevert(env e) {
|
||||
require nonpayable(e);
|
||||
require sanity(e);
|
||||
|
||||
address caller;
|
||||
address target;
|
||||
bytes data;
|
||||
bytes4 selector;
|
||||
uint64 roleId;
|
||||
|
||||
canCall@withrevert(e, caller, target, selector);
|
||||
assert !lastReverted;
|
||||
|
||||
// require data.length <= max_uint64;
|
||||
//
|
||||
// canCallExtended@withrevert(e, caller, target, data);
|
||||
// assert !lastReverted;
|
||||
|
||||
getAccess@withrevert(e, roleId, caller);
|
||||
assert !lastReverted;
|
||||
|
||||
hasRole@withrevert(e, roleId, caller);
|
||||
assert !lastReverted;
|
||||
|
||||
isTargetClosed@withrevert(target);
|
||||
assert !lastReverted;
|
||||
|
||||
getTargetFunctionRole@withrevert(target, selector);
|
||||
assert !lastReverted;
|
||||
|
||||
// Not covered:
|
||||
// - getAdminRestrictions (_1, _2 & _3)
|
||||
// - getSelector
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Functions: admin restrictions are correct │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getAdminRestrictions(env e, bytes data) {
|
||||
bool restricted = getAdminRestrictions_restricted(e, data);
|
||||
uint64 roleId = getAdminRestrictions_roleAdminId(e, data);
|
||||
uint32 delay = getAdminRestrictions_executionDelay(e, data);
|
||||
bytes4 selector = getSelector(data);
|
||||
|
||||
if (data.length < 4) {
|
||||
assert restricted == false;
|
||||
assert roleId == 0;
|
||||
assert delay == 0;
|
||||
} else {
|
||||
assert restricted ==
|
||||
isOnlyAuthorized(selector);
|
||||
|
||||
assert roleId == (
|
||||
(restricted && selector == to_bytes4(sig:grantRole(uint64,address,uint32).selector)) ||
|
||||
(restricted && selector == to_bytes4(sig:revokeRole(uint64,address).selector ))
|
||||
? getRoleAdmin(getFirstArgumentAsUint64(data))
|
||||
: ADMIN_ROLE()
|
||||
);
|
||||
|
||||
assert delay == (
|
||||
(restricted && selector == to_bytes4(sig:updateAuthority(address,address).selector )) ||
|
||||
(restricted && selector == to_bytes4(sig:setTargetClosed(address,bool).selector )) ||
|
||||
(restricted && selector == to_bytes4(sig:setTargetFunctionRole(address,bytes4[],uint64).selector))
|
||||
? getTargetAdminDelay(e, getFirstArgumentAsAddress(data))
|
||||
: 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Functions: canCall │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule canCall(env e) {
|
||||
address caller;
|
||||
address target;
|
||||
bytes4 selector;
|
||||
|
||||
// Get relevant values
|
||||
bool immediate = canCall_immediate(e, caller, target, selector);
|
||||
uint32 delay = canCall_delay(e, caller, target, selector);
|
||||
bool closed = isTargetClosed(target);
|
||||
uint64 roleId = getTargetFunctionRole(target, selector);
|
||||
bool isMember = hasRole_isMember(e, roleId, caller);
|
||||
uint32 currentDelay = hasRole_executionDelay(e, roleId, caller);
|
||||
|
||||
// Can only execute without delay in specific cases:
|
||||
// - target not closed
|
||||
// - if self-execution: `executionId` must match
|
||||
// - if third party execution: must be member with no delay
|
||||
assert immediate <=> (
|
||||
!closed &&
|
||||
(
|
||||
(caller == currentContract && executionId() == hashExecutionId(target, selector))
|
||||
||
|
||||
(caller != currentContract && isMember && currentDelay == 0)
|
||||
)
|
||||
);
|
||||
|
||||
// Can only execute with delay in specific cases:
|
||||
// - target not closed
|
||||
// - third party execution
|
||||
// - caller is a member and has an execution delay
|
||||
assert delay > 0 <=> (
|
||||
!closed &&
|
||||
caller != currentContract &&
|
||||
isMember &&
|
||||
currentDelay > 0
|
||||
);
|
||||
|
||||
// If there is a delay, then it must be the caller's execution delay
|
||||
assert delay > 0 => delay == currentDelay;
|
||||
|
||||
// Immediate execute means no delayed execution
|
||||
assert immediate => delay == 0;
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Functions: canCallExtended │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule canCallExtended(env e) {
|
||||
address caller;
|
||||
address target;
|
||||
bytes data;
|
||||
bytes4 selector = getSelector(data);
|
||||
|
||||
bool immediate = canCallExtended_immediate(e, caller, target, data);
|
||||
uint32 delay = canCallExtended_delay(e, caller, target, data);
|
||||
bool enabled = getAdminRestrictions_restricted(e, data);
|
||||
uint64 roleId = getAdminRestrictions_roleAdminId(e, data);
|
||||
uint32 operationDelay = getAdminRestrictions_executionDelay(e, data);
|
||||
bool inRole = hasRole_isMember(e, roleId, caller);
|
||||
uint32 executionDelay = hasRole_executionDelay(e, roleId, caller);
|
||||
|
||||
if (target == currentContract) {
|
||||
// Can only execute without delay in the specific cases:
|
||||
// - caller is the AccessManager and the executionId is set
|
||||
// or
|
||||
// - data matches an admin restricted function
|
||||
// - caller has the necessary role
|
||||
// - operation delay is not set
|
||||
// - execution delay is not set
|
||||
assert immediate <=> (
|
||||
(
|
||||
caller == currentContract &&
|
||||
data.length >= 4 &&
|
||||
executionId() == hashExecutionId(target, selector)
|
||||
) || (
|
||||
caller != currentContract &&
|
||||
enabled &&
|
||||
inRole &&
|
||||
operationDelay == 0 &&
|
||||
executionDelay == 0
|
||||
)
|
||||
);
|
||||
|
||||
// Immediate execute means no delayed execution
|
||||
// This is equivalent to "delay > 0 => !immediate"
|
||||
assert immediate => delay == 0;
|
||||
|
||||
// Can only execute with delay in specific cases:
|
||||
// - caller is a third party
|
||||
// - data matches an admin restricted function
|
||||
// - caller has the necessary role
|
||||
// -operation delay or execution delay is set
|
||||
assert delay > 0 <=> (
|
||||
caller != currentContract &&
|
||||
enabled &&
|
||||
inRole &&
|
||||
(operationDelay > 0 || executionDelay > 0)
|
||||
);
|
||||
|
||||
// If there is a delay, then it must be the maximum of caller's execution delay and the operation delay
|
||||
assert delay > 0 => to_mathint(delay) == max(operationDelay, executionDelay);
|
||||
} else if (data.length < 4) {
|
||||
assert immediate == false;
|
||||
assert delay == 0;
|
||||
} else {
|
||||
// results are equivalent when targeting third party contracts
|
||||
assert immediate == canCall_immediate(e, caller, target, selector);
|
||||
assert delay == canCall_delay(e, caller, target, selector);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: getAccess │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getAccessChangeTime(uint64 roleId, address account) {
|
||||
env e1;
|
||||
env e2;
|
||||
|
||||
// values before
|
||||
mathint getAccess1Before = getAccess_since(e1, roleId, account);
|
||||
mathint getAccess2Before = getAccess_currentDelay(e1, roleId, account);
|
||||
mathint getAccess3Before = getAccess_pendingDelay(e1, roleId, account);
|
||||
mathint getAccess4Before = getAccess_effect(e1, roleId, account);
|
||||
|
||||
// time pass: e1 → e2
|
||||
require clock(e1) <= clock(e2);
|
||||
|
||||
// values after
|
||||
mathint getAccess1After = getAccess_since(e2, roleId, account);
|
||||
mathint getAccess2After = getAccess_currentDelay(e2, roleId, account);
|
||||
mathint getAccess3After = getAccess_pendingDelay(e2, roleId, account);
|
||||
mathint getAccess4After = getAccess_effect(e2, roleId, account);
|
||||
|
||||
// member "since" cannot change as a consequence of time passing
|
||||
assert getAccess1Before == getAccess1After;
|
||||
|
||||
// any change of any other value should be a consequence of the effect timepoint being reached
|
||||
assert (
|
||||
getAccess2Before != getAccess2After ||
|
||||
getAccess3Before != getAccess3After ||
|
||||
getAccess4Before != getAccess4After
|
||||
) => (
|
||||
getAccess4Before != 0 &&
|
||||
getAccess4Before > clock(e1) &&
|
||||
getAccess4Before <= clock(e2) &&
|
||||
getAccess2After == getAccess3Before &&
|
||||
getAccess3After == 0 &&
|
||||
getAccess4After == 0
|
||||
);
|
||||
}
|
||||
|
||||
rule getAccessChangeCall(uint64 roleId, address account) {
|
||||
env e;
|
||||
|
||||
// sanity
|
||||
require sanity(e);
|
||||
|
||||
// values before
|
||||
mathint getAccess1Before = getAccess_since(e, roleId, account);
|
||||
mathint getAccess2Before = getAccess_currentDelay(e, roleId, account);
|
||||
mathint getAccess3Before = getAccess_pendingDelay(e, roleId, account);
|
||||
mathint getAccess4Before = getAccess_effect(e, roleId, account);
|
||||
|
||||
// arbitrary function call
|
||||
method f; calldataarg args; f(e, args);
|
||||
|
||||
// values before
|
||||
mathint getAccess1After = getAccess_since(e, roleId, account);
|
||||
mathint getAccess2After = getAccess_currentDelay(e, roleId, account);
|
||||
mathint getAccess3After = getAccess_pendingDelay(e, roleId, account);
|
||||
mathint getAccess4After = getAccess_effect(e, roleId, account);
|
||||
|
||||
// transitions
|
||||
assert (
|
||||
getAccess1Before != getAccess1After ||
|
||||
getAccess2Before != getAccess2After ||
|
||||
getAccess3Before != getAccess3After ||
|
||||
getAccess4Before != getAccess4After
|
||||
) => (
|
||||
(
|
||||
f.selector == sig:grantRole(uint64,address,uint32).selector &&
|
||||
getAccess1After > 0
|
||||
) || (
|
||||
(
|
||||
f.selector == sig:revokeRole(uint64,address).selector ||
|
||||
f.selector == sig:renounceRole(uint64,address).selector
|
||||
) &&
|
||||
getAccess1After == 0 &&
|
||||
getAccess2After == 0 &&
|
||||
getAccess3After == 0 &&
|
||||
getAccess4After == 0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: isTargetClosed │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule isTargetClosedChangeTime(address target) {
|
||||
env e1;
|
||||
env e2;
|
||||
|
||||
// values before
|
||||
bool isClosedBefore = isTargetClosed(e1, target);
|
||||
|
||||
// time pass: e1 → e2
|
||||
require clock(e1) <= clock(e2);
|
||||
|
||||
// values after
|
||||
bool isClosedAfter = isTargetClosed(e2, target);
|
||||
|
||||
// transitions
|
||||
assert isClosedBefore == isClosedAfter;
|
||||
}
|
||||
|
||||
rule isTargetClosedChangeCall(address target) {
|
||||
env e;
|
||||
|
||||
// values before
|
||||
bool isClosedBefore = isTargetClosed(e, target);
|
||||
|
||||
// arbitrary function call
|
||||
method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
bool isClosedAfter = isTargetClosed(e, target);
|
||||
|
||||
// transitions
|
||||
assert isClosedBefore != isClosedAfter => (
|
||||
f.selector == sig:setTargetClosed(address,bool).selector
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: getTargetFunctionRole │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getTargetFunctionRoleChangeTime(address target, bytes4 selector) {
|
||||
env e1;
|
||||
env e2;
|
||||
|
||||
// values before
|
||||
mathint roleIdBefore = getTargetFunctionRole(e1, target, selector);
|
||||
|
||||
// time pass: e1 → e2
|
||||
require clock(e1) <= clock(e2);
|
||||
|
||||
// values after
|
||||
mathint roleIdAfter = getTargetFunctionRole(e2, target, selector);
|
||||
|
||||
// transitions
|
||||
assert roleIdBefore == roleIdAfter;
|
||||
}
|
||||
|
||||
rule getTargetFunctionRoleChangeCall(address target, bytes4 selector) {
|
||||
env e;
|
||||
|
||||
// values before
|
||||
mathint roleIdBefore = getTargetFunctionRole(e, target, selector);
|
||||
|
||||
// arbitrary function call
|
||||
method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
mathint roleIdAfter = getTargetFunctionRole(e, target, selector);
|
||||
|
||||
// transitions
|
||||
assert roleIdBefore != roleIdAfter => (
|
||||
f.selector == sig:setTargetFunctionRole(address,bytes4[],uint64).selector
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: getTargetAdminDelay │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getTargetAdminDelayChangeTime(address target) {
|
||||
env e1;
|
||||
env e2;
|
||||
|
||||
// values before
|
||||
mathint delayBefore = getTargetAdminDelay(e1, target);
|
||||
mathint delayPendingBefore = getTargetAdminDelay_after(e1, target);
|
||||
mathint delayEffectBefore = getTargetAdminDelay_effect(e1, target);
|
||||
|
||||
// time pass: e1 → e2
|
||||
require clock(e1) <= clock(e2);
|
||||
|
||||
// values after
|
||||
mathint delayAfter = getTargetAdminDelay(e2, target);
|
||||
mathint delayPendingAfter = getTargetAdminDelay_after(e2, target);
|
||||
mathint delayEffectAfter = getTargetAdminDelay_effect(e2, target);
|
||||
|
||||
assert (
|
||||
delayBefore != delayAfter ||
|
||||
delayPendingBefore != delayPendingAfter ||
|
||||
delayEffectBefore != delayEffectAfter
|
||||
) => (
|
||||
delayEffectBefore > clock(e1) &&
|
||||
delayEffectBefore <= clock(e2) &&
|
||||
delayAfter == delayPendingBefore &&
|
||||
delayPendingAfter == 0 &&
|
||||
delayEffectAfter == 0
|
||||
);
|
||||
}
|
||||
|
||||
rule getTargetAdminDelayChangeCall(address target) {
|
||||
env e;
|
||||
|
||||
// values before
|
||||
mathint delayBefore = getTargetAdminDelay(e, target);
|
||||
mathint delayPendingBefore = getTargetAdminDelay_after(e, target);
|
||||
mathint delayEffectBefore = getTargetAdminDelay_effect(e, target);
|
||||
|
||||
// arbitrary function call
|
||||
method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
mathint delayAfter = getTargetAdminDelay(e, target);
|
||||
mathint delayPendingAfter = getTargetAdminDelay_after(e, target);
|
||||
mathint delayEffectAfter = getTargetAdminDelay_effect(e, target);
|
||||
|
||||
// if anything changed ...
|
||||
assert (
|
||||
delayBefore != delayAfter ||
|
||||
delayPendingBefore != delayPendingAfter ||
|
||||
delayEffectBefore != delayEffectAfter
|
||||
) => (
|
||||
(
|
||||
// ... it was the consequence of a call to setTargetAdminDelay
|
||||
f.selector == sig:setTargetAdminDelay(address,uint32).selector
|
||||
) && (
|
||||
// ... delay cannot decrease instantly
|
||||
delayAfter >= delayBefore
|
||||
) && (
|
||||
// ... if setback is not 0, value cannot change instantly
|
||||
minSetback() > 0 => (
|
||||
delayBefore == delayAfter
|
||||
)
|
||||
) && (
|
||||
// ... if the value did not change and there is a minSetback, there must be something scheduled in the future
|
||||
delayAfter == delayBefore && minSetback() > 0 => (
|
||||
delayEffectAfter >= clock(e) + minSetback()
|
||||
)
|
||||
// note: if there is no minSetback, and if the caller "confirms" the current value,
|
||||
// then this as immediate effect and nothing is scheduled
|
||||
) && (
|
||||
// ... if the value changed, then no further change should be scheduled
|
||||
delayAfter != delayBefore => (
|
||||
delayPendingAfter == 0 &&
|
||||
delayEffectAfter == 0
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: getRoleGrantDelay │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getRoleGrantDelayChangeTime(uint64 roleId) {
|
||||
env e1;
|
||||
env e2;
|
||||
|
||||
// values before
|
||||
mathint delayBefore = getRoleGrantDelay(e1, roleId);
|
||||
mathint delayPendingBefore = getRoleGrantDelay_after(e1, roleId);
|
||||
mathint delayEffectBefore = getRoleGrantDelay_effect(e1, roleId);
|
||||
|
||||
// time pass: e1 → e2
|
||||
require clock(e1) <= clock(e2);
|
||||
|
||||
// values after
|
||||
mathint delayAfter = getRoleGrantDelay(e2, roleId);
|
||||
mathint delayPendingAfter = getRoleGrantDelay_after(e2, roleId);
|
||||
mathint delayEffectAfter = getRoleGrantDelay_effect(e2, roleId);
|
||||
|
||||
assert (
|
||||
delayBefore != delayAfter ||
|
||||
delayPendingBefore != delayPendingAfter ||
|
||||
delayEffectBefore != delayEffectAfter
|
||||
) => (
|
||||
delayEffectBefore > clock(e1) &&
|
||||
delayEffectBefore <= clock(e2) &&
|
||||
delayAfter == delayPendingBefore &&
|
||||
delayPendingAfter == 0 &&
|
||||
delayEffectAfter == 0
|
||||
);
|
||||
}
|
||||
|
||||
rule getRoleGrantDelayChangeCall(uint64 roleId) {
|
||||
env e;
|
||||
|
||||
// values before
|
||||
mathint delayBefore = getRoleGrantDelay(e, roleId);
|
||||
mathint delayPendingBefore = getRoleGrantDelay_after(e, roleId);
|
||||
mathint delayEffectBefore = getRoleGrantDelay_effect(e, roleId);
|
||||
|
||||
// arbitrary function call
|
||||
method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
mathint delayAfter = getRoleGrantDelay(e, roleId);
|
||||
mathint delayPendingAfter = getRoleGrantDelay_after(e, roleId);
|
||||
mathint delayEffectAfter = getRoleGrantDelay_effect(e, roleId);
|
||||
|
||||
// if anything changed ...
|
||||
assert (
|
||||
delayBefore != delayAfter ||
|
||||
delayPendingBefore != delayPendingAfter ||
|
||||
delayEffectBefore != delayEffectAfter
|
||||
) => (
|
||||
(
|
||||
// ... it was the consequence of a call to setTargetAdminDelay
|
||||
f.selector == sig:setGrantDelay(uint64,uint32).selector
|
||||
) && (
|
||||
// ... delay cannot decrease instantly
|
||||
delayAfter >= delayBefore
|
||||
) && (
|
||||
// ... if setback is not 0, value cannot change instantly
|
||||
minSetback() > 0 => (
|
||||
delayBefore == delayAfter
|
||||
)
|
||||
) && (
|
||||
// ... if the value did not change and there is a minSetback, there must be something scheduled in the future
|
||||
delayAfter == delayBefore && minSetback() > 0 => (
|
||||
delayEffectAfter >= clock(e) + minSetback()
|
||||
)
|
||||
// note: if there is no minSetback, and if the caller "confirms" the current value,
|
||||
// then this as immediate effect and nothing is scheduled
|
||||
) && (
|
||||
// ... if the value changed, then no further change should be scheduled
|
||||
delayAfter != delayBefore => (
|
||||
delayPendingAfter == 0 &&
|
||||
delayEffectAfter == 0
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: getRoleAdmin & getRoleGuardian │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getRoleAdminChangeCall(uint64 roleId) {
|
||||
// values before
|
||||
mathint adminIdBefore = getRoleAdmin(roleId);
|
||||
|
||||
// arbitrary function call
|
||||
env e; method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
mathint adminIdAfter = getRoleAdmin(roleId);
|
||||
|
||||
// transitions
|
||||
assert adminIdBefore != adminIdAfter => f.selector == sig:setRoleAdmin(uint64,uint64).selector;
|
||||
}
|
||||
|
||||
rule getRoleGuardianChangeCall(uint64 roleId) {
|
||||
// values before
|
||||
mathint guardianIdBefore = getRoleGuardian(roleId);
|
||||
|
||||
// arbitrary function call
|
||||
env e; method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
mathint guardianIdAfter = getRoleGuardian(roleId);
|
||||
|
||||
// transitions
|
||||
assert guardianIdBefore != guardianIdAfter => (
|
||||
f.selector == sig:setRoleGuardian(uint64,uint64).selector
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: getNonce │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getNonceChangeCall(bytes32 operationId) {
|
||||
// values before
|
||||
mathint nonceBefore = getNonce(operationId);
|
||||
|
||||
// reasonable assumption
|
||||
require nonceBefore < max_uint32;
|
||||
|
||||
// arbitrary function call
|
||||
env e; method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
mathint nonceAfter = getNonce(operationId);
|
||||
|
||||
// transitions
|
||||
assert nonceBefore != nonceAfter => (
|
||||
f.selector == sig:schedule(address,bytes,uint48).selector &&
|
||||
nonceAfter == nonceBefore + 1
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ State transitions: getSchedule │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule getScheduleChangeTime(bytes32 operationId) {
|
||||
env e1;
|
||||
env e2;
|
||||
|
||||
// values before
|
||||
mathint scheduleBefore = getSchedule(e1, operationId);
|
||||
|
||||
// time pass: e1 → e2
|
||||
require clock(e1) <= clock(e2);
|
||||
|
||||
// values after
|
||||
mathint scheduleAfter = getSchedule(e2, operationId);
|
||||
|
||||
// transition
|
||||
assert scheduleBefore != scheduleAfter => (
|
||||
scheduleBefore + expiration() > clock(e1) &&
|
||||
scheduleBefore + expiration() <= clock(e2) &&
|
||||
scheduleAfter == 0
|
||||
);
|
||||
}
|
||||
|
||||
rule getScheduleChangeCall(bytes32 operationId) {
|
||||
env e;
|
||||
|
||||
// values before
|
||||
mathint scheduleBefore = getSchedule(e, operationId);
|
||||
|
||||
// arbitrary function call
|
||||
method f; calldataarg args; f(e, args);
|
||||
|
||||
// values after
|
||||
mathint scheduleAfter = getSchedule(e, operationId);
|
||||
|
||||
// transitions
|
||||
assert scheduleBefore != scheduleAfter => (
|
||||
(f.selector == sig:schedule(address,bytes,uint48).selector && scheduleAfter >= clock(e)) ||
|
||||
(f.selector == sig:execute(address,bytes).selector && scheduleAfter == 0 ) ||
|
||||
(f.selector == sig:cancel(address,address,bytes).selector && scheduleAfter == 0 ) ||
|
||||
(f.selector == sig:consumeScheduledOp(address,bytes).selector && scheduleAfter == 0 ) ||
|
||||
(isOnlyAuthorized(to_bytes4(f.selector)) && scheduleAfter == 0 )
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Functions: restricted functions can only be called by owner │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
rule restrictedFunctions(env e) {
|
||||
require nonpayable(e);
|
||||
require sanity(e);
|
||||
|
||||
method f;
|
||||
calldataarg args;
|
||||
|
||||
f(e,args);
|
||||
|
||||
assert (
|
||||
f.selector == sig:labelRole(uint64,string).selector ||
|
||||
f.selector == sig:setRoleAdmin(uint64,uint64).selector ||
|
||||
f.selector == sig:setRoleGuardian(uint64,uint64).selector ||
|
||||
f.selector == sig:setGrantDelay(uint64,uint32).selector ||
|
||||
f.selector == sig:setTargetAdminDelay(address,uint32).selector ||
|
||||
f.selector == sig:updateAuthority(address,address).selector ||
|
||||
f.selector == sig:setTargetClosed(address,bool).selector ||
|
||||
f.selector == sig:setTargetFunctionRole(address,bytes4[],uint64).selector
|
||||
) => (
|
||||
hasRole_isMember(e, ADMIN_ROLE(), e.msg.sender) || e.msg.sender == currentContract
|
||||
);
|
||||
}
|
||||
|
||||
rule restrictedFunctionsGrantRole(env e) {
|
||||
require nonpayable(e);
|
||||
require sanity(e);
|
||||
|
||||
uint64 roleId;
|
||||
address account;
|
||||
uint32 executionDelay;
|
||||
|
||||
// We want to check that the caller has the admin role before we possibly grant it.
|
||||
bool hasAdminRoleBefore = hasRole_isMember(e, getRoleAdmin(roleId), e.msg.sender);
|
||||
|
||||
grantRole(e, roleId, account, executionDelay);
|
||||
|
||||
assert hasAdminRoleBefore || e.msg.sender == currentContract;
|
||||
}
|
||||
|
||||
rule restrictedFunctionsRevokeRole(env e) {
|
||||
require nonpayable(e);
|
||||
require sanity(e);
|
||||
|
||||
uint64 roleId;
|
||||
address account;
|
||||
|
||||
// This is needed if roleId is self-administered, the `revokeRole` call could target
|
||||
// e.msg.sender and remove the very role that is necessary for authorizing the call.
|
||||
bool hasAdminRoleBefore = hasRole_isMember(e, getRoleAdmin(roleId), e.msg.sender);
|
||||
|
||||
revokeRole(e, roleId, account);
|
||||
|
||||
assert hasAdminRoleBefore || e.msg.sender == currentContract;
|
||||
}
|
||||
|
||||
/*
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Functions: canCall delay is enforced for calls to execute (only for others target) │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
// getScheduleChangeCall proves that only {schedule} can set an operation schedule to a non 0 value
|
||||
rule callDelayEnforce_scheduleInTheFuture(env e) {
|
||||
address target;
|
||||
bytes data;
|
||||
uint48 when;
|
||||
|
||||
// Condition: calling a third party with a delay
|
||||
mathint delay = canCallExtended_delay(e, e.msg.sender, target, data);
|
||||
require delay > 0;
|
||||
|
||||
// Schedule
|
||||
schedule(e, target, data, when);
|
||||
|
||||
// Get operation schedule
|
||||
mathint timepoint = getSchedule(e, hashOperation(e.msg.sender, target, data));
|
||||
|
||||
// Schedule is far enough in the future
|
||||
assert timepoint == max(clock(e) + delay, when);
|
||||
}
|
||||
|
||||
rule callDelayEnforce_executeAfterDelay(env e) {
|
||||
address target;
|
||||
bytes data;
|
||||
|
||||
// Condition: calling a third party with a delay
|
||||
mathint delay = canCallExtended_delay(e, e.msg.sender, target, data);
|
||||
|
||||
// Get operation schedule before
|
||||
mathint scheduleBefore = getSchedule(e, hashOperation(e.msg.sender, target, data));
|
||||
|
||||
// Do call
|
||||
execute@withrevert(e, target, data);
|
||||
bool success = !lastReverted;
|
||||
|
||||
// Get operation schedule after
|
||||
mathint scheduleAfter = getSchedule(e, hashOperation(e.msg.sender, target, data));
|
||||
|
||||
// Can only execute if delay is set and has passed
|
||||
assert success => (
|
||||
delay > 0 => (
|
||||
scheduleBefore != 0 &&
|
||||
scheduleBefore <= clock(e)
|
||||
) &&
|
||||
scheduleAfter == 0
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,12 @@
|
||||
// environment
|
||||
definition nonpayable(env e) returns bool = e.msg.value == 0;
|
||||
definition nonzerosender(env e) returns bool = e.msg.sender != 0;
|
||||
definition sanity(env e) returns bool = clock(e) > 0 && clock(e) <= max_uint48;
|
||||
|
||||
// math
|
||||
definition min(mathint a, mathint b) returns mathint = a < b ? a : b;
|
||||
definition max(mathint a, mathint b) returns mathint = a > b ? a : b;
|
||||
|
||||
// time
|
||||
definition clock(env e) returns mathint = to_mathint(e.block.timestamp);
|
||||
definition isSetAndPast(env e, uint48 timepoint) returns bool = timepoint != 0 && to_mathint(timepoint) <= clock(e);
|
||||
|
||||
5
certora/specs/methods/IAccessManaged.spec
Normal file
5
certora/specs/methods/IAccessManaged.spec
Normal file
@ -0,0 +1,5 @@
|
||||
methods {
|
||||
function authority() external returns (address) envfree;
|
||||
function isConsumingScheduledOp() external returns (bytes4) envfree;
|
||||
function setAuthority(address) external;
|
||||
}
|
||||
33
certora/specs/methods/IAccessManager.spec
Normal file
33
certora/specs/methods/IAccessManager.spec
Normal file
@ -0,0 +1,33 @@
|
||||
methods {
|
||||
function ADMIN_ROLE() external returns (uint64) envfree;
|
||||
function PUBLIC_ROLE() external returns (uint64) envfree;
|
||||
function canCall(address,address,bytes4) external returns (bool,uint32);
|
||||
function expiration() external returns (uint32) envfree;
|
||||
function minSetback() external returns (uint32) envfree;
|
||||
function isTargetClosed(address) external returns (bool) envfree;
|
||||
function getTargetFunctionRole(address,bytes4) external returns (uint64) envfree;
|
||||
function getTargetAdminDelay(address) external returns (uint32);
|
||||
function getRoleAdmin(uint64) external returns (uint64) envfree;
|
||||
function getRoleGuardian(uint64) external returns (uint64) envfree;
|
||||
function getRoleGrantDelay(uint64) external returns (uint32);
|
||||
function getAccess(uint64,address) external returns (uint48,uint32,uint32,uint48);
|
||||
function hasRole(uint64,address) external returns (bool,uint32);
|
||||
function labelRole(uint64,string) external;
|
||||
function grantRole(uint64,address,uint32) external;
|
||||
function revokeRole(uint64,address) external;
|
||||
function renounceRole(uint64,address) external;
|
||||
function setRoleAdmin(uint64,uint64) external;
|
||||
function setRoleGuardian(uint64,uint64) external;
|
||||
function setGrantDelay(uint64,uint32) external;
|
||||
function setTargetFunctionRole(address,bytes4[],uint64) external;
|
||||
function setTargetAdminDelay(address,uint32) external;
|
||||
function setTargetClosed(address,bool) external;
|
||||
function hashOperation(address,address,bytes) external returns (bytes32) envfree;
|
||||
function getNonce(bytes32) external returns (uint32) envfree;
|
||||
function getSchedule(bytes32) external returns (uint48);
|
||||
function schedule(address,bytes,uint48) external returns (bytes32,uint32);
|
||||
function execute(address,bytes) external returns (uint32);
|
||||
function cancel(address,address,bytes) external returns (uint32);
|
||||
function consumeScheduledOp(address,bytes) external;
|
||||
function updateAuthority(address,address) external;
|
||||
}
|
||||
@ -586,7 +586,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
|
||||
uint48 minWhen = Time.timestamp() + setback;
|
||||
|
||||
// if call with delay is not authorized, or if requested timing is too soon
|
||||
// If call with delay is not authorized, or if requested timing is too soon, revert
|
||||
if (setback == 0 || (when > 0 && when < minWhen)) {
|
||||
revert AccessManagerUnauthorizedCall(caller, target, _checkSelector(data));
|
||||
}
|
||||
@ -639,7 +639,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
|
||||
// Fetch restrictions that apply to the caller on the targeted function
|
||||
(bool immediate, uint32 setback) = _canCallExtended(caller, target, data);
|
||||
|
||||
// If caller is not authorised, revert
|
||||
// If call is not authorized, revert
|
||||
if (!immediate && setback == 0) {
|
||||
revert AccessManagerUnauthorizedCall(caller, target, _checkSelector(data));
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
certora-cli==4.8.0
|
||||
certora-cli==4.13.1
|
||||
|
||||
Reference in New Issue
Block a user