Skip to content
Open
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
158 changes: 106 additions & 52 deletions script/universal/MultisigScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ import {StateDiff} from "./StateDiff.sol";
/// │ │ │ │ │ │ │ run() │
/// │ │ │ │ │ │ │─────────────────────────────>│
abstract contract MultisigScript is Script {
struct SafeTx {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to add this to encapsulate inputs for one of the internal functions to avoid a stack too deep error

address safe;
address to;
bytes data;
uint256 value;
}

bytes32 internal constant SAFE_NONCE_SLOT = bytes32(uint256(5));

address internal constant CB_MULTICALL = 0x8BDE8F549F56D405f07e1aA15Df9e1FC69839881;
Expand Down Expand Up @@ -183,6 +190,13 @@ abstract contract MultisigScript is Script {
// By default, an empty (no-op) override is returned.
function _simulationOverrides() internal view virtual returns (Simulation.StateOverride[] memory overrides_) {}

/// @notice If set to true, the executed call is a multicall. Most tasks should leave this as-is.
/// For special cases, i.e. tasks that invoke OPCM and need to be a delegate call, we set this to false
/// @dev If set to false, the task must configure only a single call
function _useMulticall() internal pure virtual returns (bool) {
return true;
}

constructor() {
bool useCbMulticall;
try vm.envBool("USE_CB_MULTICALL") {
Expand Down Expand Up @@ -217,11 +231,11 @@ abstract contract MultisigScript is Script {
originalNonces[i] = _getNonce({safe: safes[i]});
}

(bytes[] memory datas, uint256 value) = _transactionDatas({safes: safes});
(bytes[] memory datas, uint256 value, address target) = _transactionDatas({safes: safes});

vm.startMappingRecording();
(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) =
_simulateForSigner({safes: safes, datas: datas, value: value});
_simulateForSigner({safes: safes, to: target, datas: datas, value: value});
(StateDiff.MappingParent[] memory parents, string memory json) =
StateDiff.collectStateDiff(StateDiff.CollectStateDiffOpts({accesses: accesses, simPayload: simPayload}));
vm.stopMappingRecording();
Expand All @@ -234,10 +248,12 @@ abstract contract MultisigScript is Script {
vm.store({target: safes[i], slot: SAFE_NONCE_SLOT, value: bytes32(originalNonces[i])});
}

bytes memory txData = _encodeTransactionData({safe: safes[0], data: datas[0], value: value});
address initialTarget = _getInitialTarget(safes, target);
bytes memory txData =
_encodeTransactionData(SafeTx({safe: safes[0], to: initialTarget, data: datas[0], value: value}));
StateDiff.recordStateDiff({json: json, parents: parents, txData: txData, targetSafe: _ownerSafe()});

_printDataToSign({safe: safes[0], data: datas[0], value: value, txData: txData});
_printDataToSign({safe: safes[0], to: initialTarget, data: datas[0], value: value, txData: txData});
}

/// Step 1.1 (optional)
Expand All @@ -249,8 +265,10 @@ abstract contract MultisigScript is Script {
/// @param signatures The signatures to verify (concatenated, 65-bytes per sig).
function verify(address[] memory safes, bytes memory signatures) public view {
safes = _appendOwnerSafe({safes: safes});
(bytes[] memory datas, uint256 value) = _transactionDatas({safes: safes});
_checkSignatures({safe: safes[0], data: datas[0], value: value, signatures: signatures});
(bytes[] memory datas, uint256 value, address target) = _transactionDatas({safes: safes});
_checkSignatures({
safe: safes[0], to: _getInitialTarget(safes, target), data: datas[0], value: value, signatures: signatures
});
}

/// Step 2 (optional for non-nested setups)
Expand All @@ -267,9 +285,10 @@ abstract contract MultisigScript is Script {
/// @param signatures The signatures from step 1 (concatenated, 65-bytes per sig)
function approve(address[] memory safes, bytes memory signatures) public {
safes = _appendOwnerSafe({safes: safes});
(bytes[] memory datas, uint256 value) = _transactionDatas({safes: safes});
(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) =
_executeTransaction({safe: safes[0], data: datas[0], value: value, signatures: signatures, broadcast: true});
(bytes[] memory datas, uint256 value,) = _transactionDatas({safes: safes});
(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = _executeTransaction({
safe: safes[0], to: multicallAddress, data: datas[0], value: value, signatures: signatures, broadcast: true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this to: still be multicallAddress?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so, yes. Only the final execute call should be directed to OPCM. The approval flow still uses multicall

});
_postApprove({accesses: accesses, simPayload: simPayload});
}

Expand All @@ -284,12 +303,12 @@ abstract contract MultisigScript is Script {
/// @param signatures The signatures from step 1 (concatenated, 65-bytes per sig)
function simulate(bytes memory signatures) public {
address ownerSafe = _ownerSafe();
(bytes[] memory datas, uint256 value) = _transactionDatas({safes: _toArray(ownerSafe)});
(bytes[] memory datas, uint256 value, address target) = _transactionDatas({safes: _toArray(ownerSafe)});

vm.store({target: ownerSafe, slot: SAFE_NONCE_SLOT, value: bytes32(_getNonce({safe: ownerSafe}))});

(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = _executeTransaction({
safe: ownerSafe, data: datas[0], value: value, signatures: signatures, broadcast: false
safe: ownerSafe, to: target, data: datas[0], value: value, signatures: signatures, broadcast: false
});

_postRun({accesses: accesses, simPayload: simPayload});
Expand All @@ -305,10 +324,10 @@ abstract contract MultisigScript is Script {
/// @param signatures The signatures from step 1 (concatenated, 65-bytes per sig)
function run(bytes memory signatures) public {
address ownerSafe = _ownerSafe();
(bytes[] memory datas, uint256 value) = _transactionDatas({safes: _toArray(ownerSafe)});
(bytes[] memory datas, uint256 value, address target) = _transactionDatas({safes: _toArray(ownerSafe)});

(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) = _executeTransaction({
safe: ownerSafe, data: datas[0], value: value, signatures: signatures, broadcast: true
safe: ownerSafe, to: target, data: datas[0], value: value, signatures: signatures, broadcast: true
});

_postRun({accesses: accesses, simPayload: simPayload});
Expand All @@ -328,7 +347,11 @@ abstract contract MultisigScript is Script {
return extendedSafes;
}

function _transactionDatas(address[] memory safes) private view returns (bytes[] memory datas, uint256 value) {
function _transactionDatas(address[] memory safes)
private
view
returns (bytes[] memory datas, uint256 value, address target)
{
// Build the calls and sum the values
IMulticall3.Call3Value[] memory calls = _buildCalls();
for (uint256 i; i < calls.length; i++) {
Expand All @@ -339,27 +362,38 @@ abstract contract MultisigScript is Script {
datas = new bytes[](safes.length);
datas[datas.length - 1] = abi.encodeCall(IMulticall3.aggregate3Value, (calls));

target = multicallAddress;
if (!_useMulticall()) {
require(calls.length == 1, "MultisigScript::_transactionDatas must use a single call if not multicall");
target = calls[0].target;
datas[datas.length - 1] = calls[0].callData;
}

// The first n-1 calls are the nested approval calls
uint256 valueForCallToApprove = value;
for (uint256 i = safes.length - 1; i > 0; i--) {
address targetSafe = safes[i];
bytes memory callToApprove = datas[i];
address to = target;
if (i < safes.length - 1) {
to = multicallAddress;
}

IMulticall3.Call3[] memory approvalCall = new IMulticall3.Call3[](1);
approvalCall[0] =
_generateApproveCall({safe: targetSafe, data: callToApprove, value: valueForCallToApprove});
_generateApproveCall({safe: targetSafe, to: to, data: callToApprove, value: valueForCallToApprove});
datas[i - 1] = abi.encodeCall(IMulticall3.aggregate3, (approvalCall));

valueForCallToApprove = 0;
}
}

function _generateApproveCall(address safe, bytes memory data, uint256 value)
function _generateApproveCall(address safe, address to, bytes memory data, uint256 value)
internal
view
returns (IMulticall3.Call3 memory)
{
bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value});
bytes32 hash = _getTransactionHash({safe: safe, to: to, data: data, value: value});

console.log("---\nNested hash for safe %s:", safe);
console.logBytes32(hash);
Expand All @@ -369,8 +403,10 @@ abstract contract MultisigScript is Script {
});
}

function _printDataToSign(address safe, bytes memory data, uint256 value, bytes memory txData) internal {
bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value});
function _printDataToSign(address safe, address to, bytes memory data, uint256 value, bytes memory txData)
internal
{
bytes32 hash = _getTransactionHash({safe: safe, to: to, data: data, value: value});

emit DataToSign({data: txData});

Expand All @@ -393,20 +429,23 @@ abstract contract MultisigScript is Script {

function _executeTransaction(
address safe,
address to,
bytes memory data,
uint256 value,
bytes memory signatures,
bool broadcast
) internal returns (Vm.AccountAccess[] memory, Simulation.Payload memory) {
bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value});
bytes32 hash = _getTransactionHash({safe: safe, to: to, data: data, value: value});
signatures = Signatures.prepareSignatures({safe: safe, hash: hash, signatures: signatures});

bytes memory simData = _execTransactionCalldata({safe: safe, data: data, value: value, signatures: signatures});
bytes memory simData =
_execTransactionCalldata({safe: safe, to: to, data: data, value: value, signatures: signatures});
Simulation.logSimulationLink({to: safe, data: simData, from: msg.sender});

vm.startStateDiffRecording();
bool success =
_execTransaction({safe: safe, data: data, value: value, signatures: signatures, broadcast: broadcast});
bool success = _execTransaction({
safe: safe, to: to, data: data, value: value, signatures: signatures, broadcast: broadcast
});
Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff();
require(success, "MultisigBase::_executeTransaction: Transaction failed");
require(accesses.length > 0, "MultisigBase::_executeTransaction: No state changes");
Expand All @@ -419,13 +458,14 @@ abstract contract MultisigScript is Script {
return (accesses, simPayload);
}

function _simulateForSigner(address[] memory safes, bytes[] memory datas, uint256 value)
function _simulateForSigner(address[] memory safes, address to, bytes[] memory datas, uint256 value)
internal
returns (Vm.AccountAccess[] memory, Simulation.Payload memory)
{
IMulticall3.Call3[] memory calls = _simulateForSignerCalls({safes: safes, datas: datas, value: value});
IMulticall3.Call3[] memory calls = _simulateForSignerCalls({safes: safes, to: to, datas: datas, value: value});

bytes32 firstCallDataHash = _getTransactionHash({safe: safes[0], data: datas[0], value: value});
bytes32 firstCallDataHash =
_getTransactionHash({safe: safes[0], to: _getInitialTarget(safes, to), data: datas[0], value: value});

// Now define the state overrides for the simulation.
Simulation.StateOverride[] memory overrides = _overrides({safes: safes, firstCallDataHash: firstCallDataHash});
Expand All @@ -443,7 +483,7 @@ abstract contract MultisigScript is Script {
return (accesses, simPayload);
}

function _simulateForSignerCalls(address[] memory safes, bytes[] memory datas, uint256 value)
function _simulateForSignerCalls(address[] memory safes, address to, bytes[] memory datas, uint256 value)
private
view
returns (IMulticall3.Call3[] memory)
Expand All @@ -457,6 +497,7 @@ abstract contract MultisigScript is Script {
allowFailure: false,
callData: _execTransactionCalldata({
safe: safes[i],
to: i == safes.length - 1 ? to : multicallAddress,
data: datas[i],
value: value,
signatures: Signatures.genPrevalidatedSignature(signer)
Expand Down Expand Up @@ -531,57 +572,66 @@ abstract contract MultisigScript is Script {
}
}

function _checkSignatures(address safe, bytes memory data, uint256 value, bytes memory signatures) internal view {
bytes32 hash = _getTransactionHash({safe: safe, data: data, value: value});
function _checkSignatures(address safe, address to, bytes memory data, uint256 value, bytes memory signatures)
internal
view
{
bytes32 hash = _getTransactionHash({safe: safe, to: to, data: data, value: value});
signatures = Signatures.prepareSignatures({safe: safe, hash: hash, signatures: signatures});
IGnosisSafe(safe).checkSignatures({dataHash: hash, data: data, signatures: signatures});
}

function _getTransactionHash(address safe, bytes memory data, uint256 value) internal view returns (bytes32) {
return keccak256(_encodeTransactionData({safe: safe, data: data, value: value}));
}

function _encodeTransactionData(address safe, bytes memory data, uint256 value)
function _getTransactionHash(address safe, address to, bytes memory data, uint256 value)
internal
view
returns (bytes memory)
returns (bytes32)
{
return IGnosisSafe(safe)
return keccak256(_encodeTransactionData(SafeTx({safe: safe, to: to, data: data, value: value})));
}

function _encodeTransactionData(SafeTx memory t) internal view returns (bytes memory) {
return IGnosisSafe(t.safe)
.encodeTransactionData({
to: multicallAddress,
value: value,
data: data,
operation: _getOperation(value),
to: t.to,
value: t.value,
data: t.data,
operation: _getOperation(t.value),
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: address(0),
_nonce: _getNonce(safe)
_nonce: _getNonce(t.safe)
});
}

function _execTransactionCalldata(address safe, bytes memory data, uint256 value, bytes memory signatures)
internal
view
returns (bytes memory)
{
function _execTransactionCalldata(
address safe,
address to,
bytes memory data,
uint256 value,
bytes memory signatures
) internal view returns (bytes memory) {
return abi.encodeCall(
IGnosisSafe(safe).execTransaction,
(multicallAddress, value, data, _getOperation(value), 0, 0, 0, address(0), payable(address(0)), signatures)
(to, value, data, _getOperation(value), 0, 0, 0, address(0), payable(address(0)), signatures)
);
}

function _execTransaction(address safe, bytes memory data, uint256 value, bytes memory signatures, bool broadcast)
internal
returns (bool)
{
function _execTransaction(
address safe,
address to,
bytes memory data,
uint256 value,
bytes memory signatures,
bool broadcast
) internal returns (bool) {
if (broadcast) {
vm.broadcast();
}
return IGnosisSafe(safe)
.execTransaction({
to: multicallAddress,
to: to,
value: value,
data: data,
operation: _getOperation(value),
Expand Down Expand Up @@ -614,4 +664,8 @@ abstract contract MultisigScript is Script {

return Enum.Operation.Call;
}

function _getInitialTarget(address[] memory safes, address target) private view returns (address) {
return safes.length > 1 ? multicallAddress : target;
}
}
Loading