Commit 96a7781a authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #7540 from ethereum-optimism/jm/safe-liveness

Safe liveness checking module and guard
parents cfc08e23 43cb263a
...@@ -17,6 +17,3 @@ ...@@ -17,6 +17,3 @@
path = packages/contracts-bedrock/lib/safe-contracts path = packages/contracts-bedrock/lib/safe-contracts
url = https://github.com/safe-global/safe-contracts url = https://github.com/safe-global/safe-contracts
branch = v1.4.0 branch = v1.4.0
...@@ -9,8 +9,9 @@ comment: ...@@ -9,8 +9,9 @@ comment:
ignore: ignore:
- "op-e2e" - "op-e2e"
- "**/*.t.sol"
- "op-bindings/bindings/*.go" - "op-bindings/bindings/*.go"
- "**/*.t.sol"
- "packages/contracts-bedrock/test/**/*.sol"
- "packages/contracts-bedrock/contracts/vendor/WETH9.sol" - "packages/contracts-bedrock/contracts/vendor/WETH9.sol"
- 'packages/contracts-bedrock/contracts/EAS/**/*.sol' - 'packages/contracts-bedrock/contracts/EAS/**/*.sol'
coverage: coverage:
......
This diff is collapsed.
This diff is collapsed.
...@@ -305,6 +305,33 @@ LegacyERC20ETH_Test:test_transferFrom_doesNotExist_reverts() (gas: 12957) ...@@ -305,6 +305,33 @@ LegacyERC20ETH_Test:test_transferFrom_doesNotExist_reverts() (gas: 12957)
LegacyERC20ETH_Test:test_transfer_doesNotExist_reverts() (gas: 10755) LegacyERC20ETH_Test:test_transfer_doesNotExist_reverts() (gas: 10755)
LegacyMessagePasser_Test:test_passMessageToL1_succeeds() (gas: 34524) LegacyMessagePasser_Test:test_passMessageToL1_succeeds() (gas: 34524)
LibPosition_Test:test_pos_correctness_succeeds() (gas: 38689) LibPosition_Test:test_pos_correctness_succeeds() (gas: 38689)
LivenessGuard_CheckAfterExecution_TestFails:test_checkAfterExecution_callerIsNotSafe_revert() (gas: 8531)
LivenessGuard_CheckTx_Test:test_checkTransaction_succeeds() (gas: 233535)
LivenessGuard_CheckTx_TestFails:test_checkTransaction_callerIsNotSafe_revert() (gas: 10358)
LivenessGuard_Constructor_Test:test_constructor_works() (gas: 1198965)
LivenessGuard_Getters_Test:test_getters_works() (gas: 10662)
LivenessGuard_OwnerManagement_Test:test_addOwner_succeeds() (gas: 274366)
LivenessGuard_OwnerManagement_Test:test_removeOwner_succeeds() (gas: 246263)
LivenessGuard_OwnerManagement_Test:test_swapOwner_succeeds() (gas: 284880)
LivenessGuard_ShowLiveness_Test:test_showLiveness_succeeds() (gas: 28831)
LivenessGuard_ShowLiveness_TestFail:test_showLiveness_callIsNotSafeOwner_reverts() (gas: 18770)
LivenessModule_CanRemove_Test:test_canRemove_works() (gas: 33026)
LivenessModule_CanRemove_TestFail:test_canRemove_notSafeOwner_reverts() (gas: 20489)
LivenessModule_Constructor_TestFail:test_constructor_minOwnersGreaterThanOwners_reverts() (gas: 83623)
LivenessModule_Constructor_TestFail:test_constructor_wrongThreshold_reverts() (gas: 92925)
LivenessModule_Get75PercentThreshold_Test:test_get75PercentThreshold_Works() (gas: 26339)
LivenessModule_Getters_Test:test_getters_works() (gas: 14853)
LivenessModule_RemoveOwners_Test:test_removeOwners_allOwners_succeeds() (gas: 1326177)
LivenessModule_RemoveOwners_Test:test_removeOwners_oneOwner_succeeds() (gas: 133975)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_belowEmptiedButNotShutDown_reverts() (gas: 1278643)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_belowMinButNotEmptied_reverts() (gas: 1281685)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_differentArrayLengths_reverts() (gas: 10502)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_guardChanged_reverts() (gas: 2839358)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_invalidThreshold_reverts() (gas: 69358)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_ownerHasShownLivenessRecently_reverts() (gas: 80971)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_ownerHasSignedRecently_reverts() (gas: 617629)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_swapToFallbackOwner_reverts() (gas: 1288036)
LivenessModule_RemoveOwners_TestFail:test_removeOwners_wrongPreviousOwner_reverts() (gas: 73954)
MIPS_Test:test_add_succeeds() (gas: 122932) MIPS_Test:test_add_succeeds() (gas: 122932)
MIPS_Test:test_addiSign_succeeds() (gas: 122923) MIPS_Test:test_addiSign_succeeds() (gas: 122923)
MIPS_Test:test_addi_succeeds() (gas: 123120) MIPS_Test:test_addi_succeeds() (gas: 123120)
......
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
"src/L2/L2StandardBridge.sol": "0x284ebf5569c75d98f2d1920a276d1116524399355708c4a60ea5892283c56719", "src/L2/L2StandardBridge.sol": "0x284ebf5569c75d98f2d1920a276d1116524399355708c4a60ea5892283c56719",
"src/L2/L2ToL1MessagePasser.sol": "0xafc710b4d320ef450586d96a61cbd58cac814cb3b0c4fdc280eace3efdcdf321", "src/L2/L2ToL1MessagePasser.sol": "0xafc710b4d320ef450586d96a61cbd58cac814cb3b0c4fdc280eace3efdcdf321",
"src/L2/SequencerFeeVault.sol": "0x883e434a69b4789997a4a9a32060dbbd2e12db6f1970927f1310820336119575", "src/L2/SequencerFeeVault.sol": "0x883e434a69b4789997a4a9a32060dbbd2e12db6f1970927f1310820336119575",
"src/Safe/LivenessGuard.sol": "0xa08460138c22a337f8f5d3a17e02beffe8136c4dba58935cc5c9c2d7ffe1222c",
"src/Safe/LivenessModule.sol": "0x45621d74ea464c75064f9194261d29d47552cf4a9c4f4b3a733f5df5803fc0dd",
"src/dispute/BlockOracle.sol": "0x7e724b1ee0116dfd744f556e6237af449c2f40c6426d6f1462ae2a47589283bb", "src/dispute/BlockOracle.sol": "0x7e724b1ee0116dfd744f556e6237af449c2f40c6426d6f1462ae2a47589283bb",
"src/dispute/DisputeGameFactory.sol": "0xfdfa141408d7f8de7e230ff4bef088e30d0e4d569ca743d60d292abdd21ff270", "src/dispute/DisputeGameFactory.sol": "0xfdfa141408d7f8de7e230ff4bef088e30d0e4d569ca743d60d292abdd21ff270",
"src/dispute/FaultDisputeGame.sol": "0x0766707ab32338a6586c2340ddfbfd4e9023eeb9dfa3ef87e4b404fb0260479f", "src/dispute/FaultDisputeGame.sol": "0x0766707ab32338a6586c2340ddfbfd4e9023eeb9dfa3ef87e4b404fb0260479f",
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Safe } from "safe-contracts/Safe.sol";
import { BaseGuard, GuardManager } from "safe-contracts/base/GuardManager.sol";
import { ModuleManager } from "safe-contracts/base/ModuleManager.sol";
import { SafeSigners } from "src/Safe/SafeSigners.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
import { ISemver } from "src/universal/ISemver.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
/// @title LivenessGuard
/// @notice This Guard contract is used to track the liveness of Safe owners.
/// @dev It keeps track of the last time each owner participated in signing a transaction.
/// If an owner does not participate in a transaction for a certain period of time, they are considered inactive.
/// This Guard is intended to be used in conjunction with the LivenessModule contract, but does
/// not depend on it.
/// Note: Both `checkTransaction` and `checkAfterExecution` are called once each by the Safe contract
/// before and after the execution of a transaction. It is critical that neither function revert,
/// otherwise the Safe contract will be unable to execute a transaction.
contract LivenessGuard is ISemver, BaseGuard {
using EnumerableSet for EnumerableSet.AddressSet;
/// @notice Emitted when an owner is recorded.
/// @param owner The owner's address.
event OwnerRecorded(address owner);
/// @notice Semantic version.
/// @custom:semver 1.0.0
string public constant version = "1.0.0";
/// @notice The safe account for which this contract will be the guard.
Safe internal immutable SAFE;
/// @notice A mapping of the timestamp at which an owner last participated in signing a
/// an executed transaction, or called showLiveness.
mapping(address => uint256) public lastLive;
/// @notice An enumerable set of addresses used to store the list of owners before execution,
/// and then to update the lastLive mapping according to changes in the set observed
/// after execution.
EnumerableSet.AddressSet internal ownersBefore;
/// @notice Constructor.
/// @param _safe The safe account for which this contract will be the guard.
constructor(Safe _safe) {
SAFE = _safe;
address[] memory owners = _safe.getOwners();
for (uint256 i = 0; i < owners.length; i++) {
address owner = owners[i];
lastLive[owner] = block.timestamp;
emit OwnerRecorded(owner);
}
}
/// @notice Getter function for the Safe contract instance
/// @return safe_ The Safe contract instance
function safe() public view returns (Safe safe_) {
safe_ = SAFE;
}
/// @notice Internal function to ensure that only the Safe can call certain functions.
function _requireOnlySafe() internal view {
require(msg.sender == address(SAFE), "LivenessGuard: only Safe can call this function");
}
/// @notice Records the most recent time which any owner has signed a transaction.
/// @dev Called by the Safe contract before execution of a transaction.
function checkTransaction(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes memory signatures,
address msgSender
)
external
{
msgSender; // silence unused variable warning
_requireOnlySafe();
// Cache the set of owners prior to execution.
// This will be used in the checkAfterExecution method.
address[] memory owners = SAFE.getOwners();
for (uint256 i = 0; i < owners.length; i++) {
ownersBefore.add(owners[i]);
}
// This call will reenter to the Safe which is calling it. This is OK because it is only reading the
// nonce, and using the getTransactionHash() method.
bytes32 txHash = SAFE.getTransactionHash({
to: to,
value: value,
data: data,
operation: operation,
safeTxGas: safeTxGas,
baseGas: baseGas,
gasPrice: gasPrice,
gasToken: gasToken,
refundReceiver: refundReceiver,
_nonce: SAFE.nonce() - 1
});
uint256 threshold = SAFE.getThreshold();
address[] memory signers =
SafeSigners.getNSigners({ dataHash: txHash, signatures: signatures, requiredSignatures: threshold });
for (uint256 i = 0; i < signers.length; i++) {
lastLive[signers[i]] = block.timestamp;
emit OwnerRecorded(signers[i]);
}
}
/// @notice Update the lastLive mapping according to the set of owners before and after execution.
/// @dev Called by the Safe contract after the execution of a transaction.
/// We use this post execution hook to compare the set of owners before and after.
/// If the set of owners has changed then we:
/// 1. Add new owners to the lastLive mapping
/// 2. Delete removed owners from the lastLive mapping
function checkAfterExecution(bytes32, bool) external {
_requireOnlySafe();
// Get the current set of owners
address[] memory ownersAfter = SAFE.getOwners();
// Iterate over the current owners, and remove one at a time from the ownersBefore set.
for (uint256 i = 0; i < ownersAfter.length; i++) {
// If the value was present, remove() returns true.
address ownerAfter = ownersAfter[i];
if (ownersBefore.remove(ownerAfter) == false) {
// This address was not already an owner, add it to the lastLive mapping
lastLive[ownerAfter] = block.timestamp;
}
}
// Now iterate over the remaining ownersBefore entries. Any remaining addresses are no longer an owner, so we
// delete them from the lastLive mapping.
// We cache the ownersBefore set before iterating over it, because the remove() method mutates the set.
address[] memory ownersBeforeCache = ownersBefore.values();
for (uint256 i = 0; i < ownersBeforeCache.length; i++) {
address ownerBefore = ownersBeforeCache[i];
delete lastLive[ownerBefore];
ownersBefore.remove(ownerBefore);
}
}
/// @notice Enables an owner to demonstrate liveness by calling this method directly.
/// This is useful for owners who have not recently signed a transaction via the Safe.
function showLiveness() external {
require(SAFE.isOwner(msg.sender), "LivenessGuard: only Safe owners may demonstrate liveness");
lastLive[msg.sender] = block.timestamp;
emit OwnerRecorded(msg.sender);
}
}
This diff is collapsed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library SafeSigners {
/// @notice Splits signature bytes into `uint8 v, bytes32 r, bytes32 s`.
/// Copied directly from
/// https://github.com/safe-global/safe-contracts/blob/e870f514ad34cd9654c72174d6d4a839e3c6639f/contracts/common/SignatureDecoder.sol
/// @dev Make sure to perform a bounds check for @param pos, to avoid out of bounds access on @param signatures
/// The signature format is a compact form of {bytes32 r}{bytes32 s}{uint8 v}
/// Compact means uint8 is not padded to 32 bytes.
/// @param pos Which signature to read.
/// A prior bounds check of this parameter should be performed, to avoid out of bounds access.
/// @param signatures Concatenated {r, s, v} signatures.
/// @return v Recovery ID or Safe signature type.
/// @return r Output value r of the signature.
/// @return s Output value s of the signature.
function signatureSplit(
bytes memory signatures,
uint256 pos
)
internal
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
// solhint-disable-next-line no-inline-assembly
assembly {
let signaturePos := mul(0x41, pos)
r := mload(add(signatures, add(signaturePos, 0x20)))
s := mload(add(signatures, add(signaturePos, 0x40)))
/**
* Here we are loading the last 32 bytes, including 31 bytes
* of 's'. There is no 'mload8' to do this.
* 'byte' is not working due to the Solidity parser, so lets
* use the second best option, 'and'
*/
v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
}
}
/// @notice Extract the signers from a set of signatures.
/// This method is based closely on the code in the Safe.checkNSignatures() method.
/// https://github.com/safe-global/safe-contracts/blob/e870f514ad34cd9654c72174d6d4a839e3c6639f/contracts/Safe.sol#L274
/// It has been modified by removing all signature _validation_ code. We trust the Safe to properly validate
/// the signatures.
/// This method therefore simply extracts the addresses from the signatures.
function getNSigners(
bytes32 dataHash,
bytes memory signatures,
uint256 requiredSignatures
)
internal
pure
returns (address[] memory _owners)
{
_owners = new address[](requiredSignatures);
address currentOwner;
uint8 v;
bytes32 r;
bytes32 s;
uint256 i;
for (i = 0; i < requiredSignatures; i++) {
(v, r, s) = signatureSplit(signatures, i);
if (v == 0) {
// If v is 0 then it is a contract signature
// When handling contract signatures the address of the contract is encoded into r
currentOwner = address(uint160(uint256(r)));
} else if (v == 1) {
// If v is 1 then it is an approved hash
// When handling approved hashes the address of the approver is encoded into r
currentOwner = address(uint160(uint256(r)));
} else if (v > 30) {
// If v > 30 then default va (27,28) has been adjusted for eth_sign flow
// To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix
// before applying ecrecover
currentOwner =
ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s);
} else {
// Default is the ecrecover flow with the provided data hash
// Use ecrecover with the messageHash for EOA signatures
currentOwner = ecrecover(dataHash, v, r, s);
}
_owners[i] = currentOwner;
}
}
}
This diff is collapsed.
This diff is collapsed.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol";
import { Safe } from "safe-contracts/Safe.sol";
import { SafeSigners } from "src/Safe/SafeSigners.sol";
import "test/safe-tools/SafeTestTools.sol";
import { SignatureDecoder } from "safe-contracts/common/SignatureDecoder.sol";
contract SafeSigners_Test is Test, SafeTestTools {
bytes4 internal constant EIP1271_MAGIC_VALUE = 0x20c13b0b;
enum SigTypes {
Eoa,
EthSign,
ApprovedHash,
Contract
}
/// @dev Maps every key to one of the 4 signatures types.
/// This is used in the tests below as a pseudorandom mechanism for determining which
/// signature type to use for each key.
/// @param _key The key to map to a signature type.
function sigType(uint256 _key) internal pure returns (SigTypes sigType_) {
uint256 t = _key % 4;
sigType_ = SigTypes(t);
}
/// @dev Test that for a given set of signatures:
/// 1. safe.checkNSignatures() succeeds
/// 2. the getSigners() method returns the expected signers
/// 3. the expected signers are all owners of the safe.
/// Demonstrating these three properties is sufficient to prove that the getSigners() method
/// returns the same signatures as those recovered by safe.checkNSignatures().
function testDiff_getSignaturesVsCheckSignatures_succeeds(bytes memory _data, uint256 _numSigs) external {
bytes32 digest = keccak256(_data);
// Limit the number of signatures to 25
uint256 numSigs = bound(_numSigs, 1, 25);
(, uint256[] memory keys) = SafeTestLib.makeAddrsAndKeys("getSigsTest", numSigs);
for (uint256 i = 0; i < keys.length; i++) {
if (sigType(keys[i]) == SigTypes.Contract) {
keys[i] =
SafeTestLib.encodeSmartContractWalletAsPK(SafeTestLib.decodeSmartContractWalletAsAddress(keys[i]));
}
}
// Create a new safeInstance with M=N, so that it requires a signature from each key.
SafeInstance memory safeInstance = SafeTestTools._setupSafe(keys, numSigs, 0);
// Next we will generate signatures by iterating over the keys, and choosing the signature type
// based on the key.
uint8 v;
bytes32 r;
bytes32 s;
uint256 contractSigs;
bytes memory signatures;
uint256[] memory pks = safeInstance.ownerPKs;
for (uint256 i; i < pks.length; i++) {
if (sigType(pks[i]) == SigTypes.Eoa) {
(v, r, s) = vm.sign(pks[i], digest);
signatures = bytes.concat(signatures, abi.encodePacked(r, s, v));
} else if (sigType(pks[i]) == SigTypes.EthSign) {
(v, r, s) = vm.sign(pks[i], keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)));
v += 4;
signatures = bytes.concat(signatures, abi.encodePacked(r, s, v));
} else if (sigType(pks[i]) == SigTypes.ApprovedHash) {
vm.prank(SafeTestLib.getAddr(pks[i]));
safeInstance.safe.approveHash(digest);
v = 1;
// s is not checked on approved hash signatures, so we can leave it as zero.
r = bytes32(uint256(uint160(SafeTestLib.getAddr(pks[i]))));
signatures = bytes.concat(signatures, abi.encodePacked(r, s, v));
} else if (sigType(pks[i]) == SigTypes.Contract) {
contractSigs++;
address addr = SafeTestLib.decodeSmartContractWalletAsAddress(pks[i]);
r = bytes32(uint256(uint160(addr)));
vm.mockCall(
addr, abi.encodeWithSignature("isValidSignature(bytes,bytes)"), abi.encode(EIP1271_MAGIC_VALUE)
);
v = 0;
// s needs to point to data that comes after the signatures
s = bytes32(numSigs * 65);
signatures = bytes.concat(signatures, abi.encodePacked(r, s, v));
}
}
// For each contract sig, add 64 bytes to the signature data. This is necessary to satisfy
// the validation checks that the Safe contract performs on the value of s on contract
// signatures. The Safe contract checks that s correctly points to additional data appended
// after the signatures, and that the length of the data is within bounds.
for (uint256 i = 0; i < contractSigs; i++) {
signatures = bytes.concat(signatures, abi.encode(32, 1));
}
// Signature checking on the Safe should succeed.
safeInstance.safe.checkNSignatures(digest, _data, signatures, numSigs);
// Recover the signatures using the _getNSigners() method.
address[] memory gotSigners =
SafeSigners.getNSigners({ dataHash: digest, signatures: signatures, requiredSignatures: numSigs });
// Compare the list of recovered signers to the expected signers.
assertEq(gotSigners.length, numSigs);
assertEq(gotSigners.length, safeInstance.owners.length);
for (uint256 i; i < numSigs; i++) {
assertEq(safeInstance.owners[i], gotSigners[i]);
}
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import "safe-contracts/interfaces/ERC1155TokenReceiver.sol";
import "safe-contracts/interfaces/ERC721TokenReceiver.sol";
import "safe-contracts/interfaces/ERC777TokensRecipient.sol";
import "safe-contracts/interfaces/IERC165.sol";
import "safe-contracts/interfaces/ISignatureValidator.sol";
import { Safe as GnosisSafe } from "safe-contracts/Safe.sol";
/// author: Colin Nielsen
/// https://github.com/colinnielsen/safe-tools/blob/ce6c654a76d91b619ab7778c77d1a76b3ced6666/src/CompatibilityFallbackHandler_1_3_0.sol
contract DefaultCallbackHandler is ERC1155TokenReceiver, ERC777TokensRecipient, ERC721TokenReceiver, IERC165 {
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes calldata
)
external
pure
override
returns (bytes4)
{
return 0xf23a6e61;
}
function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
)
external
pure
override
returns (bytes4)
{
return 0xbc197c81;
}
function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
return 0x150b7a02;
}
function tokensReceived(
address,
address,
address,
uint256,
bytes calldata,
bytes calldata
)
external
pure
override
{
// We implement this for completeness, doesn't really have any value
}
function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) {
return interfaceId == type(ERC1155TokenReceiver).interfaceId
|| interfaceId == type(ERC721TokenReceiver).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
address constant SENTINEL_MODULES = address(0x1);
/// @title Compatibility Fallback Handler - fallback handler to provider compatibility between pre 1.3.0 and 1.3.0+ Safe
/// contracts
/// @author Richard Meissner - <richard@gnosis.pm>
contract CompatibilityFallbackHandler is DefaultCallbackHandler, ISignatureValidator {
//keccak256(
// "SafeMessage(bytes message)"
//);
bytes32 private constant SAFE_MSG_TYPEHASH = 0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca;
bytes4 internal constant SIMULATE_SELECTOR = bytes4(keccak256("simulate(address,bytes)"));
bytes4 internal constant UPDATED_MAGIC_VALUE = 0x1626ba7e;
/**
* Implementation of ISignatureValidator (see `interfaces/ISignatureValidator.sol`)
* @dev Should return whether the signature provided is valid for the provided data.
* @param _data Arbitrary length data signed on the behalf of address(msg.sender)
* @param _signature Signature byte array associated with _data
* @return a bool upon valid or invalid signature with corresponding _data
*/
function isValidSignature(bytes memory _data, bytes memory _signature) public view override returns (bytes4) {
// Caller should be a Safe
GnosisSafe safe = GnosisSafe(payable(msg.sender));
bytes32 messageHash = getMessageHashForSafe(safe, _data);
if (_signature.length == 0) {
require(safe.signedMessages(messageHash) != 0, "Hash not approved");
} else {
safe.checkSignatures(messageHash, _data, _signature);
}
return EIP1271_MAGIC_VALUE;
}
/// @dev Returns hash of a message that can be signed by owners.
/// @param message Message that should be hashed
/// @return Message hash.
function getMessageHash(bytes memory message) public view returns (bytes32) {
return getMessageHashForSafe(GnosisSafe(payable(msg.sender)), message);
}
/// @dev Returns hash of a message that can be signed by owners.
/// @param safe Safe to which the message is targeted
/// @param message Message that should be hashed
/// @return Message hash.
function getMessageHashForSafe(GnosisSafe safe, bytes memory message) public view returns (bytes32) {
bytes32 safeMessageHash = keccak256(abi.encode(SAFE_MSG_TYPEHASH, keccak256(message)));
return keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), safe.domainSeparator(), safeMessageHash));
}
/**
* Implementation of updated EIP-1271
* @dev Should return whether the signature provided is valid for the provided data.
* The save does not implement the interface since `checkSignatures` is not a view method.
* The method will not perform any state changes (see parameters of `checkSignatures`)
* @param _dataHash Hash of the data signed on the behalf of address(msg.sender)
* @param _signature Signature byte array associated with _dataHash
* @return a bool upon valid or invalid signature with corresponding _dataHash
* @notice See
* https://github.com/gnosis/util-contracts/blob/bb5fe5fb5df6d8400998094fb1b32a178a47c3a1/contracts/StorageAccessible.sol
*/
function isValidSignature(bytes32 _dataHash, bytes calldata _signature) external view returns (bytes4) {
ISignatureValidator validator = ISignatureValidator(msg.sender);
bytes4 value = validator.isValidSignature(abi.encode(_dataHash), _signature);
return (value == EIP1271_MAGIC_VALUE) ? UPDATED_MAGIC_VALUE : bytes4(0);
}
/// @dev Returns array of first 10 modules.
/// @return Array of modules.
function getModules() external view returns (address[] memory) {
// Caller should be a Safe
GnosisSafe safe = GnosisSafe(payable(msg.sender));
(address[] memory array,) = safe.getModulesPaginated(SENTINEL_MODULES, 10);
return array;
}
/**
* @dev Performs a delegetecall on a targetContract in the context of self.
* Internally reverts execution to avoid side effects (making it static). Catches revert and returns encoded result
* as bytes.
* @param targetContract Address of the contract containing the code to execute.
* @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments).
*/
function simulate(
address targetContract,
bytes calldata calldataPayload
)
external
returns (bytes memory response)
{
// Suppress compiler warnings about not using parameters, while allowing
// parameters to keep names for documentation purposes. This does not
// generate code.
targetContract;
calldataPayload;
// solhint-disable-next-line no-inline-assembly
assembly {
let internalCalldata := mload(0x40)
// Store `simulateAndRevert.selector`.
// String representation is used to force right padding
mstore(internalCalldata, "\xb4\xfa\xba\x09")
// Abuse the fact that both this and the internal methods have the
// same signature, and differ only in symbol name (and therefore,
// selector) and copy calldata directly. This saves us approximately
// 250 bytes of code and 300 gas at runtime over the
// `abi.encodeWithSelector` builtin.
calldatacopy(add(internalCalldata, 0x04), 0x04, sub(calldatasize(), 0x04))
// `pop` is required here by the compiler, as top level expressions
// can't have return values in inline assembly. `call` typically
// returns a 0 or 1 value indicated whether or not it reverted, but
// since we know it will always revert, we can safely ignore it.
pop(
call(
gas(),
// address() has been changed to caller() to use the implementation of the Safe
caller(),
0,
internalCalldata,
calldatasize(),
// The `simulateAndRevert` call always reverts, and
// instead encodes whether or not it was successful in the return
// data. The first 32-byte word of the return data contains the
// `success` value, so write it to memory address 0x00 (which is
// reserved Solidity scratch space and OK to use).
0x00,
0x20
)
)
// Allocate and copy the response bytes, making sure to increment
// the free memory pointer accordingly (in case this method is
// called as an internal function). The remaining `returndata[0x20:]`
// contains the ABI encoded response bytes, so we can just write it
// as is to memory.
let responseSize := sub(returndatasize(), 0x20)
response := mload(0x40)
mstore(0x40, add(response, responseSize))
returndatacopy(response, 0x20, responseSize)
if iszero(mload(0x00)) { revert(add(response, 0x20), mload(response)) }
}
}
}
This diff is collapsed.
# Safe Liveness Checking
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Liveness checking Mechanism](#liveness-checking-mechanism)
- [Liveness checking methodology](#liveness-checking-methodology)
- [The liveness guard](#the-liveness-guard)
- [The liveness module](#the-liveness-module)
- [Owner removal call flow](#owner-removal-call-flow)
- [Shutdown](#shutdown)
- [Security Properties](#security-properties)
- [Interdependency between the guard and module](#interdependency-between-the-guard-and-module)
- [Deploying the liveness checking system](#deploying-the-liveness-checking-system)
- [Modify the liveness checking system](#modify-the-liveness-checking-system)
- [Replacing the module](#replacing-the-module)
- [Replacing the guard](#replacing-the-guard)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Liveness checking Mechanism
The Security Security Council uses a specially extended Safe multisig contract to ensure that
any loss of access to a signer's keys is identified and addressed within a predictable period of
time.
This mechanism is intended only to be used to remove signers who have lost access to their keys, or
are otherwise inactive. It is not intended to be used to remove signers who are acting in bad faith,
or any other subjective criteria, such cases should be addressed by governance, and the removal
handled via the standard Safe ownership management functionality.
## Liveness checking methodology
This is achieved using two types of contracts which the Safe contract has built-in support for:
1. **Guard contracts:** can execute pre- and post- transaction checks.
1. **Module contracts:** a contract which is added to the Safe by the signers, and thenceforth is
authorized to execute transactions via the Safe. This means the module must properly implement
auth conditions internally.
### The liveness guard
For implementing liveness checks a `LivenessGuard` is created which receives the signatures from
each executed transaction, and tracks the latest time at which a transaction was signed by each
signer. This time is made publicly available by calling a `lastLive(address)(Timestamp)` method.
Signers may also call the contract's `showLiveness()()` method directly in order to prove liveness.
### The liveness module
A `LivenessModule` is also created which does the following:
1. Has a function `removeOwners()` that anyone may call to specify one or more owners to be removed from the
Safe.
1. The Module would then check the `LivenessGuard.lastLive()` to determine if the signer is
eligible for removal.
1. If so, it will call the Safe's `removeSigner()` to remove the non-live signer, and if necessary
reduce the threshold.
1. When a member is removed, the signing parameters are modified such that `M/N` is the lowest ratio
which remains greater than or equal to 75%. Using integer math, this can be expressed as `M = (N * 75 + 99) / 100`.
### Owner removal call flow
The following diagram illustrates the flow for removing a single owner. The `verifyFinalState`
box indicates calls to the Safe which ensure the final state is valid.
```mermaid
sequenceDiagram
participant User
participant LivenessModule
participant LivenessGuard
participant Safe
User->>LivenessModule: removeOwners([previousOwner], [owner])
LivenessModule->>LivenessGuard: lastLive(owner)
LivenessModule->>Safe: getOwners()
LivenessModule->>Safe: removeOwner(previousOwner, owner)
alt verifyFinalState
LivenessModule->>Safe: getOwners()
LivenessModule->>Safe: getThreshold()
LivenessModule->>Safe: getGuard()
end
```
### Shutdown
In the unlikely event that the signer set (`N`) is reduced below the allowed threshold, then (and only then) is a
shutdown mechanism activated which removes the existing signers, and hands control of the
multisig over to a predetermined entity.
### Security Properties
The following security properties must be upheld:
1. Signatures are assigned to the correct signer.
1. Non-signers are unable to create a record of having signed.
1. A signer cannot be censored or griefed such that their signing is not recorded.
1. Signers may demonstrate liveness either by signing a transaction or by calling directly to the
guard.
1. The module only removes a signer if they have demonstrated liveness during the interval, or
if necessary to convert the safe to a 1 of 1.
1. The module sets the correct 75% threshold upon removing a signer.
1. During a shutdown the module correctly removes all signers, and converts the safe to a 1 of 1.
1. It must be impossible for the guard's checkTransaction or checkAfterExecution to permanently
revert given any calldata and the current state.
Note: neither the module nor guard attempt to prevent a quorum of owners from removing either the liveness
module or guard. There are legitimate reasons they might wish to do so. Moreover, if such a quorum
of owners exists, there is no benefit to removing them, as they are defacto 'sufficiently live'.
### Interdependency between the guard and module
The guard has no dependency on the module, and can be used independently to track liveness of
Safe owners.
This means that the module can be removed or replaced without any affect on the guard.
The module however does have a dependency on the guard; if the guard is removed from the Safe, then
the module will no longer be functional and calls to its `removeOwners` function will revert.
### Deploying the liveness checking system
[deploying]: #deploying-the-liveness-checking-system
The module and guard are intended to be deployed and installed on the safe in the following
sequence:
1. Deploy the guard contract
2. The guard's constructor will read the Safe's owners and set a timestamp
1. Deploy the module.
1. Set the guard on the safe.
1. Enable the module on the safe.
This order of operations is necessary to satisfy the constructor checks in the module, and is
intended to prevent owners from being immediately removable.
Note that changes to the owners set should not be made between the time the module is deployed, and
when it is enabled on the Safe, otherwise the checks made in the module's constructor may be
invalidated. If such changes are made, a new module should be deployed.
### Modify the liveness checking system
Changes to the liveness checking system should be done in the following manner:
#### Replacing the module
The module can safely be removed without affecting the operation of the guard. A new module can then
be added.
Note: none of the module's parameters are modifiable. In order to update the security properties
enforced by the module, it must be replaced.
#### Replacing the guard
The safe can only have one guard contract at a time, and if the guard is removed the module will
cease to function. This does not affect the ability of the Safe to operate normally, however the
module should be removed as a best practice.
If a new guard is added, eg. as a means of upgrading it, then a new module will also need to be
deployed and enabled. Once both the guard and module have been removed, they can be replaced
according to the steps in the [Deployment][deploying] section above.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment