├── .env.sample ├── .gitignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── contracts ├── AccessControl.sol ├── Address.sol ├── Multicall.sol ├── TasksManager.sol ├── Web3Task.sol └── interfaces │ └── IWeb3Task.sol ├── hardhat.config.ts ├── package.json ├── scripts ├── cancelTask.ts ├── completeTask.ts ├── createTasks.ts ├── deploy.ts ├── emergengyWithdraw.ts ├── fundingContract.ts ├── getMulticallTask.ts ├── getTask.ts ├── reviewTask.ts ├── setOperator.ts ├── setQuorum.ts ├── setRole.ts ├── startTask.ts ├── utils.ts └── withdraw.ts ├── test └── Web3Task.test.ts ├── tsconfig.json └── utils └── saveDataContract.ts /.env.sample: -------------------------------------------------------------------------------- 1 | MUMBAI_URL=https://polygon-mumbai.g.alchemy.com/v2/API 2 | POLYGON_URL=https://polygon-mainnet.g.alchemy.com/v2/API 3 | 4 | ETHERSCAN_KEY=SCANKEY 5 | CONTRACT_ADDRESS=0xeC20dCBf0380F1C9856Ee345aF41F62Ee45a95a1 6 | 7 | # These are public private keys, that are generated by hardhat local node 8 | PRIVATE_KEY_LEADER=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 9 | PRIVATE_KEY_MEMBER=59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Allow a dummy project in this directory for testing purposes 2 | myproject 3 | 4 | /node_modules 5 | /.idea 6 | *.tsbuildinfo 7 | 8 | # VS Code workspace config 9 | workspace.code-workspace 10 | 11 | .DS_Store 12 | 13 | # Below is Github's node gitignore template, 14 | # ignoring the node_modules part, as it'd ignore every node_modules, and we have some for testing 15 | 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | lerna-debug.log* 23 | 24 | # Diagnostic reports (https://nodejs.org/api/report.html) 25 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | *.pid.lock 32 | 33 | # Directory for instrumented libs generated by jscoverage/JSCover 34 | lib-cov 35 | 36 | # Coverage directory used by tools like istanbul 37 | coverage 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (https://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | #node_modules/ 56 | jspm_packages/ 57 | 58 | # TypeScript v1 declaration files 59 | typings/ 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # next.js build output 80 | .next 81 | 82 | # nuxt.js build output 83 | .nuxt 84 | 85 | # vuepress build output 86 | .vuepress/dist 87 | 88 | # Serverless directories 89 | .serverless/ 90 | 91 | # FuseBox cache 92 | .fusebox/ 93 | 94 | # DynamoDB Local files 95 | .dynamodb/ 96 | 97 | 98 | docs/.env.example 99 | 100 | # Generated by Cargo 101 | # will have compiled files and executables 102 | debug/ 103 | target/ 104 | 105 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 106 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 107 | Cargo.lock 108 | 109 | # These are backup files generated by rustfmt 110 | **/*.rs.bk 111 | 112 | # MSVC Windows builds of rustc generate these, which store debugging information 113 | *.pdb 114 | 115 | # VSCode settings 116 | .vscode/ 117 | node_modules 118 | .env 119 | coverage 120 | coverage.json 121 | typechain 122 | typechain-types 123 | yarn.lock 124 | 125 | # Hardhat files 126 | cache 127 | artifacts 128 | 129 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": false, 4 | "semi": true, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Web3Task Contracts 2 | 3 | We would love for you to contribute to Web3Task and help make it even better than it is today! 4 | As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [ Code of Conduct](#-code-of-conduct) 7 | - [ Reporting Bugs](#-reporting-bugs) 8 | - [ Suggesting Enhancements](#-suggesting-enhancements) 9 | - [ Your First Code Contribution](#-your-first-code-contribution) 10 | - [ Submission Guidelines](#-submission-guidelines) 11 | - [ Coding Rules](#-coding-rules) 12 | - [ Contact](#-contact) 13 | 14 | ## Code of Conduct 15 | 16 | Help us keep Web3Task open and inclusive. Please read and follow our [Code of Conduct](https://github.com/w3b3d3v/code-of-conduct/blob/main/CODE_OF_CONDUCT.md). 17 | 18 | ## Reporting Bugs 19 | 20 | This section guides you through submitting a bug report for this project. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. 21 | 22 | - Use a clear and descriptive title for the issue to identify the problem. 23 | - Describe the exact steps which reproduce the problem in as many details as possible. 24 | 25 | If you find a bug in the source code, you can help us by [submitting an issue](#submit-issue) to our [GitHub Repository](https://github.com/w3b3d3v/web3task-contracts). 26 | Even better, you can [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Suggesting Enhancements 29 | 30 | This section guides you through submitting an enhancement suggestion for this project, including completely new features and minor improvements to existing functionality. 31 | 32 | - Use a clear and descriptive title for the issue to identify the suggestion. 33 | - Provide a step-by-step description of the suggested enhancement in as many details as possible. 34 | 35 | ## Your First Code Contribution 36 | 37 | Unsure where to begin contributing to this project? You can start by looking through these `good first issue` and `dog` issues: 38 | 39 | - `good first issue` - issues which should only require a few lines of code, and a test or two. 40 | - `dog` - issues which should be a bit more involved than beginner issues. 41 | 42 | ## Submission Guidelines 43 | 44 | Before you submit your Pull Request (PR) consider the following guidelines: 45 | 46 | 1. Search [GitHub](https://github.com/w3b3d3v/web3task-contracts/pulls) for an open or closed PR that relates to your submission. 47 | You don't want to duplicate existing efforts. 48 | 49 | 2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. 50 | Discussing the design upfront helps to ensure that we're ready to accept your work. 51 | 52 | 3. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the repository. 53 | 54 | 4. In your forked repository, make your changes in a new git branch: 55 | 56 | ```shell 57 | git checkout -b my-fix-branch main 58 | ``` 59 | 60 | 5. Create your patch, **including appropriate test cases**. 61 | 62 | 6. Follow our [Coding Rules](#rules). 63 | 64 | 7. Commit your changes using a descriptive commit message that follows our [commit message conventions](#commit). 65 | Adherence to these conventions is necessary because release notes are automatically generated from these messages. 66 | 67 | ```shell 68 | git commit --all 69 | ``` 70 | Note: the optional commit `--all` command line option will automatically "add" and "rm" edited files. 71 | 72 | 8. Push your branch to GitHub: 73 | 74 | ```shell 75 | git push origin my-fix-branch 76 | ``` 77 | 78 | 9. In GitHub, send a pull request to `web3task-contracts:dev`. 79 | 80 | ## Coding Rules 81 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 82 | 83 | * All features or bug fixes **must be tested** by one or more specs (unit-tests). 84 | * All public API methods **must be documented**. 85 | 86 | ## Contact 87 | 88 | This project and everyone participating in it is governed by the [Code of Conduct](https://github.com/w3b3d3v/code-of-conduct/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [labs@w3d.community](mailto:labs@w3d.community). 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 WEB3DEV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | mocks: 4 | make deploy 5 | make funding 6 | make roles 7 | make operators 8 | make tasks 9 | 10 | deploy: 11 | npx hardhat run --network localhost scripts/deploy.ts 12 | 13 | funding: 14 | npx hardhat run --network localhost scripts/fundingContract.ts 15 | 16 | roles: 17 | npx hardhat run --network localhost scripts/setRole.ts 18 | 19 | operators: 20 | npx hardhat run --network localhost scripts/setOperator.ts 21 | 22 | tasks: 23 | npx hardhat run --network localhost scripts/createTasks.ts -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web3Task-contracts 2 | 3 | This protocol aims to solve the problem of monetized contributions by fractionalizing work and demands just like the GitHub issues, where the company creates the issues and places a bounty for users to solve them under the company's terms. 4 | 5 | ## Current Status 6 | 7 | The project is currently in the MVP stage. The following features will be implemented in the next versions: 8 | 9 | - Bid 10 | - Disputes 11 | - Ranking 12 | - Proxy 13 | - Fee Mechanism 14 | - Deployment for 3rd-parties (Multiple projects using the same contract to optimize front-end indexing) 15 | 16 | if you are a developer and want to contribute to these features, don't hesitate to get in touch with us as we are still in the conception phase of them. 17 | 18 | ## Disputes 19 | 20 | We are aware that disputes are common ground in this kind of service, so we are not mainly working on a solution to address this problem as a lot of people tried and failed. Instead, our solution is a gamified approach for both task creator and task assignee. Delivering prior to the date will harvest a better score for the assignee, while having its created tasks completed the task creator will also harvest the profile score. This score will be used to rank users in the platform, and the higher the score the more trustable the user is. Opening disputes are supposed to be a risky and unworthy move by both sides, as disputes will drastically decrease the score of both sides of the wrong side according to the DAO final saying. 21 | 22 | ## Bounties 23 | 24 | The task creator will be able to create a task and set a bounty for it, and the task assignee will be able to start the task and submit it for review. If the task creator approves the task, the bounty will be sent to the task assignee, otherwise, the task creator will be able to cancel the task and get the bounty back. 25 | 26 | ## Ranking 27 | 28 | The ranking system will be based on the score of the user, which will be calculated based on the following factors: 29 | 30 | - Deliver time prior to the deadline 31 | - Reward Amount 32 | - Disputes during the execution of the task 33 | 34 | The scores will only be applied after the task's final complitude. 35 | 36 | ## Features 37 | 38 | Regarding Task management, the following features are implemented: 39 | 40 | - Create 41 | - Start 42 | - Review 43 | - Complete 44 | - Cancel 45 | - Edit Title 46 | - Edit Description 47 | - Edit Deadline 48 | - Edit Metadata 49 | 50 | Note that editing sensitive information that might affect the task assignee will not be allowed. 51 | 52 | To manage the Access Control of users, the following methods are implemented: 53 | 54 | - Set Role: Sets the role ID for an address as true, 55 | - Set Operator: Sets which role it can manage the contract functions. 56 | 57 | Note that every function in the contract must have a role ID assigned to operate it and an address authorized by a role ID. 58 | 59 | ## Contracts 60 | 61 | - TaskManager: The main contract that manages the tasks. 62 | - AccessControl: The contract that manages the access control of the users. 63 | - Web3Task: The core implementation of the task marketplace. 64 | - IW3Task: The interface of the Web3Task contract. 65 | 66 | ## Installation 67 | 68 | ```bash 69 | yarn 70 | npm i 71 | ``` 72 | 73 | ## Setting Up A Stanrdad Formatting Code 74 | 75 | 1. Install the Prettier - Code Formatter in the VsCode extension with `esbenp.prettier-vscode`. 76 | 77 | 2. Modify the settings json. 78 | - 2.1. Press `ctrl` + `shift` + `p` to oppen the command pallets. 79 | - 2.2. Type "Open Settings (JSON)". 80 | - 2.3. Add the following lines to the JSON file: 81 | ``` 82 | "[solidity]": { 83 | "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" 84 | }, 85 | "[typescript]": { 86 | "editor.defaultFormatter": "esbenp.prettier-vscode" 87 | } 88 | ``` 89 | 90 | ## Setting Up Local Blockchain 91 | 92 | Run a Hardhat node: 93 | 94 | ```bash 95 | npx hardhat node 96 | ``` 97 | 98 | The node will generate some accounts. You can realize they are already set at .env.sample, you should just let it be. 99 | 100 | Add the first one to Metamask to be the leader and the second to be the member. The account will be: 101 | 102 | ``` 103 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 104 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 105 | ``` 106 | 107 | To add the localhost network to metamask, click on the network dropdown and select `Custom RPC`. Fill in the following fields: 108 | 109 | - Network Name: `localhost` 110 | - New RPC URL: `http://localhost:8545` 111 | - Chain ID: `31337` 112 | - Currency Symbol: `ETH` 113 | 114 | > **_NOTE:_** Use the recommended accounts to avoid errors. 115 | 116 | ## Deploying Smart Contracts in the Localhost 117 | 118 | Makefile will set everything for us, just run: 119 | 120 | ```bash 121 | make mocks 122 | ``` 123 | 124 | ## Livenet Deployment 125 | 126 | Remove the `.sample` from the `.env.sample` file and fill in the values with the corresponding API from RPC providers. 127 | 128 | ## Usage 129 | 130 | If you are not using livenet, you should comment chain configurations at `hardhat.config.ts` or mock the keys in the `.env` file, otherwise you will get an error from hardhat. 131 | 132 | To run all the unitary tests, run: 133 | 134 | ## Address deployed to the Sepolia Testnet 135 | 136 | SEPOLIA -> 0x20B1752b1374bAad268B4c94DC7484aBdbBA9a01 137 | 138 | ```bash 139 | yarn test 140 | ``` 141 | 142 | ## Contact 143 | 144 | Advisor: 0xneves (@ownerlessinc) 145 | 146 | This project is under the MIT license, feel free to use it and contribute. If you have any questions, please contact us at https://discord.gg/web3dev under the `pod-labs` channel. 147 | -------------------------------------------------------------------------------- /contracts/AccessControl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | abstract contract AccessControl { 5 | /** 6 | * @dev Emitted when `msg.sender` is not authorized to operate the contract. 7 | */ 8 | error Unauthorized(address operator); 9 | 10 | /** 11 | * @dev Emitted when `roleId` is invalid. 12 | */ 13 | error InvalidRoleId(uint256 roleId); 14 | 15 | /** 16 | * @dev Emitted when a new address is added to an `roleId`. 17 | */ 18 | event AuthorizePersonnel( 19 | uint256 indexed roleId, 20 | address indexed authorizedAddress, 21 | bool isAuthorized 22 | ); 23 | 24 | /** 25 | * @dev Emitted when an `roleId` is added as an operator of 26 | * a function in the contract. 27 | */ 28 | event AuthorizeOperator( 29 | bytes4 indexed interfaceId, 30 | uint256 indexed roleId, 31 | bool isAuthorized 32 | ); 33 | 34 | /// @dev The owner of the contract. 35 | address private _owner; 36 | 37 | /// @dev Mapping of `roleId` and `address` to boolean. 38 | mapping(uint256 => mapping(address => bool)) private _roles; 39 | 40 | /// @dev Mapping of `interfaceId` and `roleId` to boolean. 41 | mapping(bytes4 => mapping(uint256 => bool)) private _operators; 42 | 43 | /** 44 | * @dev Modifier to check if `msg.sender` is authorized to operate a 45 | * given interfaceId from one of the contract's function. 46 | */ 47 | modifier onlyOperator( 48 | bytes4 _interfaceId, 49 | uint256 _roleId, 50 | address _operator 51 | ) { 52 | if ( 53 | !hasRole(_roleId, _operator) || !isOperator(_interfaceId, _roleId) 54 | ) { 55 | revert Unauthorized(_operator); 56 | } 57 | _; 58 | } 59 | 60 | /** 61 | * @dev Modifier to check if `msg.sender` is the owner of the contract. 62 | */ 63 | modifier onlyOwner() { 64 | if (_owner != msg.sender) { 65 | revert Unauthorized(msg.sender); 66 | } 67 | _; 68 | } 69 | 70 | /** 71 | * @dev Initializes the contract setting the deployer as the initial owner. 72 | */ 73 | constructor() { 74 | _owner = msg.sender; 75 | } 76 | 77 | /** 78 | * @dev Returns the address of the current owner. 79 | */ 80 | function owner() public view virtual returns (address) { 81 | return _owner; 82 | } 83 | 84 | /** 85 | * @dev This function sets an role to an address. 86 | * 87 | * Emits a {AuthorizePersonnel} event. 88 | * 89 | * Requirements: 90 | * 91 | * - `msg.sender` must be the owner of the contract. 92 | * - `_roleId` must not be 0. 93 | */ 94 | function setRole( 95 | uint256 _roleId, 96 | address _authorizedAddress, 97 | bool _isAuthorized 98 | ) public virtual onlyOwner { 99 | if (_roleId == 0) { 100 | revert InvalidRoleId(_roleId); 101 | } 102 | 103 | _roles[_roleId][_authorizedAddress] = _isAuthorized; 104 | 105 | emit AuthorizePersonnel(_roleId, _authorizedAddress, _isAuthorized); 106 | } 107 | 108 | /** 109 | * @dev This function sets an authorized role as the operator of a 110 | * given interface id. 111 | * 112 | * Emits a {AuthorizeOperator} event. 113 | * 114 | * Requirements: 115 | * 116 | * - `msg.sender` must be the owner of the contract. 117 | * - `_roleId` must not be 0. 118 | */ 119 | function setOperator( 120 | bytes4 _interfaceId, 121 | uint256 _roleId, 122 | bool _isAuthorized 123 | ) public virtual onlyOwner { 124 | if (_roleId == 0) { 125 | revert InvalidRoleId(_roleId); 126 | } 127 | 128 | _operators[_interfaceId][_roleId] = _isAuthorized; 129 | 130 | emit AuthorizeOperator(_interfaceId, _roleId, _isAuthorized); 131 | } 132 | 133 | /** 134 | * @dev This function checks if an address holds a given `roleId`. 135 | * 136 | * NOTE: The owner of the contract is always authorized. 137 | */ 138 | function hasRole( 139 | uint256 _roleId, 140 | address _address 141 | ) public view virtual returns (bool) { 142 | if (owner() == msg.sender) { 143 | return true; 144 | } 145 | return _roles[_roleId][_address]; 146 | } 147 | 148 | /** 149 | * @dev This function checks if an `authorizedId` is allowed to operate 150 | * a given `_interfaceId`. 151 | * 152 | * NOTE: The owner of the contract is always authorized. 153 | */ 154 | function isOperator( 155 | bytes4 _interfaceId, 156 | uint256 _roleId 157 | ) public view virtual returns (bool) { 158 | if (owner() == msg.sender) { 159 | return true; 160 | } 161 | return _operators[_interfaceId][_roleId]; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /contracts/Address.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) 3 | 4 | pragma solidity ^0.8.1; 5 | 6 | /** 7 | * @dev Collection of functions related to the address type 8 | */ 9 | library Address { 10 | /** 11 | * @dev Returns true if `account` is a contract. 12 | * 13 | * [IMPORTANT] 14 | * ==== 15 | * It is unsafe to assume that an address for which this function returns 16 | * false is an externally-owned account (EOA) and not a contract. 17 | * 18 | * Among others, `isContract` will return false for the following 19 | * types of addresses: 20 | * 21 | * - an externally-owned account 22 | * - a contract in construction 23 | * - an address where a contract will be created 24 | * - an address where a contract lived, but was destroyed 25 | * 26 | * Furthermore, `isContract` will also return true if the target contract within 27 | * the same transaction is already scheduled for destruction by `SELFDESTRUCT`, 28 | * which only has an effect at the end of a transaction. 29 | * ==== 30 | * 31 | * [IMPORTANT] 32 | * ==== 33 | * You shouldn't rely on `isContract` to protect against flash loan attacks! 34 | * 35 | * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets 36 | * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract 37 | * constructor. 38 | * ==== 39 | */ 40 | function isContract(address account) internal view returns (bool) { 41 | // This method relies on extcodesize/address.code.length, which returns 0 42 | // for contracts in construction, since the code is only stored at the end 43 | // of the constructor execution. 44 | 45 | return account.code.length > 0; 46 | } 47 | 48 | /** 49 | * @dev Replacement for Solidity's `transfer`: sends `amount` wei to 50 | * `recipient`, forwarding all available gas and reverting on errors. 51 | * 52 | * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost 53 | * of certain opcodes, possibly making contracts go over the 2300 gas limit 54 | * imposed by `transfer`, making them unable to receive funds via 55 | * `transfer`. {sendValue} removes this limitation. 56 | * 57 | * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. 58 | * 59 | * IMPORTANT: because control is transferred to `recipient`, care must be 60 | * taken to not create reentrancy vulnerabilities. Consider using 61 | * {ReentrancyGuard} or the 62 | * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. 63 | */ 64 | function sendValue(address payable recipient, uint256 amount) internal { 65 | require( 66 | address(this).balance >= amount, 67 | "Address: insufficient balance" 68 | ); 69 | 70 | (bool success, ) = recipient.call{value: amount}(""); 71 | require( 72 | success, 73 | "Address: unable to send value, recipient may have reverted" 74 | ); 75 | } 76 | 77 | /** 78 | * @dev Performs a Solidity function call using a low level `call`. A 79 | * plain `call` is an unsafe replacement for a function call: use this 80 | * function instead. 81 | * 82 | * If `target` reverts with a revert reason, it is bubbled up by this 83 | * function (like regular Solidity function calls). 84 | * 85 | * Returns the raw returned data. To convert to the expected return value, 86 | * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. 87 | * 88 | * Requirements: 89 | * 90 | * - `target` must be a contract. 91 | * - calling `target` with `data` must not revert. 92 | * 93 | * _Available since v3.1._ 94 | */ 95 | function functionCall( 96 | address target, 97 | bytes memory data 98 | ) internal returns (bytes memory) { 99 | return 100 | functionCallWithValue( 101 | target, 102 | data, 103 | 0, 104 | "Address: low-level call failed" 105 | ); 106 | } 107 | 108 | /** 109 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with 110 | * `errorMessage` as a fallback revert reason when `target` reverts. 111 | * 112 | * _Available since v3.1._ 113 | */ 114 | function functionCall( 115 | address target, 116 | bytes memory data, 117 | string memory errorMessage 118 | ) internal returns (bytes memory) { 119 | return functionCallWithValue(target, data, 0, errorMessage); 120 | } 121 | 122 | /** 123 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 124 | * but also transferring `value` wei to `target`. 125 | * 126 | * Requirements: 127 | * 128 | * - the calling contract must have an ETH balance of at least `value`. 129 | * - the called Solidity function must be `payable`. 130 | * 131 | * _Available since v3.1._ 132 | */ 133 | function functionCallWithValue( 134 | address target, 135 | bytes memory data, 136 | uint256 value 137 | ) internal returns (bytes memory) { 138 | return 139 | functionCallWithValue( 140 | target, 141 | data, 142 | value, 143 | "Address: low-level call with value failed" 144 | ); 145 | } 146 | 147 | /** 148 | * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but 149 | * with `errorMessage` as a fallback revert reason when `target` reverts. 150 | * 151 | * _Available since v3.1._ 152 | */ 153 | function functionCallWithValue( 154 | address target, 155 | bytes memory data, 156 | uint256 value, 157 | string memory errorMessage 158 | ) internal returns (bytes memory) { 159 | require( 160 | address(this).balance >= value, 161 | "Address: insufficient balance for call" 162 | ); 163 | (bool success, bytes memory returndata) = target.call{value: value}( 164 | data 165 | ); 166 | return 167 | verifyCallResultFromTarget( 168 | target, 169 | success, 170 | returndata, 171 | errorMessage 172 | ); 173 | } 174 | 175 | /** 176 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 177 | * but performing a static call. 178 | * 179 | * _Available since v3.3._ 180 | */ 181 | function functionStaticCall( 182 | address target, 183 | bytes memory data 184 | ) internal view returns (bytes memory) { 185 | return 186 | functionStaticCall( 187 | target, 188 | data, 189 | "Address: low-level static call failed" 190 | ); 191 | } 192 | 193 | /** 194 | * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], 195 | * but performing a static call. 196 | * 197 | * _Available since v3.3._ 198 | */ 199 | function functionStaticCall( 200 | address target, 201 | bytes memory data, 202 | string memory errorMessage 203 | ) internal view returns (bytes memory) { 204 | (bool success, bytes memory returndata) = target.staticcall(data); 205 | return 206 | verifyCallResultFromTarget( 207 | target, 208 | success, 209 | returndata, 210 | errorMessage 211 | ); 212 | } 213 | 214 | /** 215 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 216 | * but performing a delegate call. 217 | * 218 | * _Available since v3.4._ 219 | */ 220 | function functionDelegateCall( 221 | address target, 222 | bytes memory data 223 | ) internal returns (bytes memory) { 224 | return 225 | functionDelegateCall( 226 | target, 227 | data, 228 | "Address: low-level delegate call failed" 229 | ); 230 | } 231 | 232 | /** 233 | * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], 234 | * but performing a delegate call. 235 | * 236 | * _Available since v3.4._ 237 | */ 238 | function functionDelegateCall( 239 | address target, 240 | bytes memory data, 241 | string memory errorMessage 242 | ) internal returns (bytes memory) { 243 | (bool success, bytes memory returndata) = target.delegatecall(data); 244 | return 245 | verifyCallResultFromTarget( 246 | target, 247 | success, 248 | returndata, 249 | errorMessage 250 | ); 251 | } 252 | 253 | /** 254 | * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling 255 | * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. 256 | * 257 | * _Available since v4.8._ 258 | */ 259 | function verifyCallResultFromTarget( 260 | address target, 261 | bool success, 262 | bytes memory returndata, 263 | string memory errorMessage 264 | ) internal view returns (bytes memory) { 265 | if (success) { 266 | if (returndata.length == 0) { 267 | // only check isContract if the call was successful and the return data is empty 268 | // otherwise we already know that it was a contract 269 | require(isContract(target), "Address: call to non-contract"); 270 | } 271 | return returndata; 272 | } else { 273 | _revert(returndata, errorMessage); 274 | } 275 | } 276 | 277 | /** 278 | * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the 279 | * revert reason or using the provided one. 280 | * 281 | * _Available since v4.3._ 282 | */ 283 | function verifyCallResult( 284 | bool success, 285 | bytes memory returndata, 286 | string memory errorMessage 287 | ) internal pure returns (bytes memory) { 288 | if (success) { 289 | return returndata; 290 | } else { 291 | _revert(returndata, errorMessage); 292 | } 293 | } 294 | 295 | function _revert( 296 | bytes memory returndata, 297 | string memory errorMessage 298 | ) private pure { 299 | // Look for revert reason and bubble it up if present 300 | if (returndata.length > 0) { 301 | // The easiest way to bubble the revert reason is using memory via assembly 302 | /// @solidity memory-safe-assembly 303 | assembly { 304 | let returndata_size := mload(returndata) 305 | revert(add(32, returndata), returndata_size) 306 | } 307 | } else { 308 | revert(errorMessage); 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /contracts/Multicall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.9.0) (utils/Multicall.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./Address.sol"; 7 | 8 | /** 9 | * @dev Provides a function to batch together multiple calls in a single external call. 10 | * 11 | * _Available since v4.1._ 12 | */ 13 | abstract contract Multicall { 14 | /** 15 | * @dev Receives and executes a batch of function calls on this contract. 16 | * @custom:oz-upgrades-unsafe-allow-reachable delegatecall 17 | */ 18 | function multicall( 19 | bytes[] calldata data 20 | ) external virtual returns (bytes[] memory results) { 21 | results = new bytes[](data.length); 22 | for (uint256 i = 0; i < data.length; i++) { 23 | results[i] = Address.functionDelegateCall(address(this), data[i]); 24 | } 25 | return results; 26 | } 27 | 28 | /** 29 | * @dev Readonly a batch of function calls on this contract. 30 | * @custom:oz-upgrades-unsafe-allow-reachable delegatecall 31 | */ 32 | function multicallRead( 33 | bytes[] calldata data 34 | ) external view virtual returns (bytes[] memory results) { 35 | results = new bytes[](data.length); 36 | for (uint256 i = 0; i < data.length; i++) { 37 | (, bytes memory response) = address(this).staticcall(data[i]); 38 | results[i] = response; 39 | } 40 | return results; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/TasksManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Web3Task} from "./Web3Task.sol"; 5 | import {AccessControl} from "./AccessControl.sol"; 6 | import {Multicall} from "./Multicall.sol"; 7 | 8 | contract TasksManager is AccessControl, Web3Task, Multicall { 9 | /// @dev Emitted when a task cannot be changed. 10 | error TaskCannotBeChanged(uint256 taskId, Status status); 11 | 12 | /// @dev Emitted when a title is updated. 13 | event TitleUpdated(uint256 indexed taskId, string title); 14 | 15 | /// @dev Emitted when a description is updated. 16 | event DescriptionUpdated(uint256 indexed taskId, string description); 17 | 18 | /// @dev Emitted when an end date is updated. 19 | event EndDateUpdated(uint256 indexed taskId, uint256 endDate); 20 | 21 | /// @dev Emitted when metadata is updated. 22 | event MetadataUpdated(uint256 indexed taskId, string metadata); 23 | 24 | /** 25 | * @dev This function sets a new `title` for a task. 26 | * 27 | * Emits a {TitleUpdated} event. 28 | */ 29 | function setTitle(uint256 _taskId, string memory _title) public virtual { 30 | validateTask(_taskId); 31 | _tasks[_taskId].title = _title; 32 | emit TitleUpdated(_taskId, _title); 33 | } 34 | 35 | /** 36 | * @dev This function sets a new `description` for a task. 37 | * 38 | * Emits a {DescriptionUpdated} event. 39 | */ 40 | function setDescription( 41 | uint256 _taskId, 42 | string memory _description 43 | ) public virtual { 44 | validateTask(_taskId); 45 | _tasks[_taskId].description = _description; 46 | emit DescriptionUpdated(_taskId, _description); 47 | } 48 | 49 | /** 50 | * @dev This function sets a new `endDate` for a task. 51 | * 52 | * Emits a {EndDateUpdated} event. 53 | */ 54 | function setEndDate(uint256 _taskId, uint256 _endDate) public virtual { 55 | validateTask(_taskId); 56 | _tasks[_taskId].endDate = _endDate; 57 | emit EndDateUpdated(_taskId, _endDate); 58 | } 59 | 60 | /** 61 | * @dev This function sets a new `metadata` for a task. 62 | * 63 | * Emits a {MetadataUpdated} event. 64 | * 65 | * NOTE: This function is not restricted by the task status. It can be 66 | * called at any time by the authorized operators. 67 | */ 68 | function setMetadata( 69 | uint256 _taskId, 70 | string memory _metadata 71 | ) public virtual { 72 | validateTask(_taskId); 73 | _tasks[_taskId].metadata = _metadata; 74 | emit MetadataUpdated(_taskId, _metadata); 75 | } 76 | 77 | /** 78 | * @dev This function checks if a task can be changed. 79 | * 80 | * Requirements: 81 | * 82 | * - `_taskId` must be a valid task id withing the right timestamp. 83 | * - `task.status` must not be `Status.Completed` or `Status.Canceled`. 84 | * - `msg.sender` must be an authorized operator (see {AccessControl}). 85 | */ 86 | function validateTask(uint256 _taskId) internal view { 87 | Task memory task = getTask(_taskId); 88 | 89 | if (task.status == Status.Completed || task.status == Status.Canceled) { 90 | revert TaskCannotBeChanged(_taskId, task.status); 91 | } 92 | 93 | if (!hasRole(task.creatorRole, msg.sender)) { 94 | revert Unauthorized(msg.sender); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /contracts/Web3Task.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import {AccessControl} from "./AccessControl.sol"; 6 | import {IWeb3Task} from "./interfaces/IWeb3Task.sol"; 7 | 8 | abstract contract Web3Task is ERC721, AccessControl, IWeb3Task { 9 | /// @dev Current taskId, aslo used as token id. 10 | uint256 private taskId; 11 | 12 | /// @dev Amount of necessary approvals to finish a task. 13 | uint256 private APPROVALS = 2; 14 | 15 | ///@dev Mapping of taskId to Task. 16 | mapping(uint256 => Task) internal _tasks; 17 | 18 | /// @dev Mapping of access control id to balance. 19 | mapping(uint256 => uint256) private _balances; 20 | 21 | /// @dev Mapping of taskId to its approvals for conclusion. 22 | mapping(uint256 => uint256) private _approvals; 23 | 24 | /// @dev Mapping of taskId and address to already voted boolean. 25 | mapping(uint256 => mapping(address => bool)) private _alreadyVoted; 26 | 27 | /// @dev Mapping of address to its tasks. 28 | mapping(address => uint256[]) private _countOfTasks; 29 | 30 | /// @dev Mapping of taskId to points. 31 | mapping(uint256 => string[]) private _reviewed; 32 | 33 | /// @dev Mapping of taskId to creation time. 34 | mapping(uint256 => uint256) private _createTime; 35 | 36 | /// @dev Mapping of address to points. 37 | mapping(address => uint256) private _points; 38 | 39 | /** 40 | * @dev Sets the values for {name} and {symbol}. 41 | */ 42 | constructor() ERC721("Web3Task", "W3TH") {} 43 | 44 | /** 45 | * @dev See {IWeb3Task-setMinQuorum}. 46 | */ 47 | function setMinQuorum(uint256 _value) public virtual onlyOwner { 48 | if (_value == 0) { 49 | revert("Invalid minimum quorum"); 50 | } 51 | APPROVALS = _value; 52 | emit QuorumUpdated(_value); 53 | } 54 | 55 | /** 56 | * @dev See {IWeb3Task-createTask}. 57 | */ 58 | function createTask( 59 | Task calldata _task 60 | ) 61 | public 62 | virtual 63 | onlyOperator(this.createTask.selector, _task.creatorRole, msg.sender) 64 | returns (uint256) 65 | { 66 | if (_task.endDate < block.timestamp) { 67 | revert InvalidEndDate(_task.endDate, block.timestamp); 68 | } 69 | 70 | if (_task.status != Status.Created) { 71 | revert InvalidStatus(_task.status); 72 | } 73 | 74 | uint256 balance = _balances[_task.creatorRole]; 75 | if (_task.reward > balance || _task.reward < 10e12) { 76 | revert InsufficientBalance(balance, _task.reward); 77 | } 78 | 79 | // Overflow not possible: taskId <= max uint256. 80 | unchecked { 81 | taskId++; 82 | } 83 | 84 | _tasks[taskId] = _task; 85 | _countOfTasks[msg.sender].push(taskId); 86 | _createTime[taskId] = block.timestamp; 87 | 88 | emit TaskCreated( 89 | taskId, 90 | msg.sender, 91 | _task.assignee, 92 | _task.reward, 93 | _task.endDate 94 | ); 95 | 96 | return taskId; 97 | } 98 | 99 | /** 100 | * @dev See {IWeb3Task-startTask}. 101 | */ 102 | function startTask( 103 | uint256 _taskId, 104 | uint256 _roleId 105 | ) 106 | public 107 | virtual 108 | onlyOperator(this.startTask.selector, _roleId, msg.sender) 109 | returns (bool) 110 | { 111 | Task memory task = getTask(_taskId); 112 | 113 | if (task.status != Status.Created) { 114 | revert InvalidStatus(task.status); 115 | } 116 | 117 | if (!_isRoleAllowed(task.authorizedRoles, _roleId)) { 118 | revert InvalidRoleId(_roleId); 119 | } 120 | 121 | if (task.assignee == address(0)) { 122 | _tasks[_taskId].assignee = msg.sender; 123 | } else if (task.assignee != msg.sender) { 124 | revert Unauthorized(msg.sender); 125 | } 126 | 127 | _tasks[_taskId].status = Status.Progress; 128 | _countOfTasks[task.assignee].push(_taskId); 129 | 130 | emit TaskStarted(_taskId, msg.sender); 131 | 132 | return true; 133 | } 134 | 135 | /** 136 | * @dev See {IWeb3Task-reviewTask}. 137 | */ 138 | function reviewTask( 139 | uint256 _taskId, 140 | uint256 _roleId, 141 | string memory _metadata 142 | ) 143 | public 144 | virtual 145 | onlyOperator(this.reviewTask.selector, _roleId, msg.sender) 146 | returns (bool) 147 | { 148 | Task memory task = getTask(_taskId); 149 | 150 | if (task.assignee != msg.sender) { 151 | if (task.creatorRole != _roleId) { 152 | revert Unauthorized(msg.sender); 153 | } 154 | } 155 | 156 | if (task.status == Status.Progress) { 157 | _tasks[_taskId].status = Status.Review; 158 | } else if (task.status != Status.Review) { 159 | revert InvalidStatus(task.status); 160 | } 161 | 162 | _reviewed[_taskId].push(_metadata); 163 | 164 | emit TaskReviewed(_taskId, msg.sender, _metadata); 165 | 166 | return true; 167 | } 168 | 169 | /** 170 | * @dev See {IWeb3Task-completeTask}. 171 | */ 172 | function completeTask( 173 | uint256 _taskId, 174 | uint256 _roleId 175 | ) 176 | public 177 | virtual 178 | onlyOperator(this.completeTask.selector, _roleId, msg.sender) 179 | returns (bool) 180 | { 181 | Task memory task = getTask(_taskId); 182 | 183 | if (task.status != Status.Review) { 184 | revert InvalidStatus(task.status); 185 | } 186 | 187 | if (task.creatorRole != _roleId) { 188 | revert Unauthorized(msg.sender); 189 | } 190 | 191 | if (_alreadyVoted[_taskId][msg.sender]) { 192 | revert AlreadyVoted(msg.sender); 193 | } 194 | 195 | _alreadyVoted[_taskId][msg.sender] = true; 196 | _approvals[_taskId]++; 197 | 198 | if (_approvals[_taskId] >= APPROVALS) { 199 | _tasks[_taskId].status = Status.Completed; 200 | 201 | _mint(task.assignee, _taskId); 202 | _setScore(_taskId, task.reward, task.assignee); 203 | 204 | (bool sent, ) = payable(task.assignee).call{value: task.reward}(""); 205 | if (!sent || task.reward > _balances[_roleId]) { 206 | revert InsufficientBalance(_balances[_roleId], task.reward); 207 | } 208 | 209 | emit TaskUpdated(_taskId, Status.Completed); 210 | emit Withdraw(_roleId, task.assignee, task.reward); 211 | } 212 | 213 | return true; 214 | } 215 | 216 | /** 217 | * @dev See {IWeb3Task-cancelTask}. 218 | */ 219 | function cancelTask( 220 | uint256 _taskId, 221 | uint256 _roleId 222 | ) 223 | public 224 | virtual 225 | onlyOperator(this.cancelTask.selector, _roleId, msg.sender) 226 | returns (bool) 227 | { 228 | Task memory task = getTask(_taskId); 229 | 230 | if (task.status == Status.Canceled || task.status == Status.Completed) { 231 | revert InvalidStatus(task.status); 232 | } 233 | 234 | if (task.creatorRole != _roleId) { 235 | revert Unauthorized(msg.sender); 236 | } 237 | 238 | _tasks[_taskId].status = Status.Canceled; 239 | 240 | emit TaskUpdated(_taskId, Status.Canceled); 241 | 242 | return true; 243 | } 244 | 245 | /** 246 | * @dev See {IWeb3Task-getTask}. 247 | */ 248 | function getTask( 249 | uint256 _taskId 250 | ) public view virtual returns (Task memory) { 251 | Task memory task = _tasks[_taskId]; 252 | 253 | if (task.endDate < block.timestamp) { 254 | if (task.endDate == 0) { 255 | revert InvalidTaskId(_taskId); 256 | } 257 | if (task.status != Status.Completed) { 258 | task.status = Status.Canceled; 259 | } 260 | } 261 | 262 | return task; 263 | } 264 | 265 | /** 266 | * @dev See {IWeb3Task-getUserTasks}. 267 | */ 268 | function getUserTasks( 269 | address _address 270 | ) public view virtual returns (uint256[] memory) { 271 | return _countOfTasks[_address]; 272 | } 273 | 274 | /** 275 | * @dev See {IWeb3Task-getReviews}. 276 | */ 277 | function getReviews( 278 | uint256 _taskId 279 | ) public view virtual returns (string[] memory) { 280 | return _reviewed[_taskId]; 281 | } 282 | 283 | /** 284 | * @dev See {IWeb3Task-getBalance}. 285 | */ 286 | function getBalance(uint256 _roleId) public view virtual returns (uint256) { 287 | return _balances[_roleId]; 288 | } 289 | 290 | /** 291 | * @dev See {IWeb3Task-getTaskId}. 292 | */ 293 | function getTaskId() public view virtual returns (uint256) { 294 | return taskId; 295 | } 296 | 297 | /** 298 | * @dev See {IWeb3Task-getMinQuorum}. 299 | */ 300 | function getMinQuorum() public view virtual returns (uint256) { 301 | return APPROVALS; 302 | } 303 | 304 | /** 305 | * @dev See {IWeb3Task-getQuorumApprovals}. 306 | */ 307 | function getQuorumApprovals( 308 | uint256 _taskId 309 | ) public view virtual returns (uint256) { 310 | return _approvals[_taskId]; 311 | } 312 | 313 | /** 314 | * @dev See {IWeb3Task-getScore}. 315 | */ 316 | function getScore(address _address) public view virtual returns (uint256) { 317 | return _points[_address]; 318 | } 319 | 320 | /** 321 | * @dev See {IWeb3Task-hasVoted}. 322 | */ 323 | function hasVoted( 324 | uint256 _taskId, 325 | address _addr 326 | ) public view virtual returns (bool) { 327 | bool voted = _alreadyVoted[_taskId][_addr]; 328 | return voted; 329 | } 330 | 331 | /** 332 | * @dev See {IWeb3Task-deposit}. 333 | */ 334 | function deposit(uint256 _roleId) public payable virtual returns (bool) { 335 | _balances[_roleId] = msg.value; 336 | emit Deposit(_roleId, msg.sender, msg.value); 337 | return true; 338 | } 339 | 340 | /** 341 | * @dev See {IWeb3Task-withdraw}. 342 | */ 343 | function withdraw( 344 | uint256 _roleId, 345 | uint256 _amount 346 | ) 347 | public 348 | virtual 349 | onlyOperator(this.withdraw.selector, _roleId, msg.sender) 350 | returns (bool) 351 | { 352 | uint256 balance = _balances[_roleId]; 353 | 354 | if (balance < _amount) { 355 | revert InsufficientBalance(_balances[_roleId], _amount); 356 | } 357 | 358 | _balances[_roleId] -= _amount; 359 | 360 | (bool sent, ) = payable(msg.sender).call{value: _amount}(""); 361 | if (!sent) { 362 | revert InsufficientBalance(balance, _amount); 363 | } 364 | 365 | emit Withdraw(_roleId, msg.sender, _amount); 366 | 367 | return true; 368 | } 369 | 370 | /** 371 | * @dev See {IWeb3Task-emergengyWithdraw}. 372 | */ 373 | function emergengyWithdraw() public virtual onlyOwner { 374 | payable(msg.sender).call{value: address(this).balance}(""); 375 | emit Withdraw(0, msg.sender, address(this).balance); 376 | } 377 | 378 | /** 379 | * @dev Will loop in search for a valid assignee's roleId. 380 | * 381 | * IMPORTANT: The user might be an operator of the `startTask` 382 | * function, but might not have the assignee's roleId. In this 383 | * case it will revert. 384 | * 385 | * Since the entire mapping for role structure cannot be looped, 386 | * we ask for the user to point the role he is using to call 387 | * the function. We then check if the auth provided matches any 388 | * of the initially auths, settled in the current task. 389 | */ 390 | function _isRoleAllowed( 391 | uint256[] memory _authorizedRoles, 392 | uint256 _roleId 393 | ) internal pure virtual returns (bool) { 394 | for (uint256 i; i < _authorizedRoles.length; ) { 395 | if (_authorizedRoles[i] == _roleId) { 396 | return true; 397 | } 398 | 399 | // Overflow not possible: i <= max uint256. 400 | unchecked { 401 | i++; 402 | } 403 | } 404 | return false; 405 | } 406 | 407 | /** 408 | * @dev Will set the score after completing the task. 409 | */ 410 | function _setScore( 411 | uint256 _taskId, 412 | uint256 _reward, 413 | address _assignee 414 | ) internal virtual { 415 | uint256 newScore = (block.timestamp - _createTime[_taskId]) * 416 | (_reward / 10e12); 417 | _points[_assignee] += newScore; 418 | _points[msg.sender] += newScore; 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /contracts/interfaces/IWeb3Task.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface IWeb3Task { 5 | /** 6 | * @dev Emitted when `msg.sender` already voted for a specific task. 7 | */ 8 | error AlreadyVoted(address voter); 9 | 10 | /** 11 | * @dev Emitted when authorization Id (Role) does not hold enough balance to operate. 12 | */ 13 | error InsufficientBalance(uint256 balance, uint256 withdrawAmount); 14 | 15 | /** 16 | * @dev Emitted when `endDate` is less than `block.timestamp`. 17 | */ 18 | error InvalidEndDate(uint256 endDate, uint256 blockTimestamp); 19 | 20 | /** 21 | * @dev Emitted when `status` provided mismatches the one asked by the function. 22 | */ 23 | error InvalidStatus(Status status); 24 | 25 | /** 26 | * @dev Emitted when `taskId` is not valid when calling {Web3Task-getTask}. 27 | */ 28 | error InvalidTaskId(uint256 taskId); 29 | 30 | /** 31 | * @dev Emmited when the minimum `APPROVALS` to complete a task is updated. 32 | */ 33 | event QuorumUpdated(uint256 value); 34 | 35 | /** 36 | * @dev Emitted when a new `taskId` is created. 37 | */ 38 | event TaskCreated( 39 | uint256 indexed taskId, 40 | address indexed creator, 41 | address indexed assignee, 42 | uint256 reward, 43 | uint256 endDate 44 | ); 45 | 46 | /** 47 | * @dev Emitted when a task is started. 48 | */ 49 | event TaskStarted(uint256 indexed taskId, address indexed assignee); 50 | 51 | /** 52 | * @dev Emitted when a task is reviewed. 53 | */ 54 | event TaskUpdated(uint256 indexed taskId, Status status); 55 | 56 | /** 57 | * @dev Emitted when a review is pushed for a task. 58 | */ 59 | event TaskReviewed( 60 | uint256 indexed taskId, 61 | address indexed reviewer, 62 | string metadata 63 | ); 64 | 65 | /** 66 | * @dev Emitted when a deposit is made. 67 | */ 68 | event Deposit( 69 | uint256 indexed authorizationId, 70 | address indexed depositor, 71 | uint256 amount 72 | ); 73 | 74 | /** 75 | * @dev Emitted when a withdraw is made. 76 | */ 77 | event Withdraw( 78 | uint256 indexed authorizationId, 79 | address indexed withdrawer, 80 | uint256 amount 81 | ); 82 | 83 | /** 84 | * @dev Enum representing the possible states of a task. 85 | */ 86 | enum Status { 87 | Created, 88 | Progress, 89 | Review, 90 | Completed, 91 | Canceled 92 | } 93 | 94 | /** 95 | * @dev Core struct of a task. 96 | */ 97 | struct Task { 98 | Status status; 99 | string title; 100 | string description; 101 | uint256 reward; 102 | uint256 endDate; 103 | uint256[] authorizedRoles; 104 | uint256 creatorRole; 105 | address assignee; 106 | string metadata; 107 | } 108 | 109 | /** 110 | * @dev This function sets the minimum quorum of approvals to complete a task. 111 | * 112 | * Emit a {QuorumUpdated} event. 113 | */ 114 | function setMinQuorum(uint256 value) external; 115 | 116 | /** 117 | * @dev The core function to create a task. 118 | * 119 | * Will increment global `_taskId`. 120 | * 121 | * Emits a {TaskCreated} event. 122 | * 123 | * Requirements: 124 | * 125 | * - `task.endDate` must be greater than `block.timestamp`. 126 | * - `task.status` must be `Status.Created`. 127 | * - `msg.sender` must be an authorized operator (see {AccessControl}). 128 | */ 129 | function createTask(Task calldata task) external returns (uint256); 130 | 131 | /** 132 | * @dev This function starts a task. It will set the `msg.sender` as the 133 | * assignee in case none is provided. Meaning anyone with the authorization 134 | * can start the task. 135 | * 136 | * The task status will be set to `Status.Progress` and the next step is 137 | * to execute the task and call a {reviewTask} when ready. 138 | * 139 | * Emits a {TaskStarted} event. 140 | * 141 | * Requirements: 142 | * 143 | * - `_taskId` must be a valid task id. 144 | * - `task.status` must be `Status.Created`. 145 | * - `task.endDate` must be greater than `block.timestamp`. 146 | * - `msg.sender` must be an authorized operator (see {AccessControl}). 147 | */ 148 | function startTask(uint256 taskId, uint256 authId) external returns (bool); 149 | 150 | /** 151 | * @dev This function reviews a task and let the caller push a metadata. 152 | * 153 | * Metadata is a string that can be used to store any kind of information 154 | * related to the task. It can be used to store a link to a file using IPFS. 155 | * 156 | * IMPORTANT: This function can be called more than once by both task creator 157 | * or asssignee. This is because we want to allow a ping-pong of reviews until 158 | * the due date or completion. This will create a history of reviews that can 159 | * be used to track the progress of the task and raise a dispute if needed. 160 | * 161 | * Emits a {TaskUpdated} event. 162 | * 163 | * Requirements: 164 | * 165 | * - `_taskId` must be a valid task id. 166 | * - `msg.sender` must be `_task.assignee` or the `_task.creator`. 167 | * - `task.status` must be `Status.Progress` or `Status.Review`. 168 | * - `task.endDate` must be greater than `block.timestamp`. 169 | * - `msg.sender` must be an authorized operator (see {AccessControl}). 170 | * 171 | * NOTE: If the status is `Status.Progress` it will be set to `Status.Review` once. 172 | */ 173 | function reviewTask( 174 | uint256 taskId, 175 | uint256 authId, 176 | string memory metadata 177 | ) external returns (bool); 178 | 179 | /** 180 | * @dev This function completes a task and transfers the rewards to the assignee. 181 | * 182 | * The task status will be set to `Status.Completed` and the task will be 183 | * considered done. 184 | * 185 | * The `_task.assignee` will receive the `_task.reward` and also a NFT representing 186 | * the completed task with the tokenId equal to the `_taskId`. 187 | * 188 | * IMPORTANT: The assignee agrees to the reward distribution by completing the 189 | * task and its aware that the task can be disputed by the creator. The assignee 190 | * can also open a dispute if the creator does not approve the completion by 191 | * reaching higher DAO authorities. 192 | * 193 | * Emits a {TaskUpdated} event. 194 | * 195 | * Requirements: 196 | * 197 | * - `_taskId` must be a valid task id. 198 | * - `task.status` must be `Status.Review`. 199 | * - `task.endDate` must be greater than `block.timestamp`. 200 | * - `msg.sender` must be an authorized operator (see {AccessControl}). 201 | * - `msg.sender` can only cast one vote per task. 202 | * - `APPROVALS` must reach the quorum. 203 | */ 204 | function completeTask( 205 | uint256 taskId, 206 | uint256 authId 207 | ) external returns (bool); 208 | 209 | /** 210 | * @dev This function cancels a task and invalidates its continuity. 211 | * 212 | * The task status will be set to `Status.Canceled`. 213 | * 214 | * IMPORTANT: Tasks that were previously set to `Completed` can be 215 | * canceled as well, but the assignee will keep the reward and the NFT. 216 | * 217 | * Emits a {TaskUpdated} event. 218 | * 219 | * Requirements: 220 | * 221 | * - `_taskId` must be a valid task id. 222 | * - `task.status` cannot be `Status.Canceled`. 223 | * - `task.endDate` must be greater than `block.timestamp`. 224 | * - `msg.sender` must be an authorized operator (see {AccessControl}). 225 | */ 226 | function cancelTask(uint256 taskId, uint256 authId) external returns (bool); 227 | 228 | /** 229 | * @dev This function returns a task by its id. 230 | * 231 | * Requirements: 232 | * 233 | * - `_taskId` must exist. 234 | * - `task.endDate` must be greater than `block.timestamp, otherwise 235 | * the task is considered expired. 236 | */ 237 | function getTask(uint256 taskId) external view returns (Task memory); 238 | 239 | /** 240 | * @dev This function returns all tasks created by a given address. 241 | */ 242 | function getUserTasks( 243 | address addr 244 | ) external view returns (uint256[] memory); 245 | 246 | /** 247 | * @dev This function returns all reviews for a given task 248 | */ 249 | function getReviews(uint256 taskId) external view returns (string[] memory); 250 | 251 | /** 252 | * @dev This function returns the balance of a given authorization role. 253 | * 254 | * NOTE! It will return 0 if the authorization role does not exist or 255 | * if the authorization role has not received any deposit. 256 | */ 257 | function getBalance(uint256 roleId) external view returns (uint256); 258 | 259 | /** 260 | * @dev This function returns the last taskId created. 261 | * 262 | * NOTE! taskId is an incremental number that starts at 1. 263 | * If no task was created, it will return 0. 264 | */ 265 | function getTaskId() external view returns (uint256); 266 | 267 | /** 268 | * @dev This function returns the minimum approvals required to complete a task. 269 | * 270 | * NOTE! The Quorum can be updated by the contract owner. And it will 271 | * emit a {QuorumUpdated} event. 272 | */ 273 | function getMinQuorum() external view returns (uint256); 274 | 275 | /** 276 | * @dev This function returns the amount of approvals casted into a task. 277 | */ 278 | function getQuorumApprovals( 279 | uint256 _taskId 280 | ) external view returns (uint256); 281 | 282 | /** 283 | * @dev This function returns the score of a given address. 284 | */ 285 | function getScore(address addr) external view returns (uint256); 286 | 287 | /** 288 | * @dev This function returns a boolean if the `addr` has voted for 289 | * a specific task. 290 | */ 291 | function hasVoted( 292 | uint256 taskId, 293 | address addr 294 | ) external view returns (bool); 295 | 296 | /** 297 | * @dev This function allows to deposit funds into the contract into 298 | * a specific authorization role. 299 | * 300 | * If the authorization role is e.g.: "Leader of Marketing" as the `authId` 301 | * number 5, then sending funds to this function passing the id will increase 302 | * the balance of the authorization role by `msg.value`. 303 | * 304 | * Emits a {Deposit} event. 305 | * 306 | * NOTE: Any authorization role id can be used as a parameter, even those 307 | * that are not yet created. For this and more related issues, there is 308 | * an {emergencyWithdraw} in the contract. 309 | */ 310 | function deposit(uint256 authId) external payable returns (bool); 311 | 312 | /** 313 | * @dev This function allows to withdraw funds from the contract from 314 | * a specific authorization role. 315 | * 316 | * Emits a {Withdraw} event. 317 | * 318 | * Requirements: 319 | * 320 | * - `msg.sender` must be an authorized operator (see {AccessControl}). 321 | * - `balance` of the authorization role must be greater than `_amount`. 322 | */ 323 | function withdraw(uint256 authId, uint256 amount) external returns (bool); 324 | 325 | /** 326 | * @dev This function allows to withdraw all funds from the contract. 327 | * 328 | * Emits a {Withdraw} event. 329 | * 330 | * Requirements: 331 | * 332 | * - `msg.sender` must be the contract owner (see {AccessControl}). 333 | */ 334 | function emergengyWithdraw() external; 335 | } 336 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import dotenv from "dotenv"; 4 | dotenv.config(); 5 | 6 | const { POLYGON_URL, MUMBAI_URL, PRIVATE_KEY_LEADER, ETHERSCAN_KEY } = 7 | process.env; 8 | 9 | const config: HardhatUserConfig = { 10 | solidity: { 11 | version: "0.8.20", 12 | settings: { 13 | optimizer: { 14 | enabled: true, 15 | runs: 200, 16 | }, 17 | }, 18 | }, 19 | allowUnlimitedContractSize: true, 20 | networks: { 21 | hardhat: { 22 | chainId: 31337, 23 | forking: { 24 | url: `${POLYGON_URL}`, 25 | blockNumber: 38359528, 26 | }, 27 | }, 28 | mumbai: { 29 | url: `${MUMBAI_URL}`, 30 | accounts: [`${PRIVATE_KEY_LEADER}`], 31 | }, 32 | polygon: { 33 | url: `${POLYGON_URL}`, 34 | accounts: [`${PRIVATE_KEY_LEADER}`], 35 | }, 36 | }, 37 | gasReporter: { 38 | enabled: true, 39 | }, 40 | etherscan: { 41 | apiKey: `${ETHERSCAN_KEY}`, 42 | }, 43 | }; 44 | 45 | export default config; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web3task", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "npx hardhat clean", 8 | "compile": "npx hardhat compile", 9 | "test": "npx hardhat test" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/w3b3d3v/web3task-contracts.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/w3b3d3v/web3task-contracts/issues" 19 | }, 20 | "homepage": "https://github.com/w3b3d3v/web3task-contracts#readme", 21 | "devDependencies": { 22 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", 23 | "@nomicfoundation/hardhat-toolbox": "^2.0.1", 24 | "@nomicfoundation/hardhat-network-helpers": "^1.0.8", 25 | "@nomiclabs/hardhat-ethers": "^2.2.2", 26 | "@nomiclabs/hardhat-etherscan": "^3.1.5", 27 | "@openzeppelin/contracts": "^4.9.3", 28 | "@typechain/ethers-v5": "^10.2.0", 29 | "@typechain/hardhat": "^6.1.5", 30 | "@types/mocha": "^10.0.1", 31 | "chai": "^4.3.4", 32 | "dotenv": "^16.0.3", 33 | "ethers": "^5.6.1", 34 | "hardhat": "^2.12.7", 35 | "hardhat-gas-reporter": "^1.0.9", 36 | "solidity-coverage": "^0.8.2", 37 | "ts-node": "^10.9.1", 38 | "typechain": "^8.1.1", 39 | "typescript": "^4.9.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scripts/cancelTask.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json"; 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.cancelTask(1, 5, { 11 | maxPriorityFeePerGas: 200000000000, 12 | maxFeePerGas: 200000000000, 13 | }); 14 | } 15 | 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/completeTask.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json"; 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.completeTask(1, 5, { 11 | maxPriorityFeePerGas: 200000000000, 12 | maxFeePerGas: 200000000000, 13 | }); 14 | } 15 | 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/createTasks.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { dateNowToUnixTimestamp } from "./utils"; 3 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json"; 4 | 5 | const { CONTRACT_ADDRESS } = process.env; 6 | 7 | function generateRandomReward(): string { 8 | const minReward = 0.00001; 9 | const maxReward = 0.001; 10 | 11 | const reward = minReward + Math.random() * (maxReward - minReward); 12 | return reward.toFixed(6); 13 | } 14 | 15 | async function main() { 16 | const [signer] = await ethers.getSigners(); 17 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 18 | 19 | const currentTimeStamp = await dateNowToUnixTimestamp(); 20 | 21 | for (let i = 0; i <= 50; i++) { 22 | const Task = { 23 | status: 0, 24 | title: `Task ${i + 1}`, 25 | description: `This is the task of id: ${i + 1}`, 26 | reward: ethers.utils.parseEther(generateRandomReward()), 27 | endDate: Number(currentTimeStamp) + 86400, 28 | authorizedRoles: [10, 3], 29 | creatorRole: 5, 30 | assignee: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 31 | metadata: 32 | "https://ipfs.io/ipfs/QmY5DnoeR8KvFQbf2swJcSZrBfXo4icnMuzrjGvj6q7CEh", 33 | }; 34 | 35 | await contract.createTask(Task, { 36 | maxPriorityFeePerGas: 200000000000, 37 | maxFeePerGas: 200000000000, 38 | }); 39 | } 40 | } 41 | 42 | main().catch((error) => { 43 | console.error(error); 44 | process.exitCode = 1; 45 | }); 46 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { saveContractAddress, saveFrontendFiles } from "../utils/saveDataContract" 3 | 4 | async function main() { 5 | const [deployer] = await ethers.getSigners(); 6 | const Factory = await ethers.getContractFactory("TasksManager", deployer); 7 | const Contract = await Factory.deploy({ 8 | gasLimit: 10000000, 9 | maxPriorityFeePerGas: 200000000000, 10 | maxFeePerGas: 200000000000, 11 | }); 12 | 13 | const chain = await deployer.getChainId(); 14 | saveFrontendFiles(Contract, chain) 15 | 16 | console.log( 17 | "Deploying the Web3Task contract with the address:", 18 | deployer.address 19 | ); 20 | 21 | saveContractAddress(Contract.address) 22 | 23 | console.log("Contract deployed to:", Contract.address); 24 | } 25 | 26 | main().catch((error) => { 27 | console.error(error); 28 | process.exitCode = 1; 29 | }); -------------------------------------------------------------------------------- /scripts/emergengyWithdraw.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/Web3Task.sol/Web3Task.json" 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | await contract.emergengyWithdraw(); 10 | } 11 | 12 | main().catch((error) => { 13 | console.error(error); 14 | process.exitCode = 1; 15 | }); -------------------------------------------------------------------------------- /scripts/fundingContract.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/Web3Task.sol/Web3Task.json"; 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.deposit(5, { 11 | value: ethers.utils.parseEther("5"), 12 | maxPriorityFeePerGas: 200000000000, 13 | maxFeePerGas: 200000000000, 14 | }); 15 | } 16 | 17 | main().catch((error) => { 18 | console.error(error); 19 | process.exitCode = 1; 20 | }); 21 | -------------------------------------------------------------------------------- /scripts/getMulticallTask.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json"; 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | /// Get the signer and connect to the contract 9 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 10 | 11 | /// Prepare the encoding of data and submit it to the contract 12 | const payloadArray = []; 13 | for (var i = 1; i <= 10; i++) { 14 | payloadArray.push(contract.interface.encodeFunctionData("getTask", [i])); 15 | } 16 | const response = await contract.multicallRead(payloadArray); 17 | 18 | /// Decode the results 19 | let decodedResults = []; 20 | /// Get the sighash of the function 21 | let getTaskID = contract.interface.getSighash("getTask(uint256)"); 22 | /// Map the results to the function name and the decoded arguments 23 | decodedResults = response.map((res: any) => { 24 | try { 25 | const decodedArgs = contract.interface.decodeFunctionResult( 26 | getTaskID, 27 | res 28 | ); 29 | return { 30 | name: contract.interface.getFunction(getTaskID).name, 31 | args: decodedArgs, 32 | }; 33 | } catch (error) { 34 | console.log("Could not decode result", error); 35 | } 36 | }); 37 | 38 | /// Print the result 39 | console.log(decodedResults); 40 | 41 | /// How to fetch the results (double array) 42 | console.log(decodedResults[0].args[0]); 43 | } 44 | 45 | main().catch((error) => { 46 | console.error(error); 47 | process.exitCode = 1; 48 | }); -------------------------------------------------------------------------------- /scripts/getTask.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/Web3Task.sol/Web3Task.json" 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | const getTaskResponse = await contract.getTask(1); 10 | console.log('GetTaskResponse = ', getTaskResponse); 11 | } 12 | 13 | main().catch((error) => { 14 | console.error(error); 15 | process.exitCode = 1; 16 | }); -------------------------------------------------------------------------------- /scripts/reviewTask.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json" 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.reviewTask(8, 3, "Executed", { 11 | maxPriorityFeePerGas: 200000000000, 12 | maxFeePerGas: 200000000000, 13 | }); 14 | } 15 | 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); -------------------------------------------------------------------------------- /scripts/setOperator.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json"; 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | var sigHash = contract.interface.getSighash("createTask"); 11 | await contract.setOperator(sigHash, 5, true, { 12 | maxPriorityFeePerGas: 200000000000, 13 | maxFeePerGas: 200000000000, 14 | }); 15 | 16 | var sigHash = contract.interface.getSighash("startTask"); 17 | await contract.setOperator(sigHash, 10, true, { 18 | maxPriorityFeePerGas: 200000000000, 19 | maxFeePerGas: 200000000000, 20 | }); 21 | 22 | var sigHash = contract.interface.getSighash("reviewTask"); 23 | await contract.setOperator(sigHash, 5, true, { 24 | maxPriorityFeePerGas: 200000000000, 25 | maxFeePerGas: 200000000000, 26 | }); 27 | 28 | var sigHash = contract.interface.getSighash("reviewTask"); 29 | await contract.setOperator(sigHash, 10, true, { 30 | maxPriorityFeePerGas: 200000000000, 31 | maxFeePerGas: 200000000000, 32 | }); 33 | 34 | var sigHash = contract.interface.getSighash("completeTask"); 35 | await contract.setOperator(sigHash, 5, true, { 36 | maxPriorityFeePerGas: 200000000000, 37 | maxFeePerGas: 200000000000, 38 | }); 39 | 40 | var sigHash = contract.interface.getSighash("cancelTask"); 41 | await contract.setOperator(sigHash, 5, true, { 42 | maxPriorityFeePerGas: 200000000000, 43 | maxFeePerGas: 200000000000, 44 | }); 45 | } 46 | 47 | main().catch((error) => { 48 | console.error(error); 49 | process.exitCode = 1; 50 | }); 51 | -------------------------------------------------------------------------------- /scripts/setQuorum.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json" 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.setMinQuorum(1, { 11 | maxPriorityFeePerGas: 200000000000, 12 | maxFeePerGas: 200000000000, 13 | }); 14 | } 15 | 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); -------------------------------------------------------------------------------- /scripts/setRole.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json"; 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.setRole( 11 | 5, 12 | "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 13 | true, 14 | { 15 | maxPriorityFeePerGas: 200000000000, 16 | maxFeePerGas: 200000000000, 17 | } 18 | ); 19 | 20 | await contract.setRole( 21 | 10, 22 | "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 23 | true, 24 | { 25 | maxPriorityFeePerGas: 200000000000, 26 | maxFeePerGas: 200000000000, 27 | } 28 | ); 29 | } 30 | 31 | main().catch((error) => { 32 | console.error(error); 33 | process.exitCode = 1; 34 | }); 35 | -------------------------------------------------------------------------------- /scripts/startTask.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/TasksManager.sol/TasksManager.json"; 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.startTask(1, 10, { 11 | maxPriorityFeePerGas: 200000000000, 12 | maxFeePerGas: 200000000000, 13 | }); 14 | } 15 | 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | export async function dateNowToUnixTimestamp() { 2 | const currentTimestampMs = Date.now(); 3 | const currentTimestampSec = Math.floor(currentTimestampMs / 1000); 4 | const uint256Timestamp = BigInt(currentTimestampSec); 5 | return uint256Timestamp; 6 | } 7 | 8 | export async function unixTimestampToDateNow(timestamp: any) { 9 | const unixTimestamp = timestamp * 1000; // Convert to milliseconds (Unix timestamps are in seconds) 10 | const date = new Date(unixTimestamp); 11 | const formattedDate = date.toLocaleString(); 12 | return formattedDate; 13 | } 14 | 15 | module.exports = { 16 | dateNowToUnixTimestamp, 17 | unixTimestampToDateNow, 18 | }; 19 | -------------------------------------------------------------------------------- /scripts/withdraw.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import abi from "../artifacts/contracts/Web3Task.sol/Web3Task.json" 3 | 4 | const { CONTRACT_ADDRESS } = process.env; 5 | 6 | async function main() { 7 | const [signer] = await ethers.getSigners(); 8 | const contract = new ethers.Contract(`${CONTRACT_ADDRESS}`, abi.abi, signer); 9 | 10 | await contract.withdraw(10, { value: ethers.utils.parseEther('100') }); 11 | } 12 | 13 | main().catch((error) => { 14 | console.error(error); 15 | process.exitCode = 1; 16 | }); -------------------------------------------------------------------------------- /test/Web3Task.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { Contract, ContractFactory } from "ethers"; 3 | import { expect } from "chai"; 4 | 5 | describe("Web3Task", function () { 6 | let Web3Task: Contract; 7 | let address: any; 8 | let owner: any; 9 | let userA: any; 10 | let userB: any; 11 | let userC: any; 12 | let leaderId = 5; 13 | let memberId = 10; 14 | let createdTaskId: any; 15 | const taskTitle = "123"; 16 | const taskDescription = "123"; 17 | 18 | enum Status { 19 | Created, 20 | Progress, 21 | Review, 22 | Completed, 23 | Canceled, 24 | } 25 | 26 | before(async function () { 27 | [owner, userA, userB, userC] = await ethers.getSigners(); 28 | 29 | const factory: ContractFactory = await ethers.getContractFactory( 30 | "TasksManager", 31 | owner 32 | ); 33 | 34 | Web3Task = await factory.deploy(); 35 | 36 | await Web3Task.deployed(); 37 | 38 | address = Web3Task.address; 39 | }); 40 | 41 | it("should set the minimum quorum to 2", async function () { 42 | await Web3Task.connect(owner).setMinQuorum(2); 43 | expect(await Web3Task.getMinQuorum()).to.equal(2); 44 | }); 45 | 46 | it("should fund the authorizationId (Role) {leader = 5}", async function () { 47 | await Web3Task.connect(owner).deposit(leaderId, { 48 | value: ethers.utils.parseEther("10"), 49 | }); 50 | }); 51 | 52 | it("should fund the authorizationId (Role) {memberId = 10}", async function () { 53 | await Web3Task.connect(owner).deposit(memberId, { 54 | value: ethers.utils.parseEther("50"), 55 | }); 56 | }); 57 | 58 | it("should set owner as withdraw operator, then withdraw from it", async function () { 59 | await Web3Task.connect(owner).setOperator( 60 | Web3Task.interface.getSighash("withdraw"), 61 | leaderId, 62 | true 63 | ); 64 | 65 | expect( 66 | await Web3Task.connect(owner).withdraw( 67 | leaderId, 68 | ethers.utils.parseEther("0.1") 69 | ) 70 | ) 71 | .to.emit(Web3Task, "Withdraw") 72 | .withArgs(leaderId, owner.address, ethers.utils.parseEther("0.1")); 73 | }); 74 | 75 | it("should create new authorizations { leader, member }", async function () { 76 | expect(await Web3Task.setRole(leaderId, userA.address, true)) 77 | .to.emit(Web3Task, "AuthorizePersonnel") 78 | .withArgs(leaderId, userA.address, true); 79 | expect(await Web3Task.setRole(leaderId, userB.address, true)) 80 | .to.emit(Web3Task, "AuthorizePersonnel") 81 | .withArgs(leaderId, userB.address, true); 82 | expect(await Web3Task.setRole(memberId, userC.address, true)) 83 | .to.emit(Web3Task, "AuthorizePersonnel") 84 | .withArgs(memberId, userC.address, true); 85 | }); 86 | 87 | it("should fail to create new authorizations { id = 0 }", async function () { 88 | await expect( 89 | Web3Task.setRole(0, userA.address, true) 90 | ).to.be.revertedWithCustomError(Web3Task, "InvalidRoleId"); 91 | }); 92 | 93 | it("should fail to create new operator { id = 1 }", async function () { 94 | let interfaceId = Web3Task.interface.getSighash("createTask"); 95 | expect( 96 | await Web3Task.setOperator(interfaceId, leaderId, true) 97 | ).to.be.revertedWithCustomError(Web3Task, "InvalidRoleId"); 98 | }); 99 | 100 | it("should fail to create new authorizations { sender != owner }", async function () { 101 | await expect( 102 | Web3Task.connect(userA).setRole(leaderId, userA.address, true) 103 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 104 | }); 105 | 106 | it("should create new operator { createTask }", async function () { 107 | let interfaceId = Web3Task.interface.getSighash("createTask"); 108 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 109 | .to.emit(Web3Task, "AuthorizeOperator") 110 | .withArgs(interfaceId, leaderId, true); 111 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 112 | }); 113 | 114 | it("should create new operator { startTask }", async function () { 115 | let interfaceId = Web3Task.interface.getSighash("startTask"); 116 | await expect(await Web3Task.setOperator(interfaceId, memberId, true)) 117 | .to.emit(Web3Task, "AuthorizeOperator") 118 | .withArgs(interfaceId, memberId, true); 119 | expect(await Web3Task.isOperator(interfaceId, memberId)).to.be.equal(true); 120 | }); 121 | 122 | it("should create new operator { reviewTask }", async function () { 123 | let interfaceId = Web3Task.interface.getSighash("reviewTask"); 124 | expect(await Web3Task.setOperator(interfaceId, memberId, true)) 125 | .to.emit(Web3Task, "AuthorizeOperator") 126 | .withArgs(interfaceId, memberId, true); 127 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 128 | .to.emit(Web3Task, "AuthorizeOperator") 129 | .withArgs(interfaceId, leaderId, true); 130 | expect(await Web3Task.isOperator(interfaceId, memberId)).to.be.equal(true); 131 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 132 | }); 133 | 134 | it("should create new operator { completeTask }", async function () { 135 | let interfaceId = Web3Task.interface.getSighash("completeTask"); 136 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 137 | .to.emit(Web3Task, "AuthorizeOperator") 138 | .withArgs(interfaceId, leaderId, true); 139 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 140 | }); 141 | 142 | it("should create new operator { cancelTask }", async function () { 143 | let interfaceId = Web3Task.interface.getSighash("cancelTask"); 144 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 145 | .to.emit(Web3Task, "AuthorizeOperator") 146 | .withArgs(interfaceId, leaderId, true); 147 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 148 | }); 149 | 150 | it("should create new operator { setTitle }", async function () { 151 | let interfaceId = Web3Task.interface.getSighash("setTitle"); 152 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 153 | .to.emit(Web3Task, "AuthorizeOperator") 154 | .withArgs(interfaceId, leaderId, true); 155 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 156 | }); 157 | 158 | it("should create new operator { setDescription }", async function () { 159 | let interfaceId = Web3Task.interface.getSighash("setDescription"); 160 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 161 | .to.emit(Web3Task, "AuthorizeOperator") 162 | .withArgs(interfaceId, leaderId, true); 163 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 164 | }); 165 | 166 | it("should create new operator { setEndDate }", async function () { 167 | let interfaceId = Web3Task.interface.getSighash("setEndDate"); 168 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 169 | .to.emit(Web3Task, "AuthorizeOperator") 170 | .withArgs(interfaceId, leaderId, true); 171 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 172 | }); 173 | 174 | it("should create new operator { setMetadata }", async function () { 175 | let interfaceId = Web3Task.interface.getSighash("setMetadata"); 176 | expect(await Web3Task.setOperator(interfaceId, leaderId, true)) 177 | .to.emit(Web3Task, "AuthorizeOperator") 178 | .withArgs(interfaceId, leaderId, true); 179 | expect(await Web3Task.isOperator(interfaceId, leaderId)).to.be.equal(true); 180 | }); 181 | 182 | it("should fail to create new operator { sender != owner }", async function () { 183 | let interfaceId = Web3Task.interface.getSighash("createTask"); 184 | await expect( 185 | Web3Task.connect(userA).setOperator(interfaceId, leaderId, true) 186 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 187 | }); 188 | 189 | it("should create new task", async function () { 190 | // Get the current block timestamp 191 | const latestBlock = await ethers.provider.getBlock("latest"); 192 | const currentBlockTimeStamp = latestBlock.timestamp; 193 | 194 | const Task = { 195 | status: 0, 196 | title: "Pagar membros do PodLabs", 197 | description: "Não esquecer", 198 | reward: ethers.utils.parseEther("1"), 199 | endDate: currentBlockTimeStamp + 86400, 200 | authorizedRoles: [memberId], 201 | creatorRole: leaderId, 202 | assignee: userC.address, 203 | metadata: "ipfs://0xc0/", 204 | }; 205 | 206 | const tx = await Web3Task.connect(userA).createTask(Task); 207 | const receipt = await tx.wait(); 208 | const taskId = receipt.events[0].args[0]; 209 | 210 | createdTaskId = taskId; 211 | 212 | const task = await Web3Task.getTask(taskId); 213 | expect(Task.title).equal(task.title); 214 | }); 215 | 216 | it("should fail to create new task (unauthorized user - userC)", async function () { 217 | // Get the current block timestamp 218 | const latestBlock = await ethers.provider.getBlock("latest"); 219 | const currentBlockTimeStamp = latestBlock.timestamp; 220 | 221 | const Task = { 222 | status: 0, 223 | title: "Pagar membros do PodLabs", 224 | description: "Não esquecer", 225 | reward: ethers.utils.parseEther("1"), 226 | endDate: currentBlockTimeStamp + 86400, 227 | authorizedRoles: [memberId], 228 | creatorRole: leaderId, 229 | assignee: userB.address, 230 | metadata: "ipfs://0xc0/", 231 | }; 232 | 233 | await expect( 234 | Web3Task.connect(userC).createTask(Task) 235 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 236 | }); 237 | 238 | it("should fail to create new task (invalid leaderId)", async function () { 239 | // Get the current block timestamp 240 | const latestBlock = await ethers.provider.getBlock("latest"); 241 | const currentBlockTimeStamp = latestBlock.timestamp; 242 | 243 | const Task = { 244 | status: 0, 245 | title: "Pagar membros do PodLabs", 246 | description: "Não esquecer", 247 | reward: ethers.utils.parseEther("1"), 248 | endDate: currentBlockTimeStamp + 86400, 249 | authorizedRoles: [memberId], 250 | creatorRole: 200, 251 | assignee: userB.address, 252 | metadata: "ipfs://0xc0/", 253 | }; 254 | 255 | await expect( 256 | Web3Task.connect(userA).createTask(Task) 257 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 258 | }); 259 | 260 | it("should fail to create new task (invalid status - Progress (1))", async function () { 261 | // Get the current block timestamp 262 | const latestBlock = await ethers.provider.getBlock("latest"); 263 | const currentBlockTimeStamp = latestBlock.timestamp; 264 | 265 | const Task = { 266 | status: 1, 267 | title: "Pagar membros do PodLabs", 268 | description: "Não esquecer", 269 | reward: ethers.utils.parseEther("1"), 270 | endDate: currentBlockTimeStamp + 86400, 271 | authorizedRoles: [memberId], 272 | creatorRole: leaderId, 273 | assignee: userB.address, 274 | metadata: "ipfs://0xc0/", 275 | }; 276 | 277 | await expect( 278 | Web3Task.connect(userA).createTask(Task) 279 | ).to.be.revertedWithCustomError(Web3Task, "InvalidStatus"); 280 | }); 281 | 282 | it("should fail to create new task (invalid end date)", async function () { 283 | const expiredDate = (await ethers.provider.getBlock("latest")).timestamp; 284 | 285 | const Task = { 286 | status: 0, 287 | title: "Pagar membros do PodLabs", 288 | description: "Não esquecer", 289 | reward: ethers.utils.parseEther("1"), 290 | endDate: expiredDate - 1, 291 | authorizedRoles: [memberId], 292 | creatorRole: leaderId, 293 | assignee: userB.address, 294 | metadata: "ipfs://0xc0/", 295 | }; 296 | 297 | await expect( 298 | Web3Task.connect(userA).createTask(Task) 299 | ).to.be.revertedWithCustomError(Web3Task, "InvalidEndDate"); 300 | }); 301 | 302 | it("should set title", async function () { 303 | expect(await Web3Task.connect(userA).setTitle(createdTaskId, taskTitle)) 304 | .to.emit(Web3Task, "TitleUpdated") 305 | .withArgs(createdTaskId, taskTitle); 306 | const task = await Web3Task.getTask(createdTaskId); 307 | expect(task.title).to.equal(taskTitle); 308 | }); 309 | 310 | it("should set description", async function () { 311 | expect( 312 | await Web3Task.connect(userA).setDescription( 313 | createdTaskId, 314 | taskDescription 315 | ) 316 | ) 317 | .to.emit(Web3Task, "TitleUpdated") 318 | .withArgs(createdTaskId, taskDescription); 319 | const task = await Web3Task.getTask(createdTaskId); 320 | expect(task.description).to.equal(taskDescription); 321 | }); 322 | 323 | it("should set endDate", async function () { 324 | const targetDate = Math.floor(Date.now() / 1000) + 3600; 325 | expect(await Web3Task.connect(userA).setEndDate(createdTaskId, targetDate)) 326 | .to.emit(Web3Task, "TitleUpdated") 327 | .withArgs(createdTaskId, targetDate); 328 | const task = await Web3Task.getTask(createdTaskId); 329 | expect(task.endDate).to.equal(targetDate); 330 | }); 331 | 332 | it("should set metadata", async function () { 333 | expect(await Web3Task.connect(userA).setTitle(createdTaskId, taskTitle)) 334 | .to.emit(Web3Task, "MetadataUpdated") 335 | .withArgs(createdTaskId, taskTitle); 336 | const task = await Web3Task.getTask(createdTaskId); 337 | expect(task.title).to.equal(taskTitle); 338 | }); 339 | 340 | it("should start task", async function () { 341 | expect(await Web3Task.connect(userC).startTask(createdTaskId, memberId)) 342 | .to.emit(Web3Task, "TaskStarted") 343 | .withArgs(createdTaskId, userC.address); 344 | const task = await Web3Task.getTask(createdTaskId); 345 | expect(task.status).to.equal(Status.Progress); 346 | }); 347 | 348 | it("should review task", async function () { 349 | const task2 = await Web3Task.getTask(createdTaskId); 350 | expect(task2.assignee == userC.address).to.equal(true); 351 | expect( 352 | await Web3Task.connect(userC).reviewTask( 353 | createdTaskId, 354 | memberId, 355 | "ipfs link" 356 | ) 357 | ) 358 | .to.emit(Web3Task, "TaskUpdated") 359 | .withArgs(createdTaskId, Status.Review); 360 | 361 | // Leaders can also call review to place a note during 362 | // the task review process. This is supposed to ping pong 363 | // between the leader and the assignee until the task is 364 | // completed. 365 | expect( 366 | await Web3Task.connect(userB).reviewTask( 367 | createdTaskId, 368 | leaderId, 369 | "ipfs link" 370 | ) 371 | ) 372 | .to.emit(Web3Task, "TaskUpdated") 373 | .withArgs(createdTaskId, Status.Review); 374 | 375 | const task = await Web3Task.getTask(createdTaskId); 376 | expect(task.status).to.equal(Status.Review); 377 | }); 378 | 379 | it("should complete task ", async function () { 380 | expect(await Web3Task.connect(userA).completeTask(createdTaskId, leaderId)) 381 | .to.be.ok; 382 | expect(await Web3Task.connect(userB).completeTask(createdTaskId, leaderId)) 383 | .to.emit(Web3Task, "TaskUpdated") 384 | .withArgs(createdTaskId, Status.Completed); 385 | 386 | const task = await Web3Task.getTask(createdTaskId); 387 | expect(task.status).to.equal(Status.Completed); 388 | }); 389 | 390 | it("should cancel task", async function () { 391 | expect(await Web3Task.connect(userA).cancelTask(createdTaskId, leaderId)) 392 | .to.emit(Web3Task, "TaskUpdated") 393 | .withArgs(createdTaskId, Status.Canceled); 394 | }); 395 | 396 | it("should set title failure", async function () { 397 | await expect(Web3Task.connect(userA).setTitle(createdTaskId, taskTitle)) 398 | .to.be.revertedWithCustomError(Web3Task, "TaskCannotBeChanged") 399 | .withArgs(createdTaskId, Status.Canceled); 400 | }); 401 | 402 | //New-test - Testing setting minQuorum with no owner 403 | it("should fail to set the minimum quorum when the caller is not the owner", async function () { 404 | const initialQuorum = 2; 405 | const newValue = 5; 406 | await expect( 407 | Web3Task.connect(userA).setMinQuorum(newValue) 408 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 409 | const currentQuorum = await Web3Task.getMinQuorum(); 410 | expect(currentQuorum).to.equal(initialQuorum); 411 | }); 412 | 413 | //New-test - Testing setting minQuorum with zero or negative value 414 | it("should fail when a negative value is set for the minimum quorum", async function () { 415 | await expect(Web3Task.connect(owner).setMinQuorum(-2)).to.be.rejected; 416 | }); 417 | 418 | //New-test - Testing creating a task with an invalid status (1 - 4) 419 | it("should fail to create new task ( invalid status - Progress (1), Review (2), Completed (3), Canceled (4) )", async function () { 420 | // Get the current block timestamp 421 | const latestBlock = await ethers.provider.getBlock("latest"); 422 | const currentBlockTimeStamp = latestBlock.timestamp; 423 | 424 | const invalidStatuses = [1, 2, 3, 4]; 425 | for (const status of invalidStatuses) { 426 | const Task = { 427 | status: status, 428 | title: "Invalid status task", 429 | description: "Não esquecer", 430 | reward: ethers.utils.parseEther("1"), 431 | endDate: currentBlockTimeStamp + 86400, 432 | authorizedRoles: [memberId], 433 | creatorRole: leaderId, 434 | assignee: userC.address, 435 | metadata: "ipfs://0xc0/", 436 | }; 437 | await expect( 438 | Web3Task.connect(userA).createTask(Task) 439 | ).to.be.revertedWithCustomError(Web3Task, "InvalidStatus"); 440 | } 441 | }); 442 | 443 | //New-test - Testing creating two tasks with the same parameters to return different IDs 444 | it("should create a new task with the same parameters as different tasks, not duplicate", async function () { 445 | // Get the current block timestamp 446 | const latestBlock = await ethers.provider.getBlock("latest"); 447 | const currentBlockTimeStamp = latestBlock.timestamp; 448 | 449 | const Task = { 450 | status: 0, 451 | title: "Task with same parameters", 452 | description: "Task description", 453 | reward: ethers.utils.parseEther("1"), 454 | endDate: currentBlockTimeStamp + 86400, 455 | authorizedRoles: [memberId], 456 | creatorRole: leaderId, 457 | assignee: userC.address, 458 | metadata: "ipfs://0xc0/", 459 | }; 460 | 461 | let tx = await Web3Task.connect(userA).createTask(Task); 462 | let receipt = await tx.wait(); 463 | let firstTaskId = receipt.events[0].args[0]; 464 | 465 | tx = await Web3Task.connect(userA).createTask(Task); 466 | receipt = await tx.wait(); 467 | let secondTaskId = receipt.events[0].args[0]; 468 | 469 | expect(firstTaskId).to.not.equal(secondTaskId); 470 | }); 471 | 472 | //New-test - Testing creating a task with an invalid reward (greater than the balance) 473 | it("should fail to create a task with a reward greater than the balance", async function () { 474 | // Get the current block timestamp 475 | const latestBlock = await ethers.provider.getBlock("latest"); 476 | const currentBlockTimeStamp = latestBlock.timestamp; 477 | 478 | const Task = { 479 | status: 0, 480 | title: "Task with high reward", 481 | description: "Task description", 482 | reward: ethers.utils.parseEther("200"), 483 | endDate: currentBlockTimeStamp + 86400, 484 | authorizedRoles: [memberId], 485 | creatorRole: leaderId, 486 | assignee: userC.address, 487 | metadata: "ipfs://0xc0/", 488 | }; 489 | await expect( 490 | Web3Task.connect(userA).createTask(Task) 491 | ).to.be.revertedWithCustomError(Web3Task, "InsufficientBalance"); 492 | }); 493 | 494 | //Net-Test - Testing creating a task with an unautorized user 495 | it("should fail to start a task (unauthorized user)", async function () { 496 | // Get the current block timestamp 497 | const latestBlock = await ethers.provider.getBlock("latest"); 498 | const currentBlockTimeStamp = latestBlock.timestamp; 499 | 500 | const Task = { 501 | status: 0, 502 | title: "Pagar membros do PodLabs", 503 | description: "Não esquecer", 504 | reward: ethers.utils.parseEther("1"), 505 | endDate: currentBlockTimeStamp + 86400, 506 | authorizedRoles: [memberId, leaderId], 507 | creatorRole: leaderId, 508 | assignee: userA.address, 509 | metadata: "ipfs://0xc0/", 510 | }; 511 | const tx = await Web3Task.connect(userA).createTask(Task); 512 | const receipt = await tx.wait(); 513 | const taskId = receipt.events[0].args[0]; 514 | createdTaskId = taskId; 515 | 516 | let interfaceId = Web3Task.interface.getSighash("startTask"); 517 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 518 | 519 | await expect( 520 | Web3Task.connect(userB).startTask(createdTaskId, leaderId) 521 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 522 | }); 523 | 524 | //New-Test - Testing starting a task with an invalid status 525 | it("should fail to start a task (Invalid Status)", async function () { 526 | // Get the current block timestamp 527 | const latestBlock = await ethers.provider.getBlock("latest"); 528 | const currentBlockTimeStamp = latestBlock.timestamp; 529 | 530 | const Task = { 531 | status: 0, 532 | title: "Pagar membros do PodLabs", 533 | description: "Não esquecer", 534 | reward: ethers.utils.parseEther("1"), 535 | endDate: currentBlockTimeStamp + 86400, 536 | authorizedRoles: [leaderId], 537 | creatorRole: leaderId, 538 | assignee: userA.address, 539 | metadata: "ipfs://0xc0/", 540 | }; 541 | const tx = await Web3Task.connect(userA).createTask(Task); 542 | const receipt = await tx.wait(); 543 | const taskId = receipt.events[0].args[0]; 544 | createdTaskId = taskId; 545 | 546 | let interfaceId = Web3Task.interface.getSighash("startTask"); 547 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 548 | 549 | Web3Task.connect(userA).startTask(createdTaskId, leaderId); 550 | 551 | await expect( 552 | Web3Task.connect(userA).startTask(createdTaskId, leaderId) 553 | ).to.be.revertedWithCustomError(Web3Task, "InvalidStatus"); 554 | }); 555 | 556 | //New-Test - Testing starting a task with an invalid roleId 557 | it("should fail to start a task (invalid roleId)", async function () { 558 | // Get the current block timestamp 559 | const latestBlock = await ethers.provider.getBlock("latest"); 560 | const currentBlockTimeStamp = latestBlock.timestamp; 561 | 562 | const Task = { 563 | status: 0, 564 | title: "Pagar membros do PodLabs", 565 | description: "Não esquecer", 566 | reward: ethers.utils.parseEther("1"), 567 | endDate: currentBlockTimeStamp + 86400, 568 | authorizedRoles: [memberId], 569 | creatorRole: leaderId, 570 | assignee: userA.address, 571 | metadata: "ipfs://0xc0/", 572 | }; 573 | const tx = await Web3Task.connect(userA).createTask(Task); 574 | const receipt = await tx.wait(); 575 | const taskId = receipt.events[0].args[0]; 576 | createdTaskId = taskId; 577 | 578 | await expect( 579 | Web3Task.connect(userA).startTask(createdTaskId, memberId) 580 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 581 | }); 582 | 583 | //New-test - Testing starting a task with an invalid assignee 584 | it("should set msg.sender as the task assignee when starting a task", async function () { 585 | // Get the current block timestamp 586 | const latestBlock = await ethers.provider.getBlock("latest"); 587 | const currentBlockTimeStamp = latestBlock.timestamp; 588 | 589 | const Task = { 590 | status: 0, 591 | title: "Pagar membros do PodLabs", 592 | description: "Não esquecer", 593 | reward: ethers.utils.parseEther("1"), 594 | endDate: currentBlockTimeStamp + 86400, 595 | authorizedRoles: [memberId, leaderId], 596 | creatorRole: leaderId, 597 | assignee: ethers.constants.AddressZero, 598 | metadata: "ipfs://0xc0/", 599 | }; 600 | const tx = await Web3Task.connect(userA).createTask(Task); 601 | const receipt = await tx.wait(); 602 | const taskId = receipt.events[0].args[0]; 603 | createdTaskId = taskId; 604 | 605 | let interfaceId = Web3Task.interface.getSighash("startTask"); 606 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 607 | 608 | await Web3Task.connect(userA).startTask(createdTaskId, leaderId); 609 | 610 | const task = await Web3Task.getTask(createdTaskId); 611 | expect(task.assignee).to.equal(userA.address); 612 | }); 613 | 614 | //New-test - Testing to review a task with a unauthorized user 615 | it("should fail to review a task (Unauthorized User)", async function () { 616 | // Get the current block timestamp 617 | const latestBlock = await ethers.provider.getBlock("latest"); 618 | const currentBlockTimeStamp = latestBlock.timestamp; 619 | 620 | const Task = { 621 | status: 0, 622 | title: "Pagar membros do PodLabs", 623 | description: "Não esquecer", 624 | reward: ethers.utils.parseEther("1"), 625 | endDate: currentBlockTimeStamp + 86400, 626 | authorizedRoles: [memberId, leaderId], 627 | creatorRole: leaderId, 628 | assignee: userA.address, 629 | metadata: "ipfs://0xc0/", 630 | }; 631 | const tx = await Web3Task.connect(userA).createTask(Task); 632 | const receipt = await tx.wait(); 633 | const taskId = receipt.events[0].args[0]; 634 | createdTaskId = taskId; 635 | 636 | let interfaceId = Web3Task.interface.getSighash("startTask"); 637 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 638 | await Web3Task.connect(userA).startTask(createdTaskId, leaderId); 639 | 640 | interfaceId = Web3Task.interface.getSighash("reviewTask"); 641 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 642 | 643 | await expect( 644 | Web3Task.connect(userB).reviewTask( 645 | createdTaskId, 646 | memberId, 647 | "Should fail to review this task" 648 | ) 649 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 650 | }); 651 | 652 | it("should fail to review a task (InvalidStatus)", async function () { 653 | // Get the current block timestamp 654 | const latestBlock = await ethers.provider.getBlock("latest"); 655 | const currentBlockTimeStamp = latestBlock.timestamp; 656 | 657 | const Task = { 658 | status: 0, 659 | title: "Pagar membros do PodLabs", 660 | description: "Não esquecer", 661 | reward: ethers.utils.parseEther("1"), 662 | endDate: currentBlockTimeStamp + 86400, 663 | authorizedRoles: [memberId, leaderId], 664 | creatorRole: leaderId, 665 | assignee: userA.address, 666 | metadata: "ipfs://0xc0/", 667 | }; 668 | const tx = await Web3Task.connect(userA).createTask(Task); 669 | const receipt = await tx.wait(); 670 | const taskId = receipt.events[0].args[0]; 671 | createdTaskId = taskId; 672 | 673 | let interfaceId = Web3Task.interface.getSighash("reviewTask"); 674 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 675 | 676 | await expect( 677 | Web3Task.connect(userA).reviewTask( 678 | createdTaskId, 679 | leaderId, 680 | "ipfs://0xc1/" 681 | ) 682 | ).to.be.revertedWithCustomError(Web3Task, "InvalidStatus"); 683 | }); 684 | 685 | it("should fail to complete a task (InvalidStatus)", async function () { 686 | // Get the current block timestamp 687 | const latestBlock = await ethers.provider.getBlock("latest"); 688 | const currentBlockTimeStamp = latestBlock.timestamp; 689 | 690 | const Task = { 691 | status: 0, 692 | title: "Pagar membros do PodLabs", 693 | description: "Não esquecer", 694 | reward: ethers.utils.parseEther("1"), 695 | endDate: currentBlockTimeStamp + 86400, 696 | authorizedRoles: [memberId, leaderId], 697 | creatorRole: leaderId, 698 | assignee: userA.address, 699 | metadata: "ipfs://0xc0/", 700 | }; 701 | const tx = await Web3Task.connect(userA).createTask(Task); 702 | const receipt = await tx.wait(); 703 | const taskId = receipt.events[0].args[0]; 704 | createdTaskId = taskId; 705 | 706 | let interfaceId = Web3Task.interface.getSighash("completeTask"); 707 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 708 | 709 | await expect( 710 | Web3Task.connect(userA).completeTask(createdTaskId, leaderId) 711 | ).to.be.revertedWithCustomError(Web3Task, "InvalidStatus"); 712 | }); 713 | 714 | it("should fail to complete a task (Unauthorized)", async function () { 715 | // Get the current block timestamp 716 | const latestBlock = await ethers.provider.getBlock("latest"); 717 | const currentBlockTimeStamp = latestBlock.timestamp; 718 | 719 | const Task = { 720 | status: 0, 721 | title: "Pagar membros do PodLabs", 722 | description: "Não esquecer", 723 | reward: ethers.utils.parseEther("1"), 724 | endDate: currentBlockTimeStamp + 86400, 725 | authorizedRoles: [memberId, leaderId], 726 | creatorRole: leaderId, 727 | assignee: userA.address, 728 | metadata: "ipfs://0xc0/", 729 | }; 730 | const tx = await Web3Task.connect(userA).createTask(Task); 731 | const receipt = await tx.wait(); 732 | const taskId = receipt.events[0].args[0]; 733 | createdTaskId = taskId; 734 | 735 | let interfaceId = Web3Task.interface.getSighash("startTask"); 736 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 737 | await Web3Task.connect(userA).startTask(createdTaskId, leaderId); 738 | 739 | interfaceId = Web3Task.interface.getSighash("reviewTask"); 740 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 741 | await Web3Task.connect(userA).reviewTask( 742 | createdTaskId, 743 | leaderId, 744 | "ipfs://0xc1/" 745 | ); 746 | interfaceId = Web3Task.interface.getSighash("completeTask"); 747 | await Web3Task.connect(owner).setOperator(interfaceId, memberId, true); 748 | 749 | await expect( 750 | Web3Task.connect(userA).completeTask(createdTaskId, memberId) 751 | ).to.be.revertedWithCustomError(Web3Task, "Unauthorized"); 752 | }); 753 | 754 | it("should fail to complete a task (AlreadyVoted)", async function () { 755 | // Get the current block timestamp 756 | const latestBlock = await ethers.provider.getBlock("latest"); 757 | const currentBlockTimeStamp = latestBlock.timestamp; 758 | 759 | const Task = { 760 | status: 0, 761 | title: "Pagar membros do PodLabs", 762 | description: "Não esquecer", 763 | reward: ethers.utils.parseEther("1"), 764 | endDate: currentBlockTimeStamp + 86400, 765 | authorizedRoles: [memberId, leaderId], 766 | creatorRole: leaderId, 767 | assignee: userA.address, 768 | metadata: "ipfs://0xc0/", 769 | }; 770 | const tx = await Web3Task.connect(userA).createTask(Task); 771 | const receipt = await tx.wait(); 772 | const taskId = receipt.events[0].args[0]; 773 | createdTaskId = taskId; 774 | 775 | let interfaceId = Web3Task.interface.getSighash("startTask"); 776 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 777 | await Web3Task.connect(userA).startTask(createdTaskId, leaderId); 778 | 779 | interfaceId = Web3Task.interface.getSighash("reviewTask"); 780 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 781 | await Web3Task.connect(userA).reviewTask( 782 | createdTaskId, 783 | leaderId, 784 | "ipfs://0xc1/" 785 | ); 786 | 787 | interfaceId = Web3Task.interface.getSighash("completeTask"); 788 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 789 | await Web3Task.connect(userA).completeTask(createdTaskId, leaderId); 790 | 791 | await expect( 792 | Web3Task.connect(userA).completeTask(createdTaskId, leaderId) 793 | ).to.be.revertedWithCustomError(Web3Task, "AlreadyVoted"); 794 | }); 795 | 796 | it("should fail to cancel a task (InvalidStatus)", async function () { 797 | // Get the current block timestamp 798 | const latestBlock = await ethers.provider.getBlock("latest"); 799 | const currentBlockTimeStamp = latestBlock.timestamp; 800 | 801 | const Task = { 802 | status: 0, 803 | title: "Pagar membros do PodLabs", 804 | description: "Não esquecer", 805 | reward: ethers.utils.parseEther("1"), 806 | endDate: currentBlockTimeStamp + 86400, 807 | authorizedRoles: [memberId, leaderId], 808 | creatorRole: leaderId, 809 | assignee: userA.address, 810 | metadata: "ipfs://0xc0/", 811 | }; 812 | const tx = await Web3Task.connect(userA).createTask(Task); 813 | const receipt = await tx.wait(); 814 | const taskId = receipt.events[0].args[0]; 815 | createdTaskId = taskId; 816 | 817 | let interfaceId = Web3Task.interface.getSighash("cancelTask"); 818 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 819 | await Web3Task.connect(userA).cancelTask(createdTaskId, leaderId); 820 | 821 | await expect( 822 | Web3Task.connect(userA).cancelTask(createdTaskId, leaderId) 823 | ).to.be.revertedWithCustomError(Web3Task, "InvalidStatus"); 824 | }); 825 | 826 | it("should set task status to Canceled if endDate has passed", async function () { 827 | // Get the current block timestamp 828 | const latestBlock = await ethers.provider.getBlock("latest"); 829 | const currentBlockTimeStamp = latestBlock.timestamp; 830 | 831 | const Task = { 832 | status: 0, 833 | title: "Pagar membros do PodLabs", 834 | description: "Não esquecer", 835 | reward: ethers.utils.parseEther("1"), 836 | endDate: currentBlockTimeStamp + 3600, 837 | authorizedRoles: [memberId, leaderId], 838 | creatorRole: leaderId, 839 | assignee: userA.address, 840 | metadata: "ipfs://0xc0/", 841 | }; 842 | const tx = await Web3Task.connect(userA).createTask(Task); 843 | const receipt = await tx.wait(); 844 | const taskId = receipt.events[0].args[0]; 845 | createdTaskId = taskId; 846 | 847 | const task = await Web3Task.getTask(createdTaskId); 848 | 849 | // Advance the block timestamp by 2 hours to make the task's endDate pass 850 | await ethers.provider.send("evm_increaseTime", [7200]); 851 | await ethers.provider.send("evm_mine", []); // Mine a new block to apply the time increase 852 | 853 | const updatedTask = await Web3Task.getTask(createdTaskId); 854 | 855 | expect(updatedTask.status).to.equal(Status.Canceled); 856 | }); 857 | 858 | it("should return the count of tasks for a user", async function () { 859 | // Get the current block timestamp 860 | const latestBlock = await ethers.provider.getBlock("latest"); 861 | const currentBlockTimeStamp = latestBlock.timestamp; 862 | 863 | const Task = { 864 | status: 0, 865 | title: "Pagar membros do PodLabs", 866 | description: "Não esquecer", 867 | reward: ethers.utils.parseEther("1"), 868 | endDate: currentBlockTimeStamp + 86400, 869 | authorizedRoles: [memberId, leaderId], 870 | creatorRole: leaderId, 871 | assignee: userA.address, 872 | metadata: "ipfs://0xc0/", 873 | }; 874 | 875 | // Get the initial count of tasks for userA 876 | const initialTasks = await Web3Task.getUserTasks(userA.address); 877 | const initialCount = initialTasks.length; 878 | 879 | // Create 3 tasks with userA's address 880 | for (let i = 0; i < 3; i++) { 881 | await Web3Task.connect(userA).createTask(Task); 882 | } 883 | 884 | // Get the final count of tasks for userA 885 | const finalTasks = await Web3Task.getUserTasks(userA.address); 886 | const finalCount = finalTasks.length; 887 | 888 | // Check if the count has increased by 3 889 | expect(finalCount).to.equal(initialCount + 3); 890 | }); 891 | 892 | it("should fail to withdraw (Amount higher than Balance)", async function () { 893 | let interfaceId = Web3Task.interface.getSighash("withdraw"); 894 | await Web3Task.connect(owner).setOperator(interfaceId, leaderId, true); 895 | 896 | await expect( 897 | Web3Task.connect(userA).withdraw(leaderId, ethers.utils.parseEther("200")) 898 | ).to.be.revertedWithCustomError(Web3Task, "InsufficientBalance"); 899 | }); 900 | 901 | it("should withdraw all funds and emit Withdraw event on emergency withdrawal", async function () { 902 | await expect( 903 | Web3Task.connect(owner).emergengyWithdraw({ gasLimit: 9500000 }) 904 | ) 905 | .to.emit(Web3Task, "Withdraw") 906 | .withArgs(0, owner.address, 0); 907 | 908 | const balanceAfter = await ethers.provider.getBalance(Web3Task.address); 909 | 910 | expect(balanceAfter).to.equal(0); 911 | }); 912 | }); 913 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "rootDirs": ["./src", "./scripts", "./test"], 11 | }, 12 | "exclude": ["dist", "node_modules"], 13 | "include": ["./test", "./src", "./scripts", "tsconfig.json"], 14 | "files": ["./hardhat.config.ts"] 15 | } -------------------------------------------------------------------------------- /utils/saveDataContract.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from "ethers"; 2 | import { artifacts } from "hardhat"; 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | function createPath(chain: any) { 7 | 8 | var contractsDirectory = path.join(__dirname, "../../", "web3task-front", "src", "contracts", `${chain}`); 9 | 10 | console.log('Saving frontend files...') 11 | if (!fs.existsSync(contractsDirectory)) { 12 | fs.mkdirSync(contractsDirectory); 13 | } 14 | return contractsDirectory 15 | } 16 | 17 | export async function saveFrontendFiles(contract: Contract, chain: any) { 18 | 19 | const contractsDirectory = createPath(chain) 20 | const address = (await contract).address; 21 | 22 | fs.writeFileSync( 23 | path.join(contractsDirectory, "contract-Web3Task-address.json"), 24 | JSON.stringify({ Web3Task: address }, undefined, 2) 25 | ); 26 | 27 | const Web3TaskArtifact = artifacts.readArtifactSync("Web3Task"); 28 | const TasksManagerArtifact = artifacts.readArtifactSync("TasksManager"); 29 | 30 | 31 | fs.writeFileSync( 32 | path.join(contractsDirectory, "Web3Task.json"), 33 | JSON.stringify(Web3TaskArtifact, null, 2) 34 | ); 35 | 36 | fs.writeFileSync( 37 | path.join(contractsDirectory, "TasksManager.json"), 38 | JSON.stringify(TasksManagerArtifact, null, 2) 39 | ); 40 | 41 | 42 | } 43 | 44 | export function saveContractAddress(contractAddress: string) { 45 | const filePath = path.join(__dirname, '../.env'); 46 | 47 | fs.readFile(filePath, 'utf8', (readErr: any, data: string) => { 48 | if (readErr) { 49 | console.error('Error reading .env file:', readErr); 50 | return; 51 | } 52 | 53 | const newContent = data.replace(new RegExp("CONTRACT_ADDRESS=.*"), `CONTRACT_ADDRESS=${contractAddress}`); 54 | 55 | fs.writeFile(filePath, newContent, 'utf8', (writeErr: any) => { 56 | if (writeErr) { 57 | console.error('Error writing to .env file:', writeErr); 58 | } else { 59 | console.log('Contract address saved in .env file:', contractAddress); 60 | } 61 | }); 62 | }); 63 | } 64 | --------------------------------------------------------------------------------