Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DoubleNestedMultisigBuilder script contract #112

Merged
merged 4 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions script/universal/DoubleNestedMultisigBuilder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import "./NestedMultisigBase.sol";

/**
* @title DoubleNestedMultisigBuilder
* @notice Modeled from Optimism's SafeBuilder, but built for double nested safes (Safes where
* the signers are other Safes with signers that are other Safes).
*
* There are three safes involved in a double nested multisig:
* 1. The top-level safe, which should be returned by `_ownerSafe()`.
* 2. One or more intermediate safes, which are signers for the top-level safe.
* 3. Signer safes, which are signers for the intermediate safes.
* There should be at least one signer safe per intermediate safe.
*/
abstract contract DoubleNestedMultisigBuilder is NestedMultisigBase {
/**
* Step 1
* ======
* Generate a transaction approval data to sign. This method should be called by a threshold
* of members of each of the signer safes involved in the nested multisig. Signers will pass
* their signature to a facilitator, who will execute the approval transaction for each
* signer safe (see step 2).
*/
function sign(address signerSafe, address intermediateSafe) public {
address topSafe = _ownerSafe();

// Snapshot and restore Safe nonce after simulation, otherwise the data logged to sign
// would not match the actual data we need to sign, because the simulation
// would increment the nonce.
uint256 originalTopNonce = _getNonce(topSafe);
uint256 originalIntermediateNonce = _getNonce(intermediateSafe);
uint256 originalSignerNonce = _getNonce(signerSafe);

(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) =
_simulateForSigner(intermediateSafe, topSafe, _buildCalls());

_postSign(accesses, simPayload);
_postCheck(accesses, simPayload);

// Restore the original nonce.
vm.store(topSafe, SAFE_NONCE_SLOT, bytes32(originalTopNonce));
vm.store(intermediateSafe, SAFE_NONCE_SLOT, bytes32(originalIntermediateNonce));
vm.store(signerSafe, SAFE_NONCE_SLOT, bytes32(originalSignerNonce));

_printDataToSign(signerSafe, _generateIntermediateSafeApprovalCall(intermediateSafe));
}

/**
* Step 1.1 (optional)
* ======
* Verify the signatures generated from step 1 are valid.
* This allow transactions to be pre-signed and stored safely before execution.
*/
function verify(address signerSafe, address intermediateSafe, bytes memory signatures) public view {
IMulticall3.Call3[] memory calls = _generateIntermediateSafeApprovalCall(intermediateSafe);
_checkSignatures(signerSafe, calls, signatures);
}

/**
* Step 2
* ======
* Execute an approval transaction for a signer safe. This method should be called by a facilitator
* (non-signer), once for each of the signer safes involved in the nested multisig,
* after collecting a threshold of signatures for each signer safe (see step 1).
*/
function approveOnBehalfOfSignerSafe(address signerSafe, address intermediateSafe, bytes memory signatures)
public
{
IMulticall3.Call3[] memory calls = _generateIntermediateSafeApprovalCall(intermediateSafe);
_executeTransaction(signerSafe, calls, signatures, true);
_postApprove(signerSafe, intermediateSafe);
}

/**
* Step 3
* ======
* Execute an approval transaction for an intermediate safe. This method should be called by a
* facilitator (non-signer), for each of the intermediate safes after all of their approval
* transactions have been submitted onchain by their signer safes (see step 2).
*/
function approveOnBehalfOfIntermediateSafe(address intermediateSafe) public {
IMulticall3.Call3[] memory calls = _generateTopSafeApprovalCall();
// signatures is empty, because `_executeTransaction` internally collects all of the approvedHash addresses
bytes memory signatures;
_executeTransaction(intermediateSafe, calls, signatures, true);
_postRunInit(intermediateSafe);
}

/**
* Step 4
* ======
* Execute the final transaction. This method should be called by a facilitator (non-signer), after
* all of the intermediate safe approval transactions have been submitted onchain (see step 3).
*/
function run() public {
IMulticall3.Call3[] memory calls = _buildCalls();

// signatures is empty, because `_executeTransaction` internally collects all of the approvedHash addresses
bytes memory signatures;

(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) =
_executeTransaction(_ownerSafe(), calls, signatures, true);

_postRun(accesses, simPayload);
_postCheck(accesses, simPayload);
}

/**
* @dev Follow up assertions on state and simulation after an `approve` call.
*/
function _postApprove(address signerSafe, address intermediateSafe) private view {
IMulticall3.Call3 memory topSafeApprovalCall = _generateApproveCall(_ownerSafe(), _buildCalls());
bytes memory data = abi.encodeCall(IMulticall3.aggregate3, _toArray(topSafeApprovalCall));
bytes32 approvedHash = _getTransactionHash(intermediateSafe, data);

uint256 isApproved = IGnosisSafe(intermediateSafe).approvedHashes(signerSafe, approvedHash);
require(isApproved == 1, "DoubleNestedMultisigBuilder::_postApprove: Approval failed");
}

/**
* @dev Follow up assertions on state and simulation after an `init` call.
*/
function _postRunInit(address intermediateSafe) private view {
bytes memory data = abi.encodeCall(IMulticall3.aggregate3, _buildCalls());
bytes32 approvedHash = _getTransactionHash(_ownerSafe(), data);

uint256 isApproved = IGnosisSafe(_ownerSafe()).approvedHashes(intermediateSafe, approvedHash);
require(isApproved == 1, "DoubleNestedMultisigBuilder::_postRunInit: Init transaction failed");
}

function _generateIntermediateSafeApprovalCall(address intermediateSafe)
private
view
returns (IMulticall3.Call3[] memory)
{
IMulticall3.Call3[] memory topCalls = _generateTopSafeApprovalCall();
IMulticall3.Call3 memory intermediateCall = _generateApproveCall(intermediateSafe, topCalls);
return _toArray(intermediateCall);
}

function _generateTopSafeApprovalCall() private view returns (IMulticall3.Call3[] memory) {
IMulticall3.Call3[] memory dstCalls = _buildCalls();
IMulticall3.Call3 memory topSafeApprovalCall = _generateApproveCall(_ownerSafe(), dstCalls);
return _toArray(topSafeApprovalCall);
}
}
131 changes: 131 additions & 0 deletions script/universal/NestedMultisigBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: MIT
jackchuma marked this conversation as resolved.
Show resolved Hide resolved
pragma solidity ^0.8.15;

import "./MultisigBase.sol";

abstract contract NestedMultisigBase is MultisigBase {
/**
* -----------------------------------------------------------
* Virtual Functions
* -----------------------------------------------------------
*/

/**
* @notice Returns the nested safe address to execute the final transaction from
*/
function _ownerSafe() internal view virtual returns (address);

/**
* @notice Creates the calldata for both signatures (`sign`) and execution (`run`)
*/
function _buildCalls() internal view virtual returns (IMulticall3.Call3[] memory);

/**
* @notice Follow up assertions to ensure that the script ran to completion.
* @dev Called after `sign` and `run`, but not `approve`.
*/
function _postCheck(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual;

/**
* @notice Follow up assertions on state and simulation after a `sign` call.
*/
function _postSign(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {}

/**
* @notice Follow up assertions on state and simulation after a `run` call.
*/
function _postRun(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) internal virtual {}

function _readFrom_SAFE_NONCE() internal pure override returns (bool) {
return false;
}

function _generateApproveCall(address _safe, IMulticall3.Call3[] memory _calls)
internal
view
returns (IMulticall3.Call3 memory)
{
bytes32 hash = _getTransactionHash(_safe, _calls);

console.log("---\nNested hash:");
console.logBytes32(hash);

return IMulticall3.Call3({
target: _safe,
allowFailure: false,
callData: abi.encodeCall(IGnosisSafe(_safe).approveHash, (hash))
});
}

function _simulateForSigner(address _signerSafe, address _safe, IMulticall3.Call3[] memory _calls)
internal
returns (Vm.AccountAccess[] memory, Simulation.Payload memory)
{
bytes memory data = abi.encodeCall(IMulticall3.aggregate3, (_calls));
IMulticall3.Call3[] memory calls = _simulateForSignerCalls(_signerSafe, _safe, data);

// Now define the state overrides for the simulation.
Simulation.StateOverride[] memory overrides = _overrides(_signerSafe, _safe);

bytes memory txData = abi.encodeCall(IMulticall3.aggregate3, (calls));
console.log("---\nSimulation link:");
Simulation.logSimulationLink({_to: MULTICALL3_ADDRESS, _data: txData, _from: msg.sender, _overrides: overrides});

// Forge simulation of the data logged in the link. If the simulation fails
// we revert to make it explicit that the simulation failed.
Simulation.Payload memory simPayload =
Simulation.Payload({to: MULTICALL3_ADDRESS, data: txData, from: msg.sender, stateOverrides: overrides});
Vm.AccountAccess[] memory accesses = Simulation.simulateFromSimPayload(simPayload);
return (accesses, simPayload);
}

function _simulateForSignerCalls(address _signerSafe, address _safe, bytes memory _data)
internal
view
returns (IMulticall3.Call3[] memory)
{
IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](2);
bytes32 hash = _getTransactionHash(_safe, _data);

// simulate an approveHash, so that signer can verify the data they are signing
bytes memory approveHashData = abi.encodeCall(
IMulticall3.aggregate3,
(
_toArray(
IMulticall3.Call3({
target: _safe,
allowFailure: false,
callData: abi.encodeCall(IGnosisSafe(_safe).approveHash, (hash))
})
)
)
);
bytes memory approveHashExec = _execTransationCalldata(
_signerSafe, approveHashData, Signatures.genPrevalidatedSignature(MULTICALL3_ADDRESS)
);
calls[0] = IMulticall3.Call3({target: _signerSafe, allowFailure: false, callData: approveHashExec});

// simulate the final state changes tx, so that signer can verify the final results
bytes memory finalExec = _execTransationCalldata(_safe, _data, Signatures.genPrevalidatedSignature(_signerSafe));
calls[1] = IMulticall3.Call3({target: _safe, allowFailure: false, callData: finalExec});

return calls;
}

function _overrides(address _signerSafe, address _safe) internal view returns (Simulation.StateOverride[] memory) {
Simulation.StateOverride[] memory simOverrides = _simulationOverrides();
Simulation.StateOverride[] memory overrides = new Simulation.StateOverride[](2 + simOverrides.length);
overrides[0] = _safeOverrides(_signerSafe, MULTICALL3_ADDRESS);
overrides[1] = _safeOverrides(_safe, address(0));
for (uint256 i = 0; i < simOverrides.length; i++) {
overrides[i + 2] = simOverrides[i];
}
return overrides;
}

function _toArray(IMulticall3.Call3 memory call) internal pure returns (IMulticall3.Call3[] memory) {
IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](1);
calls[0] = call;
return calls;
}
}
Loading
Loading