diff --git a/kurtosis-devnet/justfile b/kurtosis-devnet/justfile index a992e47236d..75a2872bb59 100644 --- a/kurtosis-devnet/justfile +++ b/kurtosis-devnet/justfile @@ -46,6 +46,8 @@ op-wheel-image TAG='op-wheel:devnet': (_docker_build_stack TAG "op-wheel-target" op-faucet-image TAG='op-faucet:devnet': (_docker_build_stack TAG "op-faucet-target") op-interop-mon-image TAG='op-interop-mon:devnet': (_docker_build_stack TAG "op-interop-mon-target") +safe-utils-image TAG='safe-utils:devnet': (_docker_build TAG "" "./safe-utils" "Dockerfile") + op-program-builder-image TAG='op-program-builder:devnet': _prerequisites just op-program-svc/op-program-svc {{TAG}} diff --git a/kurtosis-devnet/safe-utils/Dockerfile b/kurtosis-devnet/safe-utils/Dockerfile new file mode 100644 index 00000000000..78805e19f3b --- /dev/null +++ b/kurtosis-devnet/safe-utils/Dockerfile @@ -0,0 +1,41 @@ +FROM node:22 + +RUN apt-get update && apt-get install -y python3 python3-venv python3-dev curl jq +RUN python3 -m venv /venv + +ARG FOUNDRY_VERSION=1.1.0 +RUN curl -L https://foundry.paradigm.xyz | bash +RUN /root/.foundry/bin/foundryup -i $FOUNDRY_VERSION + +ARG SAFE_SINGLETON_VERSION=v1.0.43 + +#ARG SAFE_CONTRACT_VERSION=v1.4.1-3 +ARG SAFE_CONTRACT_VERSION=main + +ARG SAFE_CLI_VERSION=1.5.0 + +ARG SOLIDITY_VERSION=0.8.16 # needs to be aligned between contracts and singleton + +ENV SOLIDITY_VERSION=$SOLIDITY_VERSION + +WORKDIR / +RUN git clone --branch $SAFE_SINGLETON_VERSION https://github.com/safe-global/safe-singleton-factory +WORKDIR /safe-singleton-factory +RUN npm i + +WORKDIR / +RUN git clone --branch $SAFE_CONTRACT_VERSION https://github.com/safe-global/safe-smart-account +# force use of the pinned singleton factory +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/ +WORKDIR /safe-smart-account +RUN npm i + +WORKDIR / +RUN /venv/bin/pip install -U safe-cli==$SAFE_CLI_VERSION + +ADD deploy_contracts.sh . +RUN chmod +x deploy_contracts.sh +ADD provision_wallets.sh . +RUN chmod +x provision_wallets.sh + +ENTRYPOINT ["/bin/bash"] \ No newline at end of file diff --git a/kurtosis-devnet/safe-utils/deploy_contracts.sh b/kurtosis-devnet/safe-utils/deploy_contracts.sh new file mode 100644 index 00000000000..bb7182eb6a7 --- /dev/null +++ b/kurtosis-devnet/safe-utils/deploy_contracts.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# output will be a dictionary of the form +# { "contract1": "addr1", "contract2": "addr2", ... } + +# Parse command line arguments +while [ $# -gt 0 ]; do + case "$1" in + --private-key) + export PK="$2" + shift 2 + ;; + --rpc-url) + export NODE_URL="$2" + shift 2 + ;; + --contracts-output) + export OUTPUT="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 --private-key --rpc-url --contracts-output " + exit 1 + ;; + esac +done + +# Validate required arguments +if [ -z "$PK" ] || [ -z "$NODE_URL" ] || [ -z "$OUTPUT" ]; then + echo "Missing required arguments" + echo "Usage: $0 --private-key --rpc-url --contracts-output " + exit 1 +fi + +export PATH=/root/.foundry/bin:$PATH +export RPC=$NODE_URL + +# silence stupid telemetry interactive question. +export CI=1 + +# create a hash of the versions involved +SINGLETON_SHA1=$(git --git-dir /safe-singleton-factory/.git rev-parse HEAD) +CONTRACT_SHA1=$(git --git-dir /safe-smart-account/.git rev-parse HEAD) + +# a seed for new-mnemonic entropy. It doesn't have to be super robust, just to +# differentiate between successive runs if we operate under different +# conditions. +SEED=$(jq -n \ + --arg funded_wallet "$PK" \ + --arg singleton_sha1 "$SINGLETON_SHA1" \ + --arg contract_sha1 "$CONTRACT_SHA1" \ + '{ + "singleton_sha1": $singleton_sha1, + "contract_sha1": $contract_sha1, + "funded_wallet": $funded_wallet + }' | md5sum | cut -d' ' -f1) + +# we need a fresh mnemonic to own the singleton factory, because the installer requires its NONCE to be 0 to succeed. +TMP_MNEMONIC=$(cast wallet new-mnemonic -e "0x$SEED" --json | jq -r '.mnemonic') + +# owner of the singleton factory +OWNER_ADDR=$(cast wallet address --mnemonic "$TMP_MNEMONIC") + +NONCE=$(cast nonce --rpc-url "$NODE_URL" "$OWNER_ADDR") +if [ "$NONCE" -eq 0 ]; then # otherwise, we need to assume the singleton factory is already deployed + # fund the owner of the singleton factory + cast send --rpc-url "$NODE_URL" --private-key "$PK" --value 1ether "$OWNER_ADDR" + + # deploy the singleton factory + pushd /safe-singleton-factory || exit 1 + MNEMONIC="$TMP_MNEMONIC" npm run estimate-compile + MNEMONIC="$TMP_MNEMONIC" npm run submit + popd || exit +fi + +# This part is idempotent, so for clarify let's leave it alone. Worst case it's +# slightly wasteful when re-running. +pushd /safe-smart-account || exit 1 +npx --yes hardhat --network custom deploy \ + | grep 0x \ + | jq -Rn '[inputs | + if contains("reusing") then + capture("reusing \"(?.*)\" at (?.*)") + else + capture("deploying \"(?.*)\" \\(tx: [^)]+\\)\\.\\.\\.: deployed at (?[0-9a-fA-Fx]+)") + end + ] | from_entries' \ + | tee "$OUTPUT" +npx hardhat --network custom local-verify +popd || exit diff --git a/kurtosis-devnet/safe-utils/provision_wallets.sh b/kurtosis-devnet/safe-utils/provision_wallets.sh new file mode 100644 index 00000000000..93666efe471 --- /dev/null +++ b/kurtosis-devnet/safe-utils/provision_wallets.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# --contracts-input should point to the output of deploy_contracts.sh + +# --roles-spec should be a json file structured as +# request_example.json. It associates each role to a set of owners, and +# a threshold of signatures to collect. + +# The output will be of the form: +# { "role": "safe_address", ... } + +# Parse command line arguments +while [ $# -gt 0 ]; do + case "$1" in + --private-key) + export PK="$2" + shift 2 + ;; + --rpc-url) + export NODE_URL="$2" + shift 2 + ;; + --contracts-input) + export CONTRACTS_JSON="$2" + shift 2 + ;; + --roles-spec) + export ROLES_JSON="$2" + shift 2 + ;; + --safes-output) + export OUTPUT="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 --private-key --rpc-url --contracts-input --roles-spec --safes-output " + exit 1 + ;; + esac +done + +# Validate required arguments +if [ -z "$PK" ] || [ -z "$NODE_URL" ] || [ -z "$CONTRACTS_JSON" ] || [ -z "$ROLES_JSON" ] || [ -z "$OUTPUT" ]; then + echo "Missing required arguments" + echo "Usage: $0 --private-key --rpc-url --contracts-input --roles-spec --safes-output " + exit 1 +fi + +export PATH=/venv/bin:$PATH + +if [ ! -f "$CONTRACTS_JSON" ]; then + echo "Error: $CONTRACTS_JSON not found. Did you run deploy_contracts.sh?" + exit 1 +fi + +if [ ! -f "$ROLES_JSON" ]; then + echo "Error: $ROLES_JSON not found. Please provide a valid roles specification file." + exit 1 +fi + +EXT_FALLBACK_HANDLER=$(cat "$CONTRACTS_JSON" | jq -r .ExtensibleFallbackHandler) +SAFE=$(cat "$CONTRACTS_JSON" | jq -r .Safe) +SAFE_PROXY_FACTORY=$(cat "$CONTRACTS_JSON" | jq -r .SafeProxyFactory) + +# just something to make the calls repeatable +SALT_NONCE=1234567890 + +# Start with an empty JSON document +echo "{}" > "$OUTPUT" + +# Iterate through each role in the JSON file +for role in $(cat "$ROLES_JSON" | jq -r 'keys[]'); do + echo "Creating Safe wallet for role: $role" + + # Extract owners and threshold for this role + owners=$(cat "$ROLES_JSON" | jq -r ".$role.owners | join(\" \")") + threshold=$(cat "$ROLES_JSON" | jq -r ".$role.threshold") + + # Run safe-creator and capture the output + safe_output=$(safe-creator \ + --callback-handler "$EXT_FALLBACK_HANDLER" \ + --safe-contract "$SAFE" \ + --proxy-factory "$SAFE_PROXY_FACTORY" \ + --salt-nonce "$SALT_NONCE" \ + --owners "$owners" \ + --threshold "$threshold" \ + --no-confirm \ + "$NODE_URL" "$PK" 2>&1) + + # Extract the safe address from the output + 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/') + + # Add the role and safe address to the output JSON + jq --arg role "$role" --arg address "$safe_address" '. + {($role): $address}' "$OUTPUT" > "$OUTPUT.tmp" && mv "$OUTPUT.tmp" "$OUTPUT" +done + +# Display the result +echo "Deployed Safes:" +cat "$OUTPUT" diff --git a/kurtosis-devnet/safe-utils/request_example.json b/kurtosis-devnet/safe-utils/request_example.json new file mode 100644 index 00000000000..254934649e9 --- /dev/null +++ b/kurtosis-devnet/safe-utils/request_example.json @@ -0,0 +1,16 @@ +{ + "role1": { + "owners": [ + "0x1234567890123456789012345678901234567890" + ], + "threshold": 1 + }, + "role2": { + "owners": [ + "0x1234567890123456789012345678901234567890", + "0x1234567890123456789012345678901234567891", + "0x1234567890123456789012345678901234567892" + ], + "threshold": 2 + } +} \ No newline at end of file