Skip to content
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
5 changes: 2 additions & 3 deletions script/universal/MultisigScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ import {Simulation} from "./Simulation.sol";
/// │ │ │ │ │ │ │ run() │
/// │ │ │ │ │ │ │─────────────────────────────>│
abstract contract MultisigScript is Script {
bytes32 internal constant SAFE_NONCE_SLOT = bytes32(uint256(5));
address internal constant CB_MULTICALL = 0xA8B8CA1d6F0F5Ce63dCEA9121A01b302c5801303;

/// @notice A struct representing a call to a contract.
Expand Down Expand Up @@ -250,7 +249,7 @@ abstract contract MultisigScript is Script {

// Restore the original nonce.
for (uint256 i; i < safes.length; i++) {
vm.store({target: safes[i], slot: SAFE_NONCE_SLOT, value: bytes32(originalNonces[i])});
vm.store({target: safes[i], slot: Simulation.SAFE_NONCE_SLOT, value: bytes32(originalNonces[i])});
}

bytes memory txData = _encodeTransactionData({safe: safes[0], call: callsChain[0]});
Expand Down Expand Up @@ -308,7 +307,7 @@ abstract contract MultisigScript is Script {
address ownerSafe = _ownerSafe();
Call[] memory callsChain = _buildCallsChain({safes: _toArray(ownerSafe)});

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

(Vm.AccountAccess[] memory accesses, Simulation.Payload memory simPayload) =
_executeTransaction({safe: ownerSafe, call: callsChain[0], signatures: signatures, broadcast: false});
Expand Down
73 changes: 65 additions & 8 deletions script/universal/Simulation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,38 @@ import {IGnosisSafe} from "./IGnosisSafe.sol";
/// - Generating Tenderly simulation links for external transaction analysis
/// - Managing Gnosis Safe parameters (threshold, nonce, approvals) during simulation
library Simulation {
//////////////////////////////////////////////////////////////////////////////////////
/// Safe Storage Slot Constants ///
//////////////////////////////////////////////////////////////////////////////////////

/// @notice Storage slot for Safe's signature threshold
/// @dev Slot 4 in Safe's storage layout. The threshold determines how many owner signatures
/// are required to execute a transaction.
/// Valid for: Safe v1.3.0 - v1.4.1
/// @custom:warning These storage slots are internal implementation details of Safe contracts
/// and may change in future Safe versions. Verify compatibility before use.
bytes32 internal constant SAFE_THRESHOLD_SLOT = bytes32(uint256(4));

/// @notice Storage slot for Safe's transaction nonce
/// @dev Slot 5 in Safe's storage layout. The nonce is incremented after each successful
/// transaction execution to prevent replay attacks.
/// Valid for: Safe v1.3.0 - v1.4.1
/// @custom:warning These storage slots are internal implementation details of Safe contracts
/// and may change in future Safe versions. Verify compatibility before use.
bytes32 internal constant SAFE_NONCE_SLOT = bytes32(uint256(5));

/// @notice Base storage slot for Safe's approvedHashes mapping
/// @dev Slot 8 in Safe's storage layout. The approvedHashes mapping stores pre-approved
/// transaction hashes per owner: mapping(address => mapping(bytes32 => uint256))
/// Valid for: Safe v1.3.0 - v1.4.1
/// @custom:warning These storage slots are internal implementation details of Safe contracts
/// and may change in future Safe versions. Verify compatibility before use.
bytes32 internal constant SAFE_APPROVED_HASHES_SLOT = bytes32(uint256(8));

//////////////////////////////////////////////////////////////////////////////////////
/// Structs ///
//////////////////////////////////////////////////////////////////////////////////////

/// @notice Represents state overrides for a specific contract during simulation. Used to modify contract storage
/// slots temporarily for testing purposes
struct StateOverride {
Expand Down Expand Up @@ -50,6 +82,33 @@ library Simulation {
/// @notice Foundry VM instance for state manipulation during simulations
Vm internal constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));

//////////////////////////////////////////////////////////////////////////////////////
/// Storage Slot Helpers ///
//////////////////////////////////////////////////////////////////////////////////////

/// @notice Computes the storage slot key for a Safe's approvedHashes mapping entry
///
/// @dev Safe's approvedHashes is a nested mapping: mapping(address => mapping(bytes32 => uint256))
/// For Solidity mappings, the storage slot is computed as:
/// slot(mapping[key]) = keccak256(abi.encode(key, baseSlot))
/// For nested mappings, this is applied recursively:
/// slot(mapping[owner][dataHash]) = keccak256(abi.encode(dataHash, keccak256(abi.encode(owner, baseSlot))))
///
/// The formula breaks down as:
/// 1. innerSlot = keccak256(abi.encode(owner, SAFE_APPROVED_HASHES_SLOT))
/// This gives the slot for the inner mapping (owner's approved hashes)
/// 2. finalSlot = keccak256(abi.encode(dataHash, innerSlot))
/// This gives the slot for the specific dataHash within owner's mapping
///
/// @param owner The address of the Safe owner who approved the hash
/// @param dataHash The transaction hash that was approved
///
/// @return The storage slot key where the approval value (0 or 1) is stored
function computeApprovedHashSlot(address owner, bytes32 dataHash) internal pure returns (bytes32) {
bytes32 innerMappingSlot = keccak256(abi.encode(owner, SAFE_APPROVED_HASHES_SLOT));
return keccak256(abi.encode(dataHash, innerMappingSlot));
}

/// @notice Executes a simulation using the provided payload and returns state changes
///
/// @dev This is the core simulation function that applies state overrides and executes the transaction
Expand Down Expand Up @@ -140,8 +199,7 @@ library Simulation {
return addOverride({
state: state,
storageOverride: StorageOverride({
key: keccak256(abi.encode(dataHash, keccak256(abi.encode(owner, uint256(8))))),
value: bytes32(uint256(0x1))
key: computeApprovedHashSlot({owner: owner, dataHash: dataHash}), value: bytes32(uint256(0x1))
})
});
}
Expand All @@ -162,9 +220,9 @@ library Simulation {
// get the threshold and check if we need to override it
if (IGnosisSafe(safe).getThreshold() == 1) return state;

// set the threshold (slot 4) to 1
// set the threshold to 1
return addOverride({
state: state, storageOverride: StorageOverride({key: bytes32(uint256(0x4)), value: bytes32(uint256(0x1))})
state: state, storageOverride: StorageOverride({key: SAFE_THRESHOLD_SLOT, value: bytes32(uint256(0x1))})
});
}

Expand All @@ -185,10 +243,9 @@ library Simulation {
// get the nonce and check if we need to override it
if (IGnosisSafe(safe).nonce() == nonce) return state;

// set the nonce (slot 5) to the desired value
return addOverride({
state: state, storageOverride: StorageOverride({key: bytes32(uint256(0x5)), value: bytes32(nonce)})
});
// set the nonce to the desired value
return
addOverride({state: state, storageOverride: StorageOverride({key: SAFE_NONCE_SLOT, value: bytes32(nonce)})});
}

/// @notice Appends a new storage override to an existing state override
Expand Down
4 changes: 2 additions & 2 deletions script/universal/StateDiff.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ library StateDiff {
MappingParent[] memory parents = new MappingParent[](1);
// Account for the msg.sender approval override
parents[0] = MappingParent({
slot: keccak256(abi.encode(msg.sender, uint256(8))),
parent: bytes32(uint256(8)),
slot: keccak256(abi.encode(msg.sender, Simulation.SAFE_APPROVED_HASHES_SLOT)),
parent: Simulation.SAFE_APPROVED_HASHES_SLOT,
key: bytes32(bytes20(msg.sender))
});

Expand Down