├── .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 |
--------------------------------------------------------------------------------