Skip to content

Commit

Permalink
Add DoubleNestedMultisigBuilder script contract (#112)
Browse files Browse the repository at this point in the history
* add double nested multisig builder script

* implement necessary changes to support additional layer of multisig nesting

* refactor double nested multisig builder and update comments

* improve double nested multisig builder function names
  • Loading branch information
jackchuma authored Jan 15, 2025
1 parent 4945865 commit d98da18
Show file tree
Hide file tree
Showing 3 changed files with 414 additions and 0 deletions.
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
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

0 comments on commit d98da18

Please sign in to comment.