-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add DoubleNestedMultisigBuilder script contract (#112)
* 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
Showing
3 changed files
with
414 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.