Skip to content

Commit 4cecc97

Browse files
committed
feat(kt-devnet): add a safe-utils image to support gnosis safe integration
1 parent 537118a commit 4cecc97

File tree

5 files changed

+250
-0
lines changed

5 files changed

+250
-0
lines changed

kurtosis-devnet/justfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ op-wheel-image TAG='op-wheel:devnet': (_docker_build_stack TAG "op-wheel-target"
4646
op-faucet-image TAG='op-faucet:devnet': (_docker_build_stack TAG "op-faucet-target")
4747
op-interop-mon-image TAG='op-interop-mon:devnet': (_docker_build_stack TAG "op-interop-mon-target")
4848

49+
safe-utils-image TAG='safe-utils:devnet': (_docker_build TAG "" "./safe-utils" "Dockerfile")
50+
4951
op-program-builder-image TAG='op-program-builder:devnet': _prerequisites
5052
just op-program-svc/op-program-svc {{TAG}}
5153

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
FROM node:22
2+
3+
RUN apt-get update && apt-get install -y python3 python3-venv python3-dev curl jq
4+
RUN python3 -m venv /venv
5+
6+
ARG FOUNDRY_VERSION=1.1.0
7+
RUN curl -L https://foundry.paradigm.xyz | bash
8+
RUN /root/.foundry/bin/foundryup -i $FOUNDRY_VERSION
9+
10+
ARG SAFE_SINGLETON_VERSION=v1.0.43
11+
12+
#ARG SAFE_CONTRACT_VERSION=v1.4.1-3
13+
ARG SAFE_CONTRACT_VERSION=main
14+
15+
ARG SAFE_CLI_VERSION=1.5.0
16+
17+
ARG SOLIDITY_VERSION=0.8.16 # needs to be aligned between contracts and singleton
18+
19+
ENV SOLIDITY_VERSION=$SOLIDITY_VERSION
20+
21+
WORKDIR /
22+
RUN git clone --branch $SAFE_SINGLETON_VERSION https://github.com/safe-global/safe-singleton-factory
23+
WORKDIR /safe-singleton-factory
24+
RUN npm i
25+
26+
WORKDIR /
27+
RUN git clone --branch $SAFE_CONTRACT_VERSION https://github.com/safe-global/safe-smart-account
28+
# force use of the pinned singleton factory
29+
RUN jq '.devDependencies["@safe-global/safe-singleton-factory"] = "file:///safe-singleton-factory"' safe-smart-account/package.json > package.json && mv package.json safe-smart-account/
30+
WORKDIR /safe-smart-account
31+
RUN npm i
32+
33+
WORKDIR /
34+
RUN /venv/bin/pip install -U safe-cli==$SAFE_CLI_VERSION
35+
36+
ADD deploy_contracts.sh .
37+
RUN chmod +x deploy_contracts.sh
38+
ADD provision_wallets.sh .
39+
RUN chmod +x provision_wallets.sh
40+
41+
ENTRYPOINT ["/bin/bash"]
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/bin/bash
2+
3+
# output will be a dictionary of the form
4+
# { "contract1": "addr1", "contract2": "addr2", ... }
5+
6+
# Parse command line arguments
7+
while [ $# -gt 0 ]; do
8+
case "$1" in
9+
--private-key)
10+
export PK="$2"
11+
shift 2
12+
;;
13+
--rpc-url)
14+
export NODE_URL="$2"
15+
shift 2
16+
;;
17+
--contracts-output)
18+
export OUTPUT="$2"
19+
shift 2
20+
;;
21+
*)
22+
echo "Unknown option: $1"
23+
echo "Usage: $0 --private-key <key> --rpc-url <url> --contracts-output <file>"
24+
exit 1
25+
;;
26+
esac
27+
done
28+
29+
# Validate required arguments
30+
if [ -z "$PK" ] || [ -z "$NODE_URL" ] || [ -z "$OUTPUT" ]; then
31+
echo "Missing required arguments"
32+
echo "Usage: $0 --private-key <key> --rpc-url <url> --contracts-output <file>"
33+
exit 1
34+
fi
35+
36+
export PATH=/root/.foundry/bin:$PATH
37+
export RPC=$NODE_URL
38+
39+
# silence stupid telemetry interactive question.
40+
export CI=1
41+
42+
# create a hash of the versions involved
43+
SINGLETON_SHA1=$(git --git-dir /safe-singleton-factory/.git rev-parse HEAD)
44+
CONTRACT_SHA1=$(git --git-dir /safe-smart-account/.git rev-parse HEAD)
45+
46+
# a seed for new-mnemonic entropy. It doesn't have to be super robust, just to
47+
# differentiate between successive runs if we operate under different
48+
# conditions.
49+
SEED=$(jq -n \
50+
--arg funded_wallet "$PK" \
51+
--arg singleton_sha1 "$SINGLETON_SHA1" \
52+
--arg contract_sha1 "$CONTRACT_SHA1" \
53+
'{
54+
"singleton_sha1": $singleton_sha1,
55+
"contract_sha1": $contract_sha1,
56+
"funded_wallet": $funded_wallet
57+
}' | md5sum | cut -d' ' -f1)
58+
59+
# we need a fresh mnemonic to own the singleton factory, because the installer requires its NONCE to be 0 to succeed.
60+
TMP_MNEMONIC=$(cast wallet new-mnemonic -e "0x$SEED" --json | jq -r '.mnemonic')
61+
62+
# owner of the singleton factory
63+
OWNER_ADDR=$(cast wallet address --mnemonic "$TMP_MNEMONIC")
64+
65+
NONCE=$(cast nonce --rpc-url "$NODE_URL" "$OWNER_ADDR")
66+
if [ "$NONCE" -eq 0 ]; then # otherwise, we need to assume the singleton factory is already deployed
67+
# fund the owner of the singleton factory
68+
cast send --rpc-url "$NODE_URL" --private-key "$PK" --value 1ether "$OWNER_ADDR"
69+
70+
# deploy the singleton factory
71+
pushd /safe-singleton-factory || exit 1
72+
MNEMONIC="$TMP_MNEMONIC" npm run estimate-compile
73+
MNEMONIC="$TMP_MNEMONIC" npm run submit
74+
popd || exit
75+
fi
76+
77+
# This part is idempotent, so for clarify let's leave it alone. Worst case it's
78+
# slightly wasteful when re-running.
79+
pushd /safe-smart-account || exit 1
80+
npx --yes hardhat --network custom deploy \
81+
| grep 0x \
82+
| jq -Rn '[inputs |
83+
if contains("reusing") then
84+
capture("reusing \"(?<key>.*)\" at (?<value>.*)")
85+
else
86+
capture("deploying \"(?<key>.*)\" \\(tx: [^)]+\\)\\.\\.\\.: deployed at (?<value>[0-9a-fA-Fx]+)")
87+
end
88+
] | from_entries' \
89+
| tee "$OUTPUT"
90+
npx hardhat --network custom local-verify
91+
popd || exit
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/bin/bash
2+
3+
# --contracts-input should point to the output of deploy_contracts.sh
4+
5+
# --roles-spec should be a json file structured as
6+
# request_example.json. It associates each role to a set of owners, and
7+
# a threshold of signatures to collect.
8+
9+
# The output will be of the form:
10+
# { "role": "safe_address", ... }
11+
12+
# Parse command line arguments
13+
while [ $# -gt 0 ]; do
14+
case "$1" in
15+
--private-key)
16+
export PK="$2"
17+
shift 2
18+
;;
19+
--rpc-url)
20+
export NODE_URL="$2"
21+
shift 2
22+
;;
23+
--contracts-input)
24+
export CONTRACTS_JSON="$2"
25+
shift 2
26+
;;
27+
--roles-spec)
28+
export ROLES_JSON="$2"
29+
shift 2
30+
;;
31+
--safes-output)
32+
export OUTPUT="$2"
33+
shift 2
34+
;;
35+
*)
36+
echo "Unknown option: $1"
37+
echo "Usage: $0 --private-key <key> --rpc-url <url> --contracts-input <file> --roles-spec <file> --safes-output <file>"
38+
exit 1
39+
;;
40+
esac
41+
done
42+
43+
# Validate required arguments
44+
if [ -z "$PK" ] || [ -z "$NODE_URL" ] || [ -z "$CONTRACTS_JSON" ] || [ -z "$ROLES_JSON" ] || [ -z "$OUTPUT" ]; then
45+
echo "Missing required arguments"
46+
echo "Usage: $0 --private-key <key> --rpc-url <url> --contracts-input <file> --roles-spec <file> --safes-output <file>"
47+
exit 1
48+
fi
49+
50+
export PATH=/venv/bin:$PATH
51+
52+
if [ ! -f "$CONTRACTS_JSON" ]; then
53+
echo "Error: $CONTRACTS_JSON not found. Did you run deploy_contracts.sh?"
54+
exit 1
55+
fi
56+
57+
if [ ! -f "$ROLES_JSON" ]; then
58+
echo "Error: $ROLES_JSON not found. Please provide a valid roles specification file."
59+
exit 1
60+
fi
61+
62+
EXT_FALLBACK_HANDLER=$(cat "$CONTRACTS_JSON" | jq -r .ExtensibleFallbackHandler)
63+
SAFE=$(cat "$CONTRACTS_JSON" | jq -r .Safe)
64+
SAFE_PROXY_FACTORY=$(cat "$CONTRACTS_JSON" | jq -r .SafeProxyFactory)
65+
66+
# just something to make the calls repeatable
67+
SALT_NONCE=1234567890
68+
69+
# Start with an empty JSON document
70+
echo "{}" > "$OUTPUT"
71+
72+
# Iterate through each role in the JSON file
73+
for role in $(cat "$ROLES_JSON" | jq -r 'keys[]'); do
74+
echo "Creating Safe wallet for role: $role"
75+
76+
# Extract owners and threshold for this role
77+
owners=$(cat "$ROLES_JSON" | jq -r ".$role.owners | join(\" \")")
78+
threshold=$(cat "$ROLES_JSON" | jq -r ".$role.threshold")
79+
80+
# Run safe-creator and capture the output
81+
safe_output=$(safe-creator \
82+
--callback-handler "$EXT_FALLBACK_HANDLER" \
83+
--safe-contract "$SAFE" \
84+
--proxy-factory "$SAFE_PROXY_FACTORY" \
85+
--salt-nonce "$SALT_NONCE" \
86+
--owners "$owners" \
87+
--threshold "$threshold" \
88+
--no-confirm \
89+
"$NODE_URL" "$PK" 2>&1)
90+
91+
# Extract the safe address from the output
92+
safe_address=$(echo "$safe_output" | grep -E "(contract_address=|Safe on)" | head -1 | sed -E "s/.*contract_address='([^']*)'.*/\1/" | sed -E 's/.*Safe on (0x[0-9a-fA-F]*).*/\1/')
93+
94+
# Add the role and safe address to the output JSON
95+
jq --arg role "$role" --arg address "$safe_address" '. + {($role): $address}' "$OUTPUT" > "$OUTPUT.tmp" && mv "$OUTPUT.tmp" "$OUTPUT"
96+
done
97+
98+
# Display the result
99+
echo "Deployed Safes:"
100+
cat "$OUTPUT"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"role1": {
3+
"owners": [
4+
"0x1234567890123456789012345678901234567890"
5+
],
6+
"threshold": 1
7+
},
8+
"role2": {
9+
"owners": [
10+
"0x1234567890123456789012345678901234567890",
11+
"0x1234567890123456789012345678901234567891",
12+
"0x1234567890123456789012345678901234567892"
13+
],
14+
"threshold": 2
15+
}
16+
}

0 commit comments

Comments
 (0)