Skip to content

Improve wallet management UX using Keystore #1869

@hbarcelos

Description

@hbarcelos

Component

Forge, Cast

Describe the feature you would like

I'm currently migrating from dapp.tools and it had this nice UX to handle keystore files:

  1. Declare the following env vars:
    • ETH_KEYSTORE: the path to the keystore directory
    • ETH_FROM: the account from which send the tx from (I believe this was used to find the right keystore file inside the keystore directory).
    • ETH_PASSWORD: the path to a plain-text password file with the password for the keystore file in the local file system.
  2. Anything requiring wallet signing pickups the above variables automatically:
    dapp create src/MyContract.sol:MyContract
    seth send $ADDRESS "myFunc(address)" $ARG

I'm finding it a bit difficult to work with Foundry for that purpose. Specifically regarding contract deployment. Main differences are:

  1. ETH_KEYSTORE works, but it expects a keystore file, not a directory.
  2. ETH_FROM is redundant, since the sender wallet is derived from ETH_KEYSTORE
  3. ETH_PASSWORD doesn't work.

Also if I use CAST_PASSWORD env var, forge create does not pick it up, so I need to be explicit when passing it:

forge create --password=${CAST_PASSWORD} src/MyContract.sol:MyContract.sol

Also ideally such configs could be in foundry.toml as well:

[default]
password-file = '~/.eth-password'

# either
keystore-directory = '~/.ethereum/keystore'
from = '0xdeAD00000000000000000000000000000000dEAd'

# or
keystore-file = '~/.ethereum/keystore/UTC--2022-01-01T00-00-00.000000000Z--dead00000000000000000000000000000000dead'

Furthermore, users should have the liberty to overwrite any configs with environment variables. For example, it's very useful to be able to define a keystore directory containing multiple accounts and define the from address with env vars.

FOUNDRY_ETH_FROM=0x0783...122 FOUNDRY_PASSWORD_FILE=~/.eth-password-0783...122 forge deploy ...

Additional context

As a workaround, I have written a small helper script to allow me to use forge the same way I could use dapp.tools. Perhaps this can help guiding the implementation.

#!/bin/bash

# scripts/deploy.sh

set -eo pipefail

function log() {
  echo -e "$@" >&2
}

function die() {
  log "$@"
  log ""
  exit 1
}

function err_msg_keystore_file() {
cat <<MSG
ERROR: could not determine the location of the keystore file.

You should either define:

\t1. The FOUNDRY_ETH_KEYSTORE_FILE env var or;
\t2. Both FOUNDRY_ETH_KEYSTORE_DIR and FOUNDRY_ETH_FROM env vars.
MSG
}

function err_msg_etherscan_api_key() {
cat <<MSG
ERROR: cannot verify contracts without ETHERSCAN_API_KEY being set.

You should either:

\t1. Not use the --verify flag or;
\t2. Define the ETHERSCAN_API_KEY env var.
MSG
}

function usage() {
cat <<MSG
deploy.sh contract_path [--constructor-args ...args]

Examples:

\t# Constructor does not take any arguments
\tdeploy.sh src/MyContract.sol:MyContract

\t# Constructor takes (uint, address) arguments
\tdeploy.sh src/MyContract.sol:MyContract --constructor-args 1 0x0000000000000000000000000000000000000000
MSG
}

function deploy() {
  local ENV_FILE="${BASH_SOURCE%/*}/../.env"
  [ -f "$ENV_FILE" ] && source "$ENV_FILE"

  FOUNDRY_ETH_FROM="${FOUNDRY_ETH_FROM:-$ETH_FROM}"
  FOUNDRY_ETHERSCAN_API_KEY="${FOUNDRY_ETHERSCAN_API_KEY:-$ETHERSCAN_API_KEY}"
  FOUNDRY_ETH_KEYSTORE_DIRECTORY="${FOUNDRY_ETH_KEYSTORE_DIRECTORY:-$ETH_KEYSTORE}"

  if [ -z "$FOUNDRY_ETH_KEYSTORE_FILE" ]; then
    [ -z "$FOUNDRY_ETH_KEYSTORE_DIRECTORY" ] && die "$(err_msg_keystore_file)"
    # Foundy expects the Ethereum Keystore file, not the directory.
    # This step assumes the Keystore file for the deployed wallet includes $ETH_FROM in its name.
    FOUNDRY_ETH_KEYSTORE_FILE="${FOUNDRY_ETH_KEYSTORE_DIRECTORY%/}/$(ls -1 $FOUNDRY_ETH_KEYSTORE_DIRECTORY | \
      # -i: case insensitive
      # #0x: strip the 0x prefix from the the address
      grep -i ${FOUNDRY_ETH_FROM#0x})"
  fi
  [ -z "$FOUNDRY_ETH_KEYSTORE_FILE" ] && die "$(err_msg_keystore_file)"

  # Handle reading from the password file
  local PASSWORD_OPT=''
  if [ -f "$FOUNDRY_ETH_PASSWORD_FILE" ]; then
    PASSWORD_OPT="--password=$(cat "$FOUNDRY_ETH_PASSWORD_FILE")"
  fi

  # Require the Etherscan API Key if --verify option is enabled
  set +e
  if grep -- '--verify' <<< "$@" > /dev/null; then
    [ -z "$FOUNDRY_ETHERSCAN_API_KEY" ] && die "$(err_msg_etherscan_api_key)"
  fi
  set -e

  # Log the command being issued, making sure not to expose the password
  log "forge create --keystore="$FOUNDRY_ETH_KEYSTORE_FILE" $(sed 's/=.*$/=[REDACTED]/' <<<${PASSWORD_OPT}) $@"
  forge create --keystore="$FOUNDRY_ETH_KEYSTORE_FILE" ${PASSWORD_OPT} $@
}

# Executes the function if it's been called as a script.
# This will evaluate to false if this script is sourced by other script.
if [ "$0" = "$BASH_SOURCE" ]; then
  if [ $# -eq 0 ]; then
    die "$(usage)"
  fi

  [ "$1" = '-h' ] || [ "$1" = '--help' ] && {
    log "$(usage)"
    exit 0
  }

  deploy $@
fi
# .env

export FOUNDRY_ETHERSCAN_API_KEY=API_KEY
export FOUNDRY_ETH_FROM=WALLET_ADDRESS
export FOUNDRY_ETH_KEYSTORE_DIRECTORY=PATH_OF_ETHEREUM_KEYSTORE_DIR # i.e.: ${HOME}/.ethereum/keystore/
export FOUNDRY_ETH_PASSWORD=PATH_OF_ETHEREUM_PASSWORD_FILE

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Completed

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions