Commit d9163367 authored by Maurelian's avatar Maurelian

feat(ctb): Add outline for Safe liveness checks

parent 986b2b0e
...@@ -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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Safe } from "safe-contracts/Safe.sol";
import { BaseGuard } from "safe-contracts/base/GuardManager.sol";
import { SignatureDecoder } from "safe-contracts/common/SignatureDecoder.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
contract LivenessGuard is SignatureDecoder, BaseGuard {
Safe public safe;
mapping(address => uint256) public lastSigned;
constructor(Safe _safe) {
safe = _safe;
}
/// @notice We just need to satisfy the BaseGuard interfae, but we don't actually need to use this method.
function checkAfterExecution(bytes32 txHash, bool success) external {
return;
}
/// @notice This checkTransaction implementation records the most recent time which any owner has signed 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
{
require(msg.sender == address(safe), "LivenessGuard: only Safe can call this function");
if (to == address(safe) && data[0:4] == bytes4(keccak256("setGuard(address)"))) {
// We can't allow the guard to be disabled, or else the upgrade delay can be bypassed.
// TODO(Maurelian): Figure out how to best address this.
}
// This is a bit of a hack, maybe just replicate the functionality here rather than calling home
bytes memory txHashData = Safe(payable(msg.sender)).encodeTransactionData(
// Transaction info
to,
value,
data,
operation,
safeTxGas,
// Payment info
baseGas,
gasPrice,
gasToken,
refundReceiver,
// Signature info
Safe(payable(msg.sender)).nonce() // check that this works
);
address[] memory signers = _getNSigners(keccak256(txHashData), signatures);
for (uint256 i = 0; i < signers.length; i++) {
lastSigned[signers[i]] = block.timestamp;
}
}
function _getNSigners(bytes32 dataHash, bytes memory signatures) internal returns (address[] memory _owners) {
uint256 numSignatures = signatures.length / 65; // division OK?
_owners = new address[](numSignatures);
// The following code is extracted from the Safe.checkNSignatures() method. It removes the signature validation
// code,
// and keeps only the parsing code necessary to extract the owner addresses from the signatures.
// We do not double check if the owner derived from a signature is valid. As tHis is handled in
// the final require statement of Safe.checkNSignatures().
address currentOwner;
uint8 v;
bytes32 r;
bytes32 s;
uint256 i;
for (i = 0; i < numSignatures; 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;
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol";
import { Safe } from "safe-contracts/Safe.sol";
import { SafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol";
import { ModuleManager } from "safe-contracts/base/ModuleManager.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
import { LivenessGuard } from "src/Safe/LivenessGuard.sol";
contract LivnessGuard_TestInit is Test {
struct Signer {
address owner;
uint256 pk;
}
LivenessGuard livenessGuard;
Safe safe;
string mnemonic = "test test test test test test test test test test test junk";
Signer[] signers;
function newSigner(uint256 index) public returns (Signer memory signer_) {
signer_.pk = vm.deriveKey(mnemonic, uint32(index));
signer_.owner = vm.addr(signer_.pk);
}
function signTransaction(
uint256 _pk,
address _to,
uint256 _value,
bytes memory _data
)
public
view
returns (bytes memory sig_)
{
bytes32 txDataHash;
{
txDataHash = safe.getTransactionHash({
to: _to,
value: _value,
data: _data,
operation: Enum.Operation.Call,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: address(0),
_nonce: safe.nonce()
});
}
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_pk, txDataHash);
sig_ = abi.encodePacked(v, r, s);
}
function exec(Signer[] memory _signers, address _to, bytes memory _data) internal {
bytes memory sig;
for (uint256 i; i < _signers.length; i++) {
bytes.concat(sig, signTransaction(_signers[i].pk, address(safe), 0, _data));
}
safe.execTransaction({
to: _to,
value: 0,
data: _data,
operation: Enum.Operation.Call,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: payable(0),
signatures: sig
});
}
// @dev Create a new Safe instance with a minimimal proxy and implementation.
function newSafe(Signer[] memory _signers) internal returns (Safe safe_) {
SafeProxyFactory safeProxyFactory = new SafeProxyFactory();
Safe safeSingleton = new Safe();
bytes memory initData = abi.encodeWithSelector(
Safe.setup.selector, _signers, 2, address(0), hex"", address(0), address(0), 0, address(0)
);
safe_ = Safe(payable(safeProxyFactory.createProxyWithNonce(address(safeSingleton), initData, block.timestamp)));
}
function setUp() public {
// Create 3 signers
for (uint256 i; i < 3; i++) {
signers.push(newSigner(i));
}
Signer[] memory signers_ = signers;
safe = newSafe(signers_);
livenessGuard = new LivenessGuard(safe);
// enable the module
bytes memory data = abi.encodeCall(ModuleManager.enableModule, (address(livenessGuard)));
bytes memory sig1 = signTransaction(signers[0].pk, address(safe), 0, data);
bytes memory sig2 = signTransaction(signers[1].pk, address(safe), 0, data);
bytes memory sigs = bytes.concat(sig1, sig2);
safe.execTransaction({
to: address(safe),
value: 0,
data: data,
operation: Enum.Operation.Call,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: payable(0),
signatures: sigs
});
}
}
contract LivnessGuard_TestCheckTx is LivnessGuard_TestInit {
function test_checkTransaction_succeeds() external {
Signer[] memory signers_ = signers;
exec(signers, address(1111), hex"abba");
for (uint256 i; i < signers.length; i++) {
assertEq(livenessGuard.lastSigned(signers[i].owner), block.timestamp);
}
}
}
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