Skip to content

Commit ea4a921

Browse files
authored
Feature: Add nested multisig deployment script (#145)
* Upload MultisigDeploy Script and corresponding related files * add README for file config as well as update Makefile for script run * remove unnessary files and settings * Add description for the script * make filePath configurable thorugh env * forge fmt --------- Co-authored-by: Thanh Trinh <[email protected]>
1 parent cab46f4 commit ea4a921

File tree

2 files changed

+216
-2
lines changed

2 files changed

+216
-2
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ deps: clean-lib checkout-op-commit
1414
github.com/OpenZeppelin/[email protected] \
1515
github.com/OpenZeppelin/[email protected] \
1616
github.com/rari-capital/solmate@8f9b23f8838670afda0fd8983f2c41e8037ae6bc \
17-
github.com/Vectorized/solady@862a0afd3e66917f50e987e91886b9b90c4018a1
17+
github.com/Vectorized/solady@862a0afd3e66917f50e987e91886b9b90c4018a1 \
18+
github.com/safe-global/[email protected]
1819

1920
.PHONY: test
2021
test:
@@ -42,4 +43,4 @@ bindings:
4243
mkdir -p bindings
4344
abigen --abi out/BalanceTracker.sol/BalanceTracker.abi.json --pkg bindings --type BalanceTracker --out bindings/balance_tracker.go
4445
abigen --abi out/FeeDisburser.sol/FeeDisburser.abi.json --pkg bindings --type FeeDisburser --out bindings/fee_disburser.go
45-
cd bindings && go mod tidy
46+
cd bindings && go mod tidy
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {Script, console} from "forge-std/Script.sol";
5+
import {SafeProxyFactory} from "lib/safe-smart-account/contracts/proxies/SafeProxyFactory.sol";
6+
import {SafeL2} from "lib/safe-smart-account/contracts/SafeL2.sol";
7+
import {Safe} from "lib/safe-smart-account/contracts/Safe.sol";
8+
import {SafeProxy} from "lib/safe-smart-account/contracts/proxies/SafeProxy.sol";
9+
10+
/**
11+
* @title MultisigDeployScript
12+
* @notice Deploys a hierarchy of Safe multisig wallets where later safes can reference earlier ones as owners
13+
*
14+
* @dev This script enables deployment of nested/hierarchical multisig structures for complex governance systems.
15+
* Safes are deployed in array order, allowing later safes to use previously deployed safes as owners.
16+
*
17+
* EXAMPLE JSON CONFIGURATION (config/safes-nested.json):
18+
* {
19+
* "safeCount": 3,
20+
* "safes": [
21+
* {
22+
* "label": "Treasury",
23+
* "threshold": 2,
24+
* "owners": [
25+
* "0x1234567890123456789012345678901234567890",
26+
* "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
27+
* ],
28+
* "ownerRefIndices": []
29+
* },
30+
* {
31+
* "label": "Operations",
32+
* "threshold": 1,
33+
* "owners": [
34+
* "0x9876543210987654321098765432109876543210"
35+
* ],
36+
* "ownerRefIndices": [0]
37+
* },
38+
* {
39+
* "label": "Governance",
40+
* "threshold": 2,
41+
* "owners": [],
42+
* "ownerRefIndices": [0, 1]
43+
* }
44+
* ]
45+
* }
46+
*
47+
* CONFIGURATION FIELDS:
48+
* - label: Human-readable name for the safe
49+
* - threshold: Number of signatures required for transactions
50+
* - owners: Array of direct address owners (EOAs or other contracts)
51+
* - ownerRefIndices: Array of indices referencing previously deployed safes as owners
52+
*
53+
* DEPLOYMENT ORDER MATTERS:
54+
* - Safes must be ordered so that any referenced safe (via ownerRefIndices) appears earlier in the array
55+
* - This ensures referenced safes are already deployed when needed as owners
56+
*/
57+
contract MultisigDeployScript is Script {
58+
// Safe v1.4.1-3 Addresses
59+
address public constant SINGLETON = 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762;
60+
address public constant FACTORY_PROXY = 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67;
61+
address public constant COMPATBILITY_FALLBACK_HANDLER = 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99;
62+
63+
struct SafeWallet {
64+
string label;
65+
uint256 threshold;
66+
address[] owners;
67+
uint256[] ownerRefIndices;
68+
}
69+
70+
// Track deployed safes and their predicted addresses
71+
mapping(string => address) public deployedSafes;
72+
73+
SafeWallet[] public safes;
74+
75+
function setUp() public {
76+
// Read configs from JSON
77+
string memory configPath = vm.envString("MULTISIG_CONFIG_PATH");
78+
string memory json = vm.readFile(configPath);
79+
console.log("Using config path:", configPath);
80+
81+
// Read safeCount directly from JSON
82+
uint256 safeCount = vm.parseJsonUint(json, ".safeCount");
83+
console.log("Reading", safeCount, "safes from configuration");
84+
85+
// Parse each safe individually field by field
86+
for (uint256 i = 0; i < safeCount; i++) {
87+
string memory basePath = string(abi.encodePacked(".safes[", vm.toString(i), "]"));
88+
89+
safes.push();
90+
91+
// Parse simple fields (these work reliably)
92+
safes[i].label = vm.parseJsonString(json, string(abi.encodePacked(basePath, ".label")));
93+
safes[i].threshold = vm.parseJsonUint(json, string(abi.encodePacked(basePath, ".threshold")));
94+
95+
// Parse arrays (these are more reliable when done individually)
96+
safes[i].owners = vm.parseJsonAddressArray(json, string(abi.encodePacked(basePath, ".owners")));
97+
safes[i].ownerRefIndices =
98+
vm.parseJsonUintArray(json, string(abi.encodePacked(basePath, ".ownerRefIndices")));
99+
}
100+
101+
// Print out the config to verify parsing worked
102+
for (uint256 i = 0; i < safes.length; i++) {
103+
console.log("Safe:", safes[i].label);
104+
console.log(" Owners:", safes[i].owners.length);
105+
console.log(" OwnerRefIndices:", safes[i].ownerRefIndices.length);
106+
console.log(" Threshold:", safes[i].threshold);
107+
}
108+
109+
// Print out first safe owners for verification
110+
console.log("First safe owners:");
111+
for (uint256 j = 0; j < safes[0].owners.length; j++) {
112+
console.log(" Owner", j, ":", safes[0].owners[j]);
113+
}
114+
}
115+
116+
function run() public {
117+
SafeProxyFactory factory = SafeProxyFactory(FACTORY_PROXY);
118+
119+
// Start broadcasting transactions
120+
vm.startBroadcast();
121+
122+
console.log("Deploying", safes.length, "Safe(s) in sequence");
123+
console.log("--------------------");
124+
125+
uint256 baseNonce = block.timestamp;
126+
console.log("Base nonce:", baseNonce);
127+
128+
// Deploy each Safe with its configuration in array order
129+
for (uint256 i = 0; i < safes.length; i++) {
130+
SafeWallet memory config = safes[i];
131+
uint256 saltNonce = baseNonce + i;
132+
133+
console.log("Deploying Safe:", config.label);
134+
console.log(" Index:", i);
135+
console.log(" Salt Nonce:", saltNonce);
136+
137+
// Resolve owner addresses (combine direct owners + referenced safe addresses)
138+
address[] memory resolvedOwners = resolveOwnerAddresses(config, safes);
139+
140+
console.log(" Total Owners:", resolvedOwners.length);
141+
console.log(" Direct Owners:", config.owners.length);
142+
console.log(" Safe References:", config.ownerRefIndices.length);
143+
console.log(" Threshold:", config.threshold);
144+
145+
// Compose initializer data with resolved owners
146+
bytes memory initializer = abi.encodeCall(
147+
Safe.setup,
148+
(
149+
resolvedOwners,
150+
config.threshold,
151+
address(0), // to
152+
hex"", // data
153+
COMPATBILITY_FALLBACK_HANDLER,
154+
address(0), // payment token
155+
0, // payment
156+
payable(address(0)) // payment receiver
157+
)
158+
);
159+
160+
// Deploy Safe with calculated nonce
161+
SafeProxy safe = factory.createProxyWithNonce(SINGLETON, initializer, saltNonce);
162+
163+
// Store deployed address
164+
deployedSafes[config.label] = address(safe);
165+
166+
console.log(" Deployed at:", address(safe));
167+
168+
// Log resolved owners
169+
for (uint256 k = 0; k < resolvedOwners.length; k++) {
170+
console.log(" Owner", k, ":", resolvedOwners[k]);
171+
}
172+
console.log("--------------------");
173+
}
174+
175+
vm.stopBroadcast();
176+
177+
// Verify all deployments
178+
console.log("Deployment Summary:");
179+
console.log("==================");
180+
for (uint256 i = 0; i < safes.length; i++) {
181+
SafeWallet memory config = safes[i];
182+
console.log("Safe:", config.label);
183+
console.log(" Address:", deployedSafes[config.label]);
184+
console.log(" Salt Nonce:", baseNonce + i);
185+
console.log("--------------------");
186+
}
187+
}
188+
189+
function resolveOwnerAddresses(SafeWallet memory config, SafeWallet[] memory safes)
190+
internal
191+
view
192+
returns (address[] memory)
193+
{
194+
uint256 totalOwners = config.owners.length + config.ownerRefIndices.length;
195+
address[] memory resolved = new address[](totalOwners);
196+
197+
// Add direct address owners
198+
for (uint256 i = 0; i < config.owners.length; i++) {
199+
resolved[i] = config.owners[i];
200+
}
201+
202+
// Add referenced safe addresses (they must already be deployed due to array order)
203+
for (uint256 i = 0; i < config.ownerRefIndices.length; i++) {
204+
uint256 refIndex = config.ownerRefIndices[i];
205+
string memory refLabel = safes[refIndex].label;
206+
address refAddr = deployedSafes[refLabel];
207+
require(refAddr != address(0), string(abi.encodePacked("Reference not deployed: ", refLabel)));
208+
resolved[config.owners.length + i] = refAddr;
209+
}
210+
211+
return resolved;
212+
}
213+
}

0 commit comments

Comments
 (0)