├── .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 | ![diagram_ACL-smart-contract for-lit-protocol](https://user-images.githubusercontent.com/19357502/159188912-d65ea650-7e08-4c17-988e-d2567b6e78ec.jpeg) 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 | --------------------------------------------------------------------------------