├── .editorconfig
├── .env.example
├── .gitignore
├── README.md
├── contracts
├── AccessControlList.sol
└── mock
│ ├── Resource.sol
│ └── ResourceFactory.sol
├── hardhat.config.js
├── package.json
├── scripts
└── ethersjs-helper
│ └── ethersjsHelper.js
└── test
├── AccessControlList.test.js
├── Scenario.test.js
├── ethersjs-helper
└── ethersjsHelper.js
└── time-helper
└── timeHelper.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 4
9 |
10 | [*.sol]
11 | indent_style = space
12 | indent_size = 4
13 |
14 | [*.json]
15 | indent_style = space
16 | indent_size = 4
17 |
18 | [*.py]
19 | indent_style = space
20 | indent_size = 4
21 |
22 | [*.js]
23 | indent_style = space
24 | indent_size = 4
25 |
26 | [*.jsx]
27 | indent_style = space
28 | indent_size = 4
29 |
30 | [*.css]
31 | indent_style = space
32 | indent_size = 4
33 |
34 | [*.rb]
35 | indent_style = space
36 | indent_size = 2
37 |
38 | [*.java]
39 | indent_style = space
40 | indent_size = 4
41 |
42 | [*.php]
43 | indent_style = space
44 | tab_width = 4
45 |
46 | [*.html]
47 | indent_style = space
48 | indent_size = 2
49 |
50 | [*.md]
51 | trim_trailing_whitespace = false
52 |
53 | [{package.json,.travis.yml}]
54 | indent_style = space
55 | indent_size = 2
56 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | MNEMONIC=""
2 | INFURA_KEY=""
3 |
4 | ALCHEMY_KEY=""
5 |
6 | DEPLOYER_ADDRESS=""
7 | PRIVATE_KEY=""
8 | RINKEBY_PRIVKEY=""
9 | MAINNET_PRIVKEY=""
10 |
11 | REPORT_GAS = ""
12 | ETHERSCAN_API_KEY = ""
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | yarn.lock
4 |
5 | .env
6 |
7 | #Hardhat files
8 | cache
9 | artifacts
10 |
11 | .DS_Store
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ACL Smart Contract for Lit Protocol
2 | ## 【Overview】
3 | - This is a solidity smart contract that serves as an on-chain ACL (Access Control List).
4 | - This ACL smart contract includes permission and role-based governance
5 |
6 |
7 |
8 | ## 【Specifications】
9 | - Users can have permissions (read or write) on a resource identified by a uint256
10 | - "admin users" should be able to set and update those permissions
11 | - There should be some kind of grouping mechanism for both users and admin users, with the ability to apply permissions to an entire group, and to apply multiple groups to a resource.
12 | (NOTE: More detail of specifications of this ACL smart contract is here: https://docs.google.com/document/d/1obZDbb2_i0FTYNdg6uPQWWEUdyIO51bFsTEiJdokDzk/edit )
13 |
14 |
15 |
16 | ## 【Workflow】
17 | - Diagram that is workflow of this ACL smart contract
18 | - NOTE①: `AccessControlList contract (AccessControlList.sol)` is inherited by a Resource contract (Resource.sol)
19 | (Every Resource contract inherit AccessControlList contract in this repo)
20 |
21 | - NOTE②: `Resource contract (Resource.sol)` and `ResourceFactory contract (ResourceFactory.sol)` are mock contract for demo for this ACL smart contract
22 | (Therefore, Both contracts should be replaced depends on projects that use this ACL smart contract)
23 | 
24 |
25 |
26 |
27 | ## 【Test】
28 | - Run a unit test of the AccessControlList.sol
29 | ```
30 | npm run test:AccessControlList
31 | ```
32 | ( `$ npx hardhat test ./test/AccessControlList.test.js --network hardhat` )
33 |
34 |
35 |
36 | - Run a senario test
37 | ```
38 | npm run test:Scenario
39 | ```
40 | ( `$ npx hardhat test ./test/AccessControlList.test.js --network hardhat` )
41 |
42 |
43 |
44 | - Run all of unit test
45 | ```
46 | npm run test
47 | ```
48 | ( `$ npx hardhat test --network hardhat` )
49 |
50 |
51 |
52 | ## 【Demo】
53 | - This is the demo that the test of Scenario ( `./test/Scenario.test.js` ) above that includes the whole scenario of this ACL smart contracts is executed.
54 | https://youtu.be/Wc4ZrmJ-TH0
55 |
56 |
57 |
58 | ## 【References】
59 | - Lit Protocol:
60 | - Website: https://litprotocol.com/
61 |
62 |
63 |
64 | - Prize of the Lit Protocol (in ETH Denver): https://www.ethdenver.com/bounties/lit-protocol
65 | - Specifications of Access Control List (=ACL) Smart Contract: https://docs.google.com/document/d/1obZDbb2_i0FTYNdg6uPQWWEUdyIO51bFsTEiJdokDzk/edit
66 |
--------------------------------------------------------------------------------
/contracts/AccessControlList.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: Unlicense
2 | pragma solidity ^0.8.10;
3 |
4 | //@notice - OpenZepelin
5 | import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
6 |
7 | //@notice - Debug
8 | import "hardhat/console.sol";
9 |
10 |
11 | contract AccessControlList is Ownable {
12 |
13 | uint public currentUserId; // user ID is counted from 0
14 | uint public currentGroupId; // group ID is counted from 0
15 |
16 | address[] public userAddresses;
17 |
18 | address[] public currentAdminAddresses;
19 | address[] public currentMemberAddresses;
20 |
21 |
22 | //----------------------------------------
23 | // Storages
24 | //----------------------------------------
25 | mapping (uint => User) users; // [Key]: user ID -> the User struct
26 | mapping (address => UserByAddress) userByAddresses; // [Key]: user's address -> the UserByAddress struct
27 | mapping (uint => Group) groups; // [Key]: group ID -> the Group struct
28 |
29 | //@dev - Role type: Admin user can read/write <-> member user can read only
30 | enum UserRole { ADMIN, MEMBER, DELETED }
31 |
32 | struct User { // [Key]: user ID -> the User struct
33 | address userAddress;
34 | UserRole userRole; // Admin or Member
35 | }
36 |
37 | struct UserByAddress { // [Key]: user's wallet address -> the User struct
38 | uint userId;
39 | UserRole userRole; // Admin or Member
40 | }
41 |
42 | struct Group { // [Key]: group ID -> the Group struct
43 | address[] adminAddresses; //@dev - list of admin's wallet addresses
44 | address[] memberAddresses; //@dev - list of member's wallet addresses
45 | }
46 |
47 | event GroupCreated(
48 | uint groupId,
49 | address creator,
50 | address[] adminAddresses,
51 | address[] memberAddresses
52 | );
53 |
54 | event UserAsAdminRoleAssigned(
55 | // [TODO]:
56 | );
57 |
58 | event UserAsMemberRoleAssigned(
59 | // [TODO]:
60 | );
61 |
62 |
63 | //-----------------
64 | // Modifiers
65 | //-----------------
66 |
67 | /**
68 | * @dev - Check permission that only users who has an admin role can access resources
69 | * @dev - Check whether a user specified has an admin role or not
70 | */
71 | modifier onlyAdminRole(address user) {
72 | UserRole _userRole = getUserByAddress(user).userRole;
73 |
74 | //@dev - If a role of "user" is "ADMIN", this condition below can be passed.
75 | //@dev - Only case that a role of "user" is not "ADMIN", this error message below is displayed
76 | require (_userRole == UserRole.ADMIN, "Only users who has an admin role can access this resources");
77 | _;
78 | }
79 |
80 | /**
81 | * @dev - Check a permission that only users who has a member role can access resources
82 | */
83 | modifier onlyMemberRole(address user) {
84 | UserRole _userRole = getUserByAddress(user).userRole;
85 |
86 | //@dev - If a role of "user" is "MEMBER", this condition below can be passed.
87 | //@dev - Only case that a role of "user" is not "MEMBER", this error message below is displayed
88 | require (_userRole == UserRole.MEMBER, "Only users who has a member role can access this resources");
89 | _;
90 | }
91 |
92 | /**
93 | * @dev - Check a permission that only users who has admin role or member role can access resources
94 | */
95 | modifier onlyAdminOrMemberRole(address user) {
96 | UserRole _userRole = getUserByAddress(user).userRole;
97 |
98 | //@dev - If a role of "user" is "MEMBER", this condition below can be passed.
99 | //@dev - Only case that a role of "user" is not "MEMBER", this error message below is displayed
100 | require (_userRole == UserRole.ADMIN || _userRole == UserRole.MEMBER, "Only users who has an admin role or a member role can access this resources");
101 | _;
102 | }
103 |
104 | /**
105 | * @dev - Check whether a user is already registered or not. (Chekch whether a user already has a User ID or not)
106 | */
107 | modifier checkWhetherUserIsAlreadyRegisteredOrNot(address user) {
108 | bool existingUser = _checkWhetherUserIsAlreadyRegisteredOrNot(user);
109 | require (existingUser != true, "This user is already registered");
110 | _;
111 | }
112 |
113 |
114 | //-----------------
115 | // Constructor
116 | //-----------------
117 | constructor() {
118 | createInitialGroup();
119 |
120 | uint _groupId = getCurrentGroupId() - 1;
121 | assignContractCreatorAsInitialAdminRole(_groupId);
122 | }
123 |
124 |
125 | //------------------------------
126 | // Methods for creating groups
127 | //------------------------------
128 |
129 | /**
130 | * @dev - Create a initial group.
131 | * @notice - this method can be created by a contract creator (deployer).
132 | */
133 | function createInitialGroup() public onlyOwner returns (bool) {
134 | Group storage group = groups[currentGroupId];
135 | group.adminAddresses = currentAdminAddresses;
136 | group.memberAddresses = currentMemberAddresses;
137 |
138 | emit GroupCreated(currentGroupId, msg.sender, group.adminAddresses, group.memberAddresses);
139 |
140 | currentGroupId++;
141 | }
142 |
143 | /**
144 | * @dev - Create a initial group.
145 | * @notice - this method can be created by users who has an admin role.
146 | */
147 | function createGroup() public onlyAdminRole(msg.sender) returns (bool) {
148 | Group storage group = groups[currentGroupId];
149 | group.adminAddresses = currentAdminAddresses;
150 | group.memberAddresses = currentMemberAddresses;
151 |
152 | emit GroupCreated(currentGroupId, msg.sender, group.adminAddresses, group.memberAddresses);
153 |
154 | currentGroupId++;
155 | }
156 |
157 |
158 | //-------------------------------------------------------
159 | // Methods for assiging/updating/removing role of admin or member
160 | //-------------------------------------------------------
161 |
162 | /**
163 | * @dev - Assign a contract creator's address as a initial admin role
164 | * @param groupId - group ID that a user address is assigned (as a admin role)
165 | */
166 | function assignContractCreatorAsInitialAdminRole(uint groupId) public onlyOwner returns (bool) {
167 | console.log("############################## currentUserId", currentUserId);
168 |
169 | address _userAddress = msg.sender; //@dev - msg.sender is a contract creator's address
170 |
171 | User storage user = users[currentUserId];
172 | user.userAddress = _userAddress;
173 | user.userRole = UserRole.ADMIN;
174 |
175 | UserByAddress storage userByAddress = userByAddresses[_userAddress];
176 | userByAddress.userId = currentUserId;
177 | userByAddress.userRole = UserRole.ADMIN;
178 |
179 | userAddresses.push(_userAddress);
180 | currentUserId++;
181 |
182 | currentAdminAddresses.push(_userAddress);
183 | Group storage group = groups[groupId];
184 | group.adminAddresses = currentAdminAddresses;
185 | }
186 |
187 | /**
188 | * @dev - Assign a user address as a admin role
189 | * @param groupId - group ID that a user address is assigned (as a admin role)
190 | * @param _userAddress - User address that is assigned as a admin role
191 | */
192 | //function assignUserAsAdminRole(uint groupId, address _userAddress) public onlyAdminRole(msg.sender) returns (bool) {
193 | function assignUserAsAdminRole(uint groupId, address _userAddress) public checkWhetherUserIsAlreadyRegisteredOrNot(_userAddress) returns (bool) {
194 | console.log("############################## currentUserId", currentUserId);
195 |
196 | User storage user = users[currentUserId];
197 | user.userAddress = _userAddress;
198 | user.userRole = UserRole.ADMIN;
199 |
200 | UserByAddress storage userByAddress = userByAddresses[_userAddress];
201 | userByAddress.userId = currentUserId;
202 | userByAddress.userRole = UserRole.ADMIN;
203 |
204 | userAddresses.push(_userAddress);
205 | currentUserId++;
206 |
207 | currentAdminAddresses.push(_userAddress);
208 | Group storage group = groups[groupId];
209 | group.adminAddresses = currentAdminAddresses;
210 | }
211 |
212 | /**
213 | * @dev - Assign a user address as a member role
214 | * @dev - This method can be called by users who has an admin role only
215 | * @param groupId - group ID that a user address is assigned (as a member role)
216 | * @param _userAddress - User address that is assigned as a member role
217 | */
218 | //function assignUserAsMemberRole(uint groupId, address _userAddress) onlyAdminRole(msg.sender) public returns (bool) {
219 | function assignUserAsMemberRole(uint groupId, address _userAddress) public checkWhetherUserIsAlreadyRegisteredOrNot(_userAddress) returns (bool) {
220 | User storage user = users[currentUserId];
221 | user.userAddress = _userAddress;
222 | user.userRole = UserRole.MEMBER;
223 |
224 | UserByAddress storage userByAddress = userByAddresses[_userAddress];
225 | userByAddress.userId = currentUserId;
226 | userByAddress.userRole = UserRole.MEMBER;
227 |
228 | userAddresses.push(_userAddress);
229 | currentUserId++;
230 |
231 | currentMemberAddresses.push(_userAddress);
232 | Group storage group = groups[groupId];
233 | group.memberAddresses = currentMemberAddresses;
234 | }
235 |
236 | /**
237 | * @dev - Update a role of user who has a member role at the moment from "Member" to "Admin"
238 | * @param groupId - group ID that a user address is assigned (as a member role)
239 | * @param userId - User ID who has a member role at the moment
240 | */
241 | function updateRole(uint groupId, uint userId) onlyAdminRole(msg.sender) public returns (bool) {
242 | User storage user = users[userId];
243 | user.userRole = UserRole.ADMIN;
244 |
245 | address _userAddress = user.userAddress;
246 | UserByAddress storage userByAddress = userByAddresses[_userAddress];
247 | userByAddress.userRole = UserRole.ADMIN;
248 |
249 | //@dev - Add a user to admin addresses list
250 | currentAdminAddresses.push(_userAddress);
251 |
252 | //@dev - Remove a user from member addresses list
253 | for (uint i=0; i < currentMemberAddresses.length; i++) {
254 | address memberAddress = currentMemberAddresses[i];
255 | if (memberAddress == _userAddress) {
256 | delete currentMemberAddresses[i];
257 | }
258 | }
259 |
260 | Group storage group = groups[groupId];
261 | group.adminAddresses = currentAdminAddresses;
262 | group.memberAddresses = currentMemberAddresses;
263 | }
264 |
265 | /**
266 | * @dev - Remove admin role from a admin user. After that, a role status of this user become "Member"
267 | */
268 | function removeAdminRole(uint groupId, uint userId) public onlyAdminRole(msg.sender) returns (bool) {
269 | User storage user = users[userId];
270 | user.userRole = UserRole.MEMBER;
271 |
272 | for (uint i=0; i < currentAdminAddresses.length; i++) {
273 | address adminAddress = currentAdminAddresses[i];
274 | if (adminAddress == user.userAddress) {
275 | delete currentAdminAddresses[i];
276 | }
277 | }
278 |
279 | Group storage group = groups[groupId];
280 | group.adminAddresses = currentAdminAddresses;
281 | }
282 |
283 | /**
284 | * @dev - Remove admin role from a admin user. After that, a role status of this user become "Deleted"
285 | */
286 | function removeMemberRole(uint groupId, uint userId) public onlyAdminOrMemberRole(msg.sender) returns (bool) {
287 | User storage user = users[userId];
288 | user.userRole = UserRole.DELETED;
289 |
290 | for (uint i=0; i < currentMemberAddresses.length; i++) {
291 | address memberAddress = currentMemberAddresses[i];
292 | if (memberAddress == user.userAddress) {
293 | delete currentMemberAddresses[i];
294 | }
295 | }
296 |
297 | Group storage group = groups[groupId];
298 | group.memberAddresses = currentMemberAddresses;
299 | }
300 |
301 |
302 | //-------------------
303 | // Getter methods
304 | //-------------------
305 | function getCurrentGroupId() public view returns (uint _currentGroupId) {
306 | return currentGroupId;
307 | }
308 |
309 | function getCurrentUserId() public view returns (uint _currentUserId) {
310 | return currentUserId;
311 | }
312 |
313 | function getGroup(uint groupId) public view returns (Group memory _group) {
314 | return groups[groupId];
315 | }
316 |
317 | function getUser(uint userId) public view returns (User memory _user) {
318 | return users[userId];
319 | }
320 |
321 | function getUserByAddress(address user) public view returns (UserByAddress memory _userByAddress) {
322 | UserByAddress memory userByAddress = userByAddresses[user];
323 | return userByAddress;
324 | }
325 |
326 | function getUserAddresses() public view returns (address[] memory _users) {
327 | return userAddresses;
328 | }
329 |
330 | function getCurrentAdminAddresses() public view returns (address[] memory _currentAdminAddresses) {
331 | return currentAdminAddresses;
332 | }
333 |
334 | function getCurrentMemberAddresses() public view returns (address[] memory _currentMemberAddresses) {
335 | return currentMemberAddresses;
336 | }
337 |
338 |
339 | //--------------------------------------
340 | // Getter method that support modifiers
341 | //--------------------------------------
342 | function _checkWhetherUserIsAlreadyRegisteredOrNot(address user) internal view returns (bool _existingUser) {
343 | bool existingUser = false;
344 | for (uint i=0; i < userAddresses.length; i++) {
345 | if (userAddresses[i] == user) {
346 | existingUser = true;
347 | return existingUser;
348 | }
349 | }
350 | }
351 |
352 | }
353 |
--------------------------------------------------------------------------------
/contracts/mock/Resource.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: Unlicense
2 | pragma solidity ^0.8.10;
3 |
4 | import { AccessControlList } from "../AccessControlList.sol";
5 |
6 | import "hardhat/console.sol";
7 |
8 | contract Resource is AccessControlList {
9 |
10 | //@dev - Metadata that are accociated with the Resource contract
11 | struct ResourceMetadata {
12 | string resourceName;
13 | string resourceURI; // e.g). Content ID of resource that is stored in IPFS
14 | }
15 | mapping (address => ResourceMetadata) resourceMetadatas; // [Key]: This Resource contract address -> the ResourceMetadata struct
16 |
17 | /**
18 | * @dev - Constructor
19 | * @notice - Only group member who has an admin role can call this method.
20 | */
21 | constructor() {}
22 |
23 | /**
24 | * @dev - Create a new resource's metadata
25 | * @notice - Only group member who has an admin role can call this method.
26 | */
27 | function createNewResourceMetadata(
28 | string memory _resourceName,
29 | string memory _resourceURI
30 | ) public onlyAdminRole(msg.sender) returns (bool) {
31 | ResourceMetadata storage resourceMetadata = resourceMetadatas[address(this)];
32 | resourceMetadata.resourceName = _resourceName;
33 | resourceMetadata.resourceURI = _resourceURI;
34 | }
35 |
36 | /**
37 | * @dev - Edit a resource's metadata
38 | * @notice - Only group member who has an admin role can call this method.
39 | */
40 | function editResourceMetadata(
41 | string memory newResourceName,
42 | string memory newResourceURI
43 | ) public onlyAdminRole(msg.sender) returns (bool) {
44 | address adminRoleUser = msg.sender;
45 |
46 | ResourceMetadata storage resourceMetadata = resourceMetadatas[address(this)];
47 | if (keccak256(abi.encodePacked(newResourceName)) != keccak256(abi.encodePacked(""))) {
48 | resourceMetadata.resourceName = newResourceName;
49 | }
50 | if (keccak256(abi.encodePacked(newResourceURI)) != keccak256(abi.encodePacked(""))) {
51 | resourceMetadata.resourceURI = newResourceURI;
52 | }
53 | }
54 |
55 | /**
56 | * @dev - Get a resource's metadata
57 | * @notice - Only group member (who has an admin role or a member role) can call this method.
58 | */
59 | function getResourceMetadata() public view onlyAdminOrMemberRole(msg.sender) returns (ResourceMetadata memory _resourceMetadata) {
60 | return resourceMetadatas[address(this)];
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/contracts/mock/ResourceFactory.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: Unlicense
2 | pragma solidity ^0.8.10;
3 |
4 | import { Resource } from "./Resource.sol";
5 |
6 | import "hardhat/console.sol";
7 |
8 |
9 | /**
10 | * @dev -
11 | * @dev - On the assumption that, each resource has resource ID and own contract address.
12 | */
13 | contract ResourceFactory {
14 |
15 | uint currentResourceId; //@dev - resource ID is counted from 0
16 |
17 | address[] public resourceAddresses; //@dev - Every resource's addresses created are stored into this array
18 |
19 | /**
20 | * @dev - Constructor
21 | */
22 | constructor() {}
23 |
24 | /**
25 | * @dev - Create a new resource
26 | */
27 | function createNewResource() public returns (bool) {
28 | Resource resource = new Resource();
29 | resourceAddresses.push(address(resource));
30 | currentResourceId++;
31 | }
32 |
33 | //-----------------
34 | // Getter methods
35 | //-----------------
36 |
37 | /**
38 | * @dev - A resource is identified by a resourceId (uint256)
39 | * @return resourceAddress - Resource's contract address that is associated with resource ID specified
40 | */
41 | function getResource(uint resourceId) public view returns (address resourceAddress) {
42 | return resourceAddresses[resourceId];
43 | }
44 |
45 | function getCurrentResourceId() public view returns (uint _currentResourceId) {
46 | return currentResourceId;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/hardhat.config.js:
--------------------------------------------------------------------------------
1 | const { task } = require('hardhat/config')
2 |
3 | // require('@nomiclabs/hardhat-etherscan')
4 | require('@nomiclabs/hardhat-waffle')
5 | require("@nomiclabs/hardhat-web3") // For using web3.js (and Truffle) and @openzepplin/test-helper
6 | require('dotenv').config()
7 |
8 |
9 |
10 | // This is a sample Hardhat task. To learn how to create your own go to
11 | // https://hardhat.org/guides/create-task.html
12 | task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
13 | const accounts = await hre.ethers.getSigners();
14 |
15 | for (const account of accounts) {
16 | console.log(account.address);
17 | }
18 | });
19 |
20 | // You need to export an object to set up your config
21 | // Go to https://hardhat.org/config/ to learn more
22 |
23 | /**
24 | * @type import('hardhat/config').HardhatUserConfig
25 | */
26 | module.exports = {
27 | networks: {
28 | hardhat: { /// [Note]: This network is for executing test with mainnet-fork approach
29 | forking: {
30 | url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
31 | blockNumber: 11589707
32 | }
33 | },
34 |
35 | // metis_testnet: {
36 | // url: "https://stardust.metis.io/?owner=588",
37 | // accounts:
38 | // process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
39 | // },
40 |
41 | rinkeby: {
42 | url: `https://eth-rinkeby.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
43 | accounts: [process.env.RINKEBY_PRIVKEY]
44 | },
45 |
46 | // live: {
47 | // url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
48 | // accounts: [process.env.MAINNET_PRIVKEY]
49 | // }
50 | },
51 |
52 | gasReporter: {
53 | enabled: process.env.REPORT_GAS !== undefined,
54 | currency: "USD",
55 | },
56 | etherscan: {
57 | apiKey: process.env.ETHERSCAN_API_KEY,
58 | },
59 |
60 | solidity: {
61 | compilers: [
62 | // {
63 | // version: '0.6.12'
64 | // },
65 | {
66 | version: '0.8.10'
67 | },
68 | // {
69 | // version: '0.8.0'
70 | // },
71 | ]
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "retroactive-public-goods-funding",
3 | "scripts": {
4 | "accounts": "npx hardhat accounts --network hardhat",
5 | "test": "npx hardhat test --network hardhat",
6 | "test:AccessControlList": "npx hardhat test ./test/AccessControlList.test.js --network hardhat",
7 | "test:Scenario": "npx hardhat test ./test/Scenario.test.js --network hardhat",
8 | "compile": "npx hardhat compile",
9 | "deploy-local:Sample": "npx hardhat run scripts/deployment/00_deploy_Sample.js --network hardhat",
10 | "node": "npx hardhat node",
11 | "script:Sample": "npx hardhat run scripts/sample-script.js --network hardhat"
12 | },
13 | "dependencies": {
14 | "@openzeppelin/contracts": "^4.3.2",
15 | "dotenv": "^10.0.0"
16 | },
17 | "devDependencies": {
18 | "@nomiclabs/hardhat-ethers": "^2.0.3",
19 | "@nomiclabs/hardhat-waffle": "^2.0.1",
20 | "@nomiclabs/hardhat-web3": "^2.0.0",
21 | "web3": "^1.6.1",
22 | "chai": "^4.3.4",
23 | "ethereum-waffle": "^3.4.0",
24 | "ethers": "^5.5.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/scripts/ethersjs-helper/ethersjsHelper.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai")
2 | const { ethers } = require("hardhat")
3 |
4 |
5 | function convertHexToString(hex) {
6 | // [Example]: ethers.utils.arrayify("0x1234") -> Uint8Array [ 18, 52 ]
7 | return ethers.utils.arrayify(`${ hex }`)
8 | }
9 |
10 | function convertStringToHex(string) {
11 | // [Example]: ethers.utils.hexlify([1, 2, 3, 4]) -> '0x01020304'
12 | return ethers.utils.hexlify([string])
13 | }
14 |
15 |
16 | function toWei(amount) {
17 | return ethers.utils.parseEther(`${ amount }`)
18 | }
19 |
20 | function fromWei(amount) {
21 | return ethers.utils.formatEther(`${ amount }`)
22 | }
23 |
24 | //@dev - Method for retrieving an event log that is associated with "eventName" specified
25 | async function getEventLog(txReceipt, eventName) {
26 | for (let i = 0; i < txReceipt.events.length; i++) {
27 | const eventLogs = txReceipt.events[i];
28 | console.log(`eventLogs: ${ JSON.stringify(eventLogs, null, 2) }`)
29 |
30 | if (eventLogs["event"] == eventName) {
31 | const _args = eventLogs["args"]
32 | return _args // [NOTE] Return event log specified as array
33 | }
34 | }
35 | }
36 |
37 | async function getCurrentBlock() {}
38 |
39 | async function getCurrentTimestamp() {}
40 |
41 | //@dev - Export methods
42 | module.exports = { convertHexToString, convertStringToHex, toWei, fromWei, getEventLog, getCurrentBlock, getCurrentTimestamp }
43 |
--------------------------------------------------------------------------------
/test/AccessControlList.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai")
2 | const { ethers } = require("hardhat")
3 |
4 | //@dev - ethers.js related methods
5 | const { toWei, fromWei, getEventLog, getCurrentBlock, getCurrentTimestamp } = require('./ethersjs-helper/ethersjsHelper')
6 |
7 |
8 | describe("AccessControlList", function () {
9 |
10 | //@dev - Contract instance
11 | let acl
12 |
13 | //@dev - Contract addresses
14 | let ACL
15 |
16 | //@dev - Signers of wallet addresses
17 | let contractCreator
18 | let user1, user2
19 | let users
20 |
21 | //@dev - Wallet addresses
22 | let CONTRACT_CREATOR
23 | let USER_1, USER_2
24 |
25 | before(async function () {
26 | [contractCreator, user1, user2, ...users] = await ethers.getSigners()
27 |
28 | CONTRACT_CREATOR = contractCreator.address
29 | USER_1 = user1.address
30 | USER_2 = user2.address
31 | console.log(`CONTRACT_CREATOR: ${ CONTRACT_CREATOR }`)
32 | console.log(`USER_1: ${ USER_1 }`)
33 | console.log(`USER_2: ${ USER_2 }`)
34 | })
35 |
36 | it("Deploy the AccessControlList.sol", async function () {
37 | const AccessControlList = await ethers.getContractFactory("AccessControlList")
38 |
39 | //@dev - When the AccessControlList.sol is deployed, initial group is created and this contract creator is assigned as a initial admin role
40 | acl = await AccessControlList.deploy()
41 |
42 | await acl.deployed()
43 |
44 | ACL = acl.address
45 | console.log(`ACL: ${ ACL }`)
46 | })
47 |
48 |
49 | ///-------------------------------------------------------
50 | /// Test of methods defined in the AccessControlList.sol
51 | ///-------------------------------------------------------
52 |
53 | it("createGroup()", async function () {
54 | let tx = await acl.connect(user1).createGroup()
55 | let txReceipt = await tx.wait()
56 |
57 | //@dev - Retrieve an event log of "GroupCreated"
58 | let eventLog = await getEventLog(txReceipt, "GroupCreated")
59 | console.log(`eventLog of GroupCreated: ${ eventLog }`)
60 | })
61 |
62 | it("assignUserAsAdminRole()", async function () {
63 | const groupId = 0
64 | const userAddress = USER_1
65 |
66 | let tx = await acl.connect(user1).assignUserAsAdminRole(groupId, userAddress)
67 | let txReceipt = await tx.wait()
68 |
69 | //@dev - Retrieve an event log of "UserAsAdminRoleAssigned"
70 | })
71 |
72 | it("assignUserAsMemberRole()", async function () {
73 | const groupId = 0
74 | const userAddress = USER_2
75 |
76 | let tx = await acl.connect(user1).assignUserAsMemberRole(groupId, userAddress)
77 | let txReceipt = await tx.wait()
78 | })
79 |
80 |
81 | ///------------------------------------------------
82 | /// Check whether a modifier works properly or not
83 | ///------------------------------------------------
84 | it("Modifier of checkWhetherUserIsAlreadyRegisteredOrNot() - Users who has already registered should fail to be assigned as an admin or member role", async function () {
85 | const groupId = 0
86 | const userAddress = USER_1
87 |
88 | // [TODO]: @dev - Test whether a modifier of checkWhetherUserIsAlreadyRegisteredOrNot() works properly or not by using "Mocha" and "Chai"
89 | // await expect(
90 | // await acl.connect(user1).assignUserAsAdminRole(groupId, userAddress)
91 | // ).to.be.revertedWith("This user is already registered")
92 |
93 | // await expect(
94 | // await acl.connect(user1).assignUserAsAdminRole(groupId, userAddress)
95 | // ).to.equalWithError("This user is already registered")
96 | })
97 |
98 |
99 | ///--------------------------------
100 | /// Check
101 | ///--------------------------------
102 | it("getGroup()", async function () {
103 | const groupId = 0
104 | let group = await acl.getGroup(groupId)
105 | console.log(`group: ${ group }`)
106 | })
107 |
108 | it("getUser()", async function () {
109 | const userId0 = 0
110 | let user0 = await acl.getUser(userId0)
111 |
112 | const userId1 = 1
113 | let user1 = await acl.getUser(userId1)
114 |
115 | console.log(`user0: ${ user0 }`)
116 | console.log(`user1: ${ user1 }`)
117 | })
118 |
119 | it("getUserAddresses()", async function () {
120 | let users = await acl.getUserAddresses()
121 | console.log(`userAddresses: ${ users }`)
122 | })
123 |
124 | it("getCurrentAdminAddresses()", async function () {
125 | let currentAdminAddresses = await acl.getCurrentAdminAddresses()
126 | console.log(`currentAdminAddresses: ${ currentAdminAddresses }`)
127 | })
128 |
129 | it("getCurrentMemberAddresses()", async function () {
130 | let currentMemberAddresses = await acl.getCurrentMemberAddresses()
131 | console.log(`currentMemberAddresses: ${ currentMemberAddresses }`)
132 | })
133 |
134 | it("getCurrentGroupId()", async function () {
135 | let currentGroupId = await acl.getCurrentGroupId()
136 | console.log(`currentGroupId: ${ currentGroupId }`)
137 | })
138 |
139 | it("getCurrentUserId()", async function () {
140 | let currentUserId = await acl.getCurrentUserId()
141 | console.log(`currentUserId: ${ currentUserId }`)
142 | })
143 |
144 |
145 | ///--------------------------------
146 | /// Test that remove roles
147 | ///--------------------------------
148 | it("removeAdminRole()", async function () {
149 | const groupId = 0
150 | const userId = 0
151 |
152 | let tx = await acl.connect(user1).removeAdminRole(groupId, userId)
153 | let txReceipt = await tx.wait()
154 | })
155 |
156 | it("removeMemberRole()", async function () {
157 | const groupId = 0
158 | const userId = 1
159 |
160 | let tx = await acl.connect(user2).removeMemberRole(groupId, userId)
161 | let txReceipt = await tx.wait()
162 | })
163 |
164 | })
165 |
--------------------------------------------------------------------------------
/test/Scenario.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai")
2 | const { ethers } = require("hardhat")
3 |
4 | //@dev - ethers.js related methods
5 | const { toWei, fromWei, getEventLog, getCurrentBlock, getCurrentTimestamp } = require('./ethersjs-helper/ethersjsHelper')
6 |
7 |
8 | describe("Scenario Test", function () {
9 | //@dev - Contract instance
10 | let resourceFactory
11 | let resource
12 |
13 | //@dev - Contract addresses
14 | let RESOURCE_FACTORY
15 | let RESOURCE
16 |
17 | //@dev - Signers of wallet addresses
18 | let contractCreator
19 | let user1, user2
20 | let users
21 |
22 | //@dev - Wallet addresses
23 | let CONTRACT_CREATOR
24 | let USER_1, USER_2
25 |
26 | before(async function () {
27 | [contractCreator, user1, user2, ...users] = await ethers.getSigners()
28 |
29 | CONTRACT_CREATOR = contractCreator.address
30 | USER_1 = user1.address
31 | USER_2 = user2.address
32 | console.log(`CONTRACT_CREATOR: ${ CONTRACT_CREATOR }`)
33 | console.log(`USER_1: ${ USER_1 }`)
34 | console.log(`USER_2: ${ USER_2 }`)
35 | })
36 |
37 | it("Deploy the ResourceFactory.sol (that the AccessControlList.sol is inherited)", async function () {
38 | const ResourceFactory = await ethers.getContractFactory("ResourceFactory")
39 | resourceFactory = await ResourceFactory.deploy()
40 | })
41 |
42 | it("Create a resource", async function () {
43 | //@dev - When the Resource (that the AccessControlList.sol is inherited) is created (deployed), initial group is created and this contract creator is assigned as a initial admin role
44 | let tx = await resourceFactory.connect(user1).createNewResource()
45 | let txReceipt = await tx.wait()
46 |
47 | let currentResourceId = await resourceFactory.getCurrentResourceId()
48 | console.log(`currentResourceId: ${ currentResourceId }`) // [Retunr]: 1
49 |
50 | let resourceId = await Number(currentResourceId) - 1
51 | console.log(`resourceId: ${ resourceId }`) // [Retunr]: 0
52 |
53 | RESOURCE = await resourceFactory.getResource(resourceId)
54 | console.log(`RESOURCE: ${ RESOURCE }`)
55 |
56 | let Resource = await ethers.getContractFactory("Resource")
57 | resource = await ethers.getContractAt("Resource", RESOURCE)
58 | })
59 |
60 |
61 | ///-------------------------------------------------------
62 | /// Test of methods defined in the AccessControlList.sol
63 | /// (These methods are executed via Resource.sol
64 | ///-------------------------------------------------------
65 |
66 | it("createGroup()", async function () {
67 | let tx = await resource.connect(contractCreator).createGroup()
68 | let txReceipt = await tx.wait()
69 |
70 | //@dev - Retrieve an event log of "GroupCreated"
71 | let eventLog = await getEventLog(txReceipt, "GroupCreated")
72 | console.log(`eventLog of GroupCreated: ${ eventLog }`)
73 | })
74 |
75 | it("assignUserAsAdminRole()", async function () {
76 | const groupId = 0
77 | const userAddress = USER_1
78 |
79 | let tx = await resource.connect(contractCreator).assignUserAsAdminRole(groupId, userAddress)
80 | let txReceipt = await tx.wait()
81 | })
82 |
83 | it("assignUserAsMemberRole()", async function () {
84 | const groupId = 0
85 | const userAddress = USER_2
86 |
87 | let tx = await resource.connect(user1).assignUserAsMemberRole(groupId, userAddress)
88 | let txReceipt = await tx.wait()
89 | })
90 |
91 |
92 | ///--------------------------------
93 | /// Check
94 | ///--------------------------------
95 | it("getGroup()", async function () {
96 | const groupId = 0
97 | let group = await resource.connect(user1).getGroup(groupId)
98 | console.log(`group: ${ group }`)
99 | })
100 |
101 | it("getUser()", async function () {
102 | const userId0 = 0
103 | let user0 = await resource.getUser(userId0)
104 |
105 | const userId1 = 1
106 | let user1 = await resource.getUser(userId1)
107 |
108 | console.log(`user0: ${ user0 }`)
109 | console.log(`user1: ${ user1 }`)
110 | })
111 |
112 | it("getUserAddresses()", async function () {
113 | let users = await resource.getUserAddresses()
114 | console.log(`userAddresses: ${ users }`)
115 | })
116 |
117 | it("getCurrentAdminAddresses()", async function () {
118 | let currentAdminAddresses = await resource.getCurrentAdminAddresses()
119 | console.log(`currentAdminAddresses: ${ currentAdminAddresses }`)
120 | })
121 |
122 | it("getCurrentMemberAddresses()", async function () {
123 | let currentMemberAddresses = await resource.getCurrentMemberAddresses()
124 | console.log(`currentMemberAddresses: ${ currentMemberAddresses }`)
125 | })
126 |
127 | it("getCurrentGroupId()", async function () {
128 | let currentGroupId = await resource.getCurrentGroupId()
129 | console.log(`currentGroupId: ${ currentGroupId }`)
130 | })
131 |
132 |
133 | ///-------------------------------------------------------
134 | /// Test of methods defined in the Resource.sol
135 | ///-------------------------------------------------------
136 | it("getUserByAddress()", async function () {
137 | let userByAddress1 = await resource.getUserByAddress(USER_1)
138 | console.log(`userByAddress1: ${ userByAddress1 }`)
139 |
140 | let userRole1 = await resource.getUserByAddress(USER_1)[0]
141 | console.log(`userRole1: ${ userRole1 }`)
142 |
143 | let userByAddress2 = await resource.getUserByAddress(USER_2)
144 | console.log(`userByAddress2: ${ userByAddress2 }`)
145 | })
146 |
147 | it("createNewResourceMetadata()", async function () {
148 | const resourceName = "Example Resource 1"
149 | const resourceURI = "ipfs://QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
150 |
151 | let tx = await resource.connect(user1).createNewResourceMetadata(resourceName, resourceURI)
152 | let txReceipt = await tx.wait()
153 | })
154 |
155 | it("getResourceMetadata() - user who has an admin role should be able to call this method", async function () {
156 | let resourceMetadata = await resource.connect(user1).getResourceMetadata()
157 | let _resourceName = resourceMetadata.resourceName
158 | let _resourceURI = resourceMetadata.resourceURI
159 | console.log(`resourceMetadata: ${ resourceMetadata }`)
160 | console.log(`resourceName: ${ _resourceName }`)
161 | console.log(`resourceURI: ${ _resourceURI }`)
162 | })
163 |
164 | it("getResourceMetadata() - user who has a member role should be able to call this method", async function () {
165 | let resourceMetadata = await resource.connect(user2).getResourceMetadata()
166 | let _resourceName = resourceMetadata.resourceName
167 | let _resourceURI = resourceMetadata.resourceURI
168 | console.log(`resourceMetadata: ${ resourceMetadata }`)
169 | console.log(`resourceName: ${ _resourceName }`)
170 | console.log(`resourceURI: ${ _resourceURI }`)
171 | })
172 |
173 | ///--------------------------------
174 | /// Test that remove roles
175 | ///--------------------------------
176 | it("removeAdminRole()", async function () {
177 | const groupId = 0
178 | const userId = 0
179 |
180 | let tx = await resource.connect(user1).removeAdminRole(groupId, userId)
181 | let txReceipt = await tx.wait()
182 | })
183 |
184 | it("removeMemberRole()", async function () {
185 | const groupId = 0
186 | const userId = 1
187 |
188 | let tx = await resource.connect(user2).removeMemberRole(groupId, userId)
189 | let txReceipt = await tx.wait()
190 | })
191 |
192 | })
193 |
--------------------------------------------------------------------------------
/test/ethersjs-helper/ethersjsHelper.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai")
2 | const { ethers } = require("hardhat")
3 |
4 |
5 | function convertHexToString(hex) {
6 | // [Example]: ethers.utils.arrayify("0x1234") -> Uint8Array [ 18, 52 ]
7 | return ethers.utils.arrayify(`${ hex }`)
8 | }
9 |
10 | function convertStringToHex(string) {
11 | // [Example]: ethers.utils.hexlify([1, 2, 3, 4]) -> '0x01020304'
12 | return ethers.utils.hexlify([string])
13 | }
14 |
15 |
16 | function toWei(amount) {
17 | return ethers.utils.parseEther(`${ amount }`)
18 | }
19 |
20 | function fromWei(amount) {
21 | return ethers.utils.formatEther(`${ amount }`)
22 | }
23 |
24 | //@dev - Method for retrieving an event log that is associated with "eventName" specified
25 | async function getEventLog(txReceipt, eventName) {
26 | for (let i = 0; i < txReceipt.events.length; i++) {
27 | const eventLogs = txReceipt.events[i];
28 | console.log(`eventLogs: ${ JSON.stringify(eventLogs, null, 2) }`)
29 |
30 | if (eventLogs["event"] == eventName) {
31 | const _args = eventLogs["args"]
32 | return _args // [NOTE] Return event log specified as array
33 | }
34 | }
35 | }
36 |
37 | async function getCurrentBlock() {}
38 |
39 | async function getCurrentTimestamp() {}
40 |
41 | //@dev - Export methods
42 | module.exports = { convertHexToString, convertStringToHex, toWei, fromWei, getEventLog, getCurrentBlock, getCurrentTimestamp }
43 |
--------------------------------------------------------------------------------
/test/time-helper/timeHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @dev - Time-dependent tests with Hardhat
3 | * - This RPC relevant methods are referenced from: https://ethereum.stackexchange.com/questions/86633/time-dependent-tests-with-hardhat
4 | */
5 |
6 | //@dev - Returns the timestamp of the latest mined block. Should be coupled with advanceBlock to retrieve the current blockchain time.
7 | async function getLatestTimestamp() {
8 | const date = new Date() //@dev - Create a Date object
9 | const _unixTimestamp = date.getTime() //@dev - Get UNIX timestamp (mili-seconds)
10 | const unixTimestamp = Math.floor(_unixTimestamp / 1000) //@dev - Convert UNIX timestamp to seconds
11 | return unixTimestamp
12 | }
13 |
14 | //@dev - Increases the time of the blockchain by duration (in seconds), and mines a new block with that timestamp.
15 | async function increaseTime(duration) {
16 | // [TODO]: Replace
17 | // suppose the current block has a timestamp of 01:00 PM
18 | await network.provider.send("evm_increaseTime", [duration])
19 | await network.provider.send("evm_mine") // this one will have 02:00 PM as its timestamp
20 | }
21 |
22 | //@dev - Same as increase, but a target time is specified instead of a duration.
23 | async function increaseTimeTo(target) {
24 | // [TODO]: Replace
25 | //return await time.increaseTo(target)
26 | }
27 |
28 | //@dev - Export methods
29 | module.exports = { getLatestTimestamp, increaseTime, increaseTimeTo }
30 |
--------------------------------------------------------------------------------