-
Notifications
You must be signed in to change notification settings - Fork 11
Multisig example #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| node_modules | ||
| .env | ||
| coverage | ||
| coverage.json | ||
| typechain | ||
| typechain-types | ||
|
|
||
| #Hardhat files | ||
| cache | ||
| artifacts | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Web3 Actions + Multisig wallet | ||
|
|
||
| This repository shows usage of Web3 Actions running with a simple [multisig wallet](contracts/MultiSigWallet.sol). | ||
|
|
||
| The Web3 Action `multisig` is ran when a transaction emits one of following events: | ||
|
|
||
| - **TxSubmission**: run a simulation on a fork and send a message to Discord about the results | ||
| - **TxConfirmation**: send a message to Discord | ||
| - **ExecuteTransaction**: send a message to Discord | ||
|
|
||
| # Prerequisites: | ||
|
|
||
| - Tenderly [CLI installed](https://github.com/Tenderly/tenderly-cli#installation) | ||
| and [logged in via CLI](https://github.com/Tenderly/tenderly-cli#login). | ||
|
|
||
| # Running the example | ||
|
|
||
| The Web3 Actions code is located in `web3-actions` folder. You can specify this when running `tenderly actions init`. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| To run the example, you can either use [pre-deployed and Tenderly-verified contracts](#1a-using-pre-deployed-contracts) | ||
| or spare some time to [deploy your own contracts](#1b-deploy-your-own-contracts). Follow the rest of instructions to run | ||
| the example. | ||
|
|
||
| ## 0: Configuration for Web3 Actions project | ||
|
|
||
| **▶️ Action** First thing's first, install npm dependencies and ⏳. | ||
|
|
||
| ```bash | ||
| yarn | ||
| cd web3-actions && yarn && cd .. | ||
| ``` | ||
|
|
||
| Note: The Web3 Actions project is an NPM project. | ||
|
|
||
| ## 1A: Using pre-deployed contracts | ||
|
|
||
| To save some time and to manually run Web 3 Actions from Tenderly Dashboard, you can use pre-deployed contracts listed | ||
| below. | ||
|
|
||
| These include a single approved and executed transaction you can use for running [Web3](https://google.com) Actions. | ||
|
|
||
| The contracts are deployed on **Ropsten** at the follwing addresses: | ||
|
|
||
| | Contract | Address | | ||
| | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | ||
| | [MultisigWallet](https://dashboard.tenderly.co/contract/ropsten/0x418ebb95eaa40c119408143056cad984c6129d45) | `0x418ebb95eaa40c119408143056cad984c6129d45` | | ||
| | [Token](https://dashboard.tenderly.co/contract/ropsten/0x4e97c71a44ef1b78a7caee159aa016be53e04c32) | `0x4e97c71a44ef1b78a7caee159aa016be53e04c32` | | ||
|
|
||
| **▶️ Action**: Add the contracts (addresses and links below) to your project in Tenderly. Click the links in the table to open the Dashboard and then click "Add to Project". As soon as you add the | ||
| contracts, the transactions will be shown in **Transactions** page. | ||
|
|
||
| Transactions executed on these contracts: | ||
|
|
||
| | Action | TX Hash | | ||
| | --------- | -------------------------------------------------------------------- | | ||
| | Submit | `0xc6dff6c01d1043025b4c372047f88887eb3c36168f298acd75de63f0099087eb` | | ||
| | Confirm 1 | `0xad91a78d2f544371237ec63d5a47f2d6775784266eeb81ed7ea304d1f146c46c` | | ||
| | Confirm 2 | `0x8e7288f232f0468cad59637906fed715f953bc4d77bd13103fd6f56b2710b030` | | ||
| | Execute | `0x7faeb8641d16ad5179ed0d4fb7b478311b85ab41e186c8180d23357f5e9a675f` | | ||
|
|
||
| You can use these TX hashes to run Web3 Actions manually from the Dashboard. Skip | ||
| to [Deploying Web3 Actions](#3-deploying-web3-actions). | ||
|
|
||
| Note: since the contracts are deployed with a pre-defined list of owners, you can simulate transactions, but you can't | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider putting this paragraph as "NOTE" section |
||
| submit, approve or execute any of these transactions on Ropsten. | ||
|
|
||
| ## 1B: Deploy your own contracts | ||
|
|
||
| If you want to run Web3 Actions on your own contracts, you can deploy them manually. For this example, we will use | ||
| the [Ropsten testnet](https://ropsten.etherscan.io/). | ||
|
|
||
| 0. **▶️ Action**: Set up a hardhat project .env file and replace placeholders. Source | ||
| the [demo-setup-scripts/init-env-hardhat.sh](demo-setup-scripts/init-env-hardhat.sh) script and replace the | ||
| placeholders. | ||
|
|
||
| ```bash | ||
| source demo-setup-scripts/init-env-hardhat.sh | ||
| ``` | ||
|
|
||
| 1. **▶️ Action**: Deploy the Contracts to Ropsten. Use shorthand script `yarn deploy:ropsten`. Hardhat project is set | ||
| up | ||
| with [Tenderly Hardhat Plugin automatic contract verification](https://github.com/Tenderly/hardhat-tenderly#automatic-verification) | ||
| so your contracts are verified automatically as they're deployed. The contracts are vefified privately,see [hardhat.config.ts](hardhat.config.ts). | ||
| 2. **▶️ Action**: Run a first set of transactions: submit 1 tx to multisig, do two approvals and optionally execute it. | ||
| Use the script `yarn multisig-play:ropsten`. | ||
| 3. **▶️ Action**: Copy the address of Multisig from deployment step (also found in `deployments.json`) and paste it | ||
| into `tenderly.yaml` in place of `MULTISIG_ADDRESS` placeholder. | ||
|
|
||
| ## 2: Run it locally | ||
|
|
||
| Before deploying the action to Tenderly, you can test it out locally. | ||
|
|
||
| ```bash | ||
| source demo-setup-scripts/init-env-hardhat.sh | ||
| yarn multisig-local | ||
| ``` | ||
|
|
||
| 👾 You should get several Discord messages and see some meaningful output. If all went well, you can go on and deploy the | ||
| action to Tenderly. | ||
|
|
||
| The file `web3-actions/multisig-local.js` is a simple script that runs Web3 Actions locally, | ||
| using [@tenderly/actions-test](https://github.com/Tenderly/tenderly-actions). | ||
|
|
||
| It requires Transaction payload to be passed as an argument. In the [web3-actions/payload](web3-actions/payload/) | ||
| directory you'll find JSON file corresponding to an example transactions that submit, approve and execute a multisig | ||
| transaction. For running locally, it's fine to use the provided ones. | ||
|
|
||
| ## 3: Deploying Web3 Actions | ||
|
|
||
| **▶️ Action**: Open `tenderly.yaml` and replace placeholders with your [Username](https://docs.tenderly.co/other/platform-access/how-to-find-the-project-slug-username-and-organization-name) | ||
| and [Project Slug](https://docs.tenderly.co/other/platform-access/how-to-find-the-project-slug-username-and-organization-name). | ||
|
|
||
| **▶️ Action**: Go to Tenderly Dashboard, open **Actions** and click **Secrets**. Add: | ||
|
|
||
| 1. `TENDERLY_ACCESS_KEY` [following these instructions](https://docs.tenderly.co/other/platform-access/how-to-generate-api-access-tokens). | ||
| 2. `multisig.DISCORD_URL` | ||
|
|
||
| **▶️ Action**: Go to [web3-actions/multisig.ts](multisig.ts) and replace the placeholders at the top with appropriate values. | ||
|
|
||
| You can either use contract addresses of pre-deployed contracts or your own | ||
| **▶️ Action**: From directory containing `tenderly.yaml` (the root of this example project), run | ||
|
|
||
| ```bash | ||
| tenderly actions deploy | ||
| ``` | ||
|
|
||
| ## 4: Manually triggering Web3 Actions: | ||
|
|
||
| **▶️ Action**: From the Web3 Actions page: | ||
|
|
||
| 1. open the `multisig` action | ||
| 2. click **Manual Run** | ||
| 3. copy TX hash either from Tenderly Dashboard or from the table | ||
| in [1A: Using pre-deployed contracts](#1a-using-pre-deployed-contracts) | ||
| 4. paste the hash of transaction | ||
| 5. select **Copy on write** storage option | ||
| 6. click **⚡️ Trigger** button at the bottom of the page. Your Web3 Action should run, your Discord should receive a | ||
| message | ||
|
|
||
| ## 5: Trying it out with real-time transactions | ||
|
|
||
| If you deployed your own contracts in step [1B: Deploy your own contracts](#1b-deploy-your-own-contracts), you can run | ||
| the `multisig-play` script. Your Web3 Actions should pick up on the new transactions and run accordingly. | ||
|
|
||
| ```bash | ||
| tenderly actions multisig-play:ropsten | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.9; | ||
|
|
||
| contract MultiSigWallet { | ||
| // Events | ||
| event Deposit(address indexed sender, uint amount, uint balance); | ||
| event TxSubmission( | ||
| address indexed owner, | ||
| uint indexed txIndex, | ||
| address indexed to, | ||
| uint value, | ||
| bytes data | ||
| ); | ||
| event TxConfirmation(address indexed owner, uint indexed txIndex); | ||
| event ConfirmationRevocation(address indexed owner, uint indexed txIndex); | ||
| event ExecuteTransaction(address indexed owner, uint indexed txIndex); | ||
|
|
||
| // List of owners of the wallet | ||
| address[] public owners; | ||
| // Mapping to allow for easy checks if someone is an owner | ||
| mapping(address => bool) public isOwner; | ||
| // Number of confirmations required to execute a transaction | ||
| uint public numConfirmationsRequired; | ||
|
|
||
| // Tx object | ||
| struct Transaction { | ||
| address to; | ||
| uint value; | ||
| bytes data; | ||
| bool executed; | ||
| uint numConfirmations; | ||
| } | ||
|
|
||
| // mapping from tx index => owner => bool | ||
| // use this to check if a transaction is confirmed by an owner | ||
| mapping(uint => mapping(address => bool)) public isConfirmed; | ||
|
|
||
| // List of all tracked transactions | ||
| Transaction[] public transactions; | ||
|
|
||
| // Helper functions | ||
| modifier onlyOwner() { | ||
| require(isOwner[msg.sender], "not owner"); | ||
| _; | ||
| } | ||
|
|
||
| modifier txExists(uint _txIndex) { | ||
| require(_txIndex < transactions.length, "tx does not exist"); | ||
| _; | ||
| } | ||
|
|
||
| modifier notExecuted(uint _txIndex) { | ||
| require(!transactions[_txIndex].executed, "tx already executed"); | ||
| _; | ||
| } | ||
|
|
||
| modifier notConfirmed(uint _txIndex) { | ||
| require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed"); | ||
| _; | ||
| } | ||
|
|
||
| constructor(address[] memory _owners, uint _numConfirmationsRequired) { | ||
| require(_owners.length > 0, "owners required"); | ||
| require( | ||
| _numConfirmationsRequired > 0 && | ||
| _numConfirmationsRequired <= _owners.length, | ||
| "invalid number of required confirmations" | ||
| ); | ||
|
|
||
| for (uint i = 0; i < _owners.length; i++) { | ||
| address owner = _owners[i]; | ||
|
|
||
| require(owner != address(0), "invalid owner"); | ||
| require(!isOwner[owner], "owner not unique"); | ||
|
|
||
| isOwner[owner] = true; | ||
| owners.push(owner); | ||
| } | ||
|
|
||
| numConfirmationsRequired = _numConfirmationsRequired; | ||
| } | ||
|
|
||
| receive() external payable { | ||
| emit Deposit(msg.sender, msg.value, address(this).balance); | ||
| } | ||
|
|
||
| function submitTransaction( | ||
| address _to, | ||
| uint _value, | ||
| bytes memory _data | ||
| ) public onlyOwner { | ||
| uint txIndex = transactions.length; | ||
|
|
||
| transactions.push( | ||
| Transaction({ | ||
| to: _to, | ||
| value: _value, | ||
| data: _data, | ||
| executed: false, | ||
| numConfirmations: 0 | ||
| }) | ||
| ); | ||
|
|
||
| emit TxSubmission(msg.sender, txIndex, _to, _value, _data); | ||
| } | ||
|
|
||
| function confirmTransaction(uint _txIndex) | ||
| public | ||
| onlyOwner | ||
| txExists(_txIndex) | ||
| notExecuted(_txIndex) | ||
| notConfirmed(_txIndex) | ||
| { | ||
| Transaction storage transaction = transactions[_txIndex]; | ||
| transaction.numConfirmations += 1; | ||
| isConfirmed[_txIndex][msg.sender] = true; | ||
|
|
||
| emit TxConfirmation(msg.sender, _txIndex); | ||
| } | ||
|
|
||
| function executeTransaction(uint _txIndex) | ||
| public | ||
| onlyOwner | ||
| txExists(_txIndex) | ||
| notExecuted(_txIndex) | ||
| { | ||
| Transaction storage transaction = transactions[_txIndex]; | ||
|
|
||
| require( | ||
| transaction.numConfirmations >= numConfirmationsRequired, | ||
| "cannot execute tx" | ||
| ); | ||
|
|
||
| transaction.executed = true; | ||
|
|
||
| (bool success, ) = transaction.to.call{value: transaction.value}( | ||
| transaction.data | ||
| ); | ||
| require(success, "tx failed"); | ||
|
|
||
| emit ExecuteTransaction(msg.sender, _txIndex); | ||
| } | ||
|
|
||
| function revokeConfirmation(uint _txIndex) | ||
| public | ||
| onlyOwner | ||
| txExists(_txIndex) | ||
| notExecuted(_txIndex) | ||
| { | ||
| Transaction storage transaction = transactions[_txIndex]; | ||
|
|
||
| require(isConfirmed[_txIndex][msg.sender], "tx not confirmed"); | ||
|
|
||
| transaction.numConfirmations -= 1; | ||
| isConfirmed[_txIndex][msg.sender] = false; | ||
|
|
||
| emit ConfirmationRevocation(msg.sender, _txIndex); | ||
| } | ||
|
|
||
| function getOwners() public view returns (address[] memory) { | ||
| return owners; | ||
| } | ||
|
|
||
| function getTransactionCount() public view returns (uint) { | ||
| return transactions.length; | ||
| } | ||
|
|
||
| function getTransaction(uint _txIndex) | ||
| public | ||
| view | ||
| returns ( | ||
| address to, | ||
| uint value, | ||
| bytes memory data, | ||
| bool executed, | ||
| uint numConfirmations | ||
| ) | ||
| { | ||
| Transaction storage transaction = transactions[_txIndex]; | ||
|
|
||
| return ( | ||
| transaction.to, | ||
| transaction.value, | ||
| transaction.data, | ||
| transaction.executed, | ||
| transaction.numConfirmations | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of
showsI would putdemonstrates