├── .env.sample
├── .gitattributes
├── .gitignore
├── .husky
└── .gitignore
├── .nvmrc
├── LICENSE
├── README.md
├── examples
├── add_owner.json
├── double_transfer.json
├── eth_transfer.json
└── wrap_and_unwrap_eth.json
├── hardhat.config.ts
├── package.json
├── src
├── contracts.ts
├── creation.ts
├── execution
│ ├── index.ts
│ ├── proposing.ts
│ ├── signing.ts
│ ├── simple.ts
│ ├── submitting.ts
│ └── utils.ts
├── history
│ ├── index.ts
│ ├── loading.ts
│ └── types.ts
├── information.ts
└── tasks.ts
├── tsconfig.json
├── tx_input.sample.json
└── yarn.lock
/.env.sample:
--------------------------------------------------------------------------------
1 | MNEMONIC=""
2 | INFURA_KEY=""
3 | # Used for custom network
4 | NODE_URL=""
5 | NETWORK=""
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.sol linguist-language=Solidity
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
3 | contracts/
4 | .DS_Store
5 | .zos.session
6 | .openzeppelin/.session
7 | env/
8 | .env
9 | bin/
10 | dist/
11 | deployments/
12 | cli_cache
13 | solc
14 | coverage/
15 | coverage.json
16 | yarn-error.log
17 | templates/
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v15.6.0
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Gnosis Safe Tasks
2 | =================
3 |
4 | Install
5 | -------
6 | Set correct node version (see `.nvmrc`) with [nvm](https://github.com/nvm-sh/nvm)
7 | ```bash
8 | nvm use
9 | ```
10 |
11 | Install requirements with yarn:
12 | ```bash
13 | yarn
14 | ```
15 |
16 | Quick Start
17 | -----------
18 | ### Setup
19 |
20 | Create `.env` file to use the commands (see `.env.sample` for more info):
21 |
22 | - `NETWORK` - Network that should be used (e.g. `rinkeby`, `mainnet` or `custom`)
23 | - `PK` or `MNEMONIC`- Credentials for the account that should be used
24 | - `INFURA`- For network that use Infura based RPC
25 | - `NODE`- RPC node for `custom` network (optional)
26 |
27 | ### Help
28 |
29 | Use `yarn safe help ` to get more information about parameters of a command.
30 |
31 | Example:
32 | ```bash
33 | yarn safe help create
34 | ```
35 |
36 | ### Create Safe
37 | Creates and setups a Safe proxy via the proxy factory. All parameters of the Safe `setup` method can be configured.
38 |
39 | #### Example
40 | This will deploy a Safe that uses the first imported account as an owner and set the threshold to 1.
41 | ```bash
42 | yarn safe create
43 | ```
44 |
45 | ### Safe Info
46 | Displays information about a Safe
47 |
48 | #### Usage
49 | ```bash
50 | yarn safe info
51 | ```
52 |
53 | ### Propose Safe Transaction
54 | Creates a proposal json file for a Safe transaction that can be shared. The name of the json file will be `.proposal.json` and it will be stored in the `cli_cache` folder.
55 |
56 | #### Examples
57 | This will create a transaction from the Safe to the target without any value or data.
58 | ```bash
59 | yarn safe propose --to
60 | ```
61 |
62 | This will create a transaction based on the sample tx input json that mints some WETH and sets an approve for it.
63 | ```bash
64 | yarn safe propose-multi tx_input.sample.json
65 | ```
66 |
67 | ### Show Proposal
68 | Shows the information of the proposal.
69 | Note: This requires the proposal file created before for that Safe transaction in the `cli_cache`.
70 |
71 | #### Usage
72 | ```bash
73 | yarn safe show-proposal
74 | ```
75 |
76 | ### Sign Proposal
77 | Signs a proposal with the imported account
78 | Note: This requires the proposal file created before for that Safe transaction in the `cli_cache`.
79 |
80 | #### Usage
81 | ```bash
82 | yarn safe sign-proposal
83 | ```
84 |
85 | ### Submit Proposal
86 | Submits a proposal with the imported account
87 | Note: This requires the proposal file created before for that Safe transaction in the `cli_cache`.
88 |
89 | #### Usage
90 | ```bash
91 | yarn safe submit-proposal
92 | ```
93 |
94 | ### Show Transaction History
95 | Displays the transaction history of a Safe based on events
96 |
97 | #### Usage
98 | ```bash
99 | yarn safe history
100 | ```
101 |
102 | Security and Liability
103 | ----------------------
104 | All contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
105 |
106 | License
107 | -------
108 | All smart contracts are released under LGPL-3.0
109 |
--------------------------------------------------------------------------------
/examples/add_owner.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "to": "0xA6DdF1aa7043EE4e70537dAAaE8C249cc7E34Da4",
4 | "value": "0",
5 | "method": "addOwnerWithThreshold(address,uint256)",
6 | "params": [
7 | "0x4A4f5916Ed639d16545d84e753CB7DDCc5f50743",
8 | "2"
9 | ],
10 | "operation": 0
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/examples/double_transfer.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "to": "0xA6DdF1aa7043EE4e70537dAAaE8C249cc7E34Da4",
4 | "value": "0.001",
5 | "operation": 0
6 | },
7 | {
8 | "to": "0x4200000000000000000000000000000000000006",
9 | "value": "0",
10 | "operation": 0,
11 | "method": "transfer(address,uint256)",
12 | "params": [
13 | "0xA6DdF1aa7043EE4e70537dAAaE8C249cc7E34Da4",
14 | "10000000000000"
15 | ]
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/examples/eth_transfer.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "to": "0xBc089147F86Bcc1341D9F2870997920082A7b0BA",
4 | "value": "0.0000042",
5 | "operation": 0
6 | }
7 | ]
8 |
--------------------------------------------------------------------------------
/examples/wrap_and_unwrap_eth.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "to": "0x4200000000000000000000000000000000000006",
4 | "value": "0.000001",
5 | "operation": 0
6 | },
7 | {
8 | "to": "0x4200000000000000000000000000000000000006",
9 | "value": "0",
10 | "operation": 0,
11 | "method": "withdraw(uint256)",
12 | "params": [
13 | "1000000000000"
14 | ]
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import "@nomiclabs/hardhat-ethers";
2 | import "hardhat-deploy";
3 | import dotenv from "dotenv";
4 | import type { HardhatUserConfig, HttpNetworkUserConfig } from "hardhat/types";
5 | import yargs from "yargs";
6 |
7 | const argv = yargs
8 | .option("network", {
9 | type: "string",
10 | default: "hardhat",
11 | })
12 | .help(false)
13 | .version(false).argv;
14 |
15 | // Load environment variables.
16 | dotenv.config();
17 | const { NETWORK, NODE_URL, INFURA_KEY, MNEMONIC, PK, SOLIDITY_VERSION, SOLIDITY_SETTINGS } = process.env;
18 |
19 | const DEFAULT_MNEMONIC =
20 | "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat";
21 |
22 | const sharedNetworkConfig: HttpNetworkUserConfig = {};
23 | if (PK) {
24 | sharedNetworkConfig.accounts = [PK];
25 | } else {
26 | sharedNetworkConfig.accounts = {
27 | mnemonic: MNEMONIC || DEFAULT_MNEMONIC,
28 | };
29 | }
30 |
31 | if (["mainnet", "rinkeby", "kovan", "goerli"].includes(argv.network) && INFURA_KEY === undefined) {
32 | throw new Error(
33 | `Could not find Infura key in env, unable to connect to network ${argv.network}`,
34 | );
35 | }
36 |
37 | import "./src/tasks"
38 |
39 | const primarySolidityVersion = SOLIDITY_VERSION || "0.7.6"
40 | const soliditySettings = !!SOLIDITY_SETTINGS ? JSON.parse(SOLIDITY_SETTINGS) : undefined
41 |
42 | const userConfig: HardhatUserConfig = {
43 | paths: {
44 | artifacts: "build/artifacts",
45 | cache: "build/cache",
46 | sources: "contracts",
47 | },
48 | solidity: {
49 | compilers: [
50 | { version: primarySolidityVersion, settings: soliditySettings },
51 | { version: "0.6.12" },
52 | { version: "0.5.17" },
53 | ]
54 | },
55 | networks: {
56 | hardhat: {
57 | allowUnlimitedContractSize: true,
58 | blockGasLimit: 100000000,
59 | gas: 100000000
60 | },
61 | mainnet: {
62 | ...sharedNetworkConfig,
63 | url: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
64 | },
65 | xdai: {
66 | ...sharedNetworkConfig,
67 | url: "https://xdai.poanetwork.dev",
68 | },
69 | ewc: {
70 | ...sharedNetworkConfig,
71 | url: `https://rpc.energyweb.org`,
72 | },
73 | rinkeby: {
74 | ...sharedNetworkConfig,
75 | url: `https://rinkeby.infura.io/v3/${INFURA_KEY}`,
76 | },
77 | goerli: {
78 | ...sharedNetworkConfig,
79 | url: `https://goerli.infura.io/v3/${INFURA_KEY}`,
80 | },
81 | kovan: {
82 | ...sharedNetworkConfig,
83 | url: `https://kovan.infura.io/v3/${INFURA_KEY}`,
84 | },
85 | volta: {
86 | ...sharedNetworkConfig,
87 | url: `https://volta-rpc.energyweb.org`,
88 | },
89 | bsc: {
90 | ...sharedNetworkConfig,
91 | url: `https://bsc-dataseed.binance.org/`,
92 | },
93 | },
94 | namedAccounts: {
95 | deployer: 0,
96 | },
97 | mocha: {
98 | timeout: 2000000,
99 | },
100 | };
101 | if (NETWORK) {
102 | userConfig.defaultNetwork = NETWORK
103 | }
104 | if (NODE_URL) {
105 | userConfig.networks!!.custom = {
106 | ...sharedNetworkConfig,
107 | url: NODE_URL,
108 | }
109 | }
110 | export default userConfig
111 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@gnosis.pm/safe-tasks",
3 | "version": "1.3.0",
4 | "description": "Hardhat tasks for the Gnosis Safe",
5 | "homepage": "https://github.com/gnosis/safe-tasks/",
6 | "license": "GPL-3.0",
7 | "files": [
8 | "contracts",
9 | "test",
10 | "build",
11 | "networks.json"
12 | ],
13 | "scripts": {
14 | "lint": "eslint --max-warnings 0 .",
15 | "safe": "hardhat",
16 | "prepare": "husky install"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/gnosis/safe-tasks.git"
21 | },
22 | "keywords": [
23 | "Ethereum",
24 | "CLI",
25 | "Safe"
26 | ],
27 | "author": "stefan@gnosis.pm",
28 | "bugs": {
29 | "url": "https://github.com/gnosis/safe-tasks/issues"
30 | },
31 | "devDependencies": {
32 | "@typescript-eslint/eslint-plugin": "^4.7.0",
33 | "@typescript-eslint/parser": "^4.7.0",
34 | "eslint": "^7.13.0",
35 | "eslint-config-prettier": "^6.15.0",
36 | "eslint-plugin-import": "^2.22.1",
37 | "eslint-plugin-no-only-tests": "^2.4.0",
38 | "eslint-plugin-prettier": "^3.1.4",
39 | "husky": "^5.1.3",
40 | "rimraf": "^3.0.2",
41 | "ts-node": "^9.1.1",
42 | "typescript": "^4.1.3"
43 | },
44 | "dependencies": {
45 | "@gnosis.pm/safe-contracts": "1.3.0",
46 | "@gnosis.pm/safe-deployments": "1.5.0",
47 | "@nomiclabs/hardhat-ethers": "^2.0.0",
48 | "@types/mocha": "^8.2.0",
49 | "@types/yargs": "^15.0.10",
50 | "argv": "^0.0.2",
51 | "csv-parser": "^3.0.0",
52 | "dotenv": "^8.0.0",
53 | "ethers": "^5.1.0",
54 | "hardhat": "^2.1.2",
55 | "hardhat-deploy": "^0.7.4",
56 | "solc": "0.7.6",
57 | "yargs": "^16.1.1"
58 | },
59 | "resolutions": {
60 | "bitcore-lib": "8.1.1",
61 | "ethers": "^5.1.0"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/contracts.ts:
--------------------------------------------------------------------------------
1 | import { Contract } from "@ethersproject/contracts";
2 | import { getCompatibilityFallbackHandlerDeployment, getMultiSendDeployment, getProxyFactoryDeployment, getSafeSingletonDeployment, getSafeL2SingletonDeployment, SingletonDeployment, getMultiSendCallOnlyDeployment } from "@gnosis.pm/safe-deployments";
3 | import { HardhatRuntimeEnvironment as HRE } from "hardhat/types";
4 |
5 | export const contractFactory = (hre: HRE, contractName: string) => hre.ethers.getContractFactory(contractName);
6 |
7 | export const contractInstance = async (hre: HRE, deployment: SingletonDeployment | undefined, address?: string): Promise => {
8 | if (!deployment) throw Error("No deployment provided")
9 | // TODO: use network
10 | const contractAddress = address || deployment.defaultAddress
11 | return await hre.ethers.getContractAt(deployment.abi, contractAddress)
12 | }
13 |
14 | export const safeSingleton = async (hre: HRE, address?: string) =>
15 | contractInstance(hre, getSafeSingletonDeployment({ released: undefined }), address)
16 |
17 | export const safeL2Singleton = async (hre: HRE, address?: string) =>
18 | contractInstance(hre, getSafeL2SingletonDeployment({ released: undefined }), address)
19 |
20 | export const proxyFactory = async (hre: HRE, address?: string) =>
21 | contractInstance(hre, getProxyFactoryDeployment(), address)
22 |
23 | export const multiSendLib = async (hre: HRE, address?: string) =>
24 | contractInstance(hre, getMultiSendDeployment(), address)
25 |
26 | export const multiSendCallOnlyLib = async (hre: HRE, address?: string) =>
27 | contractInstance(hre, getMultiSendCallOnlyDeployment(), address)
28 |
29 | export const compatHandler = async (hre: HRE, address?: string) =>
30 | contractInstance(hre, getCompatibilityFallbackHandlerDeployment(), address)
31 |
--------------------------------------------------------------------------------
/src/creation.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config";
2 | import { AddressZero } from "@ethersproject/constants";
3 | import { getAddress } from "@ethersproject/address";
4 | import { buildMultiSendSafeTx, calculateProxyAddress, encodeMultiSend, MetaTransaction } from "@gnosis.pm/safe-contracts";
5 | import { safeSingleton, proxyFactory, safeL2Singleton, multiSendCallOnlyLib, compatHandler } from "./contracts";
6 | import { readCsv, writeJson, writeTxBuilderJson } from "./execution/utils";
7 |
8 | const parseSigners = (rawSigners: string): string[] => {
9 | return rawSigners.split(",").map(address => getAddress(address))
10 | }
11 |
12 | task("create", "Create a Safe")
13 | .addFlag("l2", "Should use version of the Safe contract that is more event heave")
14 | .addFlag("buildOnly", "Indicate whether this transaction should only be logged and not submitted on-chain")
15 | .addParam("signers", "Comma separated list of signer addresses (dafault is the address of linked account)", "", types.string, true)
16 | .addParam("threshold", "Threshold that should be used", 1, types.int, true)
17 | .addParam("fallback", "Fallback handler address", AddressZero, types.string, true)
18 | .addParam("nonce", "Nonce used with factory", new Date().getTime(), types.int, true)
19 | .addParam("singleton", "Set to overwrite which singleton address to use", "", types.string, true)
20 | .addParam("factory", "Set to overwrite which factory address to use", "", types.string, true)
21 | .setAction(async (taskArgs, hre) => {
22 | const singleton = taskArgs.l2 ? await safeL2Singleton(hre, taskArgs.singleton) : await safeSingleton(hre, taskArgs.singleton)
23 | const factory = await proxyFactory(hre, taskArgs.factory)
24 | const signers: string[] = taskArgs.signers ? parseSigners(taskArgs.signers) : [(await hre.getNamedAccounts()).deployer]
25 | const fallbackHandler = getAddress(taskArgs.fallback)
26 | const setupData = singleton.interface.encodeFunctionData(
27 | "setup",
28 | [signers, taskArgs.threshold, AddressZero, "0x", fallbackHandler, AddressZero, 0, AddressZero]
29 | )
30 | const predictedAddress = await calculateProxyAddress(factory, singleton.address, setupData, taskArgs.nonce)
31 | console.log(`Deploy Safe to ${predictedAddress}`)
32 | console.log(`Singleton: ${singleton.address}`)
33 | console.log(`Setup data: ${setupData}`)
34 | console.log(`Nonce: ${taskArgs.nonce}`)
35 | console.log(`To (factory): ${factory.address}`)
36 | console.log(`Data: ${factory.interface.encodeFunctionData("createProxyWithNonce", [singleton.address, setupData, taskArgs.nonce])}`)
37 | if (!taskArgs.buildOnly)
38 | await factory.createProxyWithNonce(singleton.address, setupData, taskArgs.nonce).then((tx: any) => tx.wait())
39 | // TODO verify deployment
40 | });
41 |
42 | task("create-bulk", "Create multiple Safes from CSV")
43 | .addFlag("buildOnly", "Indicate whether this transaction should only be logged and not submitted on-chain")
44 | .addPositionalParam("csv", "CSV file with the information of the Safes that should be created", undefined, types.inputFile)
45 | .addParam("fallback", "Fallback handler address", undefined, types.string, true)
46 | .addParam("nonce", "Nonce used with factory", "0", types.string, true)
47 | .addParam("singleton", "Set to overwrite which singleton address to use", "", types.string, true)
48 | .addParam("factory", "Set to overwrite which factory address to use", "", types.string, true)
49 | .addFlag("l2", "Should use version of the Safe contract that is more event heave")
50 | .addParam("export", "If specified instead of executing the data will be exported as a json file for the transaction builder", undefined, types.string, true)
51 | .setAction(async (taskArgs, hre) => {
52 | const singleton = taskArgs.l2 ? await safeL2Singleton(hre, taskArgs.singleton) : await safeSingleton(hre, taskArgs.singleton)
53 | const factory = await proxyFactory(hre, taskArgs.factory)
54 | const fallbackHandler = await compatHandler(hre, taskArgs.fallback)
55 |
56 | const inputs: { threshold: string, signers: string, address: string }[] = await readCsv(taskArgs.csv)
57 | const encodedSetups: { data: string, signers: string[], threshold: string, expectedAddress?: string }[] = inputs.filter(entry => entry.signers.trim().length !== 0).map(entry => {
58 | const parsedThreshold = entry.threshold.split("/")[0]
59 | const expectedSignerCount = entry.threshold.split("/")[1]
60 | const parsedSigners = entry.signers.replace(/\n/g, ",").split(",")
61 | console.log({ parsedThreshold, expectedSignerCount, parsedSigners })
62 | if (expectedSignerCount && parseInt(expectedSignerCount) !== parsedSigners.length) throw Error(`Expected ${expectedSignerCount} Signers, got ${parsedSigners}`)
63 | return {
64 | data: singleton.interface.encodeFunctionData(
65 | "setup",
66 | [parsedSigners, parsedThreshold, AddressZero, "0x", fallbackHandler.address, AddressZero, 0, AddressZero]
67 | ),
68 | signers: parsedSigners,
69 | threshold: parsedThreshold,
70 | expectedAddress: entry.address
71 | }
72 | })
73 | const deploymentTxs: MetaTransaction[] = encodedSetups.map(setup => {
74 | const data = factory.interface.encodeFunctionData("createProxyWithNonce", [singleton.address, setup.data, taskArgs.nonce])
75 | return { to: factory.address, data, operation: 0, value: "0" }
76 | })
77 | const multiSend = await multiSendCallOnlyLib(hre)
78 | console.log(`Singleton: ${singleton.address}`)
79 | console.log(`Nonce: ${taskArgs.nonce}`)
80 | console.log(`Factory: ${factory.address}`)
81 | console.log(`Data: ${multiSend.interface.encodeFunctionData("multiSend", [encodeMultiSend(deploymentTxs)])}`)
82 | console.log(`Predicted Safes:`)
83 | for (const setupData of encodedSetups) {
84 | console.log(` Signers:`, setupData.signers.join(","), `Treshold:`, setupData.threshold, "of", setupData.signers.length)
85 | const calculatedAddress = await calculateProxyAddress(factory, singleton.address, setupData.data, taskArgs.nonce)
86 | console.log(` Address:`, calculatedAddress)
87 | if (setupData.expectedAddress && setupData.expectedAddress !== calculatedAddress)
88 | console.log(` Unexpected Safe address! Expected ${setupData.expectedAddress}.`)
89 | const onChainCode = await hre.ethers.provider.getCode(calculatedAddress)
90 | if (onChainCode !== "0x")
91 | console.log(` Safe already exists on this address.`)
92 | console.log()
93 | }
94 |
95 | if (taskArgs.export) {
96 | const chainId = (await hre.ethers.provider.getNetwork()).chainId.toString()
97 | await writeTxBuilderJson(taskArgs.export, chainId, deploymentTxs, "Batched Safe Creations")
98 | } else if (!taskArgs.buildOnly) {
99 | await multiSend.multiSend(encodeMultiSend(deploymentTxs)).then((tx: any) => tx.wait())
100 | }
101 | // TODO verify deployment
102 | });
103 |
104 | export { }
105 |
--------------------------------------------------------------------------------
/src/execution/index.ts:
--------------------------------------------------------------------------------
1 | import "./signing"
2 | import "./simple"
3 | import "./submitting"
4 | import "./proposing"
--------------------------------------------------------------------------------
/src/execution/proposing.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config";
2 | import { multiSendLib, safeSingleton } from "../contracts";
3 | import { buildMultiSendSafeTx, buildSafeTransaction, calculateSafeTransactionHash, SafeTransaction, MetaTransaction } from "@gnosis.pm/safe-contracts";
4 | import { parseEther } from "@ethersproject/units";
5 | import { getAddress, isHexString } from "ethers/lib/utils";
6 | import { proposalFile, readFromCliCache, writeToCliCache, writeTxBuilderJson } from "./utils";
7 | import { BigNumber } from "@ethersproject/bignumber";
8 | import { Contract, ethers } from "ethers";
9 | import fs from 'fs/promises'
10 | import { HardhatRuntimeEnvironment } from "hardhat/types";
11 |
12 | export interface SafeTxProposal {
13 | safe: string,
14 | chainId: number,
15 | safeTxHash: string,
16 | tx: SafeTransaction
17 | }
18 |
19 | const calcSafeTxHash = async (safe: Contract, tx: SafeTransaction, chainId: number, onChainOnly: boolean): Promise => {
20 | const onChainHash = await safe.getTransactionHash(
21 | tx.to, tx.value, tx.data, tx.operation, tx.safeTxGas, tx.baseGas, tx.gasPrice, tx.gasToken, tx.refundReceiver, tx.nonce
22 | )
23 | if (onChainOnly) return onChainHash
24 | const offChainHash = calculateSafeTransactionHash(safe, tx, chainId)
25 | if (onChainHash != offChainHash) throw Error("Unexpected hash! (For pre-1.3.0 version use --on-chain-hash)")
26 | return offChainHash
27 | }
28 |
29 | task("propose", "Create a Safe tx proposal json file")
30 | .addPositionalParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
31 | .addParam("to", "Address of the target", undefined, types.string)
32 | .addParam("value", "Value in ETH", "0", types.string, true)
33 | .addParam("data", "Data as hex string", "0x", types.string, true)
34 | .addFlag("delegatecall", "Indicator if tx should be executed as a delegatecall")
35 | .addFlag("onChainHash", "Get hash from chain (required for pre-1.3.0 version)")
36 | .setAction(async (taskArgs, hre) => {
37 | console.log(`Running on ${hre.network.name}`)
38 | const safe = await safeSingleton(hre, taskArgs.address)
39 | const safeAddress = await safe.resolvedAddress
40 | console.log(`Using Safe at ${safeAddress}`)
41 | const nonce = await safe.nonce()
42 | if (!isHexString(taskArgs.data)) throw Error(`Invalid hex string provided for data: ${taskArgs.data}`)
43 | const tx = buildSafeTransaction({ to: taskArgs.to, value: parseEther(taskArgs.value).toString(), data: taskArgs.data, nonce: nonce.toString(), operation: taskArgs.delegatecall ? 1 : 0 })
44 | const chainId = (await safe.provider.getNetwork()).chainId
45 | const safeTxHash = await calcSafeTxHash(safe, tx, chainId, taskArgs.onChainHash)
46 | const proposal: SafeTxProposal = {
47 | safe: safeAddress,
48 | chainId,
49 | safeTxHash,
50 | tx
51 | }
52 | await writeToCliCache(proposalFile(safeTxHash), proposal)
53 | console.log(`Safe transaction hash: ${safeTxHash}`)
54 | });
55 |
56 | interface TxDescription {
57 | to: string,
58 | value: string // in ETH
59 | data?: string
60 | method?: string
61 | params?: any[]
62 | operation: 0 | 1
63 | }
64 |
65 | const buildData = (method: string, params?: any[]): string => {
66 | const iface = new ethers.utils.Interface([`function ${method}`])
67 | return iface.encodeFunctionData(method, params)
68 | }
69 |
70 | const buildMetaTx = (description: TxDescription): MetaTransaction => {
71 | const to = getAddress(description.to)
72 | const value = parseEther(description.value).toString()
73 | const operation = description.operation
74 | const data = isHexString(description.data) ? description.data!! : (description.method ? buildData(description.method, description.params) : "0x")
75 | return { to, value, data, operation }
76 | }
77 |
78 | const loadMetaTransactions = async (file: string) => {
79 | const txsData: TxDescription[] = JSON.parse(await fs.readFile(file, 'utf8'))
80 | if (txsData.length == 0) {
81 | throw Error("No transacitons provided")
82 | }
83 | return txsData.map(desc => buildMetaTx(desc))
84 | }
85 |
86 | const parseMultiSendJsonFile = async (hre: HardhatRuntimeEnvironment, txs: MetaTransaction[], nonce: number, multiSendAddress?: string): Promise => {
87 | if (txs.length == 1) {
88 | return buildSafeTransaction({ ...txs[0], nonce: nonce })
89 | }
90 | const multiSend = await multiSendLib(hre, multiSendAddress)
91 | return buildMultiSendSafeTx(multiSend, txs, nonce)
92 | }
93 |
94 | task("propose-multi", "Create a Safe tx proposal json file")
95 | .addPositionalParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
96 | .addPositionalParam("txs", "Json file with transactions", undefined, types.inputFile)
97 | .addParam("multiSend", "Set to overwrite which multiSend address to use", "", types.string, true)
98 | .addParam("nonce", "Set nonce to use (will default to on-chain nonce)", "", types.string, true)
99 | .addParam("export", "If specified instead of executing the data will be exported as a json file for the transaction builder", undefined, types.string)
100 | .addParam("name", "Name to be used for the transaction builder json", undefined, types.string, true)
101 | .addFlag("onChainHash", "Get hash from chain (required for pre-1.3.0 version)")
102 | .setAction(async (taskArgs, hre) => {
103 | console.log(`Running on ${hre.network.name}`)
104 | const safe = await safeSingleton(hre, taskArgs.address)
105 | const safeAddress = await safe.resolvedAddress
106 | console.log(`Using Safe at ${safeAddress}`)
107 | const nonce = taskArgs.nonce || await safe.nonce()
108 | const txs = await loadMetaTransactions(taskArgs.txs)
109 | const chainId = (await safe.provider.getNetwork()).chainId
110 | if (taskArgs.export) {
111 | await writeTxBuilderJson(taskArgs.export, chainId.toString(), txs, taskArgs.name || "Custom Transactions")
112 | return
113 | }
114 | const tx = await parseMultiSendJsonFile(hre, txs, BigNumber.from(nonce).toNumber(), taskArgs.multiSend)
115 | console.log("Safe transaction", tx)
116 | const safeTxHash = await calcSafeTxHash(safe, tx, chainId, taskArgs.onChainHash)
117 | const proposal: SafeTxProposal = {
118 | safe: safeAddress,
119 | chainId,
120 | safeTxHash,
121 | tx
122 | }
123 | await writeToCliCache(proposalFile(safeTxHash), proposal)
124 | console.log("Safe transaction hash:", safeTxHash)
125 | return safeTxHash
126 | });
127 |
128 | task("show-proposal", "Shows details for a Safe transaction")
129 | .addPositionalParam("hash", "Hash of Safe transaction to display", undefined, types.string)
130 | .setAction(async (taskArgs, hre) => {
131 | const proposal: SafeTxProposal = await readFromCliCache(proposalFile(taskArgs.hash))
132 | const safe = await safeSingleton(hre, taskArgs.address)
133 | const safeAddress = await safe.resolvedAddress
134 | console.log(`Using Safe at ${safeAddress}@${proposal.chainId}`)
135 | const nonce = await safe.nonce()
136 | if (BigNumber.from(proposal.tx.nonce).lt(nonce)) {
137 | console.log(`!Nonce has already been used!`)
138 | }
139 | console.log("Details")
140 | console.log(proposal.tx)
141 | });
142 |
--------------------------------------------------------------------------------
/src/execution/signing.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config";
2 | import { safeSingleton } from "../contracts";
3 | import { buildSafeTransaction, SafeSignature, safeSignMessage, signHash } from "@gnosis.pm/safe-contracts";
4 | import { parseEther } from "@ethersproject/units";
5 | import { isHexString } from "ethers/lib/utils";
6 | import { SafeTxProposal } from "./proposing";
7 | import { proposalFile, signaturesFile, readFromCliCache, writeToCliCache, loadSignatures } from "./utils";
8 |
9 | task("sign-tx", "Signs a Safe transaction")
10 | .addPositionalParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
11 | .addParam("to", "Address of the target", undefined, types.string)
12 | .addParam("value", "Value in ETH", "0", types.string, true)
13 | .addParam("data", "Data as hex string", "0x", types.string, true)
14 | .addParam("signerIndex", "Index of the signer to use", 0, types.int, true)
15 | .addFlag("delegatecall", "Indicator if tx should be executed as a delegatecall")
16 | .setAction(async (taskArgs, hre) => {
17 | console.log(`Running on ${hre.network.name}`)
18 | const signers = await hre.ethers.getSigners()
19 | const signer = signers[taskArgs.signerIndex]
20 | const safe = await safeSingleton(hre, taskArgs.address)
21 | const safeAddress = await safe.resolvedAddress
22 | console.log(`Using Safe at ${safeAddress} with ${signer.address}`)
23 | const nonce = await safe.nonce()
24 | if (!isHexString(taskArgs.data)) throw Error(`Invalid hex string provided for data: ${taskArgs.data}`)
25 | const tx = buildSafeTransaction({ to: taskArgs.to, value: parseEther(taskArgs.value), data: taskArgs.data, nonce, operation: taskArgs.delegatecall ? 1 : 0 })
26 | const signature = await safeSignMessage(signer, safe, tx)
27 | console.log(`Signature: ${signature.data}`)
28 | });
29 |
30 | const updateSignatureFile = async(safeTxHash: string, signature: SafeSignature) => {
31 | const signatures: Record = await loadSignatures(safeTxHash)
32 | signatures[signature.signer] = signature.data
33 | await writeToCliCache(signaturesFile(safeTxHash), signatures)
34 | }
35 |
36 | task("sign-proposal", "Signs a Safe transaction")
37 | .addPositionalParam("hash", "Hash of Safe transaction to display", undefined, types.string)
38 | .addParam("signerIndex", "Index of the signer to use", 0, types.int, true)
39 | .setAction(async (taskArgs, hre) => {
40 | const proposal: SafeTxProposal = await readFromCliCache(proposalFile(taskArgs.hash))
41 | const signers = await hre.ethers.getSigners()
42 | const signer = signers[taskArgs.signerIndex]
43 | const safe = await safeSingleton(hre, proposal.safe)
44 | const safeAddress = await safe.resolvedAddress
45 | console.log(`Using Safe at ${safeAddress} with ${signer.address}`)
46 | const owners: string[] = await safe.getOwners()
47 | if (owners.indexOf(signer.address) < 0) {
48 | throw Error(`Signer is not an owner of the Safe. Owners: ${owners}`)
49 | }
50 | const signature = await signHash(signer, taskArgs.hash)
51 | await updateSignatureFile(taskArgs.hash, signature)
52 | console.log(`Signature: ${signature.data}`)
53 | });
--------------------------------------------------------------------------------
/src/execution/simple.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config";
2 |
3 | task("submit-multi", "Executes a Safe transaction from a file")
4 | .addPositionalParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
5 | .addPositionalParam("txs", "Json file with transactions", undefined, types.inputFile)
6 | .addFlag("onChainHash", "Get hash from chain (required for pre-1.3.0 version)")
7 | .addParam("multiSend", "Set to overwrite which multiSend address to use", "", types.string, true)
8 | .addParam("signerIndex", "Index of the signer to use", 0, types.int, true)
9 | .addParam("signatures", "Comma seperated list of signatures", undefined, types.string, true)
10 | .addParam("gasPrice", "Gas price to be used", undefined, types.int, true)
11 | .addParam("gasLimit", "Gas limit to be used", undefined, types.int, true)
12 | .addFlag("buildOnly", "Flag to only output the final transaction")
13 | .setAction(async (taskArgs, hre) => {
14 | const safeTxHash = await hre.run("propose-multi", {
15 | address: taskArgs.address,
16 | txs: taskArgs.txs,
17 | onChainHash: taskArgs.onChainHash,
18 | multiSend: taskArgs.multiSend
19 | })
20 | await hre.run("submit-proposal", {
21 | hash: safeTxHash,
22 | onChainHash: taskArgs.onChainHash,
23 | signerIndex: taskArgs.signerIndex,
24 | signatures: taskArgs.signatures,
25 | gasLimit: taskArgs.gasLimit,
26 | buildOnly: taskArgs.buildOnly,
27 | })
28 | });
29 |
--------------------------------------------------------------------------------
/src/execution/submitting.ts:
--------------------------------------------------------------------------------
1 | import { BigNumber, Contract, PopulatedTransaction, Signer, utils } from "ethers";
2 | import { task, types } from "hardhat/config";
3 | import { safeSingleton } from "../contracts";
4 | import { buildSafeTransaction, calculateSafeTransactionHash, populateExecuteTx, safeApproveHash, SafeSignature, SafeTransaction } from "@gnosis.pm/safe-contracts";
5 | import { parseEther } from "@ethersproject/units";
6 | import { getAddress } from "@ethersproject/address";
7 | import { isHexString } from "ethers/lib/utils";
8 | import { SafeTxProposal } from "./proposing";
9 | import { loadSignatures, proposalFile, readFromCliCache } from "./utils";
10 |
11 | const parsePreApprovedConfirmation = (data: string): SafeSignature => {
12 | const signer = getAddress("0x" + data.slice(26, 66))
13 | return {
14 | signer, data
15 | }
16 | }
17 |
18 | const parseTypeDataConfirmation = (safeTxHash: string, data: string): SafeSignature => {
19 | const signer = utils.recoverAddress(safeTxHash, data)
20 | return {
21 | signer, data
22 | }
23 | }
24 |
25 | const parseEthSignConfirmation = (safeTxHash: string, data: string): SafeSignature => {
26 | const signer = utils.recoverAddress(utils.hashMessage(utils.arrayify(safeTxHash)), data.replace(/1f$/, "1b").replace(/20$/, "1c"))
27 | return {
28 | signer, data
29 | }
30 | }
31 |
32 | const parseSignature = (safeTxHash: string, signature: string): SafeSignature => {
33 | if (!isHexString(signature, 65)) throw Error(`Unsupported signature: ${signature}`)
34 | const type = parseInt(signature.slice(signature.length - 2), 16)
35 | switch (type) {
36 | case 1: return parsePreApprovedConfirmation(signature)
37 | case 27:
38 | case 28:
39 | return parseTypeDataConfirmation(safeTxHash, signature)
40 | case 31:
41 | case 32:
42 | return parseEthSignConfirmation(safeTxHash, signature)
43 | case 0:
44 | default:
45 | throw Error(`Unsupported type ${type} in ${signature}`)
46 | }
47 | }
48 |
49 | const isOwnerSignature = (owners: string[], signature: SafeSignature): SafeSignature => {
50 | if (owners.indexOf(signature.signer) < 0) throw Error(`Signer ${signature.signer} not found in owners ${owners}`)
51 | return signature
52 | }
53 |
54 | const prepareSignatures = async (safe: Contract, tx: SafeTransaction, signaturesCSV: string | undefined, submitter?: Signer, knownSafeTxHash?: string): Promise => {
55 | const owners = await safe.getOwners()
56 | const signatures = new Map()
57 | const submitterAddress = submitter && await submitter.getAddress()
58 | if (signaturesCSV) {
59 | const chainId = (await safe.provider.getNetwork()).chainId
60 | const safeTxHash = knownSafeTxHash ?? calculateSafeTransactionHash(safe, tx, chainId)
61 | for (const signatureString of signaturesCSV.split(",")) {
62 | const signature = isOwnerSignature(owners, parseSignature(safeTxHash, signatureString))
63 | if (submitterAddress === signature.signer || signatures.has(signature.signer)) continue
64 | signatures.set(signature.signer, signature)
65 | }
66 | }
67 | const threshold = (await safe.getThreshold()).toNumber()
68 | const submitterIsOwner = submitterAddress && owners.indexOf(submitterAddress) >= 0
69 | const requiredSigntures = submitterIsOwner ? threshold - 1 : threshold
70 | if (requiredSigntures > signatures.size) throw Error(`Not enough signatures (${signatures.size} of ${threshold})`)
71 | const signatureArray = []
72 | if (submitterIsOwner) {
73 | signatureArray.push(await safeApproveHash(submitter!!, safe, tx, true))
74 | }
75 | return signatureArray.concat(Array.from(signatures.values()).slice(0, requiredSigntures))
76 | }
77 |
78 | task("submit-tx", "Executes a Safe transaction")
79 | .addPositionalParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
80 | .addParam("to", "Address of the target", undefined, types.string)
81 | .addParam("value", "Value in ETH", "0", types.string, true)
82 | .addParam("data", "Data as hex string", "0x", types.string, true)
83 | .addParam("signatures", "Comma seperated list of signatures", undefined, types.string, true)
84 | .addParam("gasPrice", "Gas price to be used", undefined, types.int, true)
85 | .addParam("gasLimit", "Gas limit to be used", undefined, types.int, true)
86 | .addFlag("delegatecall", "Indicator if tx should be executed as a delegatecall")
87 | .setAction(async (taskArgs, hre) => {
88 | console.log(`Running on ${hre.network.name}`)
89 | const [signer] = await hre.ethers.getSigners()
90 | const safe = await safeSingleton(hre, taskArgs.address)
91 | const safeAddress = await safe.resolvedAddress
92 | console.log(`Using Safe at ${safeAddress} with ${signer.address}`)
93 | const nonce = await safe.nonce()
94 | if (!isHexString(taskArgs.data)) throw Error(`Invalid hex string provided for data: ${taskArgs.data}`)
95 | const tx = buildSafeTransaction({
96 | to: taskArgs.to,
97 | value: parseEther(taskArgs.value),
98 | data: taskArgs.data,
99 | nonce,
100 | operation: taskArgs.delegatecall ? 1 : 0
101 | })
102 | const signatures = await prepareSignatures(safe, tx, taskArgs.signatures, signer)
103 | const populatedTx: PopulatedTransaction = await populateExecuteTx(safe, tx, signatures, { gasLimit: taskArgs.gasLimit, gasPrice: taskArgs.gasPrice })
104 | const receipt = await signer.sendTransaction(populatedTx).then(tx => tx.wait())
105 | console.log(receipt.transactionHash)
106 | });
107 |
108 |
109 | task("submit-proposal", "Executes a Safe transaction")
110 | .addPositionalParam("hash", "Hash of Safe transaction to display", undefined, types.string)
111 | .addParam("signerIndex", "Index of the signer to use", 0, types.int, true)
112 | .addParam("signatures", "Comma seperated list of signatures", undefined, types.string, true)
113 | .addParam("gasPrice", "Gas price to be used", undefined, types.int, true)
114 | .addParam("gasLimit", "Gas limit to be used", undefined, types.int, true)
115 | .addFlag("buildOnly", "Flag to only output the final transaction")
116 | .setAction(async (taskArgs, hre) => {
117 | console.log(`Running on ${hre.network.name}`)
118 | const proposal: SafeTxProposal = await readFromCliCache(proposalFile(taskArgs.hash))
119 | const signers = await hre.ethers.getSigners()
120 | const signer = signers[taskArgs.signerIndex]
121 | const safe = await safeSingleton(hre, proposal.safe)
122 | const safeAddress = await safe.resolvedAddress
123 | console.log(`Using Safe at ${safeAddress} with ${signer.address}`)
124 | const currentNonce = await safe.nonce()
125 | if (!BigNumber.from(proposal.tx.nonce).eq(currentNonce)) {
126 | throw Error("Proposal does not have correct nonce!")
127 | }
128 | const signatureStrings: Record = await loadSignatures(taskArgs.hash)
129 | const signatureArray = Object.values(signatureStrings)
130 | if (taskArgs.signatures) {
131 | signatureArray.push(taskArgs.signatures)
132 | }
133 | const signatures = await prepareSignatures(safe, proposal.tx, signatureArray.join(","), signer, taskArgs.hash)
134 | const populatedTx: PopulatedTransaction = await populateExecuteTx(safe, proposal.tx, signatures, { gasLimit: taskArgs.gasLimit, gasPrice: taskArgs.gasPrice })
135 |
136 | if (taskArgs.buildOnly) {
137 | console.log("Ethereum transaction:", populatedTx)
138 | return
139 | }
140 |
141 | const receipt = await signer.sendTransaction(populatedTx).then(tx => tx.wait())
142 | console.log("Ethereum transaction hash:", receipt.transactionHash)
143 | return receipt.transactionHash
144 | });
--------------------------------------------------------------------------------
/src/execution/utils.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import fs from 'fs/promises'
3 | import fsSync from 'fs'
4 | import csvParser from "csv-parser"
5 | import { MetaTransaction } from '@gnosis.pm/safe-contracts'
6 |
7 | const cliCacheDir = "cli_cache"
8 |
9 | export const proposalFile = (safeTxHash: string) => `${safeTxHash}.proposal.json`
10 | export const signaturesFile = (safeTxHash: string) => `${safeTxHash}.signatures.json`
11 |
12 | export const writeToCliCache = async(key: string, content: any) => {
13 | const folder = path.join(process.cwd(), cliCacheDir)
14 | try {
15 | await fs.access(folder)
16 | } catch (e) {
17 | await fs.mkdir(folder);
18 | }
19 | await fs.writeFile(path.join(folder, key), JSON.stringify(content, null, 2))
20 | }
21 |
22 | export const writeJson = async(file: string, content: any) => {
23 | await fs.writeFile(file, JSON.stringify(content, null, 2))
24 | }
25 |
26 | export const writeTxBuilderJson = async(file: string, chainId: string, transactions: MetaTransaction[], name?: string, description?: string) => {
27 | return writeJson(file, {
28 | version: "1.0",
29 | chainId,
30 | createdAt: new Date().getTime(),
31 | meta: {
32 | name,
33 | description
34 | },
35 | transactions
36 | })
37 | }
38 |
39 | export const readFromCliCache = async(key: string): Promise => {
40 | const content = await fs.readFile(path.join(process.cwd(), cliCacheDir, key), 'utf8')
41 | return JSON.parse(content)
42 | }
43 |
44 | export const loadSignatures = async(safeTxHash: string): Promise> => {
45 | try {
46 | return await readFromCliCache(signaturesFile(safeTxHash))
47 | } catch {
48 | return {}
49 | }
50 | }
51 |
52 | export const readCsv = async(file: string): Promise => new Promise((resolve, reject) => {
53 | const results: T[] = [];
54 | fsSync.createReadStream(file).pipe(csvParser())
55 | .on("data", (data) => results.push(data))
56 | .on("error", (err) => { reject(err) })
57 | .on("end", () => { resolve(results)})
58 | })
--------------------------------------------------------------------------------
/src/history/index.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config";
2 | import { safeSingleton } from "../contracts";
3 | import { loadHistoryTxs } from "./loading";
4 |
5 | task("history", `WIP: Displays the transaction history of a Safe based on events (ordered newest first).
6 | Only outgoing transactions made with a Safe >=1.1.0 will be displayed.`)
7 | .addPositionalParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
8 | .addParam("start", "Start index of the tx to load", 0, types.int, true)
9 | .setAction(async (taskArgs, hre) => {
10 | const safe = await safeSingleton(hre, taskArgs.address)
11 | const safeAddress = await safe.resolvedAddress
12 | console.log(`Checking Safe at ${safeAddress}`)
13 | console.log(await loadHistoryTxs(hre.ethers.provider, safeAddress, taskArgs.start))
14 | });
--------------------------------------------------------------------------------
/src/history/loading.ts:
--------------------------------------------------------------------------------
1 | import { calculateSafeTransactionHash, EIP712_SAFE_TX_TYPE } from '@gnosis.pm/safe-contracts'
2 | import { getSafeL2SingletonDeployment } from '@gnosis.pm/safe-deployments'
3 | import { BigNumber, ethers } from 'ethers'
4 | import { EtherDetails, EventTx, ModuleTx, MultisigTx, MultisigUnknownTx, TransferDetails, TransferTx } from './types'
5 |
6 | const erc20InterfaceDefinition = [
7 | "event Transfer(address indexed from, address indexed to, uint256 amount)"
8 | ]
9 | const erc20OldInterfaceDefinition = [
10 | "event Transfer(address indexed from, address to, uint256 amount)"
11 | ]
12 | const erc721InterfaceDefinition = [
13 | "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)"
14 | ]
15 | const erc20Interface = new ethers.utils.Interface(erc20InterfaceDefinition)
16 | const erc20OldInterface = new ethers.utils.Interface(erc20OldInterfaceDefinition)
17 | const erc721Interface = new ethers.utils.Interface(erc721InterfaceDefinition)
18 | // The same for all interfaces as `indexed` has no impact on the topic
19 | const transferTopic = erc20Interface.getEventTopic("Transfer")
20 |
21 | const safeAbi = getSafeL2SingletonDeployment({ released: undefined })!!.abi
22 | const safeInterface = new ethers.utils.Interface(safeAbi)
23 | const successTopic = safeInterface.getEventTopic("ExecutionSuccess")
24 | const failureTopic = safeInterface.getEventTopic("ExecutionFailure")
25 | const multisigDetailsTopic = safeInterface.getEventTopic("SafeMultiSigTransaction")
26 | const moduleSuccessTopic = safeInterface.getEventTopic("ExecutionFromModuleSuccess")
27 | const moduleFailureTopic = safeInterface.getEventTopic("ExecutionFromModuleFailure")
28 | const moduleDetailsTopic = safeInterface.getEventTopic("SafeModuleTransaction")
29 | const etherReceivedTopic = safeInterface.getEventTopic("SafeReceived")
30 | // Failure topics cannot generate sub events, we should remove them in the future
31 | const parentTopics = [successTopic, moduleSuccessTopic, failureTopic, moduleFailureTopic]
32 | const detailsTopics = [multisigDetailsTopic, moduleDetailsTopic]
33 |
34 | const loadBlock = async (provider: ethers.providers.Provider, blockHash: string): Promise => {
35 | return await provider.getBlock(blockHash)
36 | }
37 |
38 | const loadEthTx = async (provider: ethers.providers.Provider, txHash: string): Promise => {
39 | return await provider.getTransaction(txHash)
40 | }
41 |
42 | export interface DecodedMultisigTx {
43 | to: string
44 | value: string
45 | data: string
46 | operation: number
47 | safeTxGas: string
48 | baseGas: string
49 | gasPrice: string
50 | gasToken: string
51 | refundReceiver: string
52 | signatures: string
53 | nonce?: number
54 | }
55 |
56 | const decodeTx = (account: string, tx: ethers.providers.TransactionResponse): DecodedMultisigTx | undefined => {
57 | try {
58 | const result = safeInterface.decodeFunctionData("execTransaction", tx.data)
59 | if (tx.to !== account) return undefined
60 | return {
61 | to: result.to,
62 | value: result.value.toString(),
63 | data: result.data,
64 | operation: result.operation,
65 | safeTxGas: result.safeTxGas.toString(),
66 | baseGas: result.baseGas.toString(),
67 | gasPrice: result.gasPrice.toString(),
68 | gasToken: result.gasToken,
69 | refundReceiver: result.refundReceiver,
70 | signatures: result.signatures,
71 | }
72 | } catch (e) {
73 | // TODO: try to decode other ways
74 | console.log("Unknown function", tx.data.slice(0, 10))
75 | return undefined
76 | }
77 | }
78 |
79 | const decodeMultisigDetails = (log: ethers.providers.Log | undefined): DecodedMultisigTx | undefined => {
80 | if (!log) return undefined
81 | const event = safeInterface.decodeEventLog("SafeMultiSigTransaction", log.data, log.topics)
82 | return {
83 | to: event.to,
84 | value: event.value.toString(),
85 | data: event.data,
86 | operation: event.operation,
87 | safeTxGas: event.safeTxGas.toString(),
88 | baseGas: event.baseGas.toString(),
89 | gasPrice: event.gasPrice.toString(),
90 | gasToken: event.gasToken,
91 | refundReceiver: event.refundReceiver,
92 | signatures: event.signatures,
93 | nonce: BigNumber.from(event.additionalInfo.slice(0, 66)).toNumber()
94 | }
95 | }
96 |
97 | const mutlisigTxEntry = async (provider: ethers.providers.Provider, account: string, nonceMapper: NonceMapper, log: ethers.providers.Log, safeTxHash: string, success: boolean, subLogs: ethers.providers.Log[], details: ethers.providers.Log | undefined): Promise => {
98 | let decodedTx: DecodedMultisigTx | undefined
99 | decodedTx = decodeMultisigDetails(details)
100 | if (!decodedTx) {
101 | console.log("Fallback to transaction decoding")
102 | const ethTx = await loadEthTx(provider, log.transactionHash)
103 | decodedTx = decodeTx(account, ethTx)
104 | }
105 | const block = await loadBlock(provider, log.blockHash)
106 | if (!decodedTx) return {
107 | type: "MultisigUnknown",
108 | id: "multisig_" + safeTxHash,
109 | timestamp: block.timestamp,
110 | logs: subLogs,
111 | txHash: log.transactionHash,
112 | safeTxHash,
113 | success
114 | }
115 | return {
116 | type: "Multisig",
117 | id: "multisig_" + safeTxHash,
118 | timestamp: block.timestamp,
119 | logs: subLogs,
120 | txHash: log.transactionHash,
121 | safeTxHash,
122 | success,
123 | ...decodedTx,
124 | nonce: decodedTx.nonce || await nonceMapper.map(safeTxHash, decodedTx)
125 | }
126 | }
127 |
128 | const decodeModuleDetails = (log: ethers.providers.Log | undefined): any => {
129 | if (!log) return undefined
130 | const event = safeInterface.decodeEventLog("SafeModuleTransaction", log.data, log.topics)
131 | return event
132 | }
133 |
134 | const moduleTxEntry = async (provider: ethers.providers.Provider, log: ethers.providers.Log, moduleAddress: string, success: boolean, subLogs: ethers.providers.Log[], details: ethers.providers.Log | undefined): Promise => {
135 | console.log("Module details:", decodeModuleDetails(details))
136 | const block = await loadBlock(provider, log.blockHash)
137 | return {
138 | type: "Module",
139 | txHash: log.transactionHash,
140 | id: "module_" + log.blockNumber + " " + log.transactionIndex + " " + log.logIndex,
141 | timestamp: block.timestamp,
142 | module: moduleAddress,
143 | success,
144 | logs: subLogs
145 | }
146 | }
147 |
148 | const transferEntry = async (provider: ethers.providers.Provider, account: string, log: ethers.providers.Log): Promise => {
149 | const block = await loadBlock(provider, log.blockHash)
150 | let type: string = ""
151 | let eventInterface
152 | if (log.topics.length === 4) {
153 | eventInterface = erc721Interface
154 | type = "ERC721"
155 | } else if (log.topics.length === 3) {
156 | eventInterface = erc20Interface
157 | type = "ERC20"
158 | } else if (log.topics.length === 2) {
159 | eventInterface = erc20OldInterface
160 | type = "ERC20"
161 | } else {
162 | return undefined
163 | }
164 | const event = eventInterface.decodeEventLog("Transfer", log.data, log.topics)
165 | let details: TransferDetails
166 | if (type === "ERC20") {
167 | details = {
168 | type: "ERC20",
169 | tokenAddress: log.address,
170 | value: event.amount.toString()
171 | }
172 | } else {
173 | details = {
174 | type: "ERC721",
175 | tokenAddress: log.address,
176 | tokenId: event.tokenId.toString()
177 | }
178 | }
179 | return {
180 | type: "Transfer",
181 | id: `transfer_${log.blockNumber}_${log.transactionIndex}_${log.logIndex}`,
182 | timestamp: block.timestamp,
183 | sender: event.from,
184 | receipient: event.to,
185 | direction: (event.to.toLowerCase() === account.toLowerCase() ? "INCOMING" : "OUTGOING"),
186 | details
187 | }
188 | }
189 |
190 | const incomingEthEntry = async (provider: ethers.providers.Provider, account: string, log: ethers.providers.Log): Promise => {
191 | const block = await loadBlock(provider, log.blockHash)
192 | const event = safeInterface.decodeEventLog("SafeReceived", log.data, log.topics)
193 | let details: EtherDetails = {
194 | type: "ETHER",
195 | value: event.value.toString()
196 | }
197 | return {
198 | type: "Transfer",
199 | id: `transfer_${log.blockNumber}_${log.transactionIndex}_${log.logIndex}`,
200 | timestamp: block.timestamp,
201 | sender: event.sender,
202 | receipient: account,
203 | direction: "INCOMING",
204 | details
205 | }
206 | }
207 |
208 | const mapLog = async (provider: ethers.providers.Provider, account: string, nonceMapper: NonceMapper, group: GroupedLogs): Promise => {
209 | const { parent, children, details } = group
210 | switch (parent.topics[0]) {
211 | case successTopic: {
212 | const event = safeInterface.decodeEventLog("ExecutionSuccess", parent.data, parent.topics)
213 | return await mutlisigTxEntry(provider, account, nonceMapper, parent, event.txHash, true, children, details)
214 | }
215 | case failureTopic: {
216 | const event = safeInterface.decodeEventLog("ExecutionFailure", parent.data, parent.topics)
217 | return await mutlisigTxEntry(provider, account, nonceMapper, parent, event.txHash, false, children, details)
218 | }
219 | case moduleSuccessTopic: {
220 | const event = safeInterface.decodeEventLog("ExecutionFromModuleSuccess", parent.data, parent.topics)
221 | return await moduleTxEntry(provider, parent, event.module, true, children, details)
222 | }
223 | case moduleFailureTopic: {
224 | const event = safeInterface.decodeEventLog("ExecutionFromModuleFailure", parent.data, parent.topics)
225 | return await moduleTxEntry(provider, parent, event.module, false, children, details)
226 | }
227 | case transferTopic: {
228 | if (children.length > 0) console.error("Sub logs for transfer entry!", parent, children)
229 | return await transferEntry(provider, account, parent)
230 | }
231 | case etherReceivedTopic: {
232 | if (children.length > 0) console.error("Sub logs for transfer entry!", parent, children)
233 | return await incomingEthEntry(provider, account, parent)
234 | }
235 | default:
236 | console.error("Received unknown event", parent)
237 | return undefined
238 | }
239 | }
240 |
241 | const loadOutgoingTransfer = (provider: ethers.providers.Provider, account: string): Promise => {
242 | const filter = {
243 | topics: [[transferTopic], [ethers.utils.defaultAbiCoder.encode(["address"], [account])]],
244 | fromBlock: "earliest"
245 | }
246 | return provider.getLogs(filter).then((e) => {
247 | console.log("OUT", e.length)
248 | return e
249 | })
250 | }
251 |
252 | const loadIncomingTransfer = (provider: ethers.providers.Provider, account: string): Promise => {
253 | const filter = {
254 | topics: [[transferTopic], null as any, [ethers.utils.defaultAbiCoder.encode(["address"], [account])]],
255 | fromBlock: "earliest"
256 | }
257 | return provider.getLogs(filter).then((e) => {
258 | console.log("IN", e.length)
259 | return e
260 | })
261 | }
262 |
263 | const loadIncomingEther = (provider: ethers.providers.Provider, account: string): Promise => {
264 | const filter = {
265 | topics: [[etherReceivedTopic]],
266 | address: account,
267 | fromBlock: "earliest"
268 | }
269 | return provider.getLogs(filter).then((e) => {
270 | console.log("ETH", e.length)
271 | return e
272 | })
273 | }
274 |
275 | const loadSafeModuleTransactions = (provider: ethers.providers.Provider, account: string): Promise => {
276 | const filter = {
277 | topics: [[moduleSuccessTopic, moduleFailureTopic, moduleDetailsTopic]],
278 | address: account,
279 | fromBlock: "earliest"
280 | }
281 | return provider.getLogs(filter).then((e) => {
282 | console.log("MODULE", e.length)
283 | return e
284 | })
285 | }
286 |
287 | const loadSafeMultisigTransactions = (provider: ethers.providers.Provider, account: string): Promise => {
288 | const filter = {
289 | topics: [[successTopic, failureTopic, multisigDetailsTopic]],
290 | address: account,
291 | fromBlock: "earliest"
292 | }
293 |
294 | return provider.getLogs(filter).then((e) => {
295 | console.log("MULTISIG", e.length)
296 | return e
297 | })
298 | }
299 |
300 | const isOlder = (compare: ethers.providers.Log | undefined, base: ethers.providers.Log | undefined) => {
301 | if (compare === undefined) return false
302 | if (base === undefined) return true
303 | if (compare.blockNumber != base.blockNumber) return compare.blockNumber < base.blockNumber
304 | if (compare.transactionIndex != base.transactionIndex) return compare.transactionIndex < base.transactionIndex
305 | if (compare.logIndex != base.logIndex) return compare.logIndex < base.logIndex
306 | return false // Equal defaults to false
307 | }
308 |
309 | /// Oldest first
310 | const mergeLogs = async (...loaders: Promise[]): Promise => {
311 | const loaderCount = loaders.length
312 | if (loaderCount == 0) return []
313 |
314 | const logResults = await Promise.all(loaders)
315 | if (loaderCount == 1) return logResults[0]
316 | const currentLogIndex: number[] = new Array(loaderCount).fill(0)
317 | for (var i = 0; i < loaderCount; i++) currentLogIndex[i] = 0;
318 | const out: ethers.providers.Log[] = []
319 | var runs = 0
320 | // Panic check against endless loop (10k is max amount of events, per loader)
321 | while (runs < 10000 * loaderCount) {
322 | let resultIndex = 0
323 | let nextLog = logResults[0][currentLogIndex[0]]
324 | for (var i = 1; i < loaderCount; i++) {
325 | let candidate = logResults[i][currentLogIndex[i]]
326 | if (isOlder(candidate, nextLog)) {
327 | resultIndex = i
328 | nextLog = candidate
329 | }
330 | }
331 | currentLogIndex[resultIndex]++
332 | if (nextLog) out.push(nextLog)
333 | else break
334 | runs++
335 | }
336 | return out
337 | }
338 |
339 | interface GroupedLogs {
340 | parent: ethers.providers.Log,
341 | details?: ethers.providers.Log, // This is for L2 Safes, we expect the event order details -> children -> parent
342 | children: ethers.providers.Log[]
343 | }
344 |
345 | const groupIdFromLog = (log: ethers.providers.Log): string => `${log.blockNumber}_${log.transactionIndex}`
346 |
347 | const updateGroupedLogs = (groups: GroupedLogs[], detailsCandidate: ethers.providers.Log | undefined, parentCandidate: ethers.providers.Log | undefined, currentChildren: ethers.providers.Log[]) => {
348 | if (parentCandidate) {
349 | groups.push({
350 | parent: parentCandidate,
351 | details: detailsCandidate,
352 | children: currentChildren
353 | })
354 | } else if (currentChildren.length > 0) {
355 | groups.push(...currentChildren.map((log) => { return { parent: log, children: [] } }))
356 | }
357 | }
358 |
359 | const groupLogs = (logs: ethers.providers.Log[]): GroupedLogs[] => {
360 | const out: GroupedLogs[] = []
361 | let currentChildren: ethers.providers.Log[] = []
362 | let detailsCandidates: ethers.providers.Log[] = []
363 | let parentCandidate: ethers.providers.Log | undefined
364 | let currentGroupId: string | undefined = undefined
365 | for (const log of logs) {
366 | const groupId = groupIdFromLog(log)
367 | const isParentCandidate = parentTopics.indexOf(log.topics[0]) >= 0
368 | const isDetailsCandidate = detailsTopics.indexOf(log.topics[0]) >= 0
369 | if (currentGroupId !== groupId || (isParentCandidate && parentCandidate)) {
370 | updateGroupedLogs(out, detailsCandidates.pop(), parentCandidate, currentChildren)
371 | parentCandidate = undefined
372 | detailsCandidates = []
373 | currentChildren = []
374 | currentGroupId = undefined
375 | }
376 | if (!currentGroupId) currentGroupId = groupId
377 | if (isParentCandidate) {
378 | parentCandidate = log
379 | } else if (isDetailsCandidate) {
380 | detailsCandidates.push(log)
381 | } else {
382 | currentChildren.push(log)
383 | }
384 | }
385 | updateGroupedLogs(out, detailsCandidates.pop(), parentCandidate, currentChildren)
386 | return out
387 | }
388 |
389 | class NonceMapper {
390 |
391 | safe: ethers.Contract
392 | lastNonce: number|undefined
393 | chainId: number|undefined
394 |
395 | constructor(provider: ethers.providers.Provider, account: string) {
396 | this.safe = new ethers.Contract(account, safeAbi, provider)
397 | }
398 |
399 | async init() {
400 | this.lastNonce = (await this.safe.nonce()).toNumber()
401 | this.chainId = (await this.safe.provider.getNetwork()).chainId
402 | }
403 |
404 | calculateHash111(tx: DecodedMultisigTx, nonce: number): string {
405 | return ethers.utils._TypedDataEncoder.hash({ verifyingContract: this.safe.address }, EIP712_SAFE_TX_TYPE, {...tx, nonce})
406 | }
407 |
408 | async map(expectedHash: string, tx: DecodedMultisigTx): Promise {
409 | if (!this.lastNonce || !this.chainId) {
410 | await this.init()
411 | }
412 | for (let nonce = this.lastNonce!!; nonce >= 0; nonce--) {
413 | if (this.calculateHash111(tx, nonce) === expectedHash) return nonce
414 | if (calculateSafeTransactionHash(this.safe, {...tx, nonce}, this.chainId!!) === expectedHash) return nonce
415 | }
416 | return -1
417 | }
418 | }
419 |
420 | export const loadHistoryTxs = async (provider: ethers.providers.Provider, account: string, start: number): Promise => {
421 | const txLogs = await mergeLogs(
422 | loadSafeMultisigTransactions(provider, account),
423 | loadSafeModuleTransactions(provider, account),
424 | loadOutgoingTransfer(provider, account),
425 | loadIncomingTransfer(provider, account),
426 | loadIncomingEther(provider, account)
427 | )
428 | const nonceMapper = new NonceMapper(provider, account)
429 | await nonceMapper.init()
430 | const groups = groupLogs(txLogs.reverse())
431 | const inter = groups.slice(start, start + 5).map((group) => mapLog(provider, account, nonceMapper, group))
432 | return (await Promise.all(inter)).filter((e) => e !== undefined) as EventTx[]
433 | }
--------------------------------------------------------------------------------
/src/history/types.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers"
2 |
3 | export type EventTx = MultisigTx | ModuleTx | TransferTx | MultisigUnknownTx
4 |
5 | interface Base {
6 | id: string,
7 | timestamp: number,
8 | }
9 |
10 | export interface MultisigUnknownTx extends Base {
11 | type: 'MultisigUnknown'
12 | txHash: string
13 | safeTxHash: string
14 | success: boolean
15 | logs: ethers.providers.Log[]
16 | }
17 |
18 | export interface MultisigTx extends Base {
19 | type: 'Multisig'
20 | txHash: string
21 | safeTxHash: string
22 | success: boolean
23 | to: string
24 | value: string
25 | data: string
26 | operation: number
27 | safeTxGas: string
28 | baseGas: string
29 | gasPrice: string
30 | gasToken: string
31 | refundReceiver: string
32 | signatures: string
33 | nonce: number
34 | logs: ethers.providers.Log[]
35 | }
36 |
37 | export interface ModuleTx extends Base {
38 | type: 'Module'
39 | txHash: string
40 | module: string
41 | success: boolean
42 | logs: ethers.providers.Log[]
43 | }
44 |
45 | export interface TransferTx extends Base {
46 | type: 'Transfer'
47 | sender: string
48 | receipient: string
49 | direction: 'INCOMING' | 'OUTGOING'
50 | details: TransferDetails
51 | }
52 |
53 | export type TransferDetails = Erc20Details | Erc721Details | EtherDetails
54 |
55 | export interface Erc20Details {
56 | type: "ERC20",
57 | tokenAddress: string,
58 | value: string
59 | }
60 |
61 | export interface Erc721Details {
62 | type: "ERC721",
63 | tokenAddress: string,
64 | tokenId: string
65 | }
66 |
67 | export interface EtherDetails {
68 | type: "ETHER",
69 | value: string
70 | }
--------------------------------------------------------------------------------
/src/information.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config";
2 | import { HardhatRuntimeEnvironment as HRE } from "hardhat/types";
3 | import { getAddress } from "@ethersproject/address";
4 | import { AddressOne } from "@gnosis.pm/safe-contracts";
5 | import { Contract } from "@ethersproject/contracts";
6 | import { compatHandler, contractFactory, safeSingleton } from "./contracts";
7 |
8 | export const getSingletonAddress = async (hre: HRE, address: string): Promise => {
9 | const result = await hre.ethers.provider.getStorageAt(address, 0)
10 | return getAddress("0x" + result.slice(26))
11 | }
12 |
13 | export const getFallbackHandlerAddress = async (hre: HRE, address: string): Promise => {
14 | const result = await hre.ethers.provider.getStorageAt(address, "0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5")
15 | return getAddress("0x" + result.slice(26))
16 | }
17 |
18 | const getModules = async (hre: HRE, safe: Contract): Promise => {
19 | try {
20 | return (await safe.getModulesPaginated(AddressOne, 10))[0]
21 | } catch (e) {
22 | }
23 | try {
24 | const compat = await compatHandler(hre, safe.address)
25 | return await compat.getModules()
26 | } catch (e) {
27 | }
28 | return ["Could not load modules"]
29 | }
30 |
31 | task("info", "Displays information about a Safe")
32 | .addPositionalParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
33 | .setAction(async (taskArgs, hre) => {
34 | const safe = await safeSingleton(hre, taskArgs.address)
35 | const safeAddress = await safe.resolvedAddress
36 | console.log(`Checking Safe at ${safeAddress}`)
37 | console.log(`Singleton: ${await getSingletonAddress(hre, safeAddress)}`)
38 | console.log(`Version: ${await safe.VERSION()}`)
39 | console.log(`Owners: ${await safe.getOwners()}`)
40 | console.log(`Threshold: ${await safe.getThreshold()}`)
41 | console.log(`Nonce: ${await safe.nonce()}`)
42 | console.log(`Fallback Handler: ${await getFallbackHandlerAddress(hre, safeAddress)}`)
43 | console.log(`Modules: ${await getModules(hre, safe)}`)
44 | });
--------------------------------------------------------------------------------
/src/tasks.ts:
--------------------------------------------------------------------------------
1 | import "./creation"
2 | import "./execution"
3 | import "./information"
4 | import "./history"
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
5 | "lib": ["ES2015"],
6 | "allowJs": false /* Allow javascript files to be compiled. */,
7 | "declaration": true /* Generates corresponding '.d.ts' file. */,
8 | "sourceMap": false /* Generates corresponding '.map' file. */,
9 | "outDir": "./dist" /* Redirect output structure to the directory. */,
10 | "strict": true /* Enable all strict type-checking options. */,
11 | "noUnusedParameters": false /* Report errors on unused parameters. */,
12 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
13 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
14 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
15 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
16 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
17 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
18 | "resolveJsonModule": true
19 | }
20 | }
--------------------------------------------------------------------------------
/tx_input.sample.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "to": "0xc778417E063141139Fce010982780140Aa0cD5Ab",
4 | "value": "0.1",
5 | "operation": 0
6 | },
7 | {
8 | "to": "0xc778417E063141139Fce010982780140Aa0cD5Ab",
9 | "value": "0",
10 | "method": "approve(address,uint256)",
11 | "params": [
12 | "0xd0Dab4E640D95E9E8A47545598c33e31bDb53C7c",
13 | "1000000000000"
14 | ],
15 | "operation": 0
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------