Skip to content

Commit 3eaf567

Browse files
authored
Set up sepolia nested ownership transfer (#303)
* set up sepolia nested ownership transfer * safe B should be 1-of-14
1 parent 0ec91de commit 3eaf567

File tree

6 files changed

+432
-0
lines changed

6 files changed

+432
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
OP_COMMIT=2073f4059bd806af3e8b76b820aa3fa0b42016d0
2+
BASE_CONTRACTS_COMMIT=cdedd0fe728eb1f9d63eaa4c6e59138cfb3803d3
3+
4+
L1_GNOSIS_SAFE_IMPLEMENTATION=0x41675C099F32341bf84BFc5382aF534df5C7461a
5+
L1_GNOSIS_COMPATIBILITY_FALLBACK_HANDLER=0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99
6+
7+
OWNER_SAFE=0x646132a1667ca7ad00d36616afba1a28116c770a
8+
9+
SAFE_B_OWNERS_ENCODED=
10+
SAFE_B_THRESHOLD=1
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
include ../../Makefile
2+
include ../.env
3+
include .env
4+
5+
ifndef LEDGER_ACCOUNT
6+
override LEDGER_ACCOUNT = 1
7+
endif
8+
9+
.PHONY: deps
10+
deps:
11+
forge install --no-git safe-global/safe-smart-account@21dc82410445637820f600c7399a804ad55841d5
12+
13+
.PHONY: deploy
14+
deploy:
15+
forge script --rpc-url $(L1_RPC_URL) DeploySafes --account testnet-admin --broadcast -vvvv
16+
17+
.PHONY: sign
18+
sign:
19+
$(GOPATH)/bin/eip712sign --ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" -- \
20+
forge script --rpc-url $(L1_RPC_URL) UpdateSigners --sig "sign()"
21+
22+
.PHONY: execute
23+
execute:
24+
forge script --rpc-url $(L1_RPC_URL) UpdateSigners \
25+
--sig "run(bytes)" $(SIGNATURES) \
26+
--ledger --hd-paths "m/44'/60'/$(LEDGER_ACCOUNT)'/0/0" --broadcast -vvvv
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Nested Ownership Transfer
2+
3+
Status: READY TO DEPLOY
4+
5+
## Procedure
6+
7+
### 1. Update repo:
8+
9+
```bash
10+
cd contract-deployments
11+
git pull
12+
cd sepolia/2025-04-01-nested-ownership-transfer
13+
make deps
14+
```
15+
16+
### 2. Setup Ledger
17+
18+
Your Ledger needs to be connected and unlocked. The Ethereum
19+
application needs to be opened on Ledger with the message "Application
20+
is ready".
21+
22+
### 3. Run relevant script(s)
23+
24+
#### 3.1 Deploy new Safes
25+
26+
```bash
27+
make deploy
28+
```
29+
30+
This will output the new addresses of the `SafeA` and `SafeB` contracts to an `addresses.json` file. You will need to commit this file to the repo before signers can sign.
31+
32+
#### 3.2 Sign the transaction
33+
34+
```bash
35+
make sign
36+
```
37+
38+
You will see a "Simulation link" from the output.
39+
40+
Paste this URL in your browser. A prompt may ask you to choose a
41+
project, any project will do. You can create one if necessary.
42+
43+
Click "Simulate Transaction".
44+
45+
We will be performing 3 validations and extract the domain hash and message hash to approve on your Ledger:
46+
47+
1. Validate integrity of the simulation.
48+
2. Validate correctness of the state diff.
49+
3. Validate and extract domain hash and message hash to approve.
50+
51+
##### 3.2.1 Validate integrity of the simulation.
52+
53+
Make sure you are on the "Overview" tab of the tenderly simulation, to
54+
validate integrity of the simulation, we need to check the following:
55+
56+
1. "Network": Check the network is Mainnet.
57+
2. "Timestamp": Check the simulation is performed on a block with a
58+
recent timestamp (i.e. close to when you run the script).
59+
3. "Sender": Check the address shown is your signer account. If not see the derivation path Note above.
60+
61+
##### 3.2.2. Validate correctness of the state diff.
62+
63+
Now click on the "State" tab, and refer to the [State Validations](./VALIDATION.md) instructions for the transaction you are signing.
64+
Once complete return to this document to complete the signing.
65+
66+
##### 3.2.3. Extract the domain hash and the message hash to approve.
67+
68+
Now that we have verified the transaction performs the right
69+
operation, we need to extract the domain hash and the message hash to
70+
approve.
71+
72+
Go back to the "Overview" tab, and find the
73+
`GnosisSafe.checkSignatures` call. This call's `data` parameter
74+
contains both the domain hash and the message hash that will show up
75+
in your Ledger.
76+
77+
It will be a concatenation of `0x1901`, the domain hash, and the
78+
message hash: `0x1901[domain hash][message hash]`.
79+
80+
Note down this value. You will need to compare it with the ones
81+
displayed on the Ledger screen at signing.
82+
83+
Once the validations are done, it's time to actually sign the
84+
transaction.
85+
86+
> [!WARNING]
87+
> This is the most security critical part of the playbook: make sure the
88+
> domain hash and message hash in the following two places match:
89+
>
90+
> 1. On your Ledger screen.
91+
> 2. In the Tenderly simulation. You should use the same Tenderly
92+
> simulation as the one you used to verify the state diffs, instead
93+
> of opening the new one printed in the console.
94+
>
95+
> There is no need to verify anything printed in the console. There is
96+
> no need to open the new Tenderly simulation link either.
97+
98+
After verification, sign the transaction. You will see the `Data`,
99+
`Signer` and `Signature` printed in the console. Format should be
100+
something like this:
101+
102+
```shell
103+
Data: <DATA>
104+
Signer: <ADDRESS>
105+
Signature: <SIGNATURE>
106+
```
107+
108+
Double check the signer address is the right one.
109+
110+
##### 3.2.4 Send the output to Facilitator(s)
111+
112+
Nothing has occurred onchain - these are offchain signatures which
113+
will be collected by Facilitators for execution. Execution can occur
114+
by anyone once a threshold of signatures are collected, so a
115+
Facilitator will do the final execution for convenience.
116+
117+
Share the `Data`, `Signer` and `Signature` with the Facilitator, and
118+
congrats, you are done!
119+
120+
### [For Facilitator ONLY] How to execute
121+
122+
#### Execute the transaction
123+
124+
1. IMPORTANT: Ensure op-challenger has been updated before executing.
125+
1. Collect outputs from all participating signers.
126+
1. Concatenate all signatures and export it as the `SIGNATURES`
127+
environment variable, i.e. `export
128+
SIGNATURES="[SIGNATURE1][SIGNATURE2]..."`.
129+
1. Run the `make execute` command as described below to execute the transaction.
130+
131+
For example, if the quorum is 2 and you get the following outputs:
132+
133+
```shell
134+
Data: 0xDEADBEEF
135+
Signer: 0xC0FFEE01
136+
Signature: AAAA
137+
```
138+
139+
```shell
140+
Data: 0xDEADBEEF
141+
Signer: 0xC0FFEE02
142+
Signature: BBBB
143+
```
144+
145+
Then you should run:
146+
147+
```bash
148+
SIGNATURES=AAAABBBB make execute
149+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[profile.default]
2+
src = 'src'
3+
out = 'out'
4+
libs = ['lib']
5+
broadcast = 'records'
6+
fs_permissions = [{ access = "read-write", path = "./" }]
7+
optimizer = true
8+
optimizer_runs = 999999
9+
via-ir = false
10+
remappings = [
11+
'@eth-optimism-bedrock/=lib/optimism/packages/contracts-bedrock/',
12+
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts',
13+
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts',
14+
'@rari-capital/solmate/=lib/solmate/',
15+
'@base-contracts/=lib/base-contracts',
16+
'solady/=lib/solady/src/',
17+
]
18+
19+
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
4+
import {Script} from "forge-std/Script.sol";
5+
import {Safe} from "safe-smart-account/contracts/Safe.sol";
6+
import {SafeProxy} from "safe-smart-account/contracts/proxies/SafeProxy.sol";
7+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
8+
import {console} from "forge-std/console.sol";
9+
10+
contract DeploySafes is Script {
11+
using Strings for address;
12+
13+
address private SAFE_IMPLEMENTATION = vm.envAddress("L1_GNOSIS_SAFE_IMPLEMENTATION");
14+
address private FALLBACK_HANDLER = vm.envAddress("L1_GNOSIS_COMPATIBILITY_FALLBACK_HANDLER");
15+
address private OWNER_SAFE = vm.envAddress("OWNER_SAFE");
16+
address private zAddr;
17+
18+
address[] private OWNER_SAFE_OWNERS;
19+
uint256 private OWNER_SAFE_THRESHOLD;
20+
21+
address[] private SAFE_B_OWNERS;
22+
uint256 private SAFE_B_THRESHOLD;
23+
24+
function run() public {
25+
Safe ownerSafe = Safe(payable(OWNER_SAFE));
26+
OWNER_SAFE_OWNERS = ownerSafe.getOwners();
27+
OWNER_SAFE_THRESHOLD = ownerSafe.getThreshold();
28+
29+
SAFE_B_OWNERS = abi.decode(vm.envBytes("SAFE_B_OWNERS_ENCODED"), (address[]));
30+
SAFE_B_THRESHOLD = vm.envUint("SAFE_B_THRESHOLD");
31+
32+
require(OWNER_SAFE_OWNERS.length == 14, "Owner safe owners length must be 14");
33+
require(SAFE_B_OWNERS.length == 10, "Safe B owners length must be 10");
34+
35+
require(OWNER_SAFE_THRESHOLD == 3, "Owner safe threshold must be 3");
36+
require(SAFE_B_THRESHOLD == 1, "Safe B threshold must be 1");
37+
38+
console.log("Deploying SafeA with owners:");
39+
_printOwners(OWNER_SAFE_OWNERS);
40+
41+
console.log("Deploying SafeB with owners:");
42+
_printOwners(SAFE_B_OWNERS);
43+
44+
console.log("Threshold of SafeA:", OWNER_SAFE_THRESHOLD);
45+
console.log("Threshold of SafeB:", SAFE_B_THRESHOLD);
46+
47+
vm.startBroadcast();
48+
// First safe maintains the same owners + threshold as the current owner safe
49+
address safeA = _createAndInitProxy(OWNER_SAFE_OWNERS, OWNER_SAFE_THRESHOLD);
50+
// Second safe specifies its own owners + threshold
51+
address safeB = _createAndInitProxy(SAFE_B_OWNERS, SAFE_B_THRESHOLD);
52+
vm.stopBroadcast();
53+
_postCheck(safeA, safeB);
54+
55+
vm.writeFile(
56+
"addresses.json",
57+
string.concat(
58+
"{", "\"SafeA\": \"", safeA.toHexString(), "\",", "\"SafeB\": \"", safeB.toHexString(), "\"" "}"
59+
)
60+
);
61+
}
62+
63+
function _postCheck(address safeAAddress, address safeBAddress) private view {
64+
Safe safeA = Safe(payable(safeAAddress));
65+
Safe safeB = Safe(payable(safeBAddress));
66+
67+
address[] memory safeAOwners = safeA.getOwners();
68+
uint256 safeAThreshold = safeA.getThreshold();
69+
70+
address[] memory safeBOwners = safeB.getOwners();
71+
uint256 safeBThreshold = safeB.getThreshold();
72+
73+
require(safeAThreshold == 3, "PostCheck 1");
74+
require(safeBThreshold == 1, "PostCheck 2");
75+
76+
require(safeAOwners.length == 14, "PostCheck 3");
77+
require(safeBOwners.length == 10, "PostCheck 4");
78+
79+
for (uint256 i; i < safeAOwners.length; i++) {
80+
require(safeAOwners[i] == OWNER_SAFE_OWNERS[i], "PostCheck 5");
81+
}
82+
83+
for (uint256 i; i < safeBOwners.length; i++) {
84+
require(safeBOwners[i] == SAFE_B_OWNERS[i], "PostCheck 6");
85+
}
86+
87+
console.log("PostCheck passed");
88+
}
89+
90+
function _createAndInitProxy(address[] memory owners, uint256 threshold) private returns (address) {
91+
Safe proxy = Safe(payable(address(new SafeProxy(SAFE_IMPLEMENTATION))));
92+
proxy.setup(owners, threshold, zAddr, "", FALLBACK_HANDLER, zAddr, 0, payable(zAddr));
93+
return address(proxy);
94+
}
95+
96+
function _printOwners(address[] memory owners) private pure {
97+
for (uint256 i; i < owners.length; i++) {
98+
console.logAddress(owners[i]);
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)