|
@@ -0,0 +1,275 @@
|
|
|
+import "helpers.spec"
|
|
|
+import "methods/IAccessControl.spec"
|
|
|
+
|
|
|
+methods {
|
|
|
+ TIMELOCK_ADMIN_ROLE() returns (bytes32) envfree
|
|
|
+ PROPOSER_ROLE() returns (bytes32) envfree
|
|
|
+ EXECUTOR_ROLE() returns (bytes32) envfree
|
|
|
+ CANCELLER_ROLE() returns (bytes32) envfree
|
|
|
+ isOperation(bytes32) returns (bool) envfree
|
|
|
+ isOperationPending(bytes32) returns (bool) envfree
|
|
|
+ isOperationReady(bytes32) returns (bool)
|
|
|
+ isOperationDone(bytes32) returns (bool) envfree
|
|
|
+ getTimestamp(bytes32) returns (uint256) envfree
|
|
|
+ getMinDelay() returns (uint256) envfree
|
|
|
+
|
|
|
+ hashOperation(address, uint256, bytes, bytes32, bytes32) returns(bytes32) envfree
|
|
|
+ hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) returns(bytes32) envfree
|
|
|
+
|
|
|
+ schedule(address, uint256, bytes, bytes32, bytes32, uint256)
|
|
|
+ scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256)
|
|
|
+ execute(address, uint256, bytes, bytes32, bytes32)
|
|
|
+ executeBatch(address[], uint256[], bytes[], bytes32, bytes32)
|
|
|
+ cancel(bytes32)
|
|
|
+
|
|
|
+ updateDelay(uint256)
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Helpers │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+// Uniformly handle scheduling of batched and non-batched operations.
|
|
|
+function helperScheduleWithRevert(env e, method f, bytes32 id, uint256 delay) {
|
|
|
+ if (f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector) {
|
|
|
+ address target; uint256 value; bytes data; bytes32 predecessor; bytes32 salt;
|
|
|
+ require hashOperation(target, value, data, predecessor, salt) == id; // Correlation
|
|
|
+ schedule@withrevert(e, target, value, data, predecessor, salt, delay);
|
|
|
+ } else if (f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector) {
|
|
|
+ address[] targets; uint256[] values; bytes[] payloads; bytes32 predecessor; bytes32 salt;
|
|
|
+ require hashOperationBatch(targets, values, payloads, predecessor, salt) == id; // Correlation
|
|
|
+ scheduleBatch@withrevert(e, targets, values, payloads, predecessor, salt, delay);
|
|
|
+ } else {
|
|
|
+ calldataarg args;
|
|
|
+ f@withrevert(e, args);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Uniformly handle execution of batched and non-batched operations.
|
|
|
+function helperExecuteWithRevert(env e, method f, bytes32 id, bytes32 predecessor) {
|
|
|
+ if (f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector) {
|
|
|
+ address target; uint256 value; bytes data; bytes32 salt;
|
|
|
+ require hashOperation(target, value, data, predecessor, salt) == id; // Correlation
|
|
|
+ execute@withrevert(e, target, value, data, predecessor, salt);
|
|
|
+ } else if (f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector) {
|
|
|
+ address[] targets; uint256[] values; bytes[] payloads; bytes32 salt;
|
|
|
+ require hashOperationBatch(targets, values, payloads, predecessor, salt) == id; // Correlation
|
|
|
+ executeBatch@withrevert(e, targets, values, payloads, predecessor, salt);
|
|
|
+ } else {
|
|
|
+ calldataarg args;
|
|
|
+ f@withrevert(e, args);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Definitions │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+definition DONE_TIMESTAMP() returns uint256 = 1;
|
|
|
+definition UNSET() returns uint8 = 0x1;
|
|
|
+definition PENDING() returns uint8 = 0x2;
|
|
|
+definition DONE() returns uint8 = 0x4;
|
|
|
+
|
|
|
+definition isUnset(bytes32 id) returns bool = !isOperation(id);
|
|
|
+definition isPending(bytes32 id) returns bool = isOperationPending(id);
|
|
|
+definition isDone(bytes32 id) returns bool = isOperationDone(id);
|
|
|
+definition state(bytes32 id) returns uint8 = (isUnset(id) ? UNSET() : 0) | (isPending(id) ? PENDING() : 0) | (isDone(id) ? DONE() : 0);
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Invariants: consistency of accessors │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+invariant isOperationCheck(bytes32 id)
|
|
|
+ isOperation(id) <=> getTimestamp(id) > 0
|
|
|
+ filtered { f -> !f.isView }
|
|
|
+
|
|
|
+invariant isOperationPendingCheck(bytes32 id)
|
|
|
+ isOperationPending(id) <=> getTimestamp(id) > DONE_TIMESTAMP()
|
|
|
+ filtered { f -> !f.isView }
|
|
|
+
|
|
|
+invariant isOperationDoneCheck(bytes32 id)
|
|
|
+ isOperationDone(id) <=> getTimestamp(id) == DONE_TIMESTAMP()
|
|
|
+ filtered { f -> !f.isView }
|
|
|
+
|
|
|
+invariant isOperationReadyCheck(env e, bytes32 id)
|
|
|
+ isOperationReady(e, id) <=> (isOperationPending(id) && getTimestamp(id) <= e.block.timestamp)
|
|
|
+ filtered { f -> !f.isView }
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Invariant: a proposal id is either unset, pending or done │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+invariant stateConsistency(bytes32 id, env e)
|
|
|
+ // Check states are mutually exclusive
|
|
|
+ (isUnset(id) <=> (!isPending(id) && !isDone(id) )) &&
|
|
|
+ (isPending(id) <=> (!isUnset(id) && !isDone(id) )) &&
|
|
|
+ (isDone(id) <=> (!isUnset(id) && !isPending(id))) &&
|
|
|
+ // Check that the state helper behaves as expected:
|
|
|
+ (isUnset(id) <=> state(id) == UNSET() ) &&
|
|
|
+ (isPending(id) <=> state(id) == PENDING() ) &&
|
|
|
+ (isDone(id) <=> state(id) == DONE() ) &&
|
|
|
+ // Check substate
|
|
|
+ isOperationReady(e, id) => isPending(id)
|
|
|
+ filtered { f -> !f.isView }
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Rule: state transition rules │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+rule stateTransition(bytes32 id, env e, method f, calldataarg args) {
|
|
|
+ require e.block.timestamp > 1; // Sanity
|
|
|
+
|
|
|
+ uint8 stateBefore = state(id);
|
|
|
+ f(e, args);
|
|
|
+ uint8 stateAfter = state(id);
|
|
|
+
|
|
|
+ // Cannot jump from UNSET to DONE
|
|
|
+ assert stateBefore == UNSET() => stateAfter != DONE();
|
|
|
+
|
|
|
+ // UNSET → PENDING: schedule or scheduleBatch
|
|
|
+ assert stateBefore == UNSET() && stateAfter == PENDING() => (
|
|
|
+ f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector ||
|
|
|
+ f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector
|
|
|
+ );
|
|
|
+
|
|
|
+ // PENDING → UNSET: cancel
|
|
|
+ assert stateBefore == PENDING() && stateAfter == UNSET() => (
|
|
|
+ f.selector == cancel(bytes32).selector
|
|
|
+ );
|
|
|
+
|
|
|
+ // PENDING → DONE: execute or executeBatch
|
|
|
+ assert stateBefore == PENDING() && stateAfter == DONE() => (
|
|
|
+ f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector ||
|
|
|
+ f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector
|
|
|
+ );
|
|
|
+
|
|
|
+ // DONE is final
|
|
|
+ assert stateBefore == DONE() => stateAfter == DONE();
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Rule: minimum delay can only be updated through a timelock execution │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+rule minDelayOnlyChange(env e) {
|
|
|
+ uint256 delayBefore = getMinDelay();
|
|
|
+
|
|
|
+ method f; calldataarg args;
|
|
|
+ f(e, args);
|
|
|
+
|
|
|
+ assert delayBefore != getMinDelay() => (e.msg.sender == currentContract && f.selector == updateDelay(uint256).selector), "Unauthorized delay update";
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Rule: schedule liveness and effects │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+rule schedule(env e, method f, bytes32 id, uint256 delay) filtered { f ->
|
|
|
+ f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector ||
|
|
|
+ f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector
|
|
|
+} {
|
|
|
+ require nonpayable(e);
|
|
|
+
|
|
|
+ // Basic timestamp assumptions
|
|
|
+ require e.block.timestamp > 1;
|
|
|
+ require e.block.timestamp + delay < max_uint256;
|
|
|
+ require e.block.timestamp + getMinDelay() < max_uint256;
|
|
|
+
|
|
|
+ bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId);
|
|
|
+
|
|
|
+ uint8 stateBefore = state(id);
|
|
|
+ bool isDelaySufficient = delay >= getMinDelay();
|
|
|
+ bool isProposerBefore = hasRole(PROPOSER_ROLE(), e.msg.sender);
|
|
|
+
|
|
|
+ helperScheduleWithRevert(e, f, id, delay);
|
|
|
+ bool success = !lastReverted;
|
|
|
+
|
|
|
+ // liveness
|
|
|
+ assert success <=> (
|
|
|
+ stateBefore == UNSET() &&
|
|
|
+ isDelaySufficient &&
|
|
|
+ isProposerBefore
|
|
|
+ );
|
|
|
+
|
|
|
+ // effect
|
|
|
+ assert success => state(id) == PENDING(), "State transition violation";
|
|
|
+ assert success => getTimestamp(id) == to_uint256(e.block.timestamp + delay), "Proposal timestamp not correctly set";
|
|
|
+
|
|
|
+ // no side effect
|
|
|
+ assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected";
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Rule: execute liveness and effects │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+rule execute(env e, method f, bytes32 id, bytes32 predecessor) filtered { f ->
|
|
|
+ f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector ||
|
|
|
+ f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector
|
|
|
+} {
|
|
|
+ bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId);
|
|
|
+
|
|
|
+ uint8 stateBefore = state(id);
|
|
|
+ bool isOperationReadyBefore = isOperationReady(e, id);
|
|
|
+ bool isExecutorOrOpen = hasRole(EXECUTOR_ROLE(), e.msg.sender) || hasRole(EXECUTOR_ROLE(), 0);
|
|
|
+ bool predecessorDependency = predecessor == 0 || isDone(predecessor);
|
|
|
+
|
|
|
+ helperExecuteWithRevert(e, f, id, predecessor);
|
|
|
+ bool success = !lastReverted;
|
|
|
+
|
|
|
+ // The underlying transaction can revert, and that would cause the execution to revert. We can check that all non
|
|
|
+ // reverting calls meet the requirements in terms of proposal readiness, access control and predecessor dependency.
|
|
|
+ // We can't however guarantee that these requirements being meet ensure liveness of the proposal, because the
|
|
|
+ // proposal can revert for reasons beyond our control.
|
|
|
+
|
|
|
+ // liveness, should be `<=>` but can only check `=>` (see comment above)
|
|
|
+ assert success => (
|
|
|
+ stateBefore == PENDING() &&
|
|
|
+ isOperationReadyBefore &&
|
|
|
+ predecessorDependency &&
|
|
|
+ isExecutorOrOpen
|
|
|
+ );
|
|
|
+
|
|
|
+ // effect
|
|
|
+ assert success => state(id) == DONE(), "State transition violation";
|
|
|
+
|
|
|
+ // no side effect
|
|
|
+ assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected";
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
|
+│ Rule: cancel liveness and effects │
|
|
|
+└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
|
+*/
|
|
|
+rule cancel(env e, bytes32 id) {
|
|
|
+ require nonpayable(e);
|
|
|
+
|
|
|
+ bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId);
|
|
|
+
|
|
|
+ uint8 stateBefore = state(id);
|
|
|
+ bool isCanceller = hasRole(CANCELLER_ROLE(), e.msg.sender);
|
|
|
+
|
|
|
+ cancel@withrevert(e, id);
|
|
|
+ bool success = !lastReverted;
|
|
|
+
|
|
|
+ // liveness
|
|
|
+ assert success <=> (
|
|
|
+ stateBefore == PENDING() &&
|
|
|
+ isCanceller
|
|
|
+ );
|
|
|
+
|
|
|
+ // effect
|
|
|
+ assert success => state(id) == UNSET(), "State transition violation";
|
|
|
+
|
|
|
+ // no side effect
|
|
|
+ assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected";
|
|
|
+}
|