diff --git a/.env b/.env index ad71737..ee0226e 100644 --- a/.env +++ b/.env @@ -1,7 +1,21 @@ -WEB3_INFURA_PROJECT_ID= -POLYGON_ALCHEMY_PROJECT_ID= -DELEGATE_ADDRESS= -ETH_SAFE_ADDRESS= -FTM_SAFE_ADDRESS= -POLYGON_SAFE_ADDRESS= -BSC_SAFE_ADDRESS= \ No newline at end of file +# Your Gnosis Safe Delegate Address +DELEGATE_ADDRESS= + +# Infura project id for ETH mainnet, see network-config.yaml +WEB3_INFURA_PROJECT_ID= +# Alchemy project id for Polygon, see network-config.yaml +POLYGON_ALCHEMY_PROJECT_ID= + +# Safe addresses. Please fill for each network you will use. +ETH_SAFE_ADDRESS= +FTM_SAFE_ADDRESS= +POLYGON_SAFE_ADDRESS= +BSC_SAFE_ADDRESS= +GOR_SAFE_ADDRESS= +ARB_SAFE_ADDRESS= +GNOSIS_SAFE_ADDRESS= +OPTI_SAFE_ADDRESS= +BASE_SAFE_ADDRESS= + +# Optional Sentry DSN for crash and performance analytics, sign up at sentry.io. +SENTRY_DSN= \ No newline at end of file diff --git a/.github/workflows/run-command.yml b/.github/workflows/run-command.yml index d19022e..e52f450 100644 --- a/.github/workflows/run-command.yml +++ b/.github/workflows/run-command.yml @@ -26,12 +26,10 @@ on: delete-branch-after-send: description: 'Set to true to delete the PR branch after running with send=true' default: 'true' - pull_request_author: - required: false jobs: dispatchCommand: - uses: yearn/yearn-workflows/.github/workflows/roboanimals-workflow.yml@v0.0.11 + uses: yearn/yearn-workflows/.github/workflows/roboanimals-workflow.yml@v0.10.0 with: ref: ${{ github.event.inputs.ref }} comment-id: ${{ github.event.inputs.comment-id }} @@ -41,28 +39,23 @@ jobs: send: ${{ github.event.inputs.send }} pull_request_number: ${{ github.event.inputs.pull_request_number }} delete-branch-after-send: ${{ github.event.inputs.delete-branch-after-send }} - pull_request_author: ${{ github.event.inputs.pull_request_author }} - github_run_id: $GITHUB_RUN_ID - github_repository: $GITHUB_REPOSITORY - github_workspace: $GITHUB_WORKSPACE - runs_on: ubuntu-latest - runner_name: runner - brownie_cache_version: v0.0.1 - compiler_cache_version: v0.0.1 - close_pr: 'true' - check_reviews: false group_telegram_chat_id: '' announcement_telegram_chat_id: '' failure_telegram_chat_id: '' - ftm_safe: '' - eth_safe: '' + runs_on: ubuntu-latest + close_pr: 'true' + check_reviews: false + cached_runner: false + be: gnosis secrets: - TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + BASESCAN_TOKEN: ${{ secrets.BASESCAN_TOKEN }} FTMSCAN_TOKEN: ${{ secrets.FTMSCAN_TOKEN }} ETHERSCAN_TOKEN: ${{ secrets.ETHERSCAN_TOKEN }} - POLYGONSCAN_TOKEN: '' - BSCSCAN_TOKEN: '' - ARBISCAN_TOKEN: '' - SNOWTRACE_TOKEN: '' + POLYGONSCAN_TOKEN: ${{ secrets.POLYGONSCAN_TOKEN }} + OPTISCAN_TOKEN: ${{ secrets.OPTISCAN_TOKEN }} + BSCSCAN_TOKEN: ${{ secrets.BSCSCAN_TOKEN }} + ARBISCAN_TOKEN: ${{ secrets.ARBISCAN_TOKEN }} + SNOWTRACE_TOKEN: ${{ secrets.SNOWTRACE_TOKEN }} PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} PAT: ${{ secrets.PAT }} + TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} diff --git a/.github/workflows/run-help.yml b/.github/workflows/run-help.yml deleted file mode 100644 index 42bd91c..0000000 --- a/.github/workflows/run-help.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Multisig-Run help - -jobs: - runHelp: - runs-on: ubuntu-latest - steps: - - name: Find Comment - uses: peter-evans/find-comment@v1 - id: fc - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: To transmit a multisig tx request - - - name: Create or update comment - uses: peter-evans/create-or-update-comment@v1 - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body: | - To transmit a multisig tx request, please run: - `/run file=[main|hydrate_ci_cache] network=[eth|bsc|ftm|matic] fn=[valid_brownie_function_name] send=[true|false] delete-branch-after-send=[true|false]` - - parameters: - **file** - defaults to **main**, so feel free to omit this if working in that file - **network** - defaults to **eth**, so feel free to omit this if working with eth mainnet - **fn** - any function name in the corresponding file for the file chosen (main.py for main, hydrate_ci_cache.py for hydrate_ci_cache) - **send** - defaults to **false**, setting to true will sign the TX with the delegate private key and submit to gnosis. This will also close the PR. - **delete-branch-after-send** - defaults to **true**, set to false to not have your branch deleted after completing a send. - - edit-mode: replace diff --git a/.github/workflows/shame-command.yml b/.github/workflows/shame-command.yml deleted file mode 100644 index a0217ba..0000000 --- a/.github/workflows/shame-command.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Shame the signers -on: - workflow_dispatch: - inputs: - comment-id: - description: 'The comment-id of the slash command' - required: true - pull_request_number: - description: 'Set to the pull request number for this command dispatch' - required: false - pull_request_author: - required: false - -jobs: - dispatchCommand: - uses: yearn/yearn-workflows/.github/workflows/shame-workflow.yml@master - with: - ref: ${{ github.event.inputs.ref }} - comment-id: ${{ github.event.inputs.comment-id }} - GITHUB_REPOSITORY: $GITHUB_REPOSITORY - TELEGRAM_CHAT_ID: '' - secrets: - TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} diff --git a/.github/workflows/slash-command.yml b/.github/workflows/slash-command.yml index ae629dd..dd05046 100644 --- a/.github/workflows/slash-command.yml +++ b/.github/workflows/slash-command.yml @@ -8,7 +8,7 @@ jobs: slashCommandDispatch: runs-on: ubuntu-latest steps: - - uses: actions/github-script@v3 + - uses: actions/github-script@v6.4.1 id: get-pr with: script: | @@ -19,7 +19,7 @@ jobs: } core.info(`Getting PR #${request.pull_number} from ${request.owner}/${request.repo}`) try { - const result = await github.pulls.get(request) + const result = await github.rest.pulls.get(request) return result.data } catch (err) { core.setFailed(`Request failed with error ${err}`) @@ -28,24 +28,24 @@ jobs: - uses: hmarr/debug-action@v2 - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v2 + uses: peter-evans/slash-command-dispatch@v3 with: dispatch-type: workflow token: ${{ secrets.PAT }} commands: | run - shame static-args: | comment-id=${{ github.event.comment.id }} ref=${{ fromJSON(steps.get-pr.outputs.result).head.ref }} pull_request_number=${{ github.event.issue.number }} - pull_request_author=${{ github.event.issue.user.login }} - name: Edit comment with error message if: failure() || steps.scd.outputs.error-message - uses: peter-evans/create-or-update-comment@v1 + uses: peter-evans/create-or-update-comment@v2.1.1 with: comment-id: ${{ github.event.comment.id }} body: | > command failed, check your syntax - > /run network=[eth|bsc] fn=[valid_brownie_function_name] send=[true|false] delete-branch-after-send=[true|false] + > /run network=[eth|base|opti|gc|arb|ftm|matic|bsc] fn=[valid_brownie_function_name] send=[true|false] delete-branch-after-send=[true|false] + > If that looks good, make sure you have write access on this repository. + > Also, make sure you are not running this from a repository fork. Robowoofy must be ran from branch on the repo. diff --git a/.gitignore b/.gitignore index a374db8..1ae3fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ __pycache__ build/ reports/ .DS_Store -.venv \ No newline at end of file +.venv +.python_version +settings.json \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..3236cba --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +template diff --git a/README.md b/README.md index 1e95a95..4c2cb2a 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,140 @@ -# strategist-ms +yearn-multisig-actions -Collection of useful scripts to manage gnosis multisig wallets -- [Gnosis Safe link](https://gnosis-safe.io/app/) +Template repository for automating delegate transactions to [Gnosis Safe](https://gnosis-safe.io/app/) multisig wallets through Github Actions -## Bootstrap -If you have downloaded this template repository, you need to fill in some config values and add some repository secrets. +Allows for teams to be notified in Telegram when a new transaction is queued for signature in a multisig. Preview output: -List of tasks: -- +![](https://i.imgur.com/zKTnTY4.png) + +## Bootstrapping +NOTE: Please ensure your copy of this repository is private, not public, when you use this template! Super important! You don't want randoms queuing TXs to your Gnosis Safe. + +1. Set your workflow permissions to `Read and write permissions` and your actions permissions to `Allow all actions and reusable workflows` under https://github.com/{org}/{repo}/settings/actions. Note: replace {org} and {repo} with your information. +![image](https://github.com/yearn/yearn-multisig-actions/assets/7820952/2a945da1-31be-497b-817f-0149356eaa49) +![image](https://github.com/yearn/yearn-multisig-actions/assets/7820952/67292dc2-dc02-49d7-80fa-083b6d869552) + +3. Also fork [yearn-workflows](https://github.com/yearn/yearn-workflows/fork), this should be public and you don't need to change it. You just need a fork of this because Github runners can only read workflows within the same organization/account. +4. If you have downloaded this template repository, you must fill in some config values and add some repository secrets. (see below for more details on how to do this) + +#### Adding a delegate account + +1. Create a new delegate account via brownie + - `brownie accounts generate multi-sig-delegate` + - Get the private key and save it for later: + ``` + brownie console + ``` + ``` + delegate = accounts.load('multi-sig-delegate') + print("Address: ", delegate.address) + print("Private Key: ", delegate.private_key) + ``` + Note: Do NOT use this private key for anything else. We recommend you throw it away once you add the secret. Anyone with access to your repo and the actions can take this private key, so don't make any assumptions. Be ready to revoke your delegate if you see any suspicious transactions queued to it. + +2. Authorize your new delegate on your safe. You must do this via an account that is a safe owner or signer. + - You add delegates via a UI such as https://gnosis-safe-delegate.vercel.app/ + - Alternatively, if the UI doesn't work, you can import your safe owner into brownie and run a script to add the delegate: + - Follow the steps under [Installation](#Installation) to setup this repo for running scripts locally + - run `brownie accounts new multi-sig-delegator` to import your safe owner account + - open [delegates.py](scripts/delegates.py) and add in your safe address for the `safe` variable and also change the + `brownie run delegates add_delegate_from_existing_address --network -main`. Replace `` with the short name for a network, e.g. eth, opti, ftm, arb, etc. + +### Secrets +Add these repository secrets. Go to https://github.com/{org}/{repo}/settings/secrets/actions. Note: replace {org} and {repo} with your information. + +1. `PAT` - generate a personal access token. Go to https://github.com/settings/tokens/new and click repo for scopes. Make sure to reset this secret when the PAT expires. Note: if you are in the Yearn org, ask @kx9x for a PAT from the Robowoofy Github accout instead of using your own. +2. `{NETWORK}SCAN_TOKEN` - Define multiple secrets where {NETWORK} can be ETHER, FTM, SNOW, BSC, ARBI, or POLYGON. You can generate these tokens by making an account at the respective sites (e.g. etherscan.io, ftmscan.com, etc, etc). If you don't need a token for a given network, then either set the secret to something random or edit run-command.yml to pass in '' for the token you don't need. +3. `TELEGRAM_TOKEN` - This is the token for your telegram bot that will send messages to channels. To create a bot go to: https://core.telegram.org/bots. If you are in the yearn org, contact kx9x for the robowoofy token. +4. `PRIVATE_KEY` - Private key for your delegate (get this from the previous step where you added your delegate account) + +### Config values +1. Fill in the telegram channel ids in run-command.yml. + + You can find these ids by opening your chat on Telegram web, taking the number from the url, and adding a `100` between the "-" and the number. For example, `-3456789` would become `-1003456789`. Announcements and group chats allow you to notify 2 separate channels. Leave telegram chat ids blank if you don't want notifications. + + Alternatively, you can message @username_to_id_bot on Telegram to find a chat id. + +1. Fill in values in the .env file. For any safes on networks you don't need, feel free to leave those blank. Some fields are marked optional. ## Usage -Follow the process steps below for queuing transaction to your multisig -1. Create script using ape safe syntax -2. Create PR on new branch -3. Add a comment on PR to trigger bot to dry-run the txn: +Follow the process steps below for queuing transactions to your multisig +1. Create a script using ape-safe syntax +2. Create a PR on a new branch +3. Add a comment on PR to trigger the bot to dry-run the txn: ``` - /run file=[main|hydrate_ci_cache] fn=[name_of_fxn] network=[eth|bsc|matic|ftm] + /run file=[main|hydrate_ci_cache] fn=[name_of_fxn] network=[eth|bsc|matic|ftm|rin|arb] ``` + Note: remove the [ ] symbols, e.g. /run fn=example network=matic + The file param defaults to main, so you can usually omit it + - The GitHub action runner will respond with: - a reply comment with link to the [action which was triggered](https://github.com/yearn/strategist-ms/actions/) - 👀 to indicate command is detected - 🚀 to indicate script is being run - 🎉 to indicate script is run successfully - Note: main is the default target script and eth is the default network, you can omit both -4. After successful dry run, get a peer review -5. When peer review is complete, they can indicate it by using GitHub runner bot to queue the transaction in Gnosis. This is done by adding same comment as step #3, but this time with "send=true" +4. After a successful dry run, get a peer review +5. When peer review is complete, they can indicate it by using GitHub runner bot to queue the transaction in Gnosis. To do this, add the same comment as step #3 but this time with "send=true" ``` - /run file=[main|hydrate_ci_cache] network=[eth|bsc|matic|ftm] fn=[name_of_fxn] send=[true|false] delete-branch-after-send=[true|false] + /run file=[main] fn=[name_of_fxn] network=[eth|bsc|matic|ftm|rin|arb] send=[true|false] delete-branch-after-send=[true|false] ``` - - delete-branch-after-send defaults to true, if you don't want your branch deleted, then set delete-branch-after-send=false -6. After a successful run with send=true, you can track a Gnosis TX back to its PR and original code by going to https://github.com/yearn/strategist-ms/labels and searching for the nonce number and clicking the matching nonce Github label. + - delete-branch-after-send defaults to true. If you don't want your branch deleted, then set delete-branch-after-send=false +6. After a successful run with send=true, you can track a Gnosis TX back to its PR and original code by going to https://github.com/yearn/strategist-ms/labels and searching for the nonce number, then clicking the matching nonce Github label. ![image](https://user-images.githubusercontent.com/7820952/119859130-f1d67600-bec9-11eb-8ac1-3dbc05956210.png) ## Installation -You need Python 3.8 and pip installed -Install dependencies +### Run the [pyenv installer](https://github.com/pyenv/pyenv#automatic-installer) +``` +curl https://pyenv.run | bash +``` +### Install python 3.10 ``` -pip install -r requirements-dev.txt +pyenv install 3.10 ``` -You need also ganache-cli installed (and Node) +### Make a venv +``` +pyenv virtualenv 3.10 +``` +### Make it automatically activate while in the project folder ``` -npm install -g ganache-cli +pyenv local ``` -Run a multisig tx function on ethereum +### Install deps +``` + pip install -r requirements-dev.txt +``` + +It's THAT easy! + +### You also need anvil installed +``` + curl -L https://foundry.paradigm.xyz | bash + $HOME/.foundry/bin/foundryup +``` + +Open a new terminal and make sure Anvil exists +``` +anvil --help +``` + +### Now install Brownie using [pipx](https://github.com/eth-brownie/brownie#via-pipx) ``` -brownie run main run_example --network eth-main-fork +python -m pip install --user pipx +python -m pipx ensurepath +pipx install eth-brownie==1.20.5 ``` -Run a multisig tx function on ftm +Open a new terminal. +Run a multisig tx function on ethereum +``` +brownie run main example -I ``` -brownie run main run_example --network ftm-main-fork -``` \ No newline at end of file diff --git a/brownie-config.yaml b/brownie-config.yaml index 3310ad7..dcf532f 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -2,3 +2,4 @@ networks: default: eth-main-fork autofetch_sources: true +dotenv: .env diff --git a/ci/ci_override.py b/ci/ci_override.py deleted file mode 100644 index 4125dc3..0000000 --- a/ci/ci_override.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -from ape_safe import ApeSafe -from brownie import accounts -from gnosis.safe.safe_tx import SafeTx -from typing import Optional, Union -from brownie.network.account import LocalAccount - -# CI horribleness lurks below -# If running in CI, let's override ApeSafe.post_transaction so -# that it writes a file with the nonce. This is used to later tag -# the pull request with a label matching the nonce - -# TODO: configuration file -DELEGATE_ADDRESS = os.environ.get("DELEGATE_ADDRESS") -home_directory = os.environ.get("HOME") - - -class DelegateSafe(ApeSafe): - @property - def is_ci(self): - return os.environ.get("CI", "").lower() == "true" - - @property - def is_send(self): - return os.environ.get("GITHUB_ACTION_SEND", "").lower() == "true" - - def post_transaction(self, safe_tx: SafeTx): - super().post_transaction(safe_tx) - - if self.is_ci and self.is_send: - with open(os.path.join(home_directory, "nonce.txt"), "w") as f: - f.write(str(safe_tx.safe_nonce)) - exit(0) - - def preview(self, safe_tx: SafeTx, events=True, call_trace=False, reset=True): - if self.is_ci: - events = False - call_trace = False - - return super().preview(safe_tx, events=events, call_trace=call_trace, reset=reset) - - def get_signer(self, signer: Optional[Union[LocalAccount, str]] = None) -> LocalAccount: - if self.is_ci: - if self.is_send: - key = os.environ.get("PRIVATE_KEY") - assert ( - key is not None - ), "CI environment missing PRIVATE_KEY environment variable. Please add it as a repository secret." - user = accounts.add(key) - assert ( - user.address == DELEGATE_ADDRESS - ), "Delegate address mismatch. Check you have correct private key." - return user - else: - print("CI dry-run enabled, set send to true to run to completion") - exit(0) - else: - return super().get_signer(signer) - - -with open(os.path.join(home_directory, "alive.signal"), "w") as f: - f.write("I am alive") diff --git a/ci/hydrate_ci_cache.py b/ci/hydrate_ci_cache.py deleted file mode 100644 index 7b95a81..0000000 --- a/ci/hydrate_ci_cache.py +++ /dev/null @@ -1,131 +0,0 @@ -import requests -import os, stat -from brownie import Contract, network, exceptions -from ape_safe import ApeSafe -import time -import ci.ci_override - -vyper_releases = [ - "https://github.com/vyperlang/vyper/releases/download/v0.3.0/vyper.0.3.0+commit.8a23feb.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.16/vyper.0.2.16+commit.59e1bdd.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.15/vyper.0.2.15+commit.6e7dba7.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.12/vyper.0.2.12+commit.2c6842c.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.11/vyper.0.2.11+commit.5db35ef.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.8/vyper.0.2.8+commit.069936f.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.7/vyper.0.2.7+commit.0b3f3b3.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.6/vyper.0.2.6+commit.35467d5.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.5/vyper.0.2.5+commit.a0c561c.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.4/vyper.0.2.4+commit.7949850.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.3/vyper.0.2.3+commit.006968f.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.2/vyper.0.2.2+commit.337c2ef.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.1/vyper.0.2.1+commit.cac3d7d.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.2.0/vyper.0.2.0+commit.d2c0c87.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.1.0-beta.17/vyper.0.1.0-beta.17+commit.0671b7b.linux", - "https://github.com/vyperlang/vyper/releases/download/v0.1.0-beta.16/vyper.0.1.0-beta.16+commit.5e4a94a.linux", -] - -solc_url_prefix = "https://solc-bin.ethereum.org/linux-amd64/solc-linux-amd64-" - -solc_release_versions = [ - "v0.8.9+commit.e5eed63a", - "v0.8.8+commit.dddeac2f", - "v0.8.7+commit.e28d00a7", - "v0.8.6+commit.11564f7e", - "v0.8.5+commit.a4f2e591", - "v0.8.4+commit.c7e474f2", - "v0.8.3+commit.8d00100c", - "v0.8.2+commit.661d1103", - "v0.8.1+commit.df193b15", - "v0.8.0+commit.c7dfd78e", - "v0.7.6+commit.7338295f", - "v0.7.5+commit.eb77ed08", - "v0.7.4+commit.3f05b770", - "v0.7.3+commit.9bfce1f6", - "v0.7.2+commit.51b20bc0", - "v0.7.1+commit.f4a555be", - "v0.7.0+commit.9e61f92b", - "v0.6.12+commit.27d51765", - "v0.6.11+commit.5ef660b1", - "v0.6.10+commit.00c0fcaf", - "v0.6.9+commit.3e3065ac", - "v0.6.8+commit.0bbfe453", - "v0.6.7+commit.b8d736ae", - "v0.6.6+commit.6c089d02", - "v0.6.5+commit.f956cc89", - "v0.6.4+commit.1dca32f3", - "v0.6.3+commit.8dda9521", - "v0.6.2+commit.bacdbe57", - "v0.6.1+commit.e6f7d5a4", - "v0.6.0+commit.26b70077", - "v0.5.17+commit.d19bba13", - "v0.5.16+commit.9c3226ce", - "v0.5.15+commit.6a57276f", - "v0.5.14+commit.01f1aaa4", - "v0.5.13+commit.5b0b510c", - "v0.5.12+commit.7709ece9", - "v0.5.11+commit.22be8592", - "v0.5.11+commit.c082d0b4", - "v0.5.10+commit.5a6ea5b1", - "v0.5.9+commit.c68bc34e", - "v0.5.9+commit.e560f70d", - "v0.5.8+commit.23d335f2", - "v0.5.7+commit.6da8b019", - "v0.5.6+commit.b259423e", - "v0.5.5+commit.47a71e8f", - "v0.5.4+commit.9549d8ff", - "v0.5.3+commit.10d17f24", - "v0.5.2+commit.1df8f40c", - "v0.5.1+commit.c8a2cb62", - "v0.5.0+commit.1d4f565a", - "v0.4.26+commit.4563c3fc", - "v0.4.25+commit.59dbf8f1", - "v0.4.24+commit.e67f0147", - "v0.4.23+commit.124ca40d", - "v0.4.22+commit.4cb486ee", - "v0.4.21+commit.dfe3193c", - "v0.4.20+commit.3155dd80", - "v0.4.19+commit.c4cbbb05", - "v0.4.18+commit.9cf6e910", - "v0.4.17+commit.bdeb9e52", - "v0.4.16+commit.d7661dd9", - "v0.4.15+commit.8b45bddb", - "v0.4.15+commit.bbb8e64f", - "v0.4.14+commit.c2215d46", - "v0.4.13+commit.0fb4cb1a", - "v0.4.12+commit.194ff033", - "v0.4.11+commit.68ef5810", -] - -home_directory = os.environ.get("HOME") - - -def hydrate_compiler_cache(): - for vyper_release in vyper_releases: - name = vyper_release[vyper_release.index("vyper.") : vyper_release.index("+")] - print("Downloading " + name) - r = requests.get(vyper_release, allow_redirects=True) - vvm_folder = os.path.join(home_directory, ".vvm/") - if not os.path.exists(vvm_folder): - os.mkdir(vvm_folder) - file_name = vvm_folder + name.replace(".", "-", 1) - with open(file_name, "wb+") as f: - f.write(r.content) - st = os.stat(file_name) - os.chmod(file_name, st.st_mode | stat.S_IEXEC) - - for solc_release_version in solc_release_versions: - solc_release_url = solc_url_prefix + solc_release_version - prefix = "solc-linux-amd64" - start = solc_release_url.index(prefix) + len(prefix) - end = solc_release_url.index("+") - name = "solc" + solc_release_url[start:end] - print("Downloading " + name) - r = requests.get(solc_release_url, allow_redirects=True) - solcx_folder = os.path.join(home_directory, ".solcx/") - if not os.path.exists(solcx_folder): - os.mkdir(solcx_folder) - file_name = os.path.join(home_directory, ".solcx/") + name - with open(file_name, "wb+") as f: - f.write(r.content) - st = os.stat(file_name) - os.chmod(file_name, st.st_mode | stat.S_IEXEC) \ No newline at end of file diff --git a/ci/run_brownie.py b/ci/run_brownie.py deleted file mode 100644 index 41de7f2..0000000 --- a/ci/run_brownie.py +++ /dev/null @@ -1,67 +0,0 @@ -from subprocess import Popen -from tenacity import * -import sys, time, os, signal, psutil - -home_directory = os.environ.get("HOME") -signal_file_path = os.path.join(home_directory, "alive.signal") -nonce_file_path = os.path.join(home_directory, "nonce.txt") -current_try_count = 0 - - -@retry(stop=stop_after_attempt(5)) -def run_brownie(args): - global current_try_count - - # Kill processes to make sure we start clean - kill_process_by_cmdline("ganache-cli") - kill_process_by_name("brownie") - - if os.path.exists(signal_file_path) and current_try_count == 0: - os.remove(signal_file_path) - print("cleaning up signal from last run") - - if os.path.exists(nonce_file_path): - if current_try_count == 0: - os.remove(nonce_file_path) - else: - print("nonce found, aborting before we trigger another tx") - exit(1) - - p = Popen(args) - - # sleep 10, 20, 30, 40, 50, or 60 seconds based on retries - sleep_time = 10 + min(current_try_count * 10, 50) - print(f"waiting for alive signal, sleeping for {sleep_time} seconds") - time.sleep(sleep_time) - - current_try_count += 1 - - if not os.path.exists(signal_file_path): - print(f"alive signal not found, killing brownie and ganache. queuing try #{current_try_count}") - p.terminate() - kill_process_by_cmdline("ganache-cli") - raise Exception() - - print("found alive signal, waiting for process to complete") - exit_code = p.wait() - os.remove(signal_file_path) - exit(exit_code) - - -def kill_process_by_cmdline(cmdline_arg_find): - for proc in psutil.process_iter(): - for cmdline_arg in proc.cmdline(): - if cmdline_arg_find in cmdline_arg: - pid = proc.pid - os.kill(int(pid), signal.SIGKILL) - - -def kill_process_by_name(proc_name): - for proc in psutil.process_iter(): - if proc_name == proc.name(): - pid = proc.pid - os.kill(int(pid), signal.SIGKILL) - - -if __name__ == "__main__": - run_brownie(sys.argv[1:]) diff --git a/ci/safes.py b/ci/safes.py deleted file mode 100644 index 30cdf6e..0000000 --- a/ci/safes.py +++ /dev/null @@ -1,15 +0,0 @@ -import os -from dotenv import load_dotenv -from ci.ci_override import DelegateSafe as ApeSafe -from brownie import network - - -load_dotenv() -if network.chain.id == 250: - safe = ApeSafe(os.getenv("FTM_SAFE_ADDRESS")) -elif network.chain.id == 137: - safe = ApeSafe(os.getenv("POLYGON_SAFE_ADDRESS")) -elif network.chain.id == 56: - safe = ApeSafe(os.getenv("BSC_SAFE_ADDRESS")) -else: - safe = ApeSafe(os.getenv("ETH_SAFE_ADDRESS")) diff --git a/ci/sign.py b/ci/sign.py deleted file mode 100644 index 731025b..0000000 --- a/ci/sign.py +++ /dev/null @@ -1,25 +0,0 @@ -from ci.safes import safe - - -def sign(nonce_arg = None, skip_preview = False, post_tx = False): - def _sign(func): - def wrapper(): - func() - safe_tx = safe.multisend_from_receipts(safe_nonce=nonce) - if not skip_preview: - safe.preview(safe_tx, call_trace=False) - - if not post_tx and not safe.is_ci: - print("dry-run finished, run again with @sign(post_tx = True) to sign and submit the tx.") - else: - safe.sign_transaction(safe_tx) - safe.post_transaction(safe_tx) - - return wrapper - - if callable(nonce_arg): - nonce = None - return _sign(nonce_arg) - - nonce = int(nonce_arg) if nonce_arg else nonce_arg - return _sign diff --git a/network-config.yaml b/network-config.yaml index cee065a..bf6c782 100644 --- a/network-config.yaml +++ b/network-config.yaml @@ -1,3 +1,4 @@ +# Note: you are free to use any host or explorer you like, just change them here. live: - name: Ethereum networks: @@ -5,7 +6,7 @@ live: chainid: 1 id: mainnet host: https://mainnet.infura.io/v3/$WEB3_INFURA_PROJECT_ID - explorer: https://api.etherscan.com/api + explorer: https://api.etherscan.io/api - name: Polygon networks: - name: Mainnet @@ -27,18 +28,122 @@ live: id: ftm-main host: https://rpc.ftm.tools explorer: https://api.ftmscan.com/api +- name: Arbitrum One + networks: + - chainid: 42161 + host: https://arb1.arbitrum.io/rpc + id: arb-main + name: Mainnet + explorer: https://api.arbiscan.io/api +- name: gnosis-chain + networks: + - chainid: 100 + host: https://rpc.gnosischain.com + id: gc-main + name: gnosis-chain + explorer: https://api.gnosisscan.io/api +- name: Optimism + networks: + - chainid: 10 + host: https://rpc.ankr.com/optimism + id: opti-main + name: Mainnet + explorer: https://optimistic.etherscan.io/api +- name: Base + networks: + - chainid: 8453 + host: https://base.llamarpc.com + id: base-main + name: Mainnet + explorer: https://api.basescan.org/api development: +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + fork: mainnet + gas_limit: 30000000 + port: 8545 + host: http://127.0.0.1 + id: eth-main-fork + name: Anvil-CLI (eth-Mainnet Fork) + timeout: 1200 +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + fork: bsc-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: bsc-main-fork + name: Ganache-CLI (BSC-Mainnet Fork) + timeout: 1200 +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + fork: ftm-main + gas_limit: 10000000 + port: 8545 + host: http://127.0.0.1 + id: ftm-main-fork + name: Anvil-CLI (FTM-Mainnet Fork) + timeout: 1200 +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + fork: matic-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: matic-main-fork + name: Anvil-CLI (MATIC-Mainnet Fork) + timeout: 1200 +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + fork: gc-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: gc-main-fork + name: Anvil-CLI (gnosis-chain Fork) +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + fork: opti-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: opti-main-fork + name: Anvil-CLI (Optimism-Mainnet Fork) +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + fork: base-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: base-main-fork + name: Anvil-CLI (Base-Mainnet Fork) +- cmd: anvil --steps-tracing --block-base-fee-per-gas 0 --gas-price 0 + cmd_settings: + accounts: 10 + evm_version: istanbul + fork: arb-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: arb-main-fork + name: Ganache-CLI (ARBITRUMONE-Mainnet Fork) - cmd: ganache-cli cmd_settings: accounts: 10 evm_version: istanbul fork: mainnet - gas_limit: 12000000 - mnemonic: brownie + gas_limit: 30000000 port: 8545 host: http://127.0.0.1 - id: eth-main-fork + id: eth-ganache-main-fork name: Ganache-CLI (eth-Mainnet Fork) timeout: 1200 - cmd: ganache-cli @@ -47,10 +152,9 @@ development: evm_version: istanbul fork: bsc-main gas_limit: 12000000 - mnemonic: brownie port: 8545 host: http://127.0.0.1 - id: bsc-main-fork + id: bsc-ganache-main-fork name: Ganache-CLI (BSC-Mainnet Fork) timeout: 1200 - cmd: ganache-cli @@ -58,11 +162,10 @@ development: accounts: 10 evm_version: istanbul fork: ftm-main - gas_limit: 12000000 - mnemonic: brownie + gas_limit: 10000000 port: 8545 host: http://127.0.0.1 - id: ftm-main-fork + id: ftm-ganache-main-fork name: Ganache-CLI (FTM-Mainnet Fork) timeout: 1200 - cmd: ganache-cli @@ -71,9 +174,48 @@ development: evm_version: istanbul fork: matic-main gas_limit: 12000000 - mnemonic: brownie port: 8545 host: http://127.0.0.1 - id: matic-main-fork + id: matic-ganache-main-fork name: Ganache-CLI (MATIC-Mainnet Fork) timeout: 1200 +- cmd: ganache-cli + cmd_settings: + accounts: 10 + evm_version: istanbul + fork: arb-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: arb-ganache-main-fork + name: Ganache-CLI (ARBITRUMONE-Mainnet Fork) +- cmd: ganache-cli + cmd_settings: + accounts: 10 + evm_version: istanbul + fork: gc-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: gc-ganache-main-fork + name: Ganache-CLI (gnosis-chain Fork) +- cmd: ganache-cli + cmd_settings: + accounts: 10 + evm_version: istanbul + fork: opti-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: opti-ganache-main-fork + name: Ganache-CLI (Optimism-Mainnet Fork) +- cmd: ganache-cli + cmd_settings: + accounts: 10 + evm_version: istanbul + fork: base-main + gas_limit: 12000000 + port: 8545 + host: http://127.0.0.1 + id: base-ganache-main-fork + name: Ganache-CLI (Base-Mainnet Fork) diff --git a/requirements-dev.txt b/requirements-dev.txt index dfaed42..6a8088f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,12 @@ -black==21.9b0 -eth_brownie==1.17 -gnosis-py==3.6.0 -ape_safe==0.5.0 -click==8.0.3 -regex==2021.10.8 -tomli==1.2.1 +cython<3.0 +pyyaml>=5.4.1,<6 +eth-brownie==1.20.5 +black==24.2.0 +click==8.1.7 +regex==2023.12.25 +tomli==2.0.1 tenacity==8.0.1 -psutil==5.8.0 -python-dotenv==0.16.0 \ No newline at end of file +psutil>=5.9.2 +multisig-ci==0.8.22 +tokenlists==0.1.7 +sentry-sdk==1.39.1 diff --git a/scripts/ahhh_im_noncing.py b/scripts/ahhh_im_noncing.py new file mode 100644 index 0000000..d781a15 --- /dev/null +++ b/scripts/ahhh_im_noncing.py @@ -0,0 +1,21 @@ +from urllib.parse import urljoin +import requests + +def pending_nonce_override(self) -> int: + """ + Subsequent nonce which accounts for pending transactions in the transaction service. + """ + url = urljoin(self.transaction_service.base_url, f'/api/v1/safes/{self.address}/multisig-transactions/') + results = requests.get(url).json()['results'] + # loop through the TXs return and detect a gap in nonce + # if there is a gap, return a nonce so we fill that gap + i = 1 + while i < len(results): + nonce_after = results[i-1]['nonce'] + nonce_before = results[i]['nonce'] + if abs(nonce_after-nonce_before) > 1: + print(nonce_before + 1) + return nonce_before + 1 + i += 1 + + return results[0]['nonce'] + 1 if results else 0 diff --git a/scripts/delegates.py b/scripts/delegates.py new file mode 100644 index 0000000..3923d14 --- /dev/null +++ b/scripts/delegates.py @@ -0,0 +1,73 @@ +from time import time + +import requests +from brownie import * +from brownie.network.account import LocalAccount +from eth_account import Account +from brownie_safe import TransactionServiceBackport, EthereumNetworkBackport +from gnosis.eth import EthereumClient, EthereumNetwork + +BASE_CHAIN_ID = 8453 + +if network.chain.id == BASE_CHAIN_ID: + base_url = "https://safe-transaction-base.safe.global" + +ethereum_client = EthereumClient(web3.provider.endpoint_uri) +transaction_service = TransactionServiceBackport(ethereum_client.get_network(), ethereum_client, base_url) + +BASE_URL = transaction_service.base_url + "/api/v1/" +print("BASE_URL is ", BASE_URL, "\n") +assert BASE_URL + +## Modify values here +# 1. Add your safe +safe = input(f"Please enter your safe address for network {ethereum_client.get_network()}:\n") + +# 2. Add your delegator. This account needs to be a owner of the safe +_delegator = accounts.load('multi-sig-delegator') ## TODO: Load your account + +# Use the Account from eth_account to make signing hashs easier +delegator = Account.from_key(_delegator.private_key) + + +## You can also use a hardware wallet like Trezor or Ledger with clef. See https://eth-brownie.readthedocs.io/en/stable/account-management.html?highlight=private%20key#using-a-hardware-wallet +# accounts.connect_to_clef("/Users/gazumps/Library/Signer/clef.ipc") +# delegator = accounts[1] + +# 3. Add your delegate +## Create a new throw away account + + +def list_delegates(safe: str): + response = requests.get(f"{BASE_URL}/delegates/", params={"safe": safe}) + print(response.json()["results"]) + + +def make_payload(safe: str, delegate: str, delegator: Account, label: str = None): + message = web3.keccak(text=delegate + str(int(time() // 3600))) + signature = delegator.signHash(message).signature.hex() + return {"safe": safe, "delegate": delegate, "delegator": delegator.address, "signature": signature, "label": label} + + +def add_delegate(safe: str, delegate: str, delegator: Account, label: str = None): + payload = make_payload(safe, delegate, delegator, label) + response = requests.post(f"{BASE_URL}/delegates/", json=payload) + print(f"{response.status_code}: {response.text}") + + +def create_and_add_delegate(): + delegate = Account.create() + add_delegate(safe, delegate.address, delegator, label="Robowoofy") + print("Delegate Address: ", delegate.address) + print("Delegate Private Key: ", delegate.privateKey.hex()) + print() + print("List of Delegates:") + print (list_delegates(safe)) + + +def add_delegate_from_existing_address(address): + add_delegate(safe, address, delegator, label="Robowoofy") + print("Delegate Address: ", address) + print("List of Delegates:") + print (list_delegates(safe)) + diff --git a/scripts/main.py b/scripts/main.py index a05c4b6..4cae65a 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -1,14 +1,16 @@ -import ci.ci_override -from ci.ci_override import DelegateSafe as ApeSafe -from ci.safes import safe -from ci.sign import sign +import multisig_ci.ci_override +from scripts.ahhh_im_noncing import pending_nonce_override +from multisig_ci.ci_override import DelegateSafe as ApeSafe +ApeSafe.pending_nonce = pending_nonce_override +from multisig_ci.safes import safe +from multisig_ci.sign import sign @sign -def run_example(): +def example(): safe.account.transfer(safe.account, "0 ether") @sign(420) -def run_override_nonce_example(): +def override_nonce_example(): safe.account.transfer(safe.account, "0 ether") diff --git a/scripts/shame.py b/scripts/shame.py deleted file mode 100644 index 6da61db..0000000 --- a/scripts/shame.py +++ /dev/null @@ -1,36 +0,0 @@ -from brownie import * -from ci.safes import safe -import requests -from contextlib import redirect_stdout -import os - - -SIGNERS = { - # Map address to name for each signer - #"0xdeadbeefdeadbeefdeadbeefdeadbeef": "Hard Rock Nick", -} - - -def ci_alert(): - home_directory = os.environ.get("HOME") - with open(os.path.join(home_directory, "alert.txt"), "w+") as f: - with redirect_stdout(f): - main() - - -def main(): - url = f"https://safe-transaction.mainnet.gnosis.io/api/v1/safes/{safe}/transactions/" - data = requests.get(url).json() - nonce = safe.retrieve_nonce() - pending = [tx for tx in data["results"][::-1] if not tx["isExecuted"] and tx["nonce"] >= nonce] - - if len(pending) > 4: - print( - "Okay, Okay, Okay. I need the signatures to go up. I can't take this anymore. Everyday I'm checking the signatures and it's dipping. Everyday I check the signatures, bad signatures. I can't take this anymore man. I have over-delegated, by A LOT. It is what it is but I need the signatures to go up. Can signers do something?\n" - ) - - print(f"pls sign https://gnosis-safe.io/app/eth:{safe}/transactions/queue") - for tx in pending: - unsigned = set(SIGNERS) - {x["owner"] for x in tx["confirmations"]} - users = " ".join(f"@{SIGNERS[x]}" for x in unsigned) - print(f'{tx["nonce"]}: {users}')