├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── BOUNTY.yml │ ├── bug_report.md │ ├── enhancement.md │ └── feature_request.md ├── .gitignore ├── CONTRIBUTING.md ├── FUNDING.json ├── LICENSE ├── README.md ├── audits ├── Potlock-NEAR-Rust-Smart-Contract-Security-Assessment-Guvenkaya-Jan-30-2024.pdf ├── Potlock-NEAR-Smart-Contracts-Quadratic-Funding-Audit-Ottersec-February-15-2024.pdf └── readme.md ├── contracts ├── .gitignore ├── .mocharc.json ├── Cargo.lock ├── Cargo.toml ├── README.md ├── donation │ ├── Cargo.toml │ ├── README.md │ ├── out │ │ └── main.wasm │ ├── scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── src │ │ ├── constants.rs │ │ ├── donations.rs │ │ ├── events.rs │ │ ├── internal.rs │ │ ├── lib.rs │ │ ├── owner.rs │ │ ├── source.rs │ │ ├── storage.rs │ │ └── utils.rs ├── lists │ ├── Cargo.toml │ ├── README.md │ ├── out │ │ └── main.wasm │ ├── scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── src │ │ ├── admins.rs │ │ ├── constants.rs │ │ ├── events.rs │ │ ├── internal.rs │ │ ├── lib.rs │ │ ├── lists.rs │ │ ├── registrations.rs │ │ ├── source.rs │ │ ├── utils.rs │ │ └── validation.rs ├── package.json ├── pot │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── out │ │ └── main.wasm │ ├── scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── src │ │ ├── admin.rs │ │ ├── applications.rs │ │ ├── config.rs │ │ ├── constants.rs │ │ ├── donations.rs │ │ ├── events.rs │ │ ├── internal.rs │ │ ├── lib.rs │ │ ├── payouts.rs │ │ ├── source.rs │ │ ├── utils.rs │ │ └── validation.rs ├── pot_factory │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── out │ │ └── main.wasm │ ├── scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── src │ │ ├── admin.rs │ │ ├── constants.rs │ │ ├── events.rs │ │ ├── internal.rs │ │ ├── lib.rs │ │ ├── pot.rs │ │ ├── source.rs │ │ ├── utils.rs │ │ └── validation.rs ├── registry │ ├── Cargo.toml │ ├── README.md │ ├── out │ │ └── main.wasm │ ├── scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── src │ │ ├── admins.rs │ │ ├── constants.rs │ │ ├── events.rs │ │ ├── internal.rs │ │ ├── lib.rs │ │ ├── owner.rs │ │ ├── projects.rs │ │ ├── source.rs │ │ └── utils.rs ├── scratch.js ├── sybil │ ├── Cargo.toml │ ├── README.md │ ├── out │ │ └── main.wasm │ ├── scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── src │ │ ├── admin.rs │ │ ├── constants.rs │ │ ├── events.rs │ │ ├── human.rs │ │ ├── internal.rs │ │ ├── lib.rs │ │ ├── owner.rs │ │ ├── providers.rs │ │ ├── source.rs │ │ ├── stamps.rs │ │ ├── utils.rs │ │ └── validation.rs ├── sybil_provider_simulator │ ├── Cargo.toml │ ├── README.md │ ├── out │ │ └── main.wasm │ ├── scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── src │ │ ├── lib.rs │ │ └── utils.rs ├── test │ ├── README.md │ ├── donation │ │ ├── config.ts │ │ ├── contract.test.ts │ │ ├── setup.ts │ │ └── utils.ts │ ├── pot │ │ ├── config.ts │ │ ├── contract.test.ts │ │ ├── setup.ts │ │ └── utils.ts │ ├── pot_factory │ │ ├── config.ts │ │ ├── contract.test.ts │ │ ├── setup.ts │ │ └── utils.ts │ ├── registry │ │ ├── config.ts │ │ ├── contract.test.ts │ │ ├── setup.ts │ │ └── utils.ts │ ├── types.d.ts │ └── utils │ │ ├── constants.ts │ │ ├── helpers.ts │ │ ├── patch-config.mjs │ │ └── quadratics.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | polar: # Replace with a single Polar username 14 | custom: https://bos.potlock.org/?tab=project&projectId=potlock.near 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BOUNTY.yml: -------------------------------------------------------------------------------- 1 | name: "Simple Bounty" 2 | description: "Use this template to create a HEROES Simple Bounty via Github bot" 3 | title: "Bounty: " 4 | labels: ["bounty"] 5 | assignees: heroes-bot-test 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Hi! Let's set up your bounty! Please don't change the template - @heroes-bot-test won't be able to help you. 11 | 12 | - type: dropdown 13 | id: type 14 | attributes: 15 | label: What talent are you looking for? 16 | options: 17 | - Marketing 18 | - Development 19 | - Design 20 | - Other 21 | - Content 22 | - Research 23 | - Audit 24 | 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: What you need to be done? 29 | 30 | - type: dropdown 31 | id: tags 32 | attributes: 33 | label: Tags 34 | description: Add tags that match the topic of the work 35 | multiple: true 36 | options: 37 | - API 38 | - Blockchain 39 | - Community 40 | - CSS 41 | - DAO 42 | - dApp 43 | - DeFi 44 | - Design 45 | - Documentation 46 | - HTML 47 | - Javascript 48 | - NFT 49 | - React 50 | - Rust 51 | - Smart contract 52 | - Typescript 53 | - UI/UX 54 | - web3 55 | - Translation 56 | - Illustration 57 | - Branding 58 | - Copywriting 59 | - Blogging 60 | - Editing 61 | - Video Creation 62 | - Social Media 63 | - Graphic Design 64 | - Transcription 65 | - Product Design 66 | - Artificial Intelligence 67 | - Quality Assurance 68 | - Risk Assessment 69 | - Security Audit 70 | - Bug Bounty 71 | - Code Review 72 | - Blockchain Security 73 | - Smart Contract Testing 74 | - Penetration Testing 75 | - Vulnerability Assessment 76 | - BOS 77 | - News 78 | - Hackathon 79 | - NEARCON2023 80 | - NEARWEEK 81 | 82 | - type: input 83 | id: deadline 84 | attributes: 85 | label: Deadline 86 | description: "Set a deadline for your bounty. Please enter the date in format: DD.MM.YYYY" 87 | placeholder: "19.05.2027" 88 | 89 | - type: dropdown 90 | id: currencyType 91 | attributes: 92 | label: Currency 93 | description: What is the currency you want to pay? 94 | options: 95 | - USDC.e 96 | - USDT.e 97 | - DAI 98 | - wNEAR 99 | - USDt 100 | - XP 101 | - marmaj 102 | - NEKO 103 | - JUMP 104 | - USDC 105 | - NEARVIDIA 106 | default: 0 107 | validations: 108 | required: true 109 | 110 | - type: input 111 | id: currencyAmount 112 | attributes: 113 | label: Amount 114 | description: How much it will be cost? 115 | 116 | - type: markdown 117 | attributes: 118 | value: "## Advanced settings" 119 | 120 | - type: checkboxes 121 | id: kyc 122 | attributes: 123 | label: KYC 124 | description: "Use HEROES' KYC Verification, only applicants who passed HEROES' KYC can apply and work on this bounty!" 125 | options: 126 | - label: Use KYC Verification 127 | 128 | - type: markdown 129 | attributes: 130 | value: | 131 | ### This cannot be changed once the bounty is live! 132 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement 3 | about: not a feature-request but small enhancement / improvement 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Title 11 | 12 | [Short and descriptive title of the enhancement] 13 | 14 | # Summary 15 | 16 | [Brief overview of the enhancement and why it is needed or desired] 17 | 18 | # Motivation 19 | 20 | [More detailed explanation of the motivation for the enhancement, including any benefits it would provide] 21 | 22 | # Description 23 | 24 | [Detailed description of the enhancement, including how it would work and any design considerations] 25 | 26 | # Alternatives 27 | 28 | [Discussion of any alternative solutions that were considered and why the proposed solution is preferred] 29 | 30 | # Risks 31 | 32 | [Identification and mitigation of any potential risks associated with the enhancement] 33 | 34 | # Acceptance Criteria 35 | 36 | [List of criteria that must be met for the enhancement to be considered accepted] 37 | 38 | # Additional Information 39 | 40 | [Any other relevant information, such as links to related issues or pull requests] 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a feature 4 | title: '' 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Problem 11 | Explain the problem in details 12 | 13 | # User Story(s): 14 | As a _______, I want to ____, so that I can 15 | 16 | # Description 17 | Description of solution 18 | 19 | # Acceptance Criteria 20 | Outline what needs to be done 21 | 22 | # Limitations 23 | Outline any potential limitations 24 | # Resources 25 | - outline links and relevant resources, references implementations 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #contracts 2 | **/target/ 3 | **/neardev/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Welcome to the PotLock core repository! We appreciate your interest in contributing. Before you get started, please take a moment to review these guidelines to ensure a smooth and collaborative contribution process. 4 | 5 | ## Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [How to Contribute](#how-to-contribute) 9 | - [Reporting Issues](#reporting-issues) 10 | - [Submitting Pull Requests](#submitting-pull-requests) 11 | - [Coding Guidelines](#coding-guidelines) 12 | - [Branching Strategy](#branching-strategy) 13 | - [Coding Style](#coding-style) 14 | - [Documentation](#documentation) 15 | - [Testing](#testing) 16 | - [Review Process](#review-process) 17 | - [Community and Communication](#community-and-communication) 18 | - [Attribution](#attribution) 19 | 20 | ## Code of Conduct 21 | 22 | Please review and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) in all your interactions regarding this project. 23 | 24 | ## How to Contribute 25 | 26 | ### Reporting Issues 27 | 28 | If you encounter any bugs, problems, or have suggestions for improvements, please check the [Issues](../../issues) section before creating a new one. If the issue is not yet reported, you can create a new issue following the provided template. Be sure to provide as much detail as possible to help us understand and address the problem. 29 | 30 | ### Submitting Pull Requests 31 | 32 | We welcome contributions through pull requests (PRs). To submit a PR, follow these steps: 33 | 34 | 1. Fork the repository to your GitHub account. 35 | 2. Create a new branch from the `main` branch. Please use a descriptive and concise name for your branch. 36 | 3. Commit your changes to your branch. Be sure to include clear commit messages that explain the purpose of each change. 37 | 4. Push your branch to your forked repository. 38 | 5. Create a pull request against the `main` branch of the original repository. 39 | 6. Ensure your PR description explains the changes you've made and why they are valuable. 40 | 7. All PRs must pass the automated tests and adhere to the coding guidelines. 41 | 42 | ## Coding Guidelines 43 | 44 | ### Branching Strategy 45 | 46 | We follow the [Gitflow](https://nvie.com/posts/a-successful-git-branching-model/) branching model: 47 | 48 | - `main` branch contains stable production-ready code. 49 | - `develop` branch contains the latest development code. 50 | - Feature branches should be created from `develop` and merged back into `develop`. 51 | 52 | ### Coding Style 53 | 54 | Please maintain a consistent coding style throughout the project. Follow the style guide for your programming language (e.g., PEP 8 for Python, Airbnb JavaScript Style Guide for JavaScript). 55 | 56 | ## Documentation 57 | 58 | - Document your code using clear and concise comments. 59 | - Update the README if necessary to reflect new features or changes. 60 | - Provide relevant documentation in the `docs` directory. 61 | 62 | ## Testing 63 | 64 | - Write unit tests for new code. 65 | - Ensure all tests pass before submitting a pull request. 66 | 67 | ## Review Process 68 | 69 | - Pull requests will be reviewed by maintainers. 70 | - Constructive feedback will be provided; you may need to make changes before your PR is merged. 71 | - Once approved, your PR will be merged into the appropriate branch. 72 | 73 | Sure, here's an added section for issue tracking in your contribution guidelines: 74 | 75 | ## Issue Tracking 76 | 77 | We use GitHub Issues to track and manage tasks, enhancements, and bugs for this project. This section outlines how to effectively work with issues: 78 | 79 | ### Opening Issues 80 | 81 | If you encounter a bug, have a feature request, or want to propose an enhancement, please follow these steps: 82 | 83 | 1. **Search**: Before opening a new issue, search the [existing issues](../../issues) to see if it has already been reported or discussed. 84 | 85 | 2. **Create a New Issue**: If you couldn't find a similar issue, create a new one. Use the provided issue templates to structure your report or request effectively. 86 | 87 | 3. **Provide Details**: When creating an issue, provide as much context and detail as possible. This helps contributors understand the problem and work towards a solution. 88 | 89 | ### Issue Labels 90 | 91 | We use labels to categorize and prioritize issues. These labels provide valuable information to contributors and maintainers. Here are some of the labels you might encounter: 92 | 93 | - **Bug**: Used for identifying bugs or unexpected behavior. 94 | - **Feature Request**: For suggesting new features or enhancements. 95 | - **Documentation**: Pertaining to documentation improvements or additions. 96 | - **Help Wanted**: Indicates that assistance from the community is requested. 97 | - **Good First Issue**: Suggested for newcomers as a starting point. 98 | - **Priority**: Indicates the urgency or importance of the issue. 99 | 100 | ### Working on Issues 101 | 102 | If you'd like to work on an existing issue: 103 | 104 | 1. **Assign Yourself**: If you're planning to address an issue, assign it to yourself to let others know you're working on it. 105 | 106 | 2. **Ask for Clarification**: If you're unsure about any aspect of the issue, feel free to ask for clarification in the issue thread. 107 | 108 | 3. **Create a Pull Request**: After making changes, create a pull request that references the issue. This helps reviewers understand the context and purpose of your changes. 109 | 110 | ### Closing Issues 111 | 112 | An issue can be closed once its purpose has been fulfilled. This may include fixing a bug, implementing a feature, or determining that the issue is no longer relevant. 113 | 114 | - **Closing by Contributors**: Contributors should ensure that the issue's resolution is tested and verified before closing it. 115 | 116 | - **Closing by Maintainers**: Maintainers may close an issue if it's a duplicate, not actionable, or doesn't align with project goals. A brief explanation will be provided. 117 | 118 | Remember, the issue tracker is a collaborative space, and open communication is key to successful collaboration. 119 | 120 | **Note**: These guidelines are meant to streamline our issue tracking process. Always refer to the specific issue templates and labels in the repository for precise instructions. 121 | 122 | --- 123 | Feel free to adapt and customize this section based on your project's specific issue tracking workflow and terminology. 124 | 125 | ## Community and Communication 126 | 127 | - Join the [Discussion](../../discussions) board for general questions and discussions. 128 | - Use the Issues and Pull Requests for project-specific topics. 129 | - Respect other contributors and maintain a friendly and inclusive atmosphere. 130 | 131 | ## Attribution 132 | 133 | These contribution guidelines are adapted from [Open Source Guides](https://opensource.guide/) under the Creative Commons Attribution-ShareAlike license. 134 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0x88B93d4D440155448fbB3Cf260208b75FC0117C0" 5 | } 6 | }, 7 | "potlock": { 8 | "near": { 9 | "ownedBy": "potlock.near" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Potluck Labs, Inc 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PotLock Core Contracts 2 | Welcome to the repository for the core smart contracts of the [PotLock Core] project on the NEAR blockchain. This repository contains the foundational smart contracts that power various functionalities within our decentralized application ecosystem. 3 | 4 | ## Table of Contents 5 | 6 | - [PotLock Core Contracts](#potlock-core-contracts) 7 | - [Table of Contents](#table-of-contents) 8 | - [Getting Started](#getting-started) 9 | - [Prerequisites](#prerequisites) 10 | - [Project Name - Core Smart Contracts](#project-name---core-smart-contracts) 11 | - [Table of Contents](#table-of-contents-1) 12 | - [Overview](#overview) 13 | - [Getting Started](#getting-started-1) 14 | - [Prerequisites](#prerequisites-1) 15 | - [Installation](#installation) 16 | - [Smart Contracts](#smart-contracts) 17 | - [Usage](#usage) 18 | - [Contributing](#contributing) 19 | - [License](#license) 20 | - [Contact](#contact) 21 | 22 | 23 | ## Getting Started 24 | 25 | ### Prerequisites 26 | 27 | - [NEAR CLI](https://docs.near.org/docs/tools/near-cli) installed. 28 | - NEAR TestNet account. [Create one here](https://wallet.testnet.near.org/). 29 | 30 | 31 | # Project Name - Core Smart Contracts 32 | 33 | Welcome to the repository for the core smart contracts of the [Project Name] project on the NEAR blockchain. This repository contains the foundational smart contracts that power various functionalities within our decentralized application ecosystem. 34 | 35 | ## Table of Contents 36 | 37 | - [PotLock Core Contracts](#potlock-core-contracts) 38 | - [Table of Contents](#table-of-contents) 39 | - [Getting Started](#getting-started) 40 | - [Prerequisites](#prerequisites) 41 | - [Project Name - Core Smart Contracts](#project-name---core-smart-contracts) 42 | - [Table of Contents](#table-of-contents-1) 43 | - [Overview](#overview) 44 | - [Getting Started](#getting-started-1) 45 | - [Prerequisites](#prerequisites-1) 46 | - [Installation](#installation) 47 | - [Smart Contracts](#smart-contracts) 48 | - [Usage](#usage) 49 | - [Contributing](#contributing) 50 | - [License](#license) 51 | - [Contact](#contact) 52 | 53 | ## Overview 54 | 55 | [PotLock Core] is a set of 6 contracts. For an overview of the contracts check out the docs at https://docs.potlock.io/contracts/contracts-overview. 56 | 57 | For detailed documentation on each contract, [start here](contracts) 58 | 59 | ## Getting Started 60 | 61 | ### Prerequisites 62 | 63 | - [NEAR CLI](https://docs.near.org/docs/tools/near-cli) installed. 64 | - NEAR TestNet account. [Create one here](https://wallet.testnet.near.org/). 65 | 66 | ### Installation 67 | 68 | 69 | ## Smart Contracts 70 | 71 | For more info on Smart Contracts, please visit [/contracts directory](contracts). 72 | 73 | ## Usage 74 | 75 | To be written 76 | 77 | ## Contributing 78 | 79 | We welcome contributions from the community! To contribute to our core smart contracts, please follow the guidelines outlined in [CONTRIBUTING.md](CONTRIBUTING.md). 80 | 81 | ## License 82 | 83 | This project is licensed under the [MIT License](LICENSE). 84 | 85 | ## Contact 86 | 87 | - For questions and discussions, join us on [Telegram](https://NEAReFi.org/telegram). 88 | - Follow us on [Twitter](https://twitter.com/PotLock_). 89 | - Visit our website: [https://PotLock.io](https://PotLock.io). 90 | ``` 91 | -------------------------------------------------------------------------------- /audits/Potlock-NEAR-Rust-Smart-Contract-Security-Assessment-Guvenkaya-Jan-30-2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/audits/Potlock-NEAR-Rust-Smart-Contract-Security-Assessment-Guvenkaya-Jan-30-2024.pdf -------------------------------------------------------------------------------- /audits/Potlock-NEAR-Smart-Contracts-Quadratic-Funding-Audit-Ottersec-February-15-2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/audits/Potlock-NEAR-Smart-Contracts-Quadratic-Funding-Audit-Ottersec-February-15-2024.pdf -------------------------------------------------------------------------------- /audits/readme.md: -------------------------------------------------------------------------------- 1 | # NEAR Smart Contract Security Audits Potlock 2 | [Guvenkaya](https://www.guvenkaya.co/) - January 30th, 2024 | [Click here](https://github.com/Guvenkaya/public-reports/blob/master/Potlock-NEAR-Rust-Smart-Contract-Security-Assessment.pdf) 3 | [Ottersec](https://osec.io/) - Feb TBA, 2024 | [Soon]() 4 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /contracts/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": ["test/**/contract.test.ts"], 3 | "require": ["ts-node/register"] 4 | } 5 | -------------------------------------------------------------------------------- /contracts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "donation", 4 | "lists", 5 | "pot", 6 | "pot_factory", 7 | "registry", 8 | "sybil", 9 | "sybil_provider_simulator" 10 | ] 11 | 12 | [profile.release] 13 | codegen-units = 1 14 | # Tell `rustc` to optimize for small code size. 15 | opt-level = "z" 16 | lto = true 17 | debug = false 18 | panic = "abort" 19 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 20 | overflow-checks = true 21 | -------------------------------------------------------------------------------- /contracts/README.md: -------------------------------------------------------------------------------- 1 | # Potlock Contracts 2 | 3 | Welcome to the home of public goods funding on NEAR! ✨🫕 Read more on our mission and roadmap [here](https://potlock.io). 4 | 5 | ## Introduction 6 | 7 | ## Overview 8 | 9 | The Potlock stack contains 5 main contracts: 10 | 11 | ### [Pot Factory](pot_factory) 12 | 13 | A Factory contract that deploys Pots. 14 | 15 | ### [Pot](pot) 16 | 17 | A configurable, flexible yet secure contract that manages a funding round. 18 | 19 | ### [Sybil](sybil) 20 | 21 | A registry for sybil resistance providers, allowing users to collect stamps indicating their verification with registered providers. Additionally, abstracts away individual sybil resistance providers/solutions to provide a single contract to call `is_human`. 22 | 23 | ### [Registry](registry) 24 | 25 | Projects that wish to apply for a Pot (funding round) may be required to be registered on a project Registry. Flexibility is provided to use a 3rd-party registry; this contract is the registry that Potlock uses by default. Each Pot contract that implements a registry requirement will verify the project against the specified Registry when a project applies for the Pot. 26 | 27 | ### [Donation](donation) 28 | 29 | Provides a means to donate NEAR or FTs (soon) to any account. 30 | 31 | 32 | ### [Sybil Provider Simulator](sybil_provider_simulator) 33 | 34 | Not technically a part of the PotLock stack, this contract simulates a 3rd-party Sybil Resistance Provider. 35 | 36 | 37 | ## Tests 38 | 39 | Integration tests for the earliest implementations of these contracts were written using near-api-js and can be found in the [`/test` directory](test). However, **these tests are no longer up-to-date and are not being maintained.** 40 | 41 | Before the public use of these contracts, integration tests should be added using [near-workspaces-rs](https://github.com/near/near-workspaces-rs) (check out additional resources [here](https://docs.near.org/develop/testing/introduction) and [here](https://docs.near.org/sdk/rust/testing/integration-tests)), as well as native unit tests where appropriate. 42 | 43 | In the meantime, these contracts have been thoroughly tested manually and via the original tests, and should reliably function as expected. 44 | 45 | ## Known Issues 46 | 47 | - Some TODO's need to be addressed 48 | - FTs not yet supported 49 | - Milestones not yet supported (Pot) 50 | - Additional funding mechanisms other than Quadratic Funding not yet supported (Pot) 51 | - Sybil contract `is_human` method not yet customizable 52 | 53 | -------------------------------------------------------------------------------- /contracts/donation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "potlock-donation" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [dependencies] 11 | near-sdk = "4.1.1" 12 | # near-sdk = "5.0.0-alpha.2" 13 | # [profile.release] # removed as added to root Cargo.toml 14 | # codegen-units = 1 15 | # # Tell `rustc` to optimize for small code size. 16 | # opt-level = "z" 17 | # lto = true 18 | # debug = false 19 | # panic = "abort" 20 | # # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 21 | # overflow-checks = true 22 | -------------------------------------------------------------------------------- /contracts/donation/out/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/contracts/donation/out/main.wasm -------------------------------------------------------------------------------- /contracts/donation/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ">> Building Donation contract" 4 | 5 | set -e 6 | 7 | export CARGO_TARGET_DIR=target 8 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 9 | mkdir -p ./out 10 | cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm 11 | echo ">> Finished Building Donation contract" -------------------------------------------------------------------------------- /contracts/donation/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $? -ne 0 ]; then 4 | echo ">> Error building contract" 5 | exit 1 6 | fi 7 | 8 | echo ">> Deploying Donation contract!" 9 | 10 | # https://docs.near.org/tools/near-cli#near-dev-deploy 11 | near dev-deploy --wasmFile ./out/main.wasm -------------------------------------------------------------------------------- /contracts/donation/src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub const MAX_PROTOCOL_FEE_BASIS_POINTS: u32 = 1000; 4 | pub const MAX_REFERRAL_FEE_BASIS_POINTS: u32 = 200; 5 | 6 | pub const EVENT_JSON_PREFIX: &str = "EVENT_JSON:"; 7 | 8 | pub const TGAS: u64 = 1_000_000_000_000; // 1 TGAS 9 | pub const XCC_GAS_DEFAULT: u64 = TGAS * 10; // 10 TGAS 10 | pub const NO_DEPOSIT: Balance = 0; 11 | pub const ONE_YOCTO: Balance = 1; 12 | -------------------------------------------------------------------------------- /contracts/donation/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// donation 4 | pub(crate) fn log_donation_event(donation: &DonationExternal) { 5 | env::log_str( 6 | format!( 7 | "{}{}", 8 | EVENT_JSON_PREFIX, 9 | json!({ 10 | "standard": "potlock", 11 | "version": "1.0.0", 12 | "event": "donation", 13 | "data": [ 14 | { 15 | "donation": donation, 16 | } 17 | ] 18 | }) 19 | ) 20 | .as_ref(), 21 | ); 22 | } 23 | 24 | /// source metadata update 25 | pub(crate) fn log_set_source_metadata_event(source_metadata: &ContractSourceMetadata) { 26 | env::log_str( 27 | format!( 28 | "{}{}", 29 | EVENT_JSON_PREFIX, 30 | json!({ 31 | "standard": "potlock", 32 | "version": "1.0.0", 33 | "event": "set_source_metadata", 34 | "data": [ 35 | { 36 | "source_metadata": source_metadata, 37 | } 38 | ] 39 | }) 40 | ) 41 | .as_ref(), 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /contracts/donation/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Contract { 4 | pub(crate) fn assert_at_least_one_yocto(&self) { 5 | assert!( 6 | env::attached_deposit() >= 1, 7 | "At least one yoctoNEAR must be attached" 8 | ); 9 | } 10 | 11 | pub(crate) fn assert_owner(&self) { 12 | assert_eq!( 13 | env::predecessor_account_id(), 14 | self.owner, 15 | "Owner-only action" 16 | ); 17 | // require owner to attach at least one yoctoNEAR for security purposes 18 | self.assert_at_least_one_yocto(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/donation/src/owner.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | // OWNER 6 | #[payable] 7 | pub fn owner_change_owner(&mut self, owner: AccountId) { 8 | // TODO: consider renaming to owner_set_owner, but currently deployed Registry uses owner_change_owner. 9 | self.assert_owner(); 10 | let initial_storage_usage = env::storage_usage(); 11 | self.owner = owner; 12 | refund_deposit(initial_storage_usage); 13 | } 14 | 15 | pub fn get_owner(&self) -> AccountId { 16 | self.owner.clone() 17 | } 18 | 19 | // FEES CONFIG 20 | #[payable] 21 | pub fn owner_set_protocol_fee_basis_points(&mut self, protocol_fee_basis_points: u32) { 22 | self.assert_owner(); 23 | let initial_storage_usage = env::storage_usage(); 24 | self.protocol_fee_basis_points = protocol_fee_basis_points; 25 | refund_deposit(initial_storage_usage); 26 | } 27 | 28 | // referral_fee_basis_points 29 | #[payable] 30 | pub fn owner_set_referral_fee_basis_points(&mut self, referral_fee_basis_points: u32) { 31 | self.assert_owner(); 32 | let initial_storage_usage = env::storage_usage(); 33 | self.referral_fee_basis_points = referral_fee_basis_points; 34 | refund_deposit(initial_storage_usage); 35 | } 36 | 37 | // protocol_fee_recipient_account 38 | #[payable] 39 | pub fn owner_set_protocol_fee_recipient_account( 40 | &mut self, 41 | protocol_fee_recipient_account: AccountId, 42 | ) { 43 | self.assert_owner(); 44 | let initial_storage_usage = env::storage_usage(); 45 | self.protocol_fee_recipient_account = protocol_fee_recipient_account; 46 | refund_deposit(initial_storage_usage); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/donation/src/source.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// CONTRACT SOURCE METADATA - as per NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` 4 | #[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize, PanicOnDefault)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct ContractSourceMetadata { 7 | /// Version of source code, e.g. "v1.0.0", could correspond to Git tag 8 | pub version: String, 9 | /// Git commit hash of currently deployed contract code 10 | pub commit_hash: String, 11 | /// GitHub repo url for currently deployed contract code 12 | pub link: String, 13 | } 14 | 15 | #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub enum VersionedContractSourceMetadata { 18 | Current(ContractSourceMetadata), 19 | } 20 | 21 | // Convert from VersionedContractSourceMetadata to ContractSourceMetadata 22 | impl From for ContractSourceMetadata { 23 | fn from(metadata: VersionedContractSourceMetadata) -> Self { 24 | match metadata { 25 | VersionedContractSourceMetadata::Current(current) => current, 26 | } 27 | } 28 | } 29 | 30 | #[near_bindgen] 31 | impl Contract { 32 | #[payable] 33 | pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetadata) { 34 | // only contract account (aka the account that can deploy new code to this contract) can call this method 35 | require!( 36 | env::predecessor_account_id() == env::current_account_id(), 37 | "Only contract account can call this method" 38 | ); 39 | self.contract_source_metadata 40 | .set(&VersionedContractSourceMetadata::from( 41 | VersionedContractSourceMetadata::Current(source_metadata.clone()), 42 | )); 43 | // emit event 44 | log_set_source_metadata_event(&source_metadata); 45 | } 46 | 47 | pub fn get_contract_source_metadata(&self) -> Option { 48 | let source_metadata = self.contract_source_metadata.get(); 49 | if source_metadata.is_some() { 50 | Some(ContractSourceMetadata::from(source_metadata.unwrap())) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/donation/src/storage.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn storage_deposit(&mut self) -> U128 { 7 | let mut deposit = env::attached_deposit(); 8 | let initial_storage_usage = env::storage_usage(); 9 | let existing_mapping = self.storage_deposits.get(&env::predecessor_account_id()); 10 | if existing_mapping.is_none() { 11 | // insert record here and check how much storage was used, then subtract that cost from the deposit 12 | self.storage_deposits 13 | .insert(&env::predecessor_account_id(), &0); 14 | let storage_usage = env::storage_usage() - initial_storage_usage; 15 | let required_deposit = storage_usage as u128 * env::storage_byte_cost(); 16 | assert!( 17 | deposit >= required_deposit, 18 | "The deposit is less than the required storage amount." 19 | ); 20 | deposit -= required_deposit; 21 | } 22 | let account_id = env::predecessor_account_id(); 23 | let storage_balance = self.storage_balance_of(&account_id); 24 | let new_storage_balance = storage_balance.0 + deposit; 25 | self.storage_deposits 26 | .insert(&account_id, &new_storage_balance); 27 | new_storage_balance.into() 28 | } 29 | 30 | pub fn storage_withdraw(&mut self, amount: Option) -> U128 { 31 | let account_id = env::predecessor_account_id(); 32 | let storage_balance = self.storage_balance_of(&account_id); 33 | let amount = amount.map(|a| a.0).unwrap_or(storage_balance.0); 34 | assert!( 35 | amount <= storage_balance.0, 36 | "The withdrawal amount can't exceed the account storage balance." 37 | ); 38 | let remainder = storage_balance.0 - amount; 39 | if remainder > 0 { 40 | self.storage_deposits.insert(&account_id, &remainder); 41 | Promise::new(account_id).transfer(amount); 42 | } else { 43 | // remove mapping and refund user for freed storage 44 | let initial_storage_usage = env::storage_usage(); 45 | self.storage_deposits.remove(&account_id); 46 | let storage_usage = initial_storage_usage - env::storage_usage(); 47 | let refund = storage_usage as u128 * env::storage_byte_cost(); 48 | Promise::new(account_id).transfer(refund); 49 | } 50 | remainder.into() 51 | } 52 | 53 | pub fn storage_balance_of(&self, account_id: &AccountId) -> U128 { 54 | self.storage_deposits.get(account_id).unwrap_or(0).into() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/donation/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn account_vec_to_set( 4 | account_vec: Vec, 5 | storage_key: StorageKey, 6 | ) -> UnorderedSet { 7 | let mut set = UnorderedSet::new(storage_key); 8 | for element in account_vec.iter() { 9 | set.insert(element); 10 | } 11 | set 12 | } 13 | 14 | pub fn calculate_required_storage_deposit(initial_storage_usage: u64) -> Balance { 15 | let storage_used = env::storage_usage() - initial_storage_usage; 16 | log!("Storage used: {} bytes", storage_used); 17 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 18 | required_cost 19 | } 20 | 21 | pub fn refund_deposit(initial_storage_usage: u64) { 22 | let attached_deposit = env::attached_deposit(); 23 | let mut refund = attached_deposit; 24 | if env::storage_usage() > initial_storage_usage { 25 | // caller should pay for the extra storage they used and be refunded for the rest 26 | // let storage_used = env::storage_usage() - initial_storage_usage; 27 | let required_deposit = calculate_required_storage_deposit(initial_storage_usage); 28 | // env::storage_byte_cost() * Balance::from(storage_used); 29 | require!( 30 | required_deposit <= attached_deposit, 31 | format!( 32 | "Must attach {} yoctoNEAR to cover storage", 33 | required_deposit 34 | ) 35 | ); 36 | refund -= required_deposit; 37 | } else { 38 | // storage was freed up; caller should be refunded for what they freed up, in addition to the deposit they sent 39 | let storage_freed = initial_storage_usage - env::storage_usage(); 40 | let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); 41 | refund += cost_freed; 42 | } 43 | if refund > 1 { 44 | Promise::new(env::predecessor_account_id()).transfer(refund); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /contracts/lists/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lists" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [dependencies] 11 | near-sdk = "4.1.1" 12 | # [profile.release] # removed as added to root Cargo.toml 13 | # codegen-units = 1 14 | # # Tell `rustc` to optimize for small code size. 15 | # opt-level = "z" 16 | # lto = true 17 | # debug = false 18 | # panic = "abort" 19 | # # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 20 | # overflow-checks = true 21 | -------------------------------------------------------------------------------- /contracts/lists/out/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/contracts/lists/out/main.wasm -------------------------------------------------------------------------------- /contracts/lists/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ">> Building Lists contract" 4 | 5 | set -e 6 | 7 | export CARGO_TARGET_DIR=target 8 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 9 | mkdir -p ./out 10 | cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm 11 | echo ">> Finished Building Lists contract" -------------------------------------------------------------------------------- /contracts/lists/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $? -ne 0 ]; then 4 | echo ">> Error building Lists contract" 5 | exit 1 6 | fi 7 | 8 | echo ">> Deploying Lists contract!" 9 | 10 | # https://docs.near.org/tools/near-cli#near-dev-deploy 11 | near dev-deploy --wasmFile ./out/main.wasm -------------------------------------------------------------------------------- /contracts/lists/src/admins.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn owner_change_owner(&mut self, list_id: ListId, new_owner_id: AccountId) -> AccountId { 7 | self.assert_list_owner(&list_id); 8 | let initial_storage_usage = env::storage_usage(); 9 | let mut list = 10 | ListInternal::from(self.lists_by_id.get(&list_id).expect("List does not exist")); 11 | let old_owner = list.owner.clone(); 12 | list.owner = new_owner_id.clone(); 13 | self.lists_by_id 14 | .insert(&list_id, &VersionedList::Current(list.clone())); 15 | let mut lists_for_old_owner = self 16 | .list_ids_by_owner 17 | .get(&old_owner) 18 | .expect("List IDs for old owner do not exist"); 19 | lists_for_old_owner.remove(&list_id); 20 | self.list_ids_by_owner 21 | .insert(&old_owner, &lists_for_old_owner); 22 | let mut lists_for_new_owner = 23 | self.list_ids_by_owner 24 | .get(&new_owner_id) 25 | .unwrap_or(UnorderedSet::new(StorageKey::ListIdsByOwnerInner { 26 | owner: new_owner_id.clone(), 27 | })); 28 | lists_for_new_owner.insert(&list_id); 29 | self.list_ids_by_owner 30 | .insert(&new_owner_id, &lists_for_new_owner); 31 | refund_deposit(initial_storage_usage, None); 32 | log_owner_transfer_event(list_id, new_owner_id.clone()); 33 | new_owner_id 34 | } 35 | 36 | #[payable] 37 | pub fn owner_add_admins(&mut self, list_id: ListId, admins: Vec) -> Vec { 38 | self.assert_list_owner(&list_id); 39 | let initial_storage_usage = env::storage_usage(); 40 | let mut list_admins = self 41 | .list_admins_by_list_id 42 | .get(&list_id) 43 | .expect("List admins do not exist"); 44 | for admin in admins { 45 | list_admins.insert(&admin); 46 | } 47 | self.list_admins_by_list_id.insert(&list_id, &list_admins); 48 | refund_deposit(initial_storage_usage, None); 49 | log_update_admins_event(list_id, list_admins.to_vec()); 50 | list_admins.to_vec() 51 | } 52 | 53 | #[payable] 54 | pub fn owner_remove_admins( 55 | &mut self, 56 | list_id: ListId, 57 | admins: Vec, 58 | ) -> Vec { 59 | self.assert_list_owner(&list_id); 60 | let initial_storage_usage = env::storage_usage(); 61 | let mut list_admins = self 62 | .list_admins_by_list_id 63 | .get(&list_id) 64 | .expect("List admins do not exist"); 65 | for admin in admins { 66 | list_admins.remove(&admin); 67 | } 68 | self.list_admins_by_list_id.insert(&list_id, &list_admins); 69 | refund_deposit(initial_storage_usage, None); 70 | log_update_admins_event(list_id, list_admins.to_vec()); 71 | list_admins.to_vec() 72 | } 73 | 74 | #[payable] 75 | pub fn owner_clear_admins(&mut self, list_id: ListId) -> Vec { 76 | self.assert_list_owner(&list_id); 77 | let initial_storage_usage = env::storage_usage(); 78 | let list_admins = self 79 | .list_admins_by_list_id 80 | .get(&list_id) 81 | .expect("List admins do not exist"); 82 | self.list_admins_by_list_id.insert( 83 | &list_id, 84 | &UnorderedSet::new(StorageKey::ListAdminsByListIdInner { list_id }), 85 | ); 86 | refund_deposit(initial_storage_usage, None); 87 | log_update_admins_event(list_id, vec![]); 88 | list_admins.to_vec() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/lists/src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub const EVENT_JSON_PREFIX: &str = "EVENT_JSON:"; 4 | pub const TGAS: u64 = 1_000_000_000_000; 5 | pub const GAS_PER_TRANSFER: Gas = Gas(TGAS / 2); 6 | pub const XCC_GAS: Gas = Gas(TGAS * 5); 7 | pub const MAX_LIST_NAME_LENGTH: usize = 64; 8 | pub const MAX_LIST_DESCRIPTION_LENGTH: usize = 256; 9 | pub const MAX_REGISTRATION_BATCH_SIZE: u64 = 25; 10 | -------------------------------------------------------------------------------- /contracts/lists/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// source metadata update 4 | pub(crate) fn log_set_source_metadata_event(source_metadata: &ContractSourceMetadata) { 5 | env::log_str( 6 | format!( 7 | "{}{}", 8 | EVENT_JSON_PREFIX, 9 | json!({ 10 | "standard": "potlock", 11 | "version": "1.0.0", 12 | "event": "set_source_metadata", 13 | "data": [ 14 | { 15 | "source_metadata": source_metadata, 16 | } 17 | ] 18 | }) 19 | ) 20 | .as_ref(), 21 | ); 22 | } 23 | 24 | /// create list event 25 | pub(crate) fn log_create_list_event(list: &ListExternal) { 26 | env::log_str( 27 | format!( 28 | "{}{}", 29 | EVENT_JSON_PREFIX, 30 | json!({ 31 | "standard": "potlock", 32 | "version": "1.0.0", 33 | "event": "create_list", 34 | "data": [ 35 | { 36 | "list": list 37 | } 38 | ] 39 | }) 40 | ) 41 | .as_ref(), 42 | ); 43 | } 44 | 45 | /// update list event 46 | pub(crate) fn log_update_list_event(list: &ListInternal) { 47 | env::log_str( 48 | format!( 49 | "{}{}", 50 | EVENT_JSON_PREFIX, 51 | json!({ 52 | "standard": "potlock", 53 | "version": "1.0.0", 54 | "event": "update_list", 55 | "data": [ 56 | { 57 | "list": list, 58 | } 59 | ] 60 | }) 61 | ) 62 | .as_ref(), 63 | ); 64 | } 65 | 66 | /// delete list event 67 | pub(crate) fn log_delete_list_event(list_id: ListId) { 68 | env::log_str( 69 | format!( 70 | "{}{}", 71 | EVENT_JSON_PREFIX, 72 | json!({ 73 | "standard": "potlock", 74 | "version": "1.0.0", 75 | "event": "delete_list", 76 | "data": [ 77 | { 78 | "list_id": list_id, 79 | } 80 | ] 81 | }) 82 | ) 83 | .as_ref(), 84 | ); 85 | } 86 | 87 | /// upvote list event 88 | pub(crate) fn log_upvote_event(list_id: ListId, account_id: AccountId) { 89 | env::log_str( 90 | format!( 91 | "{}{}", 92 | EVENT_JSON_PREFIX, 93 | json!({ 94 | "standard": "potlock", 95 | "version": "1.0.0", 96 | "event": "upvote", 97 | "data": [ 98 | { 99 | "list_id": list_id, 100 | "account_id": account_id, 101 | } 102 | ] 103 | }) 104 | ) 105 | .as_ref(), 106 | ); 107 | } 108 | 109 | /// remove upvote list event 110 | pub(crate) fn log_remove_upvote_event(list_id: ListId, account_id: AccountId) { 111 | env::log_str( 112 | format!( 113 | "{}{}", 114 | EVENT_JSON_PREFIX, 115 | json!({ 116 | "standard": "potlock", 117 | "version": "1.0.0", 118 | "event": "remove_upvote", 119 | "data": [ 120 | { 121 | "list_id": list_id, 122 | "account_id": account_id, 123 | } 124 | ] 125 | }) 126 | ) 127 | .as_ref(), 128 | ); 129 | } 130 | 131 | /// update (add or remove) admins event 132 | pub(crate) fn log_update_admins_event(list_id: ListId, admins: Vec) { 133 | env::log_str( 134 | format!( 135 | "{}{}", 136 | EVENT_JSON_PREFIX, 137 | json!({ 138 | "standard": "potlock", 139 | "version": "1.0.0", 140 | "event": "update_admins", 141 | "data": [ 142 | { 143 | "list_id": list_id, 144 | "admins": admins, 145 | } 146 | ] 147 | }) 148 | ) 149 | .as_ref(), 150 | ); 151 | } 152 | 153 | /// owner transfer event 154 | pub(crate) fn log_owner_transfer_event(list_id: ListId, new_owner_id: AccountId) { 155 | env::log_str( 156 | format!( 157 | "{}{}", 158 | EVENT_JSON_PREFIX, 159 | json!({ 160 | "standard": "potlock", 161 | "version": "1.0.0", 162 | "event": "owner_transfer", 163 | "data": [ 164 | { 165 | "list_id": list_id, 166 | "new_owner_id": new_owner_id, 167 | } 168 | ] 169 | }) 170 | ) 171 | .as_ref(), 172 | ); 173 | } 174 | 175 | /// create registration event 176 | pub(crate) fn log_create_registration_event(registration: &RegistrationExternal) { 177 | env::log_str( 178 | format!( 179 | "{}{}", 180 | EVENT_JSON_PREFIX, 181 | json!({ 182 | "standard": "potlock", 183 | "version": "1.0.0", 184 | "event": "create_registration", 185 | "data": [ 186 | { 187 | "registration": registration, 188 | } 189 | ] 190 | }) 191 | ) 192 | .as_ref(), 193 | ); 194 | } 195 | 196 | /// update registration event 197 | pub(crate) fn log_update_registration_event(registration: &RegistrationExternal) { 198 | env::log_str( 199 | format!( 200 | "{}{}", 201 | EVENT_JSON_PREFIX, 202 | json!({ 203 | "standard": "potlock", 204 | "version": "1.0.0", 205 | "event": "update_registration", 206 | "data": [ 207 | { 208 | "registration": registration, 209 | } 210 | ] 211 | }) 212 | ) 213 | .as_ref(), 214 | ); 215 | } 216 | 217 | /// delete registration event 218 | pub(crate) fn log_delete_registration_event(registration_id: RegistrationId) { 219 | env::log_str( 220 | format!( 221 | "{}{}", 222 | EVENT_JSON_PREFIX, 223 | json!({ 224 | "standard": "potlock", 225 | "version": "1.0.0", 226 | "event": "delete_registration", 227 | "data": [ 228 | { 229 | "registration_id": registration_id, 230 | } 231 | ] 232 | }) 233 | ) 234 | .as_ref(), 235 | ); 236 | } 237 | -------------------------------------------------------------------------------- /contracts/lists/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Contract { 4 | pub(crate) fn assert_at_least_one_yocto(&self) { 5 | assert!( 6 | env::attached_deposit() >= 1, 7 | "At least one yoctoNEAR must be attached" 8 | ); 9 | } 10 | 11 | pub(crate) fn assert_list_owner(&self, list_id: &ListId) { 12 | let list = ListInternal::from(self.lists_by_id.get(list_id).expect("List does not exist")); 13 | assert_eq!( 14 | env::predecessor_account_id(), 15 | list.owner, 16 | "List owner-only action" 17 | ); 18 | // require owner to attach at least one yoctoNEAR for security purposes 19 | self.assert_at_least_one_yocto(); 20 | } 21 | 22 | pub(crate) fn is_caller_list_admin_or_greater(&self, list_id: &ListId) -> bool { 23 | let caller_id = env::predecessor_account_id(); 24 | let list = ListInternal::from(self.lists_by_id.get(list_id).expect("List does not exist")); 25 | let list_admins = self 26 | .list_admins_by_list_id 27 | .get(list_id) 28 | .expect("List admins do not exist"); 29 | list.owner == caller_id || list_admins.contains(&caller_id) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/lists/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 3 | use near_sdk::json_types::U128; 4 | use near_sdk::serde::{Deserialize, Serialize}; 5 | use near_sdk::{ 6 | env, log, near_bindgen, require, serde_json::json, AccountId, Balance, BorshStorageKey, Gas, 7 | PanicOnDefault, Promise, PromiseError, PromiseOrValue, 8 | }; 9 | use std::collections::HashMap; 10 | 11 | pub mod admins; 12 | pub mod constants; 13 | pub mod events; 14 | pub mod internal; 15 | pub mod lists; 16 | pub mod registrations; 17 | pub mod source; 18 | pub mod utils; 19 | pub mod validation; 20 | pub use crate::admins::*; 21 | pub use crate::constants::*; 22 | pub use crate::events::*; 23 | pub use crate::internal::*; 24 | pub use crate::lists::*; 25 | pub use crate::registrations::*; 26 | pub use crate::source::*; 27 | pub use crate::utils::*; 28 | pub use crate::validation::*; 29 | 30 | type RegistrantId = AccountId; 31 | type RegistrationId = u64; 32 | type ListId = u64; 33 | type TimestampMs = u64; 34 | 35 | /// CURRENT Lists Contract (v1.0.0) 36 | #[near_bindgen] 37 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 38 | pub struct Contract { 39 | /// Incrementing ID to assign to new lists 40 | next_list_id: ListId, 41 | /// Incrementing ID to assign to new registrations 42 | next_registration_id: RegistrationId, 43 | /// Lists indexed by List ID 44 | lists_by_id: UnorderedMap, 45 | /// Lookup from owner account ID to List IDs they own 46 | list_ids_by_owner: UnorderedMap>, 47 | /// Lookup from registrant ID to List IDs it belongs to 48 | list_ids_by_registrant: UnorderedMap>, 49 | /// List admins by List ID 50 | list_admins_by_list_id: LookupMap>, 51 | /// List registrants by ID 52 | // NB: list_id is stored on registration 53 | registrations_by_id: UnorderedMap, 54 | /// Lookup from List ID to registration IDs 55 | registration_ids_by_list_id: UnorderedMap>, 56 | /// Lookup from Registrant ID to registration IDs 57 | registration_ids_by_registrant_id: UnorderedMap>, 58 | /// Lookup from List ID to upvotes (account IDs) 59 | upvotes_by_list_id: LookupMap>, 60 | /// Lookup from Account ID to List IDs upvoted 61 | upvoted_lists_by_account_id: UnorderedMap>, 62 | // // TODO: might want to add a lookup from list ID to registration IDs e.g. all_registrations_by_list_id, so don't have to iterate through all registrations sets & synthesize data 63 | // /// Pending registrations by List ID 64 | // pending_registration_ids_by_list_id: UnorderedMap>, 65 | // /// Approved registrations by List ID 66 | // approved_registration_ids_by_list_id: UnorderedMap>, 67 | // /// Rejected registration_ids by List ID 68 | // rejected_registration_ids_by_list_id: UnorderedMap>, 69 | // /// Graylisted registration_ids by List ID 70 | // graylisted_registration_ids_by_list_id: UnorderedMap>, 71 | // /// Blacklisted registration_ids by List ID 72 | // blacklisted_registration_ids_by_list_id: UnorderedMap>, 73 | /// Contract "source" metadata 74 | contract_source_metadata: LazyOption, 75 | } 76 | 77 | #[derive(BorshSerialize, BorshStorageKey)] 78 | pub enum StorageKey { 79 | ListsById, 80 | ListIdsByOwner, 81 | ListIdsByOwnerInner { owner: AccountId }, 82 | ListIdsByRegistrant, 83 | ListIdsByRegistrantInner { registrant: AccountId }, 84 | ListAdminsByListId, 85 | ListAdminsByListIdInner { list_id: ListId }, 86 | RegistrationsById, 87 | RegistrationIdsByListId, 88 | RegistrationIdsByListIdInner { list_id: ListId }, 89 | RegistrationIdsByRegistrantId, 90 | RegistrationIdsByRegistrantIdInner { registrant_id: AccountId }, 91 | UpvotesByListId, 92 | UpvotesByListIdInner { list_id: ListId }, 93 | UpvotedListsByAccountId, 94 | UpvotedListsByAccountIdInner { account_id: AccountId }, 95 | // PendingRegistrantsByListId, 96 | // ApprovedRegistrantsByListId, 97 | // RejectedRegistrantsByListId, 98 | // GraylistedRegistrantsByListId, 99 | // BlacklistedRegistrantsByListId, 100 | SourceMetadata, 101 | } 102 | 103 | #[near_bindgen] 104 | impl Contract { 105 | #[init] 106 | pub fn new(source_metadata: ContractSourceMetadata) -> Self { 107 | Self { 108 | next_list_id: 1, 109 | next_registration_id: 1, 110 | lists_by_id: UnorderedMap::new(StorageKey::ListsById), 111 | list_ids_by_owner: UnorderedMap::new(StorageKey::ListIdsByOwner), 112 | list_ids_by_registrant: UnorderedMap::new(StorageKey::ListIdsByRegistrant), 113 | list_admins_by_list_id: LookupMap::new(StorageKey::ListAdminsByListId), 114 | registrations_by_id: UnorderedMap::new(StorageKey::RegistrationsById), 115 | registration_ids_by_list_id: UnorderedMap::new(StorageKey::RegistrationIdsByListId), 116 | registration_ids_by_registrant_id: UnorderedMap::new( 117 | StorageKey::RegistrationIdsByRegistrantId, 118 | ), 119 | upvotes_by_list_id: LookupMap::new(StorageKey::UpvotesByListId), 120 | upvoted_lists_by_account_id: UnorderedMap::new(StorageKey::UpvotedListsByAccountId), 121 | contract_source_metadata: LazyOption::new( 122 | StorageKey::SourceMetadata, 123 | Some(&VersionedContractSourceMetadata::Current(source_metadata)), 124 | ), 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /contracts/lists/src/source.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// CONTRACT SOURCE METADATA - as per NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` 4 | #[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize, PanicOnDefault)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct ContractSourceMetadata { 7 | /// Version of source code, e.g. "v1.0.0", could correspond to Git tag 8 | pub version: String, 9 | /// Git commit hash of currently deployed contract code 10 | pub commit_hash: String, 11 | /// GitHub repo url for currently deployed contract code 12 | pub link: String, 13 | } 14 | 15 | #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub enum VersionedContractSourceMetadata { 18 | Current(ContractSourceMetadata), 19 | } 20 | 21 | // Convert from VersionedContractSourceMetadata to ContractSourceMetadata 22 | impl From for ContractSourceMetadata { 23 | fn from(metadata: VersionedContractSourceMetadata) -> Self { 24 | match metadata { 25 | VersionedContractSourceMetadata::Current(current) => current, 26 | } 27 | } 28 | } 29 | 30 | #[near_bindgen] 31 | impl Contract { 32 | #[payable] 33 | pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetadata) { 34 | // only contract account (aka the account that can deploy new code to this contract) can call this method 35 | require!( 36 | env::predecessor_account_id() == env::current_account_id(), 37 | "Only contract account can call this method" 38 | ); 39 | self.contract_source_metadata 40 | .set(&VersionedContractSourceMetadata::from( 41 | VersionedContractSourceMetadata::Current(source_metadata.clone()), 42 | )); 43 | // emit event 44 | log_set_source_metadata_event(&source_metadata); 45 | } 46 | 47 | pub fn get_contract_source_metadata(&self) -> Option { 48 | let source_metadata = self.contract_source_metadata.get(); 49 | if source_metadata.is_some() { 50 | Some(ContractSourceMetadata::from(source_metadata.unwrap())) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/lists/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub fn refund_deposit(initial_storage_usage: u64, refund_to: Option) { 4 | let refund_to = refund_to.unwrap_or_else(env::predecessor_account_id); 5 | let attached_deposit = env::attached_deposit(); 6 | let mut refund = attached_deposit; 7 | if env::storage_usage() > initial_storage_usage { 8 | // caller should pay for the extra storage they used and be refunded for the rest 9 | let storage_used = env::storage_usage() - initial_storage_usage; 10 | log!("Storage used: {} bytes", storage_used); 11 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 12 | require!( 13 | required_cost <= attached_deposit, 14 | format!("Must attach {} yoctoNEAR to cover storage", required_cost) 15 | ); 16 | refund -= required_cost; 17 | } else { 18 | // storage was freed up; caller should be refunded for what they freed up, in addition to the deposit they sent 19 | let storage_freed = initial_storage_usage - env::storage_usage(); 20 | log!("Storage freed: {} bytes", storage_freed); 21 | let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); 22 | refund += cost_freed; 23 | } 24 | if refund > 1 { 25 | log!("Refunding {} yoctoNEAR to {}", refund, refund_to); 26 | Promise::new(refund_to).transfer(refund); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/lists/src/validation.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn assert_valid_list_name(name: &str) { 4 | assert!( 5 | name.len() <= MAX_LIST_NAME_LENGTH, 6 | "Provider name is too long" 7 | ); 8 | } 9 | 10 | pub(crate) fn assert_valid_list_description(description: &str) { 11 | assert!( 12 | description.len() <= MAX_LIST_DESCRIPTION_LENGTH, 13 | "Provider description is too long" 14 | ); 15 | } 16 | 17 | pub(crate) fn assert_valid_url(url: &str) { 18 | assert!(url.starts_with("https://"), "Invalid URL"); 19 | } 20 | -------------------------------------------------------------------------------- /contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "potlock-core", 3 | "version": "1.0.0", 4 | "description": "PotLock Core Contracts", 5 | "main": "index.js", 6 | "repository": "git@github.com:PotLock/core.git", 7 | "author": "Lachlan Glen <54282009+lachlanglen@users.noreply.github.com>", 8 | "license": "MIT", 9 | "scripts": { 10 | "build:donation": "cd donation && ./scripts/build.sh && cd ..", 11 | "build:lists": "cd lists && ./scripts/build.sh && cd ..", 12 | "build:pot": "cd pot && ./scripts/build.sh && cd ..", 13 | "build:potfactory": "cd pot_factory && ./scripts/build.sh && cd ..", 14 | "build:registry": "cd registry && ./scripts/build.sh && cd ..", 15 | "build:sybil": "cd sybil && ./scripts/build.sh && cd ..", 16 | "build:sybilprovider": "cd sybil_provider_simulator && ./scripts/build.sh && cd ..", 17 | "dev:deploy:donation": "cd donation && ./scripts/deploy.sh && cd .. && yarn patch:config donation", 18 | "dev:deploy:donation:refresh": "cd donation && rm -rf neardev && ./scripts/deploy.sh && cd .. && yarn patch:config donation", 19 | "dev:deploy:lists": "cd lists && ./scripts/deploy.sh && cd .. && yarn patch:config lists", 20 | "dev:deploy:lists:refresh": "cd lists && rm -rf neardev && ./scripts/deploy.sh && cd .. && yarn patch:config lists", 21 | "dev:deploy:pot": "cd pot && ./scripts/deploy.sh && cd ..", 22 | "dev:deploy:pot:refresh": "cd pot && rm -rf neardev && ./scripts/deploy.sh && cd .. && yarn patch:config pot", 23 | "dev:deploy:potfactory": "cd pot_factory && ./scripts/deploy.sh && cd ..", 24 | "dev:deploy:potfactory:refresh": "cd pot_factory && rm -rf neardev && ./scripts/deploy.sh && cd .. && yarn patch:config pot_factory", 25 | "dev:deploy:registry": "cd registry && ./scripts/deploy.sh && cd .. && yarn patch:config registry", 26 | "dev:deploy:registry:refresh": "cd registry && rm -rf neardev && ./scripts/deploy.sh && cd .. && yarn patch:config registry", 27 | "dev:deploy:sybil": "cd sybil && ./scripts/deploy.sh && cd .. && yarn patch:config sybil", 28 | "dev:deploy:sybil:refresh": "cd sybil && rm -rf neardev && ./scripts/deploy.sh && cd .. && yarn patch:config sybil", 29 | "dev:deploy:sybilprovider": "cd sybil_provider_simulator && ./scripts/deploy.sh && cd .. && yarn patch:config sybil_provider_simulator", 30 | "dev:deploy:sybilprovider:refresh": "cd sybil_provider_simulator && rm -rf neardev && ./scripts/deploy.sh && cd .. && yarn patch:config sybil_provider_simulator", 31 | "test": "mocha --require ts-node/register --timeout 240000", 32 | "test:all": "yarn test test/**/contract.test.ts", 33 | "test:donation": "yarn test test/donation/contract.test.ts", 34 | "test:pot": "yarn test test/pot/contract.test.ts", 35 | "test:potfactory": "yarn test test/pot_factory/contract.test.ts", 36 | "test:registry": "yarn test test/registry/contract.test.ts", 37 | "test:sybil": "yarn test test/sybil/contract.test.ts", 38 | "works:donation": "yarn build:donation && yarn dev:deploy:donation && yarn test:donation", 39 | "works:donation:refresh": "yarn build:donation && yarn dev:deploy:donation:refresh && yarn test:donation", 40 | "works:lists": "yarn build:lists && yarn dev:deploy:lists && yarn test:lists", 41 | "works:lists:refresh": "yarn build:lists && yarn dev:deploy:lists:refresh && yarn test:lists", 42 | "works:pot": "yarn build:pot && yarn dev:deploy:pot && yarn test:pot", 43 | "works:pot:refresh": "yarn build:pot && yarn dev:deploy:pot:refresh && yarn test:pot", 44 | "works:potfactory": "yarn build:potfactory && yarn dev:deploy:potfactory && yarn test:potfactory", 45 | "works:potfactory:refresh": "yarn build:potfactory && yarn dev:deploy:potfactory:refresh && yarn test:potfactory", 46 | "works:registry": "yarn build:registry && yarn dev:deploy:registry && yarn test:registry", 47 | "works:registry:refresh": "yarn build:registry && yarn dev:deploy:registry:refresh && yarn test:registry", 48 | "works:sybil": "yarn build:sybil && yarn dev:deploy:sybil && yarn test:sybil", 49 | "works:sybil:refresh": "yarn build:sybil && yarn dev:deploy:sybil:refresh && yarn test:sybil", 50 | "patch:config": "node ./test/utils/patch-config.mjs" 51 | }, 52 | "devDependencies": { 53 | "@types/bn.js": "^5.1.2", 54 | "@types/mocha": "^10.0.1", 55 | "@types/node": "^20.6.0", 56 | "mocha": "^10.2.0", 57 | "near-api-js": "^2.1.4", 58 | "ts-node": "^10.9.1", 59 | "typescript": "^5.2.2" 60 | }, 61 | "dependencies": { 62 | "big.js": "^6.2.1", 63 | "near-cli": "^3.4.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/pot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "potlock-pot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [dependencies] 11 | near-sdk = "4.1.1" 12 | # [profile.release] # removed as added to root Cargo.toml 13 | # codegen-units = 1 14 | # # Tell `rustc` to optimize for small code size. 15 | # opt-level = "z" 16 | # lto = true 17 | # debug = false 18 | # panic = "abort" 19 | # # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 20 | # overflow-checks = true 21 | -------------------------------------------------------------------------------- /contracts/pot/out/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/contracts/pot/out/main.wasm -------------------------------------------------------------------------------- /contracts/pot/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ">> Building Pot contract" 4 | 5 | set -e 6 | 7 | export CARGO_TARGET_DIR=target 8 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 9 | mkdir -p ./out 10 | cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm -------------------------------------------------------------------------------- /contracts/pot/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $? -ne 0 ]; then 4 | echo ">> Error building contract" 5 | exit 1 6 | fi 7 | 8 | echo ">> Deploying Pot contract" 9 | 10 | # NB: Not using near dev-deploy here because it stopped working; see https://discord.com/channels/490367152054992913/542945453533036544/1157341683747393707 11 | 12 | MASTER_ACCOUNT_NAME="test-contracts.potlock.testnet" 13 | 14 | # Generate account name 15 | account_name=$(date +"%s").$MASTER_ACCOUNT_NAME 16 | echo ">> Generated account name: $account_name" 17 | 18 | # 1. Create a new account 19 | # Assuming you have a master account that will be used to create the new account. 20 | # Replace MASTER_ACCOUNT and MASTER_ACCOUNT_KEY_PATH with appropriate values 21 | near create-account $account_name --masterAccount $MASTER_ACCOUNT_NAME --initialBalance 15 --publicKey ed25519:4PAtgM8Hvz4MamxUaoSN2q3HJ3Npc3oCzyyHK7Y1vncf 22 | 23 | # 2. Funding is implicitly done in the create-account step by setting the initial balance. 24 | 25 | # 3. Create a JSON file for the new account credentials 26 | # Assuming MASTER_ACCOUNT_NAME.json is present in the ~/.near-credentials/testnet/ directory 27 | # Make sure jq is installed (you can install it using `apt install jq` or appropriate command for your OS) 28 | MASTER_ACCOUNT_JSON_PATH=~/.near-credentials/testnet/$MASTER_ACCOUNT_NAME.json 29 | NEW_ACCOUNT_JSON_PATH=~/.near-credentials/testnet/$account_name.json 30 | 31 | jq --arg account_name "$account_name" '.account_id = $account_name' $MASTER_ACCOUNT_JSON_PATH > $NEW_ACCOUNT_JSON_PATH 32 | echo ">> Created new account credentials at $NEW_ACCOUNT_JSON_PATH." 33 | 34 | # 4. Deploy the contract to the new account 35 | near deploy --wasmFile ./out/main.wasm --accountId $account_name 36 | 37 | 38 | # 5. Update ./neardev/dev-account with the new account name 39 | mkdir -p ./neardev 40 | echo $account_name > ./neardev/dev-account 41 | echo ">> Updated ./neardev/dev-account with new account name." 42 | 43 | echo ">> Deployment completed!" 44 | 45 | # # https://docs.near.org/tools/near-cli#near-dev-deploy 46 | # near dev-deploy --wasmFile ./out/main.wasm -------------------------------------------------------------------------------- /contracts/pot/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Used ephemerally in view methods 4 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct PotConfig { 7 | pub owner: AccountId, 8 | pub admins: Vec, 9 | pub chef: Option, 10 | pub pot_name: String, 11 | pub pot_description: String, 12 | pub max_projects: u32, 13 | pub base_currency: AccountId, 14 | pub application_start_ms: TimestampMs, 15 | pub application_end_ms: TimestampMs, 16 | pub public_round_start_ms: TimestampMs, 17 | pub public_round_end_ms: TimestampMs, 18 | pub deployed_by: AccountId, 19 | pub registry_provider: Option, 20 | pub min_matching_pool_donation_amount: U128, 21 | pub sybil_wrapper_provider: Option, 22 | pub custom_sybil_checks: Option>, 23 | pub custom_min_threshold_score: Option, 24 | pub referral_fee_matching_pool_basis_points: u32, 25 | pub referral_fee_public_round_basis_points: u32, 26 | pub chef_fee_basis_points: u32, 27 | pub matching_pool_balance: U128, 28 | pub total_public_donations: U128, 29 | pub public_donations_count: u32, 30 | pub payouts: Vec, 31 | pub cooldown_end_ms: Option, 32 | pub all_paid_out: bool, 33 | pub protocol_config_provider: Option, 34 | } 35 | 36 | #[near_bindgen] 37 | impl Contract { 38 | pub fn get_config(&self) -> PotConfig { 39 | PotConfig { 40 | owner: self.owner.clone(), 41 | admins: self.admins.to_vec(), 42 | chef: self.chef.get(), 43 | pot_name: self.pot_name.clone(), 44 | pot_description: self.pot_description.clone(), 45 | max_projects: self.max_projects, 46 | base_currency: self.base_currency.clone(), 47 | application_start_ms: self.application_start_ms, 48 | application_end_ms: self.application_end_ms, 49 | public_round_start_ms: self.public_round_start_ms, 50 | public_round_end_ms: self.public_round_end_ms, 51 | deployed_by: self.deployed_by.clone(), 52 | registry_provider: self.registry_provider.get(), 53 | min_matching_pool_donation_amount: self.min_matching_pool_donation_amount.into(), 54 | sybil_wrapper_provider: self.sybil_wrapper_provider.get(), 55 | custom_sybil_checks: self.custom_sybil_checks.get(), 56 | custom_min_threshold_score: self.custom_min_threshold_score.get(), 57 | referral_fee_matching_pool_basis_points: self.referral_fee_matching_pool_basis_points, 58 | referral_fee_public_round_basis_points: self.referral_fee_public_round_basis_points, 59 | chef_fee_basis_points: self.chef_fee_basis_points, 60 | matching_pool_balance: self.matching_pool_balance.into(), 61 | total_public_donations: self.total_public_donations.into(), 62 | public_donations_count: self.public_round_donation_ids.len() as u32, 63 | payouts: self.get_payouts(None, None), 64 | cooldown_end_ms: self.cooldown_end_ms.get(), 65 | all_paid_out: self.all_paid_out, 66 | protocol_config_provider: self.protocol_config_provider.get(), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /contracts/pot/src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub const ONE_DAY_MS: u64 = 86_400_000; 4 | pub const ONE_WEEK_MS: u64 = ONE_DAY_MS * 7; 5 | pub const TGAS: u64 = 1_000_000_000_000; 6 | pub const XCC_GAS: Gas = Gas(TGAS * 5); 7 | pub const EVENT_JSON_PREFIX: &str = "EVENT_JSON:"; 8 | 9 | // Pot args constraints 10 | pub const MAX_POT_NAME_LENGTH: usize = 64; 11 | pub const MAX_POT_DESCRIPTION_LENGTH: usize = 256; 12 | pub const MAX_MAX_PROJECTS: u32 = 100; // TODO: figure out actual limit based on gas 13 | pub const MAX_REFERRAL_FEE_MATCHING_POOL_BASIS_POINTS: u32 = 1000; // 10% 14 | pub const MAX_REFERRAL_FEE_PUBLIC_ROUND_BASIS_POINTS: u32 = 1000; // 10% 15 | pub const MAX_CHEF_FEE_BASIS_POINTS: u32 = 1000; // 10% 16 | pub const MAX_PROTOCOL_FEE_BASIS_POINTS: u32 = 1000; // 10% 17 | pub const MIN_COOLDOWN_PERIOD_MS: u64 = ONE_WEEK_MS; 18 | pub const DEFAULT_COOLDOWN_PERIOD_MS: u64 = ONE_WEEK_MS; 19 | -------------------------------------------------------------------------------- /contracts/pot/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// source metadata update 4 | pub(crate) fn log_set_source_metadata_event(source_metadata: &ContractSourceMetadata) { 5 | env::log_str( 6 | format!( 7 | "{}{}", 8 | EVENT_JSON_PREFIX, 9 | json!({ 10 | "standard": "potlock", 11 | "version": "1.0.0", 12 | "event": "set_source_metadata", 13 | "data": [ 14 | { 15 | "source_metadata": source_metadata, 16 | } 17 | ] 18 | }) 19 | ) 20 | .as_ref(), 21 | ); 22 | } 23 | 24 | /// Update pot 25 | pub(crate) fn log_update_pot_config_event(pot_config: &PotConfig) { 26 | env::log_str( 27 | format!( 28 | "{}{}", 29 | EVENT_JSON_PREFIX, 30 | json!({ 31 | "standard": "potlock", 32 | "version": "1.0.0", 33 | "event": "update_pot_config", 34 | "data": [ 35 | { 36 | "pot_config": pot_config, 37 | } 38 | ] 39 | }) 40 | ) 41 | .as_ref(), 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /contracts/pot/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Contract { 4 | pub(crate) fn assert_at_least_one_yocto(&self) { 5 | assert!( 6 | env::attached_deposit() >= 1, 7 | "At least one yoctoNEAR must be attached" 8 | ); 9 | } 10 | 11 | pub(crate) fn is_owner(&self, account_id: Option<&AccountId>) -> bool { 12 | account_id.unwrap_or(&env::predecessor_account_id()) == &self.owner 13 | } 14 | 15 | pub(crate) fn is_admin(&self, account_id: Option<&AccountId>) -> bool { 16 | self.admins 17 | .contains(&account_id.unwrap_or(&env::predecessor_account_id())) 18 | } 19 | 20 | pub(crate) fn is_owner_or_admin(&self, account_id: Option<&AccountId>) -> bool { 21 | self.is_owner(account_id) || self.is_admin(account_id) 22 | } 23 | 24 | pub(crate) fn assert_owner(&self) { 25 | assert!( 26 | self.is_owner(None), 27 | "Only contract owner can call this method" 28 | ); 29 | // require owner to attach at least one yoctoNEAR for security purposes 30 | self.assert_at_least_one_yocto(); 31 | } 32 | 33 | pub(crate) fn assert_admin_or_greater(&self) { 34 | assert!( 35 | self.is_owner_or_admin(None), 36 | "Only contract admin or owner can call this method" 37 | ); 38 | // require caller to attach at least one yoctoNEAR for security purposes 39 | self.assert_at_least_one_yocto(); 40 | } 41 | 42 | pub(crate) fn is_chef(&self, account_id: Option<&AccountId>) -> bool { 43 | if let Some(chef) = self.chef.get() { 44 | account_id.unwrap_or(&env::predecessor_account_id()) == &chef 45 | } else { 46 | false 47 | } 48 | } 49 | 50 | /// Asserts that caller is, at minimum, a chef (admin or owner also allowed) 51 | pub(crate) fn assert_chef_or_greater(&self) { 52 | assert!( 53 | self.is_chef(None) || self.is_admin(None) || self.is_owner(None), 54 | "Only chef, admin or owner can call this method" 55 | ); 56 | // require caller to attach at least one yoctoNEAR for security purposes 57 | self.assert_at_least_one_yocto(); 58 | } 59 | 60 | pub(crate) fn assert_round_closed(&self) { 61 | assert!( 62 | env::block_timestamp_ms() >= self.public_round_end_ms, 63 | "Round is still open" 64 | ); 65 | } 66 | 67 | pub(crate) fn assert_round_not_closed(&self) { 68 | assert!( 69 | env::block_timestamp_ms() < self.public_round_end_ms, 70 | "Round is closed" 71 | ); 72 | } 73 | 74 | pub(crate) fn assert_approved_application(&self, project_id: &ProjectId) { 75 | assert!( 76 | self.approved_application_ids.contains(project_id), 77 | "Approved application does not exist" 78 | ); 79 | } 80 | 81 | pub(crate) fn assert_cooldown_period_in_process(&self) { 82 | if let Some(cooldown_end_ms) = self.cooldown_end_ms.get() { 83 | assert!( 84 | cooldown_end_ms >= env::block_timestamp_ms(), 85 | "Cooldown period is not in process" 86 | ); 87 | } else { 88 | panic!("Cooldown period is not set"); 89 | } 90 | } 91 | 92 | pub(crate) fn assert_cooldown_period_complete(&self) { 93 | if let Some(cooldown_end_ms) = self.cooldown_end_ms.get() { 94 | assert!( 95 | cooldown_end_ms < env::block_timestamp_ms(), 96 | "Cooldown period is not over" 97 | ); 98 | } else { 99 | panic!("Cooldown period is not set"); 100 | } 101 | } 102 | 103 | pub(crate) fn assert_all_payouts_challenges_resolved(&self) { 104 | for (challenger_id, versioned_payouts_challenge) in self.payouts_challenges.iter() { 105 | let payouts_challenge = PayoutsChallenge::from(versioned_payouts_challenge); 106 | assert!( 107 | payouts_challenge.resolved, 108 | "Payouts challenge from challenger {} is not resolved", 109 | challenger_id 110 | ); 111 | } 112 | } 113 | 114 | pub(crate) fn is_application_period_open(&self) -> bool { 115 | let block_timestamp_ms = env::block_timestamp_ms(); 116 | block_timestamp_ms >= self.application_start_ms 117 | && block_timestamp_ms < self.application_end_ms 118 | } 119 | 120 | pub(crate) fn assert_application_period_open(&self) { 121 | assert!( 122 | self.is_application_period_open(), 123 | "Application period is not open" 124 | ); 125 | } 126 | 127 | pub(crate) fn assert_round_active(&self) { 128 | assert!(self.is_round_active(), "Public round is not active"); 129 | } 130 | 131 | pub(crate) fn assert_max_projects_not_reached(&self) { 132 | assert!( 133 | self.approved_application_ids.len() < self.max_projects.into(), 134 | "Max projects reached" 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /contracts/pot/src/source.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// CONTRACT SOURCE METADATA - as per NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` 4 | #[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize, PanicOnDefault)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct ContractSourceMetadata { 7 | /// Version of source code, e.g. "v1.0.0", could correspond to Git tag 8 | pub version: String, 9 | /// Git commit hash of currently deployed contract code 10 | pub commit_hash: String, 11 | /// GitHub repo url for currently deployed contract code 12 | pub link: String, 13 | } 14 | 15 | #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub enum VersionedContractSourceMetadata { 18 | Current(ContractSourceMetadata), 19 | } 20 | 21 | // Convert from VersionedContractSourceMetadata to ContractSourceMetadata 22 | impl From for ContractSourceMetadata { 23 | fn from(metadata: VersionedContractSourceMetadata) -> Self { 24 | match metadata { 25 | VersionedContractSourceMetadata::Current(current) => current, 26 | } 27 | } 28 | } 29 | 30 | #[near_bindgen] 31 | impl Contract { 32 | #[payable] 33 | pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetadata) { 34 | // only contract account (aka the account that can deploy new code to this contract) can call this method 35 | require!( 36 | env::predecessor_account_id() == env::current_account_id(), 37 | "Only contract account can call this method" 38 | ); 39 | self.contract_source_metadata 40 | .set(&VersionedContractSourceMetadata::from( 41 | VersionedContractSourceMetadata::Current(source_metadata.clone()), 42 | )); 43 | // emit event 44 | log_set_source_metadata_event(&source_metadata); 45 | } 46 | 47 | pub fn get_contract_source_metadata(&self) -> Option { 48 | let source_metadata = self.contract_source_metadata.get(); 49 | if source_metadata.is_some() { 50 | Some(ContractSourceMetadata::from(source_metadata.unwrap())) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/pot/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn account_vec_to_set( 4 | account_vec: Vec, 5 | storage_key: StorageKey, 6 | ) -> UnorderedSet { 7 | let mut set = UnorderedSet::new(storage_key); 8 | for element in account_vec.iter() { 9 | set.insert(element); 10 | } 11 | set 12 | } 13 | 14 | pub fn calculate_required_storage_deposit(initial_storage_usage: u64) -> Balance { 15 | let storage_used = env::storage_usage() - initial_storage_usage; 16 | log!("Storage used: {} bytes", storage_used); 17 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 18 | required_cost 19 | } 20 | 21 | pub fn refund_deposit(initial_storage_usage: u64) { 22 | let attached_deposit = env::attached_deposit(); 23 | let mut refund = attached_deposit; 24 | if env::storage_usage() > initial_storage_usage { 25 | // caller should pay for the extra storage they used and be refunded for the rest 26 | // let storage_used = env::storage_usage() - initial_storage_usage; 27 | let required_deposit = calculate_required_storage_deposit(initial_storage_usage); 28 | // env::storage_byte_cost() * Balance::from(storage_used); 29 | require!( 30 | required_deposit <= attached_deposit, 31 | format!( 32 | "Must attach {} yoctoNEAR to cover storage", 33 | required_deposit 34 | ) 35 | ); 36 | refund -= required_deposit; 37 | } else { 38 | // storage was freed up; caller should be refunded for what they freed up, in addition to the deposit they sent 39 | let storage_freed = initial_storage_usage - env::storage_usage(); 40 | log!("Storage freed: {} bytes", storage_freed); 41 | let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); 42 | refund += cost_freed; 43 | } 44 | if refund > 0 { 45 | Promise::new(env::predecessor_account_id()).transfer(refund); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/pot/src/validation.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn assert_valid_pot_name(name: &str) { 4 | assert!( 5 | name.len() <= MAX_POT_NAME_LENGTH, 6 | "Provider name is too long" 7 | ); 8 | } 9 | 10 | pub(crate) fn assert_valid_pot_description(description: &str) { 11 | assert!( 12 | description.len() <= MAX_POT_DESCRIPTION_LENGTH, 13 | "Provider description is too long" 14 | ); 15 | } 16 | 17 | pub(crate) fn assert_valid_max_projects(max_projects: u32) { 18 | assert!( 19 | max_projects <= MAX_MAX_PROJECTS, 20 | "Max projects cannot exceed {}", 21 | MAX_MAX_PROJECTS 22 | ); 23 | } 24 | 25 | pub(crate) fn assert_valid_referral_fee_matching_pool_basis_points(basis_points: u32) { 26 | assert!( 27 | basis_points <= MAX_REFERRAL_FEE_MATCHING_POOL_BASIS_POINTS, 28 | "Referral fee matching pool basis points cannot exceed {}", 29 | MAX_REFERRAL_FEE_MATCHING_POOL_BASIS_POINTS 30 | ); 31 | } 32 | 33 | pub(crate) fn assert_valid_referral_fee_public_round_basis_points(basis_points: u32) { 34 | assert!( 35 | basis_points <= MAX_REFERRAL_FEE_PUBLIC_ROUND_BASIS_POINTS, 36 | "Referral fee public round basis points cannot exceed {}", 37 | MAX_REFERRAL_FEE_PUBLIC_ROUND_BASIS_POINTS 38 | ); 39 | } 40 | 41 | pub(crate) fn assert_valid_chef_fee_basis_points(basis_points: u32) { 42 | assert!( 43 | basis_points <= MAX_CHEF_FEE_BASIS_POINTS, 44 | "Chef fee basis points cannot exceed {}", 45 | MAX_CHEF_FEE_BASIS_POINTS 46 | ); 47 | } 48 | 49 | pub(crate) fn assert_valid_provider_id(provider_id: &ProviderId) { 50 | provider_id.validate(); 51 | } 52 | 53 | pub(crate) fn assert_valid_cooldown_period_ms(cooldown_period_ms: u64) { 54 | assert!( 55 | cooldown_period_ms >= MIN_COOLDOWN_PERIOD_MS, 56 | "Cooldown period must be at least {} ms", 57 | MIN_COOLDOWN_PERIOD_MS 58 | ); 59 | } 60 | 61 | #[near_bindgen] 62 | impl Contract { 63 | pub(crate) fn assert_valid_timestamps( 64 | &self, 65 | application_start_ms: Option, 66 | application_end_ms: Option, 67 | public_round_start_ms: Option, 68 | public_round_end_ms: Option, 69 | ) { 70 | // validate each arg against provided args if present; if not, validate against current state 71 | let application_start_ms = application_start_ms.unwrap_or(self.application_start_ms); 72 | let application_end_ms = application_end_ms.unwrap_or(self.application_end_ms); 73 | let public_round_start_ms = public_round_start_ms.unwrap_or(self.public_round_start_ms); 74 | let public_round_end_ms = public_round_end_ms.unwrap_or(self.public_round_end_ms); 75 | assert!( 76 | application_start_ms < application_end_ms, 77 | "Application start must be before application end" 78 | ); 79 | assert!( 80 | application_end_ms < public_round_start_ms, 81 | "Application end must be before public round start" 82 | ); 83 | assert!( 84 | public_round_start_ms < public_round_end_ms, 85 | "Public round start must be before public round end" 86 | ); 87 | } 88 | 89 | pub(crate) fn assert_valid_pot_args(&self, args: &UpdatePotArgs) { 90 | if let Some(name) = &args.pot_name { 91 | assert_valid_pot_name(name); 92 | } 93 | if let Some(description) = &args.pot_description { 94 | assert_valid_pot_description(description); 95 | } 96 | if let Some(max_projects) = args.max_projects { 97 | assert_valid_max_projects(max_projects); 98 | } 99 | if let Some(referral_fee_matching_pool_basis_points) = 100 | args.referral_fee_matching_pool_basis_points 101 | { 102 | assert_valid_referral_fee_matching_pool_basis_points( 103 | referral_fee_matching_pool_basis_points, 104 | ); 105 | } 106 | if let Some(referral_fee_public_round_basis_points) = 107 | args.referral_fee_public_round_basis_points 108 | { 109 | assert_valid_referral_fee_public_round_basis_points( 110 | referral_fee_public_round_basis_points, 111 | ); 112 | } 113 | if let Some(chef_fee_basis_points) = args.chef_fee_basis_points { 114 | assert_valid_chef_fee_basis_points(chef_fee_basis_points); 115 | } 116 | if let Some(registry_provider) = &args.registry_provider { 117 | assert_valid_provider_id(registry_provider); 118 | } 119 | if let Some(sybil_wrapper_provider) = &args.sybil_wrapper_provider { 120 | assert_valid_provider_id(sybil_wrapper_provider); 121 | } 122 | if let Some(custom_sybil_checks) = &args.custom_sybil_checks { 123 | for check in custom_sybil_checks { 124 | assert_valid_provider_id(&ProviderId::new( 125 | check.contract_id.clone().to_string(), 126 | check.method_name.clone(), 127 | )); 128 | } 129 | } 130 | // validate timestamps 131 | self.assert_valid_timestamps( 132 | args.application_start_ms, 133 | args.application_end_ms, 134 | args.public_round_start_ms, 135 | args.public_round_end_ms, 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /contracts/pot_factory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "potlock-pot-factory" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [dependencies] 11 | near-sdk = "4.1.1" 12 | # [profile.release] # removed as added to root Cargo.toml 13 | # codegen-units = 1 14 | # # Tell `rustc` to optimize for small code size. 15 | # opt-level = "z" 16 | # lto = true 17 | # debug = false 18 | # panic = "abort" 19 | # # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 20 | # overflow-checks = true 21 | # [dev-dependencies] 22 | # anyhow = "1.0" 23 | # near-primitives = "0.5.0" 24 | # near-sdk = "4.0.0" 25 | # near-units = "0.2.0" 26 | # serde_json = "1.0" 27 | # tokio = { version = "1.14", features = ["full"] } 28 | # workspaces = "0.4.1" 29 | # # remember to include a line for each contract 30 | # fungible-token = { path = "./ft" } # TODO: UPDATE THIS AND THE BELOW 31 | # defi = { path = "./test-contract-defi" } 32 | # [workspace] 33 | # # remember to include a member for each contract 34 | # members = ["ft", "test-contract-defi"] 35 | -------------------------------------------------------------------------------- /contracts/pot_factory/out/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/contracts/pot_factory/out/main.wasm -------------------------------------------------------------------------------- /contracts/pot_factory/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ">> Building PotFactory contract" 4 | 5 | set -e 6 | 7 | export CARGO_TARGET_DIR=target 8 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 9 | mkdir -p ./out 10 | cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm 11 | echo ">> Finished Building PotFactory contract" -------------------------------------------------------------------------------- /contracts/pot_factory/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $? -ne 0 ]; then 4 | echo ">> Error building contract" 5 | exit 1 6 | fi 7 | 8 | echo ">> Deploying PotFactory contract!" 9 | 10 | # NB: Not using near dev-deploy here because the PotFactory contract creates subaccounts of itself, and account IDs created via near dev-deploy cannot be subaccounted. 11 | 12 | MASTER_ACCOUNT_NAME="test-contracts.potlock.testnet" 13 | 14 | # Generate account name 15 | account_name=$(date +"%s").$MASTER_ACCOUNT_NAME 16 | echo ">> Generated account name: $account_name" 17 | 18 | # 1. Create a new account 19 | # Assuming you have a master account that will be used to create the new account. 20 | # Replace MASTER_ACCOUNT and MASTER_ACCOUNT_KEY_PATH with appropriate values 21 | near create-account $account_name --masterAccount $MASTER_ACCOUNT_NAME --initialBalance 15 --publicKey ed25519:4PAtgM8Hvz4MamxUaoSN2q3HJ3Npc3oCzyyHK7Y1vncf 22 | 23 | # 2. Funding is implicitly done in the create-account step by setting the initial balance. 24 | 25 | # 3. Create a JSON file for the new account credentials 26 | # Assuming MASTER_ACCOUNT_NAME.json is present in the ~/.near-credentials/testnet/ directory 27 | # Make sure jq is installed (you can install it using `apt install jq` or appropriate command for your OS) 28 | MASTER_ACCOUNT_JSON_PATH=~/.near-credentials/testnet/$MASTER_ACCOUNT_NAME.json 29 | NEW_ACCOUNT_JSON_PATH=~/.near-credentials/testnet/$account_name.json 30 | 31 | jq --arg account_name "$account_name" '.account_id = $account_name' $MASTER_ACCOUNT_JSON_PATH > $NEW_ACCOUNT_JSON_PATH 32 | echo ">> Created new account credentials at $NEW_ACCOUNT_JSON_PATH." 33 | 34 | # 4. Deploy the contract to the new account 35 | near deploy --wasmFile ./out/main.wasm --accountId $account_name 36 | 37 | 38 | # 5. Update ./neardev/dev-account with the new account name 39 | mkdir -p ./neardev 40 | echo $account_name > ./neardev/dev-account 41 | echo ">> Updated ./neardev/dev-account with new account name." 42 | 43 | echo ">> Deployment completed!" -------------------------------------------------------------------------------- /contracts/pot_factory/src/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn owner_change_owner(&mut self, new_owner: AccountId) { 7 | self.assert_owner(); 8 | let initial_storage_usage = env::storage_usage(); 9 | self.owner = new_owner; 10 | refund_deposit(initial_storage_usage); 11 | } 12 | 13 | #[payable] 14 | pub fn owner_set_admins(&mut self, account_ids: Vec) { 15 | self.assert_owner(); 16 | let initial_storage_usage = env::storage_usage(); 17 | self.admins.clear(); 18 | for account_id in account_ids { 19 | self.admins.insert(&account_id); 20 | } 21 | refund_deposit(initial_storage_usage); 22 | } 23 | 24 | #[payable] 25 | pub fn owner_add_admins(&mut self, account_ids: Vec) { 26 | self.assert_owner(); 27 | let initial_storage_usage = env::storage_usage(); 28 | for account_id in account_ids { 29 | self.admins.insert(&account_id); 30 | } 31 | refund_deposit(initial_storage_usage); 32 | } 33 | 34 | #[payable] 35 | pub fn owner_remove_admins(&mut self, account_ids: Vec) { 36 | self.assert_owner(); 37 | let initial_storage_usage = env::storage_usage(); 38 | for account_id in account_ids { 39 | self.admins.remove(&account_id); 40 | } 41 | refund_deposit(initial_storage_usage); 42 | } 43 | 44 | #[payable] 45 | pub fn owner_clear_admins(&mut self) { 46 | self.assert_owner(); 47 | let initial_storage_usage = env::storage_usage(); 48 | self.admins.clear(); 49 | refund_deposit(initial_storage_usage); 50 | } 51 | 52 | #[payable] 53 | pub fn admin_set_protocol_fee_basis_points(&mut self, protocol_fee_basis_points: u32) { 54 | self.assert_admin_or_greater(); 55 | self.protocol_fee_basis_points = protocol_fee_basis_points; 56 | } 57 | 58 | #[payable] 59 | pub fn admin_set_protocol_fee_recipient_account( 60 | &mut self, 61 | protocol_fee_recipient_account: AccountId, 62 | ) { 63 | self.assert_admin_or_greater(); 64 | let initial_storage_usage = env::storage_usage(); 65 | self.protocol_fee_recipient_account = protocol_fee_recipient_account; 66 | refund_deposit(initial_storage_usage); 67 | } 68 | 69 | #[payable] 70 | pub fn admin_set_protocol_config( 71 | &mut self, 72 | protocol_fee_basis_points: u32, 73 | protocol_fee_recipient_account: AccountId, 74 | ) { 75 | self.assert_admin_or_greater(); 76 | let initial_storage_usage = env::storage_usage(); 77 | self.protocol_fee_basis_points = protocol_fee_basis_points; 78 | self.protocol_fee_recipient_account = protocol_fee_recipient_account; 79 | refund_deposit(initial_storage_usage); 80 | } 81 | 82 | #[payable] 83 | pub fn admin_add_whitelisted_deployers(&mut self, whitelisted_deployers: Vec) { 84 | self.assert_admin_or_greater(); 85 | let initial_storage_usage = env::storage_usage(); 86 | for account_id in whitelisted_deployers { 87 | self.whitelisted_deployers.insert(&account_id); 88 | } 89 | refund_deposit(initial_storage_usage); 90 | } 91 | 92 | #[payable] 93 | pub fn admin_remove_whitelisted_deployers(&mut self, whitelisted_deployers: Vec) { 94 | self.assert_admin_or_greater(); 95 | let initial_storage_usage = env::storage_usage(); 96 | for account_id in whitelisted_deployers { 97 | self.whitelisted_deployers.remove(&account_id); 98 | } 99 | refund_deposit(initial_storage_usage); 100 | } 101 | 102 | #[payable] 103 | pub fn admin_set_require_whitelist(&mut self, require_whitelist: bool) { 104 | self.assert_admin_or_greater(); 105 | self.require_whitelist = require_whitelist; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /contracts/pot_factory/src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub const ONE_DAY_MS: u64 = 86_400_000; 4 | pub const ONE_WEEK_MS: u64 = ONE_DAY_MS * 7; 5 | pub const EXTRA_BYTES: usize = 10_000; 6 | pub const TGAS: u64 = 1_000_000_000_000; // 1 TGAS 7 | pub const XCC_GAS: Gas = Gas(TGAS * 50); // 50 TGAS 8 | pub const NO_DEPOSIT: u128 = 0; 9 | pub const XCC_SUCCESS: u64 = 1; 10 | pub const EVENT_JSON_PREFIX: &str = "EVENT_JSON:"; 11 | 12 | // Pot args constraints 13 | pub const MAX_POT_NAME_LENGTH: usize = 64; 14 | pub const MAX_POT_DESCRIPTION_LENGTH: usize = 256; 15 | pub const MAX_MAX_PROJECTS: u32 = 100; // TODO: figure out actual limit based on gas 16 | pub const MAX_REFERRAL_FEE_MATCHING_POOL_BASIS_POINTS: u32 = 1000; // 10% 17 | pub const MAX_REFERRAL_FEE_PUBLIC_ROUND_BASIS_POINTS: u32 = 1000; // 10% 18 | pub const MAX_CHEF_FEE_BASIS_POINTS: u32 = 1000; // 10% 19 | pub const MIN_COOLDOWN_PERIOD_MS: u64 = ONE_WEEK_MS; 20 | pub const DEFAULT_COOLDOWN_PERIOD_MS: u64 = ONE_WEEK_MS; 21 | -------------------------------------------------------------------------------- /contracts/pot_factory/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// source metadata update 4 | pub(crate) fn log_set_source_metadata_event(source_metadata: &ContractSourceMetadata) { 5 | env::log_str( 6 | format!( 7 | "{}{}", 8 | EVENT_JSON_PREFIX, 9 | json!({ 10 | "standard": "potlock", 11 | "version": "1.0.0", 12 | "event": "set_source_metadata", 13 | "data": [ 14 | { 15 | "source_metadata": source_metadata, 16 | } 17 | ] 18 | }) 19 | ) 20 | .as_ref(), 21 | ); 22 | } 23 | 24 | /// deploy pot 25 | pub(crate) fn log_deploy_pot_event(pot_external: &PotExternal) { 26 | env::log_str( 27 | format!( 28 | "{}{}", 29 | EVENT_JSON_PREFIX, 30 | json!({ 31 | "standard": "potlock", 32 | "version": "1.0.0", 33 | "event": "deploy_pot", 34 | "data": [ 35 | { 36 | "pot": pot_external, 37 | } 38 | ] 39 | }) 40 | ) 41 | .as_ref(), 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /contracts/pot_factory/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Contract { 4 | pub(crate) fn assert_at_least_one_yocto(&self) { 5 | assert!( 6 | env::attached_deposit() >= 1, 7 | "At least one yoctoNEAR must be attached" 8 | ); 9 | } 10 | 11 | pub(crate) fn is_owner(&self) -> bool { 12 | env::predecessor_account_id() == self.owner 13 | } 14 | 15 | pub(crate) fn is_admin(&self) -> bool { 16 | self.admins.contains(&env::predecessor_account_id()) 17 | } 18 | 19 | pub(crate) fn assert_owner(&self) { 20 | assert!(self.is_owner(), "Only contract owner can call this method"); 21 | // require owner to attach at least one yoctoNEAR for security purposes 22 | self.assert_at_least_one_yocto(); 23 | } 24 | 25 | pub(crate) fn assert_admin_or_greater(&self) { 26 | assert!( 27 | self.is_admin() || self.is_owner(), 28 | "Only contract admin or owner can call this method" 29 | ); 30 | // require caller to attach at least one yoctoNEAR for security purposes 31 | self.assert_at_least_one_yocto(); 32 | } 33 | 34 | pub(crate) fn is_whitelisted_deployer(&self) -> bool { 35 | self.whitelisted_deployers 36 | .contains(&env::predecessor_account_id()) 37 | } 38 | 39 | pub(crate) fn assert_admin_or_whitelisted_deployer(&self) { 40 | assert!( 41 | self.is_owner() || self.is_admin() || self.is_whitelisted_deployer(), 42 | "Only contract admin or whitelisted deployer can call this method" 43 | ); 44 | // require caller to attach at least one yoctoNEAR for security purposes 45 | self.assert_at_least_one_yocto(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/pot_factory/src/source.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// CONTRACT SOURCE METADATA - as per NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` 4 | #[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize, PanicOnDefault)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct ContractSourceMetadata { 7 | /// Version of source code, e.g. "v1.0.0", could correspond to Git tag 8 | pub version: String, 9 | /// Git commit hash of currently deployed contract code 10 | pub commit_hash: String, 11 | /// GitHub repo url for currently deployed contract code 12 | pub link: String, 13 | } 14 | 15 | #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub enum VersionedContractSourceMetadata { 18 | Current(ContractSourceMetadata), 19 | } 20 | 21 | // Convert from VersionedContractSourceMetadata to ContractSourceMetadata 22 | impl From for ContractSourceMetadata { 23 | fn from(metadata: VersionedContractSourceMetadata) -> Self { 24 | match metadata { 25 | VersionedContractSourceMetadata::Current(current) => current, 26 | } 27 | } 28 | } 29 | 30 | #[near_bindgen] 31 | impl Contract { 32 | #[payable] 33 | pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetadata) { 34 | // only contract account (aka the account that can deploy new code to this contract) can call this method 35 | require!( 36 | env::predecessor_account_id() == env::current_account_id(), 37 | "Only contract account can call this method" 38 | ); 39 | self.contract_source_metadata 40 | .set(&VersionedContractSourceMetadata::from( 41 | VersionedContractSourceMetadata::Current(source_metadata.clone()), 42 | )); 43 | // emit event 44 | log_set_source_metadata_event(&source_metadata); 45 | } 46 | 47 | pub fn get_contract_source_metadata(&self) -> Option { 48 | let source_metadata = self.contract_source_metadata.get(); 49 | if source_metadata.is_some() { 50 | Some(ContractSourceMetadata::from(source_metadata.unwrap())) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/pot_factory/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn slugify(s: &str) -> String { 4 | s.to_lowercase() 5 | .chars() 6 | .filter(|c| c.is_alphanumeric() || c.is_whitespace()) 7 | .collect::() 8 | .split_whitespace() 9 | .collect::>() 10 | .join("-") 11 | } 12 | 13 | pub fn calculate_required_storage_deposit(initial_storage_usage: u64) -> Balance { 14 | let storage_used = env::storage_usage() - initial_storage_usage; 15 | log!("Storage used: {} bytes", storage_used); 16 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 17 | required_cost 18 | } 19 | 20 | pub fn refund_deposit(initial_storage_usage: u64) { 21 | let attached_deposit = env::attached_deposit(); 22 | let mut refund = attached_deposit; 23 | if env::storage_usage() > initial_storage_usage { 24 | // caller should pay for the extra storage they used and be refunded for the rest 25 | // let storage_used = env::storage_usage() - initial_storage_usage; 26 | let required_deposit = calculate_required_storage_deposit(initial_storage_usage); 27 | // env::storage_byte_cost() * Balance::from(storage_used); 28 | require!( 29 | required_deposit <= attached_deposit, 30 | format!( 31 | "Must attach {} yoctoNEAR to cover storage", 32 | required_deposit 33 | ) 34 | ); 35 | refund -= required_deposit; 36 | } else { 37 | // storage was freed up; caller should be refunded for what they freed up, in addition to the deposit they sent 38 | let storage_freed = initial_storage_usage - env::storage_usage(); 39 | let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); 40 | refund += cost_freed; 41 | } 42 | if refund > 0 { 43 | Promise::new(env::predecessor_account_id()).transfer(refund); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/pot_factory/src/validation.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn assert_valid_pot_name(name: &str) { 4 | assert!( 5 | name.len() <= MAX_POT_NAME_LENGTH, 6 | "Provider name is too long" 7 | ); 8 | } 9 | 10 | pub(crate) fn assert_valid_pot_description(description: &str) { 11 | assert!( 12 | description.len() <= MAX_POT_DESCRIPTION_LENGTH, 13 | "Provider description is too long" 14 | ); 15 | } 16 | 17 | pub(crate) fn assert_valid_max_projects(max_projects: u32) { 18 | assert!( 19 | max_projects <= MAX_MAX_PROJECTS, 20 | "Max projects cannot exceed {}", 21 | MAX_MAX_PROJECTS 22 | ); 23 | } 24 | 25 | pub(crate) fn assert_valid_referral_fee_matching_pool_basis_points(basis_points: u32) { 26 | assert!( 27 | basis_points <= MAX_REFERRAL_FEE_MATCHING_POOL_BASIS_POINTS, 28 | "Referral fee matching pool basis points cannot exceed {}", 29 | MAX_REFERRAL_FEE_MATCHING_POOL_BASIS_POINTS 30 | ); 31 | } 32 | 33 | pub(crate) fn assert_valid_referral_fee_public_round_basis_points(basis_points: u32) { 34 | assert!( 35 | basis_points <= MAX_REFERRAL_FEE_PUBLIC_ROUND_BASIS_POINTS, 36 | "Referral fee public round basis points cannot exceed {}", 37 | MAX_REFERRAL_FEE_PUBLIC_ROUND_BASIS_POINTS 38 | ); 39 | } 40 | 41 | pub(crate) fn assert_valid_chef_fee_basis_points(basis_points: u32) { 42 | assert!( 43 | basis_points <= MAX_CHEF_FEE_BASIS_POINTS, 44 | "Chef fee basis points cannot exceed {}", 45 | MAX_CHEF_FEE_BASIS_POINTS 46 | ); 47 | } 48 | 49 | pub(crate) fn assert_valid_pot_timestamps(args: &PotArgs) { 50 | assert!( 51 | args.application_start_ms < args.application_end_ms, 52 | "Application start must be before application end" 53 | ); 54 | assert!( 55 | args.application_end_ms < args.public_round_start_ms, 56 | "Application end must be before public round start" 57 | ); 58 | assert!( 59 | args.public_round_start_ms < args.public_round_end_ms, 60 | "Public round start must be before public round end" 61 | ); 62 | } 63 | 64 | pub(crate) fn assert_valid_provider_id(provider_id: &ProviderId) { 65 | provider_id.validate(); 66 | } 67 | 68 | pub(crate) fn assert_valid_cooldown_period_ms(cooldown_period_ms: u64) { 69 | assert!( 70 | cooldown_period_ms >= MIN_COOLDOWN_PERIOD_MS, 71 | "Cooldown period must be at least {} ms", 72 | MIN_COOLDOWN_PERIOD_MS 73 | ); 74 | } 75 | 76 | pub(crate) fn assert_valid_pot_args(args: &PotArgs) { 77 | assert_valid_pot_name(&args.pot_name); 78 | assert_valid_pot_description(&args.pot_description); 79 | assert_valid_max_projects(args.max_projects); 80 | assert_valid_referral_fee_matching_pool_basis_points( 81 | args.referral_fee_matching_pool_basis_points, 82 | ); 83 | assert_valid_referral_fee_public_round_basis_points( 84 | args.referral_fee_public_round_basis_points, 85 | ); 86 | assert_valid_chef_fee_basis_points(args.chef_fee_basis_points); 87 | assert_valid_pot_timestamps(args); 88 | if let Some(cooldown_period_ms) = args.cooldown_period_ms { 89 | assert_valid_cooldown_period_ms(cooldown_period_ms); 90 | } 91 | if let Some(provider_id) = &args.registry_provider { 92 | assert_valid_provider_id(provider_id); 93 | } 94 | if let Some(provider_id) = &args.sybil_wrapper_provider { 95 | assert_valid_provider_id(provider_id); 96 | } 97 | if let Some(custom_sybil_checks) = &args.custom_sybil_checks { 98 | for check in custom_sybil_checks { 99 | assert_valid_provider_id(&ProviderId::new( 100 | check.contract_id.clone().to_string(), 101 | check.method_name.clone(), 102 | )); 103 | } 104 | } 105 | if let Some(protocol_config_provider) = &args.protocol_config_provider { 106 | assert_valid_provider_id(protocol_config_provider); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /contracts/registry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "potlock-registry" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [dependencies] 11 | near-sdk = "4.1.1" 12 | # [profile.release] # removed as added to root Cargo.toml 13 | # codegen-units = 1 14 | # # Tell `rustc` to optimize for small code size. 15 | # opt-level = "z" 16 | # lto = true 17 | # debug = false 18 | # panic = "abort" 19 | # # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 20 | # overflow-checks = true 21 | -------------------------------------------------------------------------------- /contracts/registry/README.md: -------------------------------------------------------------------------------- 1 | # PotLock Registry Contract 2 | 3 | ## Purpose 4 | 5 | Projects that wish to apply for a Pot (funding round) must first be registered on the PotLock Registry (a singleton). Each Pot contract will verify the project against the Registry when a project applies for the Pot. 6 | 7 | ## Contract Structure 8 | 9 | ### General Types 10 | 11 | ```rs 12 | type ProjectId = AccountId; 13 | type TimestampMs = u64; 14 | ``` 15 | 16 | ### Contract 17 | 18 | ```rs 19 | pub struct Contract { 20 | /// Contract superuser 21 | owner: AccountId, 22 | /// Contract admins (can be added/removed by owner) 23 | admins: UnorderedSet, 24 | /// Old set (deprecated but empty set must be retained in state or serialization will break) 25 | _deprecated_project_ids: UnorderedSet, 26 | /// Old map (deprecated but empty map must be retained in state or serialization will break) 27 | _deprecated_projects_by_id: LookupMap, 28 | /// Records of all Projects deployed by this Registry, indexed at their account ID, versioned for easy upgradeability 29 | projects_by_id: UnorderedMap, 30 | /// Projects pending approval 31 | pending_project_ids: UnorderedSet, 32 | /// Projects approved 33 | approved_project_ids: UnorderedSet, 34 | /// Projects rejected 35 | rejected_project_ids: UnorderedSet, 36 | /// Contract "source" metadata, as specified in NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` 37 | contract_source_metadata: LazyOption, 38 | /// Default status when project registers 39 | default_project_status: ProjectStatus, 40 | } 41 | 42 | pub struct ContractConfig { 43 | pub owner: AccountId, 44 | pub admins: Vec, 45 | pub default_project_status: ProjectStatus, 46 | pub pending_project_count: u64, 47 | pub approved_project_count: u64, 48 | pub rejected_project_count: u64, 49 | } 50 | ``` 51 | 52 | ### Projects 53 | 54 | _NB: Projects are automatically approved by default._ 55 | 56 | ```rs 57 | pub enum ProjectStatus { 58 | Pending, 59 | Approved, 60 | Rejected, 61 | Graylisted, 62 | Blacklisted, 63 | } 64 | 65 | // ProjectInternal is the data structure that is stored within the contract 66 | pub struct ProjectInternal { 67 | pub id: ProjectId, 68 | pub status: ProjectStatus, 69 | pub submitted_ms: TimestampMs, 70 | pub updated_ms: TimestampMs, 71 | pub review_notes: Option, 72 | } 73 | 74 | // Ephemeral data structure used for view methods, not stored within contract 75 | pub struct ProjectExternal { 76 | pub id: ProjectId, 77 | pub status: ProjectStatus, 78 | pub submitted_ms: TimestampMs, 79 | pub updated_ms: TimestampMs, 80 | pub review_notes: Option, 81 | } 82 | ``` 83 | 84 | ## Methods 85 | 86 | ### Write Methods 87 | 88 | **NB: ALL privileged write methods (those beginning with `admin_*` or `owner_*`) require an attached deposit of at least one yoctoNEAR, for security purposes.** 89 | 90 | ```rs 91 | // INIT 92 | 93 | pub fn new( 94 | owner: AccountId, 95 | admins: Vec, 96 | source_metadata: ContractSourceMetadata, 97 | ) -> Self 98 | 99 | 100 | // OWNER 101 | 102 | #[payable] 103 | pub fn owner_change_owner(&mut self, owner: AccountId) 104 | 105 | 106 | // ADMINS 107 | 108 | #[payable] 109 | pub fn owner_add_admins(&mut self, admins: Vec) 110 | 111 | #[payable] 112 | pub fn owner_remove_admins(&mut self, admins: Vec) 113 | 114 | #[payable] 115 | pub fn admin_set_default_project_status(&mut self, status: ProjectStatus) 116 | 117 | #[payable] 118 | pub fn admin_set_project_status( 119 | &mut self, 120 | project_id: ProjectId, 121 | status: ProjectStatus, 122 | review_notes: Option, 123 | ) -> () 124 | 125 | 126 | // PROJECTS 127 | 128 | #[payable] 129 | pub fn register( 130 | &mut self, 131 | _project_id: Option, // NB: _project_id can only be specified by admin; otherwise, it is the caller 132 | ) -> ProjectExternal 133 | 134 | 135 | // SOURCE METADATA 136 | 137 | pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetadata) // only callable by the contract account (reasoning is that this should be able to be updated by the same account that can deploy code to the account) 138 | ``` 139 | 140 | ### Read Methods 141 | 142 | ```rs 143 | // CONTRACT 144 | 145 | pub fn get_config(&self) -> ContractConfig 146 | 147 | 148 | // PROJECTS 149 | 150 | pub fn is_registered(&self, account_id: ProjectId) -> bool 151 | 152 | pub fn get_projects( 153 | &self, 154 | status: Option, 155 | from_index: Option, 156 | limit: Option, 157 | ) -> Vec 158 | 159 | pub fn get_project_by_id(&self, project_id: ProjectId) -> ProjectExternal 160 | 161 | 162 | // SOURCE METADATA 163 | 164 | pub fn get_contract_source_metadata(&self) -> Option 165 | ``` -------------------------------------------------------------------------------- /contracts/registry/out/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/contracts/registry/out/main.wasm -------------------------------------------------------------------------------- /contracts/registry/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ">> Building Registry contract" 4 | 5 | set -e 6 | 7 | export CARGO_TARGET_DIR=target 8 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 9 | mkdir -p ./out 10 | cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm 11 | echo ">> Finished Building Registry contract" -------------------------------------------------------------------------------- /contracts/registry/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $? -ne 0 ]; then 4 | echo ">> Error building contract" 5 | exit 1 6 | fi 7 | 8 | echo ">> Deploying Registry contract!" 9 | 10 | # https://docs.near.org/tools/near-cli#near-dev-deploy 11 | near dev-deploy --wasmFile ./out/main.wasm -------------------------------------------------------------------------------- /contracts/registry/src/admins.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn owner_add_admins(&mut self, admins: Vec) { 7 | self.assert_owner(); 8 | let initial_storage_usage = env::storage_usage(); 9 | for admin in admins { 10 | self.admins.insert(&admin); 11 | } 12 | refund_deposit(initial_storage_usage); 13 | } 14 | 15 | #[payable] 16 | pub fn owner_remove_admins(&mut self, admins: Vec) { 17 | self.assert_owner(); 18 | let initial_storage_usage = env::storage_usage(); 19 | for admin in admins { 20 | self.admins.remove(&admin); 21 | } 22 | refund_deposit(initial_storage_usage); 23 | } 24 | 25 | #[payable] 26 | pub fn admin_set_default_project_status(&mut self, status: ProjectStatus) { 27 | self.assert_admin_or_greater(); 28 | let initial_storage_usage = env::storage_usage(); 29 | self.default_project_status = status; 30 | refund_deposit(initial_storage_usage); 31 | } 32 | 33 | #[payable] 34 | pub fn admin_set_project_status( 35 | &mut self, 36 | project_id: ProjectId, 37 | status: ProjectStatus, 38 | review_notes: Option, 39 | ) { 40 | self.assert_admin_or_greater(); 41 | self.assert_project_exists(&project_id); 42 | let mut project = 43 | ProjectInternal::from(self.projects_by_id.get(&project_id).expect("No project")); 44 | let old_status = project.status.clone(); 45 | project.status = status.clone(); 46 | project.review_notes = review_notes; 47 | project.updated_ms = env::block_timestamp_ms(); 48 | self.projects_by_id 49 | .insert(&project_id, &VersionedProjectInternal::Current(project)); 50 | // add to status-specific set & remove from old status-specific set 51 | match status { 52 | ProjectStatus::Pending => { 53 | self.pending_project_ids.insert(&project_id); 54 | } 55 | ProjectStatus::Approved => { 56 | self.approved_project_ids.insert(&project_id); 57 | } 58 | ProjectStatus::Rejected => { 59 | self.rejected_project_ids.insert(&project_id); 60 | } 61 | ProjectStatus::Graylisted => { 62 | self.graylisted_project_ids.insert(&project_id); 63 | } 64 | ProjectStatus::Blacklisted => { 65 | self.blacklisted_project_ids.insert(&project_id); 66 | } 67 | } 68 | match old_status { 69 | ProjectStatus::Pending => { 70 | self.pending_project_ids.remove(&project_id); 71 | } 72 | ProjectStatus::Approved => { 73 | self.approved_project_ids.remove(&project_id); 74 | } 75 | ProjectStatus::Rejected => { 76 | self.rejected_project_ids.remove(&project_id); 77 | } 78 | ProjectStatus::Graylisted => { 79 | self.graylisted_project_ids.remove(&project_id); 80 | } 81 | ProjectStatus::Blacklisted => { 82 | self.blacklisted_project_ids.remove(&project_id); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /contracts/registry/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const EVENT_JSON_PREFIX: &str = "EVENT_JSON:"; 2 | -------------------------------------------------------------------------------- /contracts/registry/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// source metadata update 4 | pub(crate) fn log_set_source_metadata_event(source_metadata: &ContractSourceMetadata) { 5 | env::log_str( 6 | format!( 7 | "{}{}", 8 | EVENT_JSON_PREFIX, 9 | json!({ 10 | "standard": "potlock", 11 | "version": "1.0.0", 12 | "event": "set_source_metadata", 13 | "data": [ 14 | { 15 | "source_metadata": source_metadata, 16 | } 17 | ] 18 | }) 19 | ) 20 | .as_ref(), 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /contracts/registry/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Contract { 4 | pub(crate) fn assert_at_least_one_yocto(&self) { 5 | assert!( 6 | env::attached_deposit() >= 1, 7 | "At least one yoctoNEAR must be attached" 8 | ); 9 | } 10 | 11 | pub(crate) fn assert_owner(&self) { 12 | assert_eq!( 13 | env::predecessor_account_id(), 14 | self.owner, 15 | "Owner-only action" 16 | ); 17 | // require owner to attach at least one yoctoNEAR for security purposes 18 | self.assert_at_least_one_yocto(); 19 | } 20 | 21 | pub(crate) fn assert_admin_or_greater(&self) { 22 | let predecessor_account_id = env::predecessor_account_id(); 23 | assert!( 24 | self.owner == predecessor_account_id || self.admins.contains(&predecessor_account_id), 25 | "Admin-only action" 26 | ); 27 | // require caller to attach at least one yoctoNEAR for security purposes 28 | self.assert_at_least_one_yocto(); 29 | } 30 | 31 | pub(crate) fn assert_project_exists(&self, project_id: &AccountId) { 32 | assert!( 33 | self.projects_by_id.get(project_id).is_some(), 34 | "Project does not exist" 35 | ); 36 | } 37 | 38 | pub(crate) fn assert_project_does_not_exist(&self, project_id: &AccountId) { 39 | assert!( 40 | !self.projects_by_id.get(project_id).is_some(), 41 | "Project already exists" 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/registry/src/owner.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn owner_change_owner(&mut self, owner: AccountId) { 7 | self.assert_owner(); 8 | self.owner = owner; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/registry/src/source.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// CONTRACT SOURCE METADATA - as per NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` 4 | #[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize, PanicOnDefault)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct ContractSourceMetadata { 7 | /// Version of source code, e.g. "v1.0.0", could correspond to Git tag 8 | pub version: String, 9 | /// Git commit hash of currently deployed contract code 10 | pub commit_hash: String, 11 | /// GitHub repo url for currently deployed contract code 12 | pub link: String, 13 | } 14 | 15 | #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub enum VersionedContractSourceMetadata { 18 | Current(ContractSourceMetadata), 19 | } 20 | 21 | // Convert from VersionedContractSourceMetadata to ContractSourceMetadata 22 | impl From for ContractSourceMetadata { 23 | fn from(metadata: VersionedContractSourceMetadata) -> Self { 24 | match metadata { 25 | VersionedContractSourceMetadata::Current(current) => current, 26 | } 27 | } 28 | } 29 | 30 | #[near_bindgen] 31 | impl Contract { 32 | #[payable] 33 | pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetadata) { 34 | // only contract account (aka the account that can deploy new code to this contract) can call this method 35 | require!( 36 | env::predecessor_account_id() == env::current_account_id(), 37 | "Only contract account can call this method" 38 | ); 39 | self.contract_source_metadata 40 | .set(&VersionedContractSourceMetadata::from( 41 | VersionedContractSourceMetadata::Current(source_metadata.clone()), 42 | )); 43 | // emit event 44 | log_set_source_metadata_event(&source_metadata); 45 | } 46 | 47 | pub fn get_contract_source_metadata(&self) -> Option { 48 | let source_metadata = self.contract_source_metadata.get(); 49 | if source_metadata.is_some() { 50 | Some(ContractSourceMetadata::from(source_metadata.unwrap())) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/registry/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn account_vec_to_set( 4 | account_vec: Vec, 5 | storage_key: StorageKey, 6 | ) -> UnorderedSet { 7 | let mut set = UnorderedSet::new(storage_key); 8 | for element in account_vec.iter() { 9 | set.insert(element); 10 | } 11 | set 12 | } 13 | 14 | pub fn refund_deposit(initial_storage_usage: u64) { 15 | let attached_deposit = env::attached_deposit(); 16 | let mut refund = attached_deposit; 17 | if env::storage_usage() > initial_storage_usage { 18 | // caller should pay for the extra storage they used and be refunded for the rest 19 | let storage_used = env::storage_usage() - initial_storage_usage; 20 | log!("Storage used: {} bytes", storage_used); 21 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 22 | require!( 23 | required_cost <= attached_deposit, 24 | format!("Must attach {} yoctoNEAR to cover storage", required_cost) 25 | ); 26 | refund -= required_cost; 27 | } else { 28 | // storage was freed up; caller should be refunded for what they freed up, in addition to the deposit they sent 29 | let storage_freed = initial_storage_usage - env::storage_usage(); 30 | let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); 31 | refund += cost_freed; 32 | } 33 | if refund > 1 { 34 | Promise::new(env::predecessor_account_id()).transfer(refund); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/scratch.js: -------------------------------------------------------------------------------- 1 | const CONTRACT_ID = "1702409170.test-contracts.potlock.testnet"; 2 | const L_NFT_TEST = "lachlan-nft-test.testnet"; 3 | const L_NFT_TEST_2 = "lachlan-nft-test-2.testnet"; 4 | const L_NFT_TEST_8 = "lachlan-nft-test-8.testnet"; 5 | 6 | // PotDeployer init args 7 | 8 | // console.log( 9 | // JSON.stringify({ 10 | // owner: CONTRACT_ID, 11 | // admins: [L_NFT_TEST], 12 | // protocol_fee_basis_points: 200, 13 | // protocol_fee_recipient_account: L_NFT_TEST_2, 14 | // default_chef_fee_basis_points: 500, 15 | // whitelisted_deployers: [L_NFT_TEST_8], 16 | // require_whitelist: true, 17 | // }) 18 | // ); 19 | 20 | // PotArgs 21 | 22 | // pub struct PotArgs { 23 | // owner: Option, 24 | // admins: Option>, 25 | // chef: Option, 26 | // pot_name: String, 27 | // pot_description: String, 28 | // max_projects: u32, 29 | // application_start_ms: TimestampMs, 30 | // application_end_ms: TimestampMs, 31 | // public_round_start_ms: TimestampMs, 32 | // public_round_end_ms: TimestampMs, 33 | // registry_provider: Option, 34 | // sybil_wrapper_provider: Option, 35 | // custom_sybil_checks: Option>, 36 | // custom_min_threshold_score: Option, 37 | // referral_fee_matching_pool_basis_points: u32, 38 | // referral_fee_public_round_basis_points: u32, 39 | // chef_fee_basis_points: u32, 40 | // } 41 | 42 | console.log( 43 | JSON.stringify({ 44 | pot_name: "test", 45 | pot_description: "test", 46 | max_projects: 3, 47 | application_start_ms: Date.now(), 48 | application_end_ms: Date.now() + 1000 * 60 * 60 * 24 * 7, 49 | public_round_start_ms: Date.now() + 1000 * 60 * 60 * 24 * 7, 50 | public_round_end_ms: Date.now() + 1000 * 60 * 60 * 24 * 7 * 2, 51 | referral_fee_matching_pool_basis_points: 500, 52 | referral_fee_public_round_basis_points: 200, 53 | chef_fee_basis_points: 500, 54 | }) 55 | ); 56 | -------------------------------------------------------------------------------- /contracts/sybil/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sybil" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [dependencies] 11 | near-sdk = "4.1.1" 12 | # [profile.release] # removed as added to root Cargo.toml 13 | # codegen-units = 1 14 | # # Tell `rustc` to optimize for small code size. 15 | # opt-level = "z" 16 | # lto = true 17 | # debug = false 18 | # panic = "abort" 19 | # # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 20 | # overflow-checks = true 21 | -------------------------------------------------------------------------------- /contracts/sybil/out/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/contracts/sybil/out/main.wasm -------------------------------------------------------------------------------- /contracts/sybil/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ">> Building Sybil contract" 4 | 5 | set -e 6 | 7 | export CARGO_TARGET_DIR=target 8 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 9 | mkdir -p ./out 10 | cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm 11 | echo ">> Finished Building Sybil contract" -------------------------------------------------------------------------------- /contracts/sybil/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $? -ne 0 ]; then 4 | echo ">> Error building Sybil contract" 5 | exit 1 6 | fi 7 | 8 | echo ">> Deploying Sybil contract!" 9 | 10 | # https://docs.near.org/tools/near-cli#near-dev-deploy 11 | near dev-deploy --wasmFile ./out/main.wasm -------------------------------------------------------------------------------- /contracts/sybil/src/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn admin_update_provider_status( 7 | &mut self, 8 | provider_id: ProviderId, 9 | status: ProviderStatus, 10 | ) -> Provider { 11 | self.assert_owner_or_admin(); 12 | // check that provider exists 13 | if let Some(versioned_provider) = self.providers_by_id.get(&provider_id) { 14 | // update provider 15 | let initial_storage_usage = env::storage_usage(); 16 | let mut provider = Provider::from(versioned_provider); 17 | let old_status = provider.status; 18 | provider.status = status.clone(); 19 | // add provider to mapping 20 | self.providers_by_id 21 | .insert(&provider_id, &VersionedProvider::Current(provider.clone())); 22 | // remove provider from old status set 23 | match old_status { 24 | ProviderStatus::Pending => { 25 | self.pending_provider_ids.remove(&provider_id); 26 | } 27 | ProviderStatus::Active => { 28 | self.active_provider_ids.remove(&provider_id); 29 | } 30 | ProviderStatus::Deactivated => { 31 | self.deactivated_provider_ids.remove(&provider_id); 32 | } 33 | } 34 | // add provider to new status set 35 | match status { 36 | ProviderStatus::Pending => { 37 | self.pending_provider_ids.insert(&provider_id); 38 | } 39 | ProviderStatus::Active => { 40 | self.active_provider_ids.insert(&provider_id); 41 | } 42 | ProviderStatus::Deactivated => { 43 | self.deactivated_provider_ids.insert(&provider_id); 44 | } 45 | } 46 | refund_deposit(initial_storage_usage); 47 | // log event 48 | log_update_provider_event(&provider_id, &provider); 49 | provider 50 | } else { 51 | env::panic_str("Provider does not exist"); 52 | } 53 | } 54 | 55 | #[payable] 56 | pub fn admin_activate_provider(&mut self, provider_id: ProviderId) -> Provider { 57 | self.admin_update_provider_status(provider_id, ProviderStatus::Active) 58 | } 59 | 60 | #[payable] 61 | pub fn admin_deactivate_provider(&mut self, provider_id: ProviderId) -> Provider { 62 | self.admin_update_provider_status(provider_id, ProviderStatus::Deactivated) 63 | } 64 | 65 | // config 66 | 67 | #[payable] 68 | pub fn admin_set_default_providers(&mut self, provider_ids: Vec) { 69 | // only contract owner or admin can call this method 70 | self.assert_owner_or_admin(); 71 | let initial_storage_usage = env::storage_usage(); 72 | // clear existing default providers 73 | self.default_provider_ids.clear(); 74 | // add new default providers 75 | for provider_id in provider_ids { 76 | self.default_provider_ids.insert(&provider_id); 77 | } 78 | // refund any unused deposit 79 | refund_deposit(initial_storage_usage); 80 | } 81 | 82 | #[payable] 83 | pub fn admin_add_default_providers(&mut self, provider_ids: Vec) { 84 | // only contract owner or admin can call this method 85 | self.assert_owner_or_admin(); 86 | let initial_storage_usage = env::storage_usage(); 87 | // add new default providers 88 | for provider_id in provider_ids { 89 | self.default_provider_ids.insert(&provider_id); 90 | } 91 | // refund any unused deposit 92 | refund_deposit(initial_storage_usage); 93 | } 94 | 95 | #[payable] 96 | pub fn admin_remove_default_providers(&mut self, provider_ids: Vec) { 97 | // only contract owner or admin can call this method 98 | self.assert_owner_or_admin(); 99 | let initial_storage_usage = env::storage_usage(); 100 | // remove default providers 101 | for provider_id in provider_ids { 102 | self.default_provider_ids.remove(&provider_id); 103 | } 104 | // refund any unused deposit 105 | refund_deposit(initial_storage_usage); 106 | } 107 | 108 | #[payable] 109 | pub fn admin_clear_default_providers(&mut self) { 110 | // only contract owner or admin can call this method 111 | self.assert_owner_or_admin(); 112 | let initial_storage_usage = env::storage_usage(); 113 | // clear default providers 114 | self.default_provider_ids.clear(); 115 | // refund any unused deposit 116 | refund_deposit(initial_storage_usage); 117 | } 118 | 119 | #[payable] 120 | pub fn admin_set_default_human_threshold(&mut self, default_human_threshold: u32) { 121 | // only contract owner or admin can call this method 122 | self.assert_owner_or_admin(); 123 | // set default human threshold 124 | self.default_human_threshold = default_human_threshold; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /contracts/sybil/src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub const TGAS: u64 = 1_000_000_000_000; // 1 TGAS 4 | pub const XCC_GAS_DEFAULT: u64 = TGAS * 10; // 10 TGAS 5 | pub const NO_DEPOSIT: Balance = 0; 6 | 7 | pub const PROVIDER_DEFAULT_WEIGHT: u32 = 100; 8 | pub const MAX_PROVIDER_NAME_LENGTH: usize = 64; 9 | pub const MAX_PROVIDER_DESCRIPTION_LENGTH: usize = 256; 10 | pub const MAX_PROVIDER_EXTERNAL_URL_LENGTH: usize = 256; 11 | pub const MAX_PROVIDER_ICON_URL_LENGTH: usize = 256; 12 | pub const MAX_TAGS_PER_PROVIDER: usize = 10; 13 | pub const MAX_TAG_LENGTH: usize = 32; 14 | pub const MAX_GAS: u64 = 100_000_000_000_000; 15 | -------------------------------------------------------------------------------- /contracts/sybil/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// source metadata update 4 | pub(crate) fn log_set_source_metadata_event(source_metadata: &ContractSourceMetadata) { 5 | env::log_str( 6 | format!( 7 | "{}{}", 8 | EVENT_JSON_PREFIX, 9 | json!({ 10 | "standard": "potlock", 11 | "version": "1.0.0", 12 | "event": "set_source_metadata", 13 | "data": [ 14 | { 15 | "source_metadata": source_metadata, 16 | } 17 | ] 18 | }) 19 | ) 20 | .as_ref(), 21 | ); 22 | } 23 | 24 | /// add provider 25 | pub(crate) fn log_add_provider_event(provider_id: &ProviderId, provider: &Provider) { 26 | env::log_str( 27 | format!( 28 | "{}{}", 29 | EVENT_JSON_PREFIX, 30 | json!({ 31 | "standard": "potlock", 32 | "version": "1.0.0", 33 | "event": "add_provider", 34 | "data": [ 35 | { 36 | "provider_id": provider_id, 37 | "provider": provider, 38 | } 39 | ] 40 | }) 41 | ) 42 | .as_ref(), 43 | ); 44 | } 45 | 46 | /// update provider 47 | pub(crate) fn log_update_provider_event(provider_id: &ProviderId, provider: &Provider) { 48 | env::log_str( 49 | format!( 50 | "{}{}", 51 | EVENT_JSON_PREFIX, 52 | json!({ 53 | "standard": "potlock", 54 | "version": "1.0.0", 55 | "event": "update_provider", 56 | "data": [ 57 | { 58 | "provider_id": provider_id, 59 | "provider": provider, 60 | } 61 | ] 62 | }) 63 | ) 64 | .as_ref(), 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /contracts/sybil/src/human.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 4 | #[serde(crate = "near_sdk::serde")] 5 | pub struct HumanScoreResponse { 6 | pub is_human: bool, 7 | pub score: u32, 8 | } 9 | 10 | #[near_bindgen] 11 | impl Contract { 12 | pub fn get_human_score(&self, account_id: AccountId) -> HumanScoreResponse { 13 | let total_score = self.get_score_for_account_id(account_id); 14 | HumanScoreResponse { 15 | is_human: total_score >= self.default_human_threshold, 16 | score: total_score, 17 | } 18 | } 19 | 20 | pub fn is_human(&self, account_id: AccountId) -> bool { 21 | // TODO: add option for caller to specify providers or custom default_human_threshold 22 | self.get_human_score(account_id).is_human 23 | } 24 | 25 | pub(crate) fn get_score_for_account_id(&self, account_id: AccountId) -> u32 { 26 | // get user stamps and add up default weights 27 | let mut total_score = 0; 28 | let user_providers = self.provider_ids_for_user.get(&account_id); 29 | if let Some(user_providers) = user_providers { 30 | for provider_id in user_providers.iter() { 31 | if let Some(versioned_provider) = self.providers_by_id.get(&provider_id) { 32 | let provider = Provider::from(versioned_provider); 33 | total_score += provider.default_weight; 34 | } 35 | } 36 | } 37 | total_score 38 | } 39 | 40 | // DEPRECATED IMPLEMENTATION 41 | // pub fn is_human(&self, account_id: AccountId) -> Promise { 42 | // // TODO: add option for caller to specify providers 43 | // let mut current_promise: Option = None; 44 | // let mut providers: Vec = Vec::new(); 45 | 46 | // for provider_id in self.default_provider_ids.iter() { 47 | // if let Some(versioned_provider) = self.providers_by_id.get(&provider_id) { 48 | // let provider = Provider::from(versioned_provider); 49 | // let provider_json = ProviderExternal::from_provider_id(&provider_id.0, provider); 50 | 51 | // let args = json!({ "account_id": account_id }).to_string().into_bytes(); 52 | 53 | // let new_promise = 54 | // Promise::new(AccountId::new_unchecked(provider_json.contract_id.clone())) 55 | // .function_call(provider_json.method_name.clone(), args, 0, XCC_GAS); 56 | 57 | // current_promise = Some(match current_promise { 58 | // Some(promise) => promise.and(new_promise), 59 | // None => new_promise, 60 | // }); 61 | 62 | // providers.push(provider_json); 63 | // } 64 | // } 65 | 66 | // match current_promise { 67 | // Some(promise) => promise.then( 68 | // Self::ext(env::current_account_id()) 69 | // .with_static_gas(XCC_GAS) 70 | // .is_human_callback(providers), 71 | // ), 72 | // None => Promise::new(env::current_account_id()), // No providers available 73 | // } 74 | // } 75 | 76 | // #[private] 77 | // pub fn is_human_callback(&self, providers: Vec) -> bool { 78 | // let mut total_score = 0; 79 | 80 | // for index in 0..providers.len() { 81 | // match env::promise_result(index as u64) { 82 | // PromiseResult::Successful(value) => { 83 | // let is_human: bool = near_sdk::serde_json::from_slice(&value).unwrap_or(false); 84 | // log!("Promise result #{}: {}", index + 1, is_human); 85 | // log!("Weight: {}", providers[index].default_weight); 86 | // if is_human { 87 | // total_score += providers[index].default_weight; 88 | // } 89 | // } 90 | // _ => {} // Handle failed or not ready promises as needed 91 | // } 92 | // } 93 | // log!("total_score: {}", total_score); 94 | // log!("default_human_threshold: {}", self.default_human_threshold); 95 | 96 | // total_score >= self.default_human_threshold 97 | // } 98 | } 99 | -------------------------------------------------------------------------------- /contracts/sybil/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Contract { 4 | pub(crate) fn assert_at_least_one_yocto(&self) { 5 | assert!( 6 | env::attached_deposit() >= 1, 7 | "At least one yoctoNEAR must be attached" 8 | ); 9 | } 10 | 11 | pub(crate) fn is_owner(&self) -> bool { 12 | env::predecessor_account_id() == self.owner 13 | } 14 | 15 | pub(crate) fn is_admin(&self) -> bool { 16 | self.admins.contains(&env::predecessor_account_id()) 17 | } 18 | 19 | pub(crate) fn is_owner_or_admin(&self) -> bool { 20 | self.is_owner() || self.is_admin() 21 | } 22 | 23 | pub(crate) fn assert_owner(&self) { 24 | assert!(self.is_owner(), "Only contract owner can call this method"); 25 | // require owner to attach at least one yoctoNEAR for security purposes 26 | self.assert_at_least_one_yocto(); 27 | } 28 | 29 | pub(crate) fn assert_admin(&self) { 30 | assert!(self.is_admin(), "Only contract admin can call this method"); 31 | // require caller to attach at least one yoctoNEAR for security purposes 32 | self.assert_at_least_one_yocto(); 33 | } 34 | 35 | pub(crate) fn assert_owner_or_admin(&self) { 36 | assert!( 37 | self.is_owner_or_admin(), 38 | "Only contract owner or admin can call this method" 39 | ); 40 | // require caller to attach at least one yoctoNEAR for security purposes 41 | self.assert_at_least_one_yocto(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/sybil/src/owner.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn owner_change_owner(&mut self, new_owner: AccountId) { 7 | self.assert_owner(); 8 | self.owner = new_owner.into(); 9 | } 10 | 11 | #[payable] 12 | pub fn owner_add_admins(&mut self, account_ids: Vec) { 13 | self.assert_owner(); 14 | for account_id in account_ids { 15 | self.admins.insert(&account_id); 16 | } 17 | } 18 | 19 | #[payable] 20 | pub fn owner_remove_admins(&mut self, account_ids: Vec) { 21 | self.assert_owner(); 22 | for account_id in account_ids { 23 | self.admins.remove(&account_id); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/sybil/src/source.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// CONTRACT SOURCE METADATA - as per NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` 4 | #[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize, PanicOnDefault)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct ContractSourceMetadata { 7 | /// Version of source code, e.g. "v1.0.0", could correspond to Git tag 8 | pub version: String, 9 | /// Git commit hash of currently deployed contract code 10 | pub commit_hash: String, 11 | /// GitHub repo url for currently deployed contract code 12 | pub link: String, 13 | } 14 | 15 | #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] 16 | #[serde(crate = "near_sdk::serde")] 17 | pub enum VersionedContractSourceMetadata { 18 | Current(ContractSourceMetadata), 19 | } 20 | 21 | // Convert from VersionedContractSourceMetadata to ContractSourceMetadata 22 | impl From for ContractSourceMetadata { 23 | fn from(metadata: VersionedContractSourceMetadata) -> Self { 24 | match metadata { 25 | VersionedContractSourceMetadata::Current(current) => current, 26 | } 27 | } 28 | } 29 | 30 | #[near_bindgen] 31 | impl Contract { 32 | #[payable] 33 | pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetadata) { 34 | // only contract account (aka the account that can deploy new code to this contract) can call this method 35 | require!( 36 | env::predecessor_account_id() == env::current_account_id(), 37 | "Only contract account can call this method" 38 | ); 39 | self.contract_source_metadata 40 | .set(&VersionedContractSourceMetadata::from( 41 | VersionedContractSourceMetadata::Current(source_metadata.clone()), 42 | )); 43 | // emit event 44 | log_set_source_metadata_event(&source_metadata); 45 | } 46 | 47 | pub fn get_contract_source_metadata(&self) -> Option { 48 | let source_metadata = self.contract_source_metadata.get(); 49 | if source_metadata.is_some() { 50 | Some(ContractSourceMetadata::from(source_metadata.unwrap())) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/sybil/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn account_vec_to_set( 4 | account_vec: Vec, 5 | storage_key: StorageKey, 6 | ) -> UnorderedSet { 7 | let mut set = UnorderedSet::new(storage_key); 8 | for element in account_vec.iter() { 9 | set.insert(element); 10 | } 11 | set 12 | } 13 | 14 | pub fn calculate_required_storage_deposit(initial_storage_usage: u64) -> Balance { 15 | let storage_used = env::storage_usage() - initial_storage_usage; 16 | log!("Storage used: {} bytes", storage_used); 17 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 18 | required_cost 19 | } 20 | 21 | pub fn refund_deposit(initial_storage_usage: u64) { 22 | let attached_deposit = env::attached_deposit(); 23 | let mut refund = attached_deposit; 24 | if env::storage_usage() > initial_storage_usage { 25 | // caller should pay for the extra storage they used and be refunded for the rest 26 | // let storage_used = env::storage_usage() - initial_storage_usage; 27 | let required_deposit = calculate_required_storage_deposit(initial_storage_usage); 28 | // env::storage_byte_cost() * Balance::from(storage_used); 29 | require!( 30 | required_deposit <= attached_deposit, 31 | format!( 32 | "Must attach {} yoctoNEAR to cover storage", 33 | required_deposit 34 | ) 35 | ); 36 | refund -= required_deposit; 37 | } else { 38 | // storage was freed up; caller should be refunded for what they freed up, in addition to the deposit they sent 39 | let storage_freed = initial_storage_usage - env::storage_usage(); 40 | let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); 41 | refund += cost_freed; 42 | } 43 | if refund > 1 { 44 | Promise::new(env::predecessor_account_id()).transfer(refund); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /contracts/sybil/src/validation.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub(crate) fn assert_valid_provider_name(name: &str) { 4 | assert!( 5 | name.len() <= MAX_PROVIDER_NAME_LENGTH, 6 | "Provider name is too long" 7 | ); 8 | } 9 | 10 | pub(crate) fn assert_valid_provider_description(description: &str) { 11 | assert!( 12 | description.len() <= MAX_PROVIDER_DESCRIPTION_LENGTH, 13 | "Provider description is too long" 14 | ); 15 | } 16 | 17 | pub(crate) fn assert_valid_provider_gas(gas: &u64) { 18 | assert!( 19 | gas > &0 && gas <= &MAX_GAS as &u64, 20 | "Provider gas is too high, must be greater than zero and less than or equal to {}", 21 | MAX_GAS 22 | ); 23 | } 24 | 25 | pub(crate) fn assert_valid_provider_external_url(external_url: &str) { 26 | assert!( 27 | external_url.len() <= MAX_PROVIDER_EXTERNAL_URL_LENGTH, 28 | "Provider external URL is too long" 29 | ); 30 | } 31 | 32 | pub(crate) fn assert_valid_provider_icon_url(icon_url: &str) { 33 | assert!( 34 | icon_url.len() <= MAX_PROVIDER_ICON_URL_LENGTH, 35 | "Provider icon URL is too long" 36 | ); 37 | } 38 | 39 | pub(crate) fn assert_valid_provider_tag(tag: &str) { 40 | assert!(tag.len() <= MAX_TAG_LENGTH, "Tag is too long"); 41 | } 42 | 43 | pub(crate) fn assert_valid_provider_tags(tags: &[String]) { 44 | assert!( 45 | tags.len() <= MAX_TAGS_PER_PROVIDER, 46 | "Too many tags for provider" 47 | ); 48 | for tag in tags { 49 | assert_valid_provider_tag(tag); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /contracts/sybil_provider_simulator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sybil_provider_simulator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [dependencies] 11 | near-sdk = "4.1.1" 12 | # [profile.release] # removed as added to root Cargo.toml 13 | # codegen-units = 1 14 | # # Tell `rustc` to optimize for small code size. 15 | # opt-level = "z" 16 | # lto = true 17 | # debug = false 18 | # panic = "abort" 19 | # # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 20 | # overflow-checks = true 21 | -------------------------------------------------------------------------------- /contracts/sybil_provider_simulator/README.md: -------------------------------------------------------------------------------- 1 | # PotLock SybilProviderSimulator Contract 2 | 3 | ## Purpose 4 | 5 | Provides an example of an extremely 3rd party Sybil resistance provider that can integrate with Nada.Bot and be used for testing purposes. (Obviously leaves out the actual Sybil resistance part; used instead to simulate API) 6 | 7 | ## Deployed Contracts 8 | 9 | - **Staging (Mainnet): `sybilprovidersimulator-1.staging.nadabot.near`** 10 | - **Testnet: `sybilprovidersimulator-1.nadabot.testnet`** 11 | 12 | ## Contract Structure 13 | 14 | ### General Types 15 | 16 | ```rs 17 | ``` 18 | 19 | ### Contract 20 | 21 | ```rs 22 | pub struct Contract { 23 | account_ids_to_bool: UnorderedMap, 24 | } 25 | ``` 26 | 27 | ## Methods 28 | 29 | ### Write Methods 30 | 31 | 32 | ```rs 33 | // INIT 34 | 35 | pub fn new() -> Self 36 | 37 | 38 | // CHECKS 39 | 40 | #[payable] 41 | pub fn get_check(&mut self) // Simulates getting a sybil-resistant check (e.g. connecting twitter, passing face scan, etc) 42 | 43 | pub fn remove_check(&mut self) 44 | 45 | ``` 46 | 47 | ### Read Methods 48 | 49 | ```rs 50 | // CHECKS 51 | 52 | pub fn has_check(&self, account_id: AccountId) -> bool // Simulates the primary method signature that must be implemented for integration with Nada.Bot 53 | 54 | ``` -------------------------------------------------------------------------------- /contracts/sybil_provider_simulator/out/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PotLock/core/7ffcf91ed26cd1c920263fb363f691f462f90958/contracts/sybil_provider_simulator/out/main.wasm -------------------------------------------------------------------------------- /contracts/sybil_provider_simulator/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ">> Building SybilProvider contract" 4 | 5 | set -e 6 | 7 | export CARGO_TARGET_DIR=target 8 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 9 | mkdir -p ./out 10 | cp target/wasm32-unknown-unknown/release/*.wasm ./out/main.wasm 11 | echo ">> Finished Building SybilProvider contract" -------------------------------------------------------------------------------- /contracts/sybil_provider_simulator/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $? -ne 0 ]; then 4 | echo ">> Error building SybilProvider contract" 5 | exit 1 6 | fi 7 | 8 | echo ">> Deploying SybilProvider contract!" 9 | 10 | # https://docs.near.org/tools/near-cli#near-dev-deploy 11 | near dev-deploy --wasmFile ./out/main.wasm -------------------------------------------------------------------------------- /contracts/sybil_provider_simulator/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 3 | use near_sdk::json_types::{U128, U64}; 4 | use near_sdk::serde::{Deserialize, Serialize}; 5 | use near_sdk::{ 6 | env, log, near_bindgen, require, serde_json::json, AccountId, Balance, BorshStorageKey, 7 | PanicOnDefault, Promise, 8 | }; 9 | 10 | pub mod utils; 11 | pub use crate::utils::*; 12 | 13 | /// SybilProvider Contract 14 | #[near_bindgen] 15 | #[derive(BorshDeserialize, BorshSerialize)] 16 | pub struct Contract { 17 | account_ids_to_bool: UnorderedMap, 18 | } 19 | 20 | #[derive(BorshSerialize, BorshDeserialize)] 21 | pub enum VersionedContract { 22 | Current(Contract), 23 | } 24 | 25 | /// Convert VersionedContract to Contract 26 | impl From for Contract { 27 | fn from(contract: VersionedContract) -> Self { 28 | match contract { 29 | VersionedContract::Current(current) => current, 30 | } 31 | } 32 | } 33 | 34 | #[derive(BorshSerialize, BorshStorageKey)] 35 | pub enum StorageKey { 36 | AccountIdsToBool, 37 | } 38 | 39 | #[near_bindgen] 40 | impl Contract { 41 | #[init] 42 | pub fn new() -> Self { 43 | Self { 44 | account_ids_to_bool: UnorderedMap::new(StorageKey::AccountIdsToBool), 45 | } 46 | } 47 | 48 | #[payable] 49 | pub fn get_check(&mut self) { 50 | let initial_storage_usage = env::storage_usage(); 51 | self.account_ids_to_bool 52 | .insert(&env::predecessor_account_id(), &true); 53 | // Refund any unused deposit after storage cost is covered 54 | refund_deposit(initial_storage_usage); 55 | } 56 | 57 | pub fn remove_check(&mut self) { 58 | let initial_storage_usage = env::storage_usage(); 59 | self.account_ids_to_bool 60 | .remove(&env::predecessor_account_id()); 61 | // Refund user for storage freed 62 | refund_deposit(initial_storage_usage); 63 | } 64 | 65 | pub fn has_check(&self, account_id: AccountId) -> bool { 66 | self.account_ids_to_bool.get(&account_id).is_some() 67 | } 68 | } 69 | 70 | impl Default for Contract { 71 | fn default() -> Self { 72 | Self { 73 | account_ids_to_bool: UnorderedMap::new(StorageKey::AccountIdsToBool), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /contracts/sybil_provider_simulator/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub fn calculate_required_storage_deposit(initial_storage_usage: u64) -> Balance { 4 | let storage_used = env::storage_usage() - initial_storage_usage; 5 | log!("Storage used: {} bytes", storage_used); 6 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 7 | required_cost 8 | } 9 | 10 | pub fn refund_deposit(initial_storage_usage: u64) { 11 | let attached_deposit = env::attached_deposit(); 12 | let mut refund = attached_deposit; 13 | if env::storage_usage() > initial_storage_usage { 14 | // caller should pay for the extra storage they used and be refunded for the rest 15 | // let storage_used = env::storage_usage() - initial_storage_usage; 16 | let required_deposit = calculate_required_storage_deposit(initial_storage_usage); 17 | // env::storage_byte_cost() * Balance::from(storage_used); 18 | require!( 19 | required_deposit <= attached_deposit, 20 | format!( 21 | "Must attach {} yoctoNEAR to cover storage", 22 | required_deposit 23 | ) 24 | ); 25 | refund -= required_deposit; 26 | } else { 27 | // storage was freed up; caller should be refunded for what they freed up, in addition to the deposit they sent 28 | let storage_freed = initial_storage_usage - env::storage_usage(); 29 | let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); 30 | refund += cost_freed; 31 | } 32 | if refund > 1 { 33 | Promise::new(env::predecessor_account_id()).transfer(refund); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/test/README.md: -------------------------------------------------------------------------------- 1 | TEST CASES 2 | 3 | Donation 4 | - User can donate 5 | - Owner can change the owner 6 | - Owner can set protocol_fee_basis_points 7 | - Owner can set referral_fee_basis_points 8 | - Owner can set protocol_fee_recipient_account 9 | 10 | Registry 11 | - Can be deployed and initialized 12 | - Owner can add & remove admins 13 | - admins must be DAO❓ 14 | - End user or Admins can register a Project 15 | - Project ID should not already be registered 16 | - Project should be approved by default❓ 17 | - Admins can change status of a Project 18 | 19 | PotDeployer 20 | - Can be deployed and initialized 21 | - Admin (DAO) or whitelisted_deployer can deploy a new Pot 22 | - Specified chef must have "chef" role in ReFi DAO 23 | - Admin (DAO) can: 24 | - Update protocol fee basis points (must be <= max_protocol_fee_basis_points) 25 | - Update default chef fee basis points (must be <= default_chef_fee_basis_points) 26 | - Update max protocol fee basis points 27 | - Update max chef fee basis points 28 | - Update max round time 29 | - Update max application time 30 | - Update max milestones 31 | - Add whitelisted deployers 32 | 33 | Pot 34 | - Can be deployed and initialized 35 | - Enforces public_round_start_ms & public_round_end_ms 36 | - Enforces application_start_ms & application_end_ms 37 | - Enforces max_projects 38 | - Enforces supported base_currency 39 | - Enforces application_requirement (SBT) 40 | - Enforces donation_requirement (SBT) 41 | - Project can apply for round 42 | - Enforces haven't already applied 43 | - Enforces max_projects not met 44 | - Enforces application period open 45 | - Enforces registered project 46 | - Enforces caller is project admin 47 | - Enforces caller (or project ID❓) meets application requirement (SBT) 48 | - Emits event 49 | - Project can unapply 50 | - Enforces caller is project admin 51 | - Enforces application is in Pending status 52 | - Emits event 53 | - Chef can update application status 54 | - Enforces only chef 55 | - Must provide notes (reason) 56 | - Patron can donate to matching pool 57 | - Protocol & chef fees paid out 58 | - Referrer paid out 59 | - Enforces round open 60 | - Emits event 61 | - End user can donate to specific project 62 | - Enforces round open 63 | - Emits event 64 | - End user can donate to all projects 65 | - Enforces round open 66 | - Emits events 67 | - PotDeployer Admin (DAO) can change chef & chef fee 68 | - Chef can set (update) the application requirement 69 | - Chef can set (update) the donation requirement 70 | - Chef can update the patron referral fee❓ 71 | - Chef can set payouts (CLR / quadratic calculations) 72 | - PotDeployer Admin (DAO) can process payouts 73 | - Can cooldown period be overridden? 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /contracts/test/donation/config.ts: -------------------------------------------------------------------------------- 1 | export const contractId = "dev-1705952970616-18051433080691"; 2 | export const networkId = "testnet"; 3 | export const nodeUrl = `https://rpc.${networkId}.near.org`; 4 | 5 | // export const DEFAULT_PROJECT_ID = "test-contracts.potlock.testnet"; // easy to set it to this value as we have the key for it 6 | -------------------------------------------------------------------------------- /contracts/test/donation/contract.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import BN from "bn.js"; 3 | import { Account } from "near-api-js"; 4 | import { contractId } from "./config"; 5 | import { contractId as registryContractId } from "../registry/config"; 6 | import { contractId as potDeployerContractId } from "../pot_factory/config"; 7 | import { 8 | contractAccount, 9 | // getChefAccount, 10 | // getPatronAccount, 11 | // getProjectAccounts, 12 | near, 13 | } from "./setup"; 14 | import { donate, getDonations, initializeContract } from "./utils"; 15 | import { 16 | DEFAULT_PARENT_ACCOUNT_ID, 17 | DEFAULT_PROTOCOL_FEE_BASIS_POINTS, 18 | DEFAULT_REFERRAL_FEE_BASIS_POINTS, 19 | } from "../utils/constants"; 20 | import { registerProject } from "../registry/utils"; 21 | import { POT_FACTORY_ALWAYS_ADMIN_ID } from "../pot_factory/config"; 22 | import { parseNearAmount } from "near-api-js/lib/utils/format"; 23 | import { 24 | convertDonationsToProjectContributions, 25 | calculateQuadraticPayouts, 26 | } from "../utils/quadratics"; 27 | 28 | /* 29 | TEST CASES (taken from ../README.md): 30 | 31 | Donation 32 | - ✅ User can donate 33 | - Owner can change the owner 34 | - Owner can set protocol_fee_basis_points 35 | - Owner can set referral_fee_basis_points 36 | - Owner can set protocol_fee_recipient_account 37 | */ 38 | 39 | describe("Donation Contract Tests", async () => { 40 | // other accounts 41 | let donorAccountId = contractId; 42 | let donorAccount: Account; 43 | // let projectAccounts: Account[]; 44 | // // let chefId: AccountId; // TODO: 45 | // let chefAccount: Account; 46 | // let potDeployerAdminId: AccountId = POT_DEPLOYER_ALWAYS_ADMIN_ID; 47 | // let potDeployerAdminAccount: Account; 48 | // let patronAccount: Account; 49 | 50 | before(async () => { 51 | // projectAccount = new Account(near.connection, projectId); 52 | donorAccount = new Account(near.connection, donorAccountId); 53 | 54 | // attempt to initialize contract; if it fails, it's already initialized 55 | const now = Date.now(); 56 | const defaultDonationInitArgs = { 57 | owner: contractId, 58 | protocol_fee_basis_points: DEFAULT_PROTOCOL_FEE_BASIS_POINTS, 59 | referral_fee_basis_points: DEFAULT_REFERRAL_FEE_BASIS_POINTS, 60 | protocol_fee_recipient_account: DEFAULT_PARENT_ACCOUNT_ID, 61 | }; 62 | try { 63 | // initialize contract unless already initialized 64 | await initializeContract(defaultDonationInitArgs); 65 | console.log(`✅ Initialized Donation contract ${contractId}`); 66 | } catch (e) { 67 | if ( 68 | JSON.stringify(e).includes("The contract has already been initialized") 69 | ) { 70 | console.log(`Donation contract ${contractId} is already initialized`); 71 | } else { 72 | console.log("🚨 Donation initialize error: ", e); 73 | assert(false); 74 | } 75 | } 76 | }); 77 | 78 | it("User can donate", async () => { 79 | try { 80 | const message = "Go go go!"; 81 | const referrerId = contractId; 82 | const donationAmount = parseNearAmount("0.1") as string; // 0.1 NEAR in YoctoNEAR 83 | await donate({ 84 | donorAccount, 85 | recipientId: DEFAULT_PARENT_ACCOUNT_ID, 86 | donationAmount, 87 | message, 88 | referrerId, 89 | }); 90 | // get donations 91 | const donations = await getDonations(); 92 | console.log("donations: ", donations); 93 | // assert that donation record was created 94 | const exists = donations.some( 95 | (d) => d.message === message && d.donor_id === donorAccountId 96 | ); 97 | assert(exists); 98 | } catch (e) { 99 | console.log("🚨 Error donating: ", e); 100 | assert(false); 101 | } 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /contracts/test/donation/setup.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair, Near, Account, Contract, keyStores } from "near-api-js"; 2 | import { contractId, networkId, nodeUrl } from "./config"; 3 | import { loadCredentials } from "../utils/helpers"; 4 | import { POT_DEPLOYER_ALWAYS_ADMIN_ID } from "../pot_factory/config"; 5 | import { 6 | DEFAULT_NEW_ACCOUNT_AMOUNT, 7 | DEFAULT_PARENT_ACCOUNT_ID, 8 | } from "../utils/constants"; 9 | import { BN } from "bn.js"; 10 | import { parseNearAmount } from "near-api-js/lib/utils/format"; 11 | 12 | // set up contract account credentials 13 | const contractCredentials = loadCredentials(networkId, contractId); 14 | const keyStore = new keyStores.InMemoryKeyStore(); 15 | keyStore.setKey( 16 | networkId, 17 | contractId, 18 | KeyPair.fromString(contractCredentials.private_key) 19 | ); 20 | 21 | // // TODO: revise this 22 | // // set project account key 23 | // const projectCredentials = loadCredentials(networkId, DEFAULT_PROJECT_ID); 24 | // keyStore.setKey( 25 | // networkId, 26 | // DEFAULT_PROJECT_ID, 27 | // KeyPair.fromString(projectCredentials.private_key) 28 | // ); 29 | // // set pot deployer admin account key 30 | // const potDeployerAdminCredentials = loadCredentials( 31 | // networkId, 32 | // POT_DEPLOYER_ALWAYS_ADMIN_ID 33 | // ); 34 | // keyStore.setKey( 35 | // networkId, 36 | // POT_DEPLOYER_ALWAYS_ADMIN_ID, 37 | // KeyPair.fromString(potDeployerAdminCredentials.private_key) 38 | // ); 39 | 40 | // set up near connection 41 | export const near = new Near({ 42 | networkId, 43 | deps: { keyStore }, 44 | nodeUrl, 45 | }); 46 | 47 | export const contractAccount = new Account(near.connection, contractId); 48 | 49 | // // set parent account key 50 | // const parentAccountCredentials = loadCredentials( 51 | // networkId, 52 | // DEFAULT_PARENT_ACCOUNT_ID 53 | // ); 54 | // keyStore.setKey( 55 | // networkId, 56 | // DEFAULT_PARENT_ACCOUNT_ID, 57 | // KeyPair.fromString(parentAccountCredentials.private_key) 58 | // ); 59 | // const parentAccount = new Account(near.connection, DEFAULT_PARENT_ACCOUNT_ID); 60 | 61 | // export const getPatronAccount = async () => { 62 | // const now = Date.now(); 63 | // const patronAccountPrefix = `patron-${now}.`; 64 | // // create account which shares the credentials of the parent account 65 | // const patronAccountId = patronAccountPrefix + DEFAULT_PARENT_ACCOUNT_ID; 66 | // console.log("🧙‍♀️ Creating patron account", patronAccountId); 67 | // try { 68 | // await parentAccount.createAccount( 69 | // patronAccountId, 70 | // parentAccountCredentials.public_key, 71 | // new BN(DEFAULT_NEW_ACCOUNT_AMOUNT as string) 72 | // ); 73 | // console.log("✅ Created patron account", patronAccountId); 74 | // keyStore.setKey( 75 | // networkId, 76 | // patronAccountId, 77 | // KeyPair.fromString(parentAccountCredentials.private_key) 78 | // ); 79 | // const patronAccount = new Account(near.connection, patronAccountId); 80 | // return patronAccount; 81 | // } catch (e) { 82 | // // console.log("error creating project account", patronAccountId, e); 83 | // throw e; 84 | // } 85 | // }; 86 | 87 | // export const getChefAccount = async () => { 88 | // // TODO: change this so that it creates a single subaccount "chef" under contract account. this way we can determine if they have already been created, and skip if so. 89 | // const now = Date.now(); 90 | // const chefAccountPrefix = `chef-${now}.`; 91 | // // create account which shares the credentials of the parent account 92 | // const chefAccountId = chefAccountPrefix + DEFAULT_PARENT_ACCOUNT_ID; 93 | // console.log("👨‍🍳 Creating chef account", chefAccountId); 94 | // try { 95 | // await parentAccount.createAccount( 96 | // chefAccountId, 97 | // parentAccountCredentials.public_key, 98 | // new BN(DEFAULT_NEW_ACCOUNT_AMOUNT as string) 99 | // ); 100 | // console.log("✅ Created chef account", chefAccountId); 101 | // keyStore.setKey( 102 | // networkId, 103 | // chefAccountId, 104 | // KeyPair.fromString(parentAccountCredentials.private_key) 105 | // ); 106 | // const chefAccount = new Account(near.connection, chefAccountId); 107 | // return chefAccount; 108 | // } catch (e) { 109 | // throw e; 110 | // } 111 | // }; 112 | 113 | // export const getProjectAccounts = async () => { 114 | // // TODO: change this so that it creates three "project" subaccounts under contract account. this way we can determine if they have already been created, and skip if so. 115 | // // create 3 x project accounts 116 | // const now = Date.now(); 117 | // const projectAccountPrefixes = [ 118 | // `project-${now}-1.`, 119 | // `project-${now}-2.`, 120 | // `project-${now}-3.`, 121 | // ]; 122 | // const projectAccounts = []; 123 | // const parentAccount = new Account(near.connection, DEFAULT_PARENT_ACCOUNT_ID); 124 | // for (const projectAccountPrefix of projectAccountPrefixes) { 125 | // // create account which shares the credentials of the parent account 126 | // const projectAccountId = projectAccountPrefix + DEFAULT_PARENT_ACCOUNT_ID; 127 | // console.log("🧙‍♀️ Creating project account", projectAccountId); 128 | // try { 129 | // await parentAccount.createAccount( 130 | // projectAccountId, 131 | // parentAccountCredentials.public_key, 132 | // new BN(DEFAULT_NEW_ACCOUNT_AMOUNT as string) 133 | // ); 134 | // console.log("✅ Created project account", projectAccountId); 135 | // keyStore.setKey( 136 | // networkId, 137 | // projectAccountId, 138 | // KeyPair.fromString(parentAccountCredentials.private_key) 139 | // ); 140 | // const projectAccount = new Account(near.connection, projectAccountId); 141 | // projectAccounts.push(projectAccount); 142 | // } catch (e) { 143 | // throw e; 144 | // } 145 | // } 146 | // return projectAccounts; 147 | // }; 148 | -------------------------------------------------------------------------------- /contracts/test/donation/utils.ts: -------------------------------------------------------------------------------- 1 | import { _contractCall, _contractView } from "../utils/helpers"; 2 | import { contractId as _contractId } from "./config"; 3 | import { Account } from "near-api-js"; 4 | import { contractAccount } from "./setup"; 5 | import { NO_DEPOSIT } from "../utils/constants"; 6 | import { parseNearAmount } from "near-api-js/lib/utils/format"; 7 | 8 | const READ_METHODS = { 9 | GET_DONATIONS: "get_donations", 10 | GET_DONATION_BY_ID: "get_donation_by_id", 11 | GET_DONATIONS_FOR_RECIPIENT: "get_donations_for_recipient", 12 | GET_DONATIONS_FOR_DONOR: "get_donations_for_donor", 13 | GET_DONATIONS_FOR_FT: "get_donations_for_ft", 14 | }; 15 | 16 | const WRITE_METHODS = { 17 | NEW: "new", 18 | DONATE: "donate", 19 | OWNER_CHANGE_OWNER: "owner_change_owner", 20 | OWNER_SET_PROTOCOL_FEE_BASIS_POINTS: "owner_set_protocol_fee_basis_points", 21 | OWNER_SET_REFERRAL_FEE_BASIS_POINTS: "owner_set_referral_fee_basis_points", 22 | OWNER_SET_PROTOCOL_FEE_RECIPIENT_ACCOUNT: 23 | "owner_set_protocol_fee_recipient_account", 24 | }; 25 | 26 | // Wrapper around contractView that defaults to the contract account 27 | const contractView = async ({ 28 | contractId, 29 | methodName, 30 | args, 31 | gas, 32 | attachedDeposit, 33 | }: { 34 | contractId?: string; 35 | methodName: string; 36 | args?: Record; 37 | gas?: string; 38 | attachedDeposit?: string; 39 | }) => { 40 | return _contractView({ 41 | contractId: contractId || _contractId, 42 | callerAccount: contractAccount, 43 | methodName, 44 | args, 45 | gas, 46 | attachedDeposit, 47 | }); 48 | }; 49 | 50 | // Wrapper around contractCall that defaults to the contract account 51 | const contractCall = async ({ 52 | callerAccount, 53 | contractId = _contractId, 54 | methodName, 55 | args, 56 | gas, 57 | attachedDeposit, 58 | }: { 59 | callerAccount: Account; 60 | contractId: string; 61 | methodName: string; 62 | args?: Record; 63 | gas?: string; 64 | attachedDeposit?: string; 65 | }) => { 66 | return _contractCall({ 67 | contractId, 68 | callerAccount, 69 | methodName, 70 | args, 71 | gas, 72 | attachedDeposit, 73 | }); 74 | }; 75 | 76 | // Helper function for the common case of contract calling itself 77 | export const callSelf = async ({ 78 | methodName, 79 | args, 80 | gas, 81 | attachedDeposit, 82 | }: { 83 | methodName: string; 84 | args?: Record; 85 | gas?: string; 86 | attachedDeposit?: string; 87 | }) => { 88 | return contractCall({ 89 | callerAccount: contractAccount, 90 | contractId: _contractId, 91 | methodName, 92 | args, 93 | gas, 94 | attachedDeposit, 95 | }); 96 | }; 97 | 98 | export const initializeContract = async ( 99 | initializeArgs?: Record 100 | ) => { 101 | return callSelf({ 102 | methodName: WRITE_METHODS.NEW, 103 | args: initializeArgs, 104 | attachedDeposit: NO_DEPOSIT, 105 | }); 106 | }; 107 | 108 | export const getDonations = async (): Promise => { 109 | return contractView({ 110 | methodName: READ_METHODS.GET_DONATIONS, 111 | }); 112 | }; 113 | 114 | export const getDonationById = async ( 115 | donationId: DonationId 116 | ): Promise => { 117 | return contractView({ 118 | methodName: READ_METHODS.GET_DONATION_BY_ID, 119 | args: { donation_id: donationId }, 120 | }); 121 | }; 122 | 123 | export const getDonationsForRecipient = async ( 124 | recipientId: AccountId 125 | ): Promise => { 126 | return contractView({ 127 | methodName: READ_METHODS.GET_DONATIONS_FOR_RECIPIENT, 128 | args: { recipient_id: recipientId }, 129 | }); 130 | }; 131 | 132 | export const getDonationsForDonor = async ( 133 | donorId: AccountId 134 | ): Promise => { 135 | return contractView({ 136 | methodName: READ_METHODS.GET_DONATIONS_FOR_DONOR, 137 | args: { donor_id: donorId }, 138 | }); 139 | }; 140 | 141 | export const getDonationsForFt = async ( 142 | ftId: AccountId 143 | ): Promise => { 144 | return contractView({ 145 | methodName: READ_METHODS.GET_DONATIONS_FOR_FT, 146 | args: { ft_id: ftId }, 147 | }); 148 | }; 149 | 150 | export const donate = async ({ 151 | donorAccount, 152 | recipientId, 153 | donationAmount, 154 | message, 155 | referrerId, 156 | }: { 157 | donorAccount: Account; 158 | recipientId: AccountId; 159 | donationAmount: string; 160 | message?: string; 161 | referrerId?: AccountId; 162 | }) => { 163 | return contractCall({ 164 | callerAccount: donorAccount, 165 | contractId: _contractId, 166 | methodName: WRITE_METHODS.DONATE, 167 | args: { 168 | recipient_id: recipientId, 169 | message: message || null, 170 | referrer_id: referrerId || null, 171 | }, 172 | attachedDeposit: donationAmount, 173 | }); 174 | }; 175 | 176 | export const ownerChangeOwner = async ( 177 | ownerAccount: Account, 178 | newOwner: AccountId 179 | ) => { 180 | return contractCall({ 181 | callerAccount: ownerAccount, 182 | contractId: _contractId, 183 | methodName: WRITE_METHODS.OWNER_CHANGE_OWNER, 184 | args: { owner: newOwner }, 185 | }); 186 | }; 187 | 188 | export const ownerSetProtocolFeeBasisPoints = async ( 189 | ownerAccount: Account, 190 | protocolFeeBasisPoints: number 191 | ) => { 192 | return contractCall({ 193 | callerAccount: ownerAccount, 194 | contractId: _contractId, 195 | methodName: WRITE_METHODS.OWNER_SET_PROTOCOL_FEE_BASIS_POINTS, 196 | args: { protocol_fee_basis_points: protocolFeeBasisPoints }, 197 | }); 198 | }; 199 | 200 | export const ownerSetReferralFeeBasisPoints = async ( 201 | ownerAccount: Account, 202 | referralFeeBasisPoints: number 203 | ) => { 204 | return contractCall({ 205 | callerAccount: ownerAccount, 206 | contractId: _contractId, 207 | methodName: WRITE_METHODS.OWNER_SET_REFERRAL_FEE_BASIS_POINTS, 208 | args: { referral_fee_basis_points: referralFeeBasisPoints }, 209 | }); 210 | }; 211 | 212 | export const ownerSetProtocolFeeRecipientAccount = async ( 213 | ownerAccount: Account, 214 | protocolFeeRecipientAccount: AccountId 215 | ) => { 216 | return contractCall({ 217 | callerAccount: ownerAccount, 218 | contractId: _contractId, 219 | methodName: WRITE_METHODS.OWNER_SET_PROTOCOL_FEE_RECIPIENT_ACCOUNT, 220 | args: { protocol_fee_recipient_account: protocolFeeRecipientAccount }, 221 | }); 222 | }; 223 | -------------------------------------------------------------------------------- /contracts/test/pot/config.ts: -------------------------------------------------------------------------------- 1 | export const contractId = "1696368802.test-contracts.potlock.testnet"; 2 | export const networkId = "testnet"; 3 | export const nodeUrl = `https://rpc.${networkId}.near.org`; 4 | 5 | export const DEFAULT_PROJECT_ID = "test-contracts.potlock.testnet"; // easy to set it to this value as we have the key for it 6 | -------------------------------------------------------------------------------- /contracts/test/pot/setup.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair, Near, Account, Contract, keyStores } from "near-api-js"; 2 | import { DEFAULT_PROJECT_ID, contractId, networkId, nodeUrl } from "./config"; 3 | import { loadCredentials } from "../utils/helpers"; 4 | import { POT_FACTORY_ALWAYS_ADMIN_ID } from "../pot_factory/config"; 5 | import { 6 | DEFAULT_NEW_ACCOUNT_AMOUNT, 7 | DEFAULT_PARENT_ACCOUNT_ID, 8 | } from "../utils/constants"; 9 | import { BN } from "bn.js"; 10 | import { parseNearAmount } from "near-api-js/lib/utils/format"; 11 | 12 | // set up contract account credentials 13 | const contractCredentials = loadCredentials(networkId, contractId); 14 | const keyStore = new keyStores.InMemoryKeyStore(); 15 | keyStore.setKey( 16 | networkId, 17 | contractId, 18 | KeyPair.fromString(contractCredentials.private_key) 19 | ); 20 | 21 | // TODO: revise this 22 | // set project account key 23 | const projectCredentials = loadCredentials(networkId, DEFAULT_PROJECT_ID); 24 | keyStore.setKey( 25 | networkId, 26 | DEFAULT_PROJECT_ID, 27 | KeyPair.fromString(projectCredentials.private_key) 28 | ); 29 | // set pot deployer admin account key 30 | const potDeployerAdminCredentials = loadCredentials( 31 | networkId, 32 | POT_FACTORY_ALWAYS_ADMIN_ID 33 | ); 34 | keyStore.setKey( 35 | networkId, 36 | POT_FACTORY_ALWAYS_ADMIN_ID, 37 | KeyPair.fromString(potDeployerAdminCredentials.private_key) 38 | ); 39 | 40 | // set up near connection 41 | export const near = new Near({ 42 | networkId, 43 | deps: { keyStore }, 44 | nodeUrl, 45 | }); 46 | 47 | export const contractAccount = new Account(near.connection, contractId); 48 | 49 | // set parent account key 50 | const parentAccountCredentials = loadCredentials( 51 | networkId, 52 | DEFAULT_PARENT_ACCOUNT_ID 53 | ); 54 | keyStore.setKey( 55 | networkId, 56 | DEFAULT_PARENT_ACCOUNT_ID, 57 | KeyPair.fromString(parentAccountCredentials.private_key) 58 | ); 59 | const parentAccount = new Account(near.connection, DEFAULT_PARENT_ACCOUNT_ID); 60 | 61 | export const getPatronAccount = async () => { 62 | const now = Date.now(); 63 | const patronAccountPrefix = `patron-${now}.`; 64 | // create account which shares the credentials of the parent account 65 | const patronAccountId = patronAccountPrefix + DEFAULT_PARENT_ACCOUNT_ID; 66 | console.log("🧙‍♀️ Creating patron account", patronAccountId); 67 | try { 68 | await parentAccount.createAccount( 69 | patronAccountId, 70 | parentAccountCredentials.public_key, 71 | new BN(DEFAULT_NEW_ACCOUNT_AMOUNT as string) 72 | ); 73 | console.log("✅ Created patron account", patronAccountId); 74 | keyStore.setKey( 75 | networkId, 76 | patronAccountId, 77 | KeyPair.fromString(parentAccountCredentials.private_key) 78 | ); 79 | const patronAccount = new Account(near.connection, patronAccountId); 80 | return patronAccount; 81 | } catch (e) { 82 | // console.log("error creating project account", patronAccountId, e); 83 | throw e; 84 | } 85 | }; 86 | 87 | export const getChefAccount = async () => { 88 | // TODO: change this so that it creates a single subaccount "chef" under contract account. this way we can determine if they have already been created, and skip if so. 89 | const now = Date.now(); 90 | const chefAccountPrefix = `chef-${now}.`; 91 | // create account which shares the credentials of the parent account 92 | const chefAccountId = chefAccountPrefix + DEFAULT_PARENT_ACCOUNT_ID; 93 | console.log("👨‍🍳 Creating chef account", chefAccountId); 94 | try { 95 | await parentAccount.createAccount( 96 | chefAccountId, 97 | parentAccountCredentials.public_key, 98 | new BN(DEFAULT_NEW_ACCOUNT_AMOUNT as string) 99 | ); 100 | console.log("✅ Created chef account", chefAccountId); 101 | keyStore.setKey( 102 | networkId, 103 | chefAccountId, 104 | KeyPair.fromString(parentAccountCredentials.private_key) 105 | ); 106 | const chefAccount = new Account(near.connection, chefAccountId); 107 | return chefAccount; 108 | } catch (e) { 109 | throw e; 110 | } 111 | }; 112 | 113 | export const getProjectAccounts = async () => { 114 | // TODO: change this so that it creates three "project" subaccounts under contract account. this way we can determine if they have already been created, and skip if so. 115 | // create 3 x project accounts 116 | const now = Date.now(); 117 | const projectAccountPrefixes = [ 118 | `project-${now}-1.`, 119 | `project-${now}-2.`, 120 | `project-${now}-3.`, 121 | ]; 122 | const projectAccounts = []; 123 | const parentAccount = new Account(near.connection, DEFAULT_PARENT_ACCOUNT_ID); 124 | for (const projectAccountPrefix of projectAccountPrefixes) { 125 | // create account which shares the credentials of the parent account 126 | const projectAccountId = projectAccountPrefix + DEFAULT_PARENT_ACCOUNT_ID; 127 | console.log("🧙‍♀️ Creating project account", projectAccountId); 128 | try { 129 | await parentAccount.createAccount( 130 | projectAccountId, 131 | parentAccountCredentials.public_key, 132 | new BN(DEFAULT_NEW_ACCOUNT_AMOUNT as string) 133 | ); 134 | console.log("✅ Created project account", projectAccountId); 135 | keyStore.setKey( 136 | networkId, 137 | projectAccountId, 138 | KeyPair.fromString(parentAccountCredentials.private_key) 139 | ); 140 | const projectAccount = new Account(near.connection, projectAccountId); 141 | projectAccounts.push(projectAccount); 142 | } catch (e) { 143 | throw e; 144 | } 145 | } 146 | return projectAccounts; 147 | }; 148 | -------------------------------------------------------------------------------- /contracts/test/pot_factory/config.ts: -------------------------------------------------------------------------------- 1 | import { utils } from "near-api-js"; 2 | import { DEFAULT_PARENT_ACCOUNT_ID } from "../utils/constants"; 3 | 4 | export const contractId = "1702409170.test-contracts.potlock.testnet"; 5 | export const parentAccountId = DEFAULT_PARENT_ACCOUNT_ID; 6 | export const networkId = "testnet"; 7 | export const nodeUrl = `https://rpc.${networkId}.near.org`; 8 | 9 | export const POT_FACTORY_ALWAYS_ADMIN_ID = contractId; 10 | export const DEFAULT_WHITELISTED_DEPLOYER_ID = DEFAULT_PARENT_ACCOUNT_ID; // easy to set it to this value as we have the key for it // TODO: consider changing this for clarity 11 | 12 | export const ASSERT_ADMIN_ERROR_STR = "Only admin can call this method"; 13 | export const ASSERT_ADMIN_OR_WHITELISTED_DEPLOYER_ERROR_STR = 14 | "Only admin or whitelisted deployers can call this method"; 15 | -------------------------------------------------------------------------------- /contracts/test/pot_factory/setup.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair, Near, Account, Contract, keyStores } from "near-api-js"; 2 | import { contractId, parentAccountId, networkId, nodeUrl } from "./config"; 3 | import { loadCredentials } from "../utils/helpers"; 4 | 5 | // set up contract account credentials 6 | const contractCredentials = loadCredentials(networkId, contractId); 7 | const keyStore = new keyStores.InMemoryKeyStore(); 8 | keyStore.setKey( 9 | networkId, 10 | contractId, 11 | KeyPair.fromString(contractCredentials.private_key) 12 | ); 13 | // set parent account key (same as contract account key) 14 | keyStore.setKey( 15 | networkId, 16 | parentAccountId, 17 | KeyPair.fromString(contractCredentials.private_key) 18 | ); 19 | 20 | // set up near connection 21 | export const near = new Near({ 22 | networkId, 23 | deps: { keyStore }, 24 | nodeUrl, 25 | }); 26 | 27 | export const contractAccount = new Account(near.connection, contractId); 28 | -------------------------------------------------------------------------------- /contracts/test/pot_factory/utils.ts: -------------------------------------------------------------------------------- 1 | import { _contractCall, _contractView } from "../utils/helpers"; 2 | import { contractId as _contractId } from "./config"; 3 | import { Account, utils } from "near-api-js"; 4 | import { contractAccount } from "./setup"; 5 | import { NO_DEPOSIT } from "../utils/constants"; 6 | 7 | /** Copy of slugify function used in contract */ 8 | export const slugify = (s: string) => { 9 | return s 10 | .toLowerCase() 11 | .split("") 12 | .filter((c) => /[a-z0-9\s]/.test(c)) 13 | .join("") 14 | .split(/\s+/) 15 | .join("-"); 16 | }; 17 | 18 | const READ_METHODS = { 19 | GET_POTS: "get_pots", 20 | GET_WHITELISTED_DEPLOYERS: "get_whitelisted_deployers", 21 | GET_CONFIG: "get_config", 22 | GET_ADMIN: "get_admin", 23 | }; 24 | 25 | const WRITE_METHODS = { 26 | NEW: "new", 27 | DEPLOY_POT: "deploy_pot", 28 | ADMIN_ADD_WHITELISTED_DEPLOYERS: "admin_add_whitelisted_deployers", 29 | ADMIN_REMOVE_WHITELISTED_DEPLOYERS: "admin_remove_whitelisted_deployers", 30 | ADMIN_SET_PROTOCOL_FEE_BASIS_POINTS: "admin_set_protocol_fee_basis_points", 31 | ADMIN_SET_DEFAULT_CHEF_FEE_BASIS_POINTS: 32 | "admin_set_default_chef_fee_basis_points", 33 | // TODO: add remaining write methods here 34 | }; 35 | 36 | // Wrapper around contractView that defaults to the contract account 37 | const contractView = async ({ 38 | contractId, 39 | methodName, 40 | args, 41 | gas, 42 | attachedDeposit, 43 | }: { 44 | contractId?: string; 45 | methodName: string; 46 | args?: Record; 47 | gas?: string; 48 | attachedDeposit?: string; 49 | }) => { 50 | return _contractView({ 51 | contractId: contractId || _contractId, 52 | callerAccount: contractAccount, 53 | methodName, 54 | args, 55 | gas, 56 | attachedDeposit, 57 | }); 58 | }; 59 | 60 | // Wrapper around contractCall that defaults to the contract account 61 | const contractCall = async ({ 62 | callerAccount, 63 | contractId = _contractId, 64 | methodName, 65 | args, 66 | gas, 67 | attachedDeposit, 68 | }: { 69 | callerAccount: Account; 70 | contractId: string; 71 | methodName: string; 72 | args?: Record; 73 | gas?: string; 74 | attachedDeposit?: string; 75 | }) => { 76 | return _contractCall({ 77 | contractId, 78 | callerAccount, 79 | methodName, 80 | args, 81 | gas, 82 | attachedDeposit, 83 | }); 84 | }; 85 | 86 | // Helper function for the common case of contract calling itself 87 | export const callSelf = async ({ 88 | methodName, 89 | args, 90 | gas, 91 | attachedDeposit, 92 | }: { 93 | methodName: string; 94 | args?: Record; 95 | gas?: string; 96 | attachedDeposit?: string; 97 | }) => { 98 | return contractCall({ 99 | callerAccount: contractAccount, 100 | contractId: _contractId, 101 | methodName, 102 | args, 103 | gas, 104 | attachedDeposit, 105 | }); 106 | }; 107 | 108 | export const initializeContract = async ( 109 | initializeArgs?: Record 110 | ) => { 111 | return callSelf({ 112 | methodName: WRITE_METHODS.NEW, 113 | args: initializeArgs, 114 | attachedDeposit: NO_DEPOSIT, 115 | }); 116 | }; 117 | 118 | // DEPLOYING POTS 119 | 120 | export const deployPot = async (callerAccount: Account, potArgs: PotArgs) => { 121 | return contractCall({ 122 | callerAccount, 123 | contractId: _contractId, 124 | methodName: WRITE_METHODS.DEPLOY_POT, 125 | args: { 126 | pot_args: potArgs, 127 | }, 128 | attachedDeposit: utils.format.parseNearAmount("1") as string, 129 | }); 130 | }; 131 | 132 | export const getPots = async (): Promise => { 133 | return contractView({ 134 | methodName: READ_METHODS.GET_POTS, 135 | }); 136 | }; 137 | 138 | // WHITELISTED DEPLOYERS 139 | 140 | export const adminAddWhitelistedDeployers = async ( 141 | adminAccount: Account, 142 | whitelistedDeployers: AccountId[] 143 | ) => { 144 | return contractCall({ 145 | callerAccount: adminAccount, 146 | contractId: _contractId, 147 | methodName: WRITE_METHODS.ADMIN_ADD_WHITELISTED_DEPLOYERS, 148 | args: { 149 | account_ids: whitelistedDeployers, 150 | }, 151 | }); 152 | }; 153 | 154 | export const adminRemoveWhitelistedDeployers = async ( 155 | adminAccount: Account, 156 | whitelistedDeployers: AccountId[] 157 | ) => { 158 | return contractCall({ 159 | callerAccount: adminAccount, 160 | contractId: _contractId, 161 | methodName: WRITE_METHODS.ADMIN_REMOVE_WHITELISTED_DEPLOYERS, 162 | args: { 163 | account_ids: whitelistedDeployers, 164 | }, 165 | }); 166 | }; 167 | 168 | export const getWhitelistedDeployers = async (): Promise => { 169 | return contractView({ 170 | methodName: READ_METHODS.GET_WHITELISTED_DEPLOYERS, 171 | }); 172 | }; 173 | 174 | // CONFIG 175 | 176 | export const getConfig = async (): Promise => { 177 | return contractView({ 178 | methodName: READ_METHODS.GET_CONFIG, 179 | }); 180 | }; 181 | 182 | export const getAdmin = async (): Promise => { 183 | return contractView({ 184 | methodName: READ_METHODS.GET_ADMIN, 185 | }); 186 | }; 187 | 188 | export const adminSetProtocolFeeBasisPoints = async ( 189 | adminAccount: Account, 190 | protocolFeeBasisPoints: number 191 | ) => { 192 | return contractCall({ 193 | callerAccount: adminAccount, 194 | contractId: _contractId, 195 | methodName: WRITE_METHODS.ADMIN_SET_PROTOCOL_FEE_BASIS_POINTS, 196 | args: { 197 | protocol_fee_basis_points: protocolFeeBasisPoints, 198 | }, 199 | }); 200 | }; 201 | 202 | export const adminSetDefaultChefFeeBasisPoints = async ( 203 | adminAccount: Account, 204 | defaultChefFeeBasisPoints: number 205 | ) => { 206 | return contractCall({ 207 | callerAccount: adminAccount, 208 | contractId: _contractId, 209 | methodName: WRITE_METHODS.ADMIN_SET_DEFAULT_CHEF_FEE_BASIS_POINTS, 210 | args: { 211 | default_chef_fee_basis_points: defaultChefFeeBasisPoints, 212 | }, 213 | }); 214 | }; 215 | -------------------------------------------------------------------------------- /contracts/test/registry/config.ts: -------------------------------------------------------------------------------- 1 | export const contractId = "dev-1702563378907-95978444960953"; 2 | export const networkId = "testnet"; 3 | export const nodeUrl = `https://rpc.${networkId}.near.org`; 4 | -------------------------------------------------------------------------------- /contracts/test/registry/contract.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { Account } from "near-api-js"; 3 | import { contractId } from "./config"; 4 | import { near } from "./setup"; 5 | import { 6 | adminSetProjectStatus, 7 | getAdmins, 8 | getProjectById, 9 | getProjects, 10 | initializeContract, 11 | ownerAddAdmins, 12 | ownerRemoveAdmins, 13 | registerProject, 14 | } from "./utils"; 15 | 16 | /* 17 | TEST CASES: 18 | - Can be deployed and initialized 19 | - Owner can add & remove admins 20 | - End user or Admins can register a Project 21 | - Project ID should not already be registered 22 | - Project should be approved by default 23 | - Admins can change status of a Project 24 | */ 25 | 26 | describe("Registry Contract Tests", () => { 27 | let ownerAccount: Account; 28 | let ownerId: AccountId = contractId; 29 | let adminId: AccountId = contractId; 30 | let adminAccount: Account; 31 | let projectAccount: Account; 32 | let projectId: AccountId = contractId; 33 | 34 | before(async () => { 35 | ownerAccount = new Account(near.connection, ownerId); 36 | adminAccount = new Account(near.connection, adminId); 37 | projectAccount = new Account(near.connection, projectId); 38 | 39 | // attempt to initialize contract; if it fails, it's already initialized 40 | try { 41 | await initializeContract({ 42 | owner: ownerId, 43 | admins: [ownerId], 44 | }); 45 | console.log(`✅ Initialized Registry contract ${contractId}`); 46 | } catch (e) { 47 | if ( 48 | JSON.stringify(e).includes("The contract has already been initialized") 49 | ) { 50 | console.log(`Registry contract ${contractId} is already initialized`); 51 | } else { 52 | console.log("Registry initialize error: ", e); 53 | assert(false); 54 | } 55 | } 56 | }); 57 | 58 | it("Owner can add & remove admins", async () => { 59 | try { 60 | // Add admin 61 | const admin = "admin1.testnet"; 62 | await ownerAddAdmins(ownerAccount, [admin]); 63 | // Verify admin was added 64 | const admins = await getAdmins(); 65 | assert(admins.includes(admin)); 66 | // Remove admin 67 | await ownerRemoveAdmins(ownerAccount, [admin]); 68 | // Verify admin was removed 69 | const updatedAdmins = await getAdmins(); 70 | assert(!updatedAdmins.includes(admin)); 71 | } catch (e) { 72 | console.log("Error adding or removing admins:", e); 73 | assert(false); 74 | } 75 | }); 76 | 77 | it("End user or Admins can register a Project", async () => { 78 | // Project ID should not already be registered 79 | // Project should be approved by default 80 | try { 81 | // End user registers a project 82 | const projectName = `${projectId}#${Date.now()}`; 83 | await registerProject(projectAccount); 84 | 85 | // Verify project was registered by end user & approved by default 86 | let projects = await getProjects(); 87 | let existing = projects.find( 88 | (p) => p.id === projectId && p.status === "Approved" 89 | ); 90 | assert(!!existing); 91 | 92 | // Cannot reregister 93 | try { 94 | await registerProject(projectAccount); 95 | assert(false); 96 | } catch (e) { 97 | assert(JSON.stringify(e).includes("Project already exists")); 98 | } 99 | 100 | // Admin registers a project, specifying _project_id 101 | const projectId2: AccountId = "project2.testnet"; 102 | await registerProject(adminAccount, projectId2); 103 | 104 | // Verify project was registered by admin & approved by default 105 | projects = await getProjects(); 106 | existing = projects.find( 107 | (p) => p.id === projectId2 && p.status === "Approved" 108 | ); 109 | assert(!!existing); 110 | } catch (e) { 111 | console.log("Error registering project:", e); 112 | assert(false); 113 | } 114 | }); 115 | 116 | it("Admins can change status of Project", async () => { 117 | try { 118 | // Get projects 119 | let projects = await getProjects(); 120 | if (projects.length === 0) { 121 | // If no projects, create new project and refetch 122 | const projectName = `${projectId}#${Date.now()}`; 123 | await registerProject(projectAccount, projectName); 124 | projects = await getProjects(); 125 | } 126 | // Update status of first project 127 | const project = projects[0]; 128 | const newStatus = "Rejected"; 129 | const reviewNotes = 130 | "This project is rejected because it gives Few and Far vibes"; 131 | await adminSetProjectStatus( 132 | projectAccount, 133 | project.id, 134 | newStatus, 135 | reviewNotes 136 | ); 137 | let updatedProject = await getProjectById(project.id); 138 | assert( 139 | updatedProject.status === newStatus && 140 | updatedProject.review_notes == reviewNotes 141 | ); 142 | } catch (e) { 143 | console.log("Error setting project status:", e); 144 | assert(false); 145 | } 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /contracts/test/registry/setup.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair, Near, Account, Contract, keyStores } from "near-api-js"; 2 | import { contractId, networkId, nodeUrl } from "./config"; 3 | import { loadCredentials } from "../utils/helpers"; 4 | 5 | // set up contract account credentials 6 | const contractCredentials = loadCredentials(networkId, contractId); 7 | const contractKeyStore = new keyStores.InMemoryKeyStore(); 8 | contractKeyStore.setKey( 9 | networkId, 10 | contractId, 11 | KeyPair.fromString(contractCredentials.private_key) 12 | ); 13 | 14 | // set up near connection 15 | export const near = new Near({ 16 | networkId, 17 | deps: { keyStore: contractKeyStore }, 18 | nodeUrl, 19 | }); 20 | 21 | export const contractAccount = new Account(near.connection, contractId); 22 | -------------------------------------------------------------------------------- /contracts/test/registry/utils.ts: -------------------------------------------------------------------------------- 1 | import { _contractCall, _contractView } from "../utils/helpers"; 2 | import { contractId as _contractId } from "./config"; 3 | import { Account } from "near-api-js"; 4 | import { contractAccount } from "./setup"; 5 | import { NO_DEPOSIT } from "../utils/constants"; 6 | import { parseNearAmount } from "near-api-js/lib/utils/format"; 7 | 8 | const READ_METHODS = { 9 | GET_ADMINS: "get_admins", 10 | GET_PROJECTS: "get_projects", 11 | GET_PROJECT_BY_ID: "get_project_by_id", 12 | }; 13 | 14 | const WRITE_METHODS = { 15 | NEW: "new", 16 | OWNER_ADD_ADMINS: "owner_add_admins", 17 | OWNER_REMOVE_ADMINS: "owner_remove_admins", 18 | REGISTER: "register", 19 | ADMIN_SET_PROJECT_STATUS: "admin_set_project_status", 20 | }; 21 | 22 | // Wrapper around contractView that defaults to the contract account 23 | const contractView = async ({ 24 | contractId, 25 | methodName, 26 | args, 27 | gas, 28 | attachedDeposit, 29 | }: { 30 | contractId?: string; 31 | methodName: string; 32 | args?: Record; 33 | gas?: string; 34 | attachedDeposit?: string; 35 | }) => { 36 | return _contractView({ 37 | contractId: contractId || _contractId, 38 | callerAccount: contractAccount, 39 | methodName, 40 | args, 41 | gas, 42 | attachedDeposit, 43 | }); 44 | }; 45 | 46 | // Wrapper around contractCall that defaults to the contract account 47 | const contractCall = async ({ 48 | callerAccount, 49 | contractId = _contractId, 50 | methodName, 51 | args, 52 | gas, 53 | attachedDeposit, 54 | }: { 55 | callerAccount: Account; 56 | contractId: string; 57 | methodName: string; 58 | args?: Record; 59 | gas?: string; 60 | attachedDeposit?: string; 61 | }) => { 62 | return _contractCall({ 63 | contractId, 64 | callerAccount, 65 | methodName, 66 | args, 67 | gas, 68 | attachedDeposit, 69 | }); 70 | }; 71 | 72 | // Helper function for the common case of contract calling itself 73 | export const callSelf = async ({ 74 | methodName, 75 | args, 76 | gas, 77 | attachedDeposit, 78 | }: { 79 | methodName: string; 80 | args?: Record; 81 | gas?: string; 82 | attachedDeposit?: string; 83 | }) => { 84 | return contractCall({ 85 | callerAccount: contractAccount, 86 | contractId: _contractId, 87 | methodName, 88 | args, 89 | gas, 90 | attachedDeposit, 91 | }); 92 | }; 93 | 94 | export const initializeContract = async ( 95 | initializeArgs?: Record 96 | ) => { 97 | return callSelf({ 98 | methodName: WRITE_METHODS.NEW, 99 | args: initializeArgs, 100 | attachedDeposit: NO_DEPOSIT, 101 | }); 102 | }; 103 | 104 | // ADMINS 105 | 106 | export const ownerAddAdmins = async ( 107 | ownerAccount: Account, 108 | admins: AccountId[] 109 | ) => { 110 | return contractCall({ 111 | callerAccount: ownerAccount, 112 | contractId: _contractId, 113 | methodName: WRITE_METHODS.OWNER_ADD_ADMINS, 114 | args: { 115 | admins, 116 | }, 117 | }); 118 | }; 119 | 120 | export const ownerRemoveAdmins = async ( 121 | ownerAccount: Account, 122 | admins: AccountId[] 123 | ) => { 124 | return contractCall({ 125 | callerAccount: ownerAccount, 126 | contractId: _contractId, 127 | methodName: WRITE_METHODS.OWNER_REMOVE_ADMINS, 128 | args: { 129 | admins, 130 | }, 131 | }); 132 | }; 133 | 134 | export const getAdmins = async (): Promise => { 135 | return contractView({ 136 | methodName: READ_METHODS.GET_ADMINS, 137 | }); 138 | }; 139 | 140 | // PROJECTS 141 | 142 | export const registerProject = async ( 143 | callerAccount: Account, 144 | projectId?: string 145 | ) => { 146 | return contractCall({ 147 | callerAccount, 148 | contractId: _contractId, 149 | methodName: WRITE_METHODS.REGISTER, 150 | args: { 151 | ...(projectId ? { _project_id: projectId } : {}), 152 | }, 153 | attachedDeposit: parseNearAmount("0.1") as string, 154 | }); 155 | }; 156 | 157 | export const getProjects = async (): Promise => { 158 | return contractView({ 159 | methodName: READ_METHODS.GET_PROJECTS, 160 | }); 161 | }; 162 | 163 | export const getProjectById = async ( 164 | projectId: AccountId 165 | ): Promise => { 166 | return contractView({ 167 | methodName: READ_METHODS.GET_PROJECT_BY_ID, 168 | args: { 169 | project_id: projectId, 170 | }, 171 | }); 172 | }; 173 | 174 | export const adminSetProjectStatus = async ( 175 | adminAccount: Account, 176 | projectId: AccountId, 177 | status: string, 178 | reviewNotes: string 179 | ) => { 180 | return contractCall({ 181 | callerAccount: adminAccount, 182 | contractId: _contractId, 183 | methodName: WRITE_METHODS.ADMIN_SET_PROJECT_STATUS, 184 | args: { 185 | project_id: projectId, 186 | status, 187 | review_notes: reviewNotes, 188 | }, 189 | }); 190 | }; 191 | -------------------------------------------------------------------------------- /contracts/test/types.d.ts: -------------------------------------------------------------------------------- 1 | type TimestampMs = number; 2 | type AccountId = string; 3 | type ProjectId = AccountId; 4 | type ApplicationId = number; // increments from 1 5 | type PayoutId = number; // increments from 1 6 | type DonationId = number; // increments from 1 7 | type ProviderId = string; // contract ID + method name separated by ":" 8 | 9 | enum ProjectStatus { 10 | Submitted = "Submitted", 11 | InReview = "InReview", 12 | Approved = "Approved", 13 | Rejected = "Rejected", 14 | Graylisted = "Graylisted", 15 | Blacklisted = "Blacklisted", 16 | } 17 | 18 | enum ApplicationStatus { 19 | Pending = "Pending", 20 | InReview = "InReview", 21 | Approved = "Approved", 22 | Rejected = "Rejected", 23 | } 24 | 25 | interface Project { 26 | id: AccountId; 27 | status: ProjectStatus; 28 | submitted_ms: TimestampMs; 29 | updated_ms: TimestampMs; 30 | review_notes: string | null; 31 | } 32 | 33 | interface CustomSybilCheck { 34 | contract_id: AccountId; 35 | method_name: string; 36 | weight: number; 37 | } 38 | 39 | interface PotArgs { 40 | owner?: AccountId; 41 | admins?: AccountId[]; 42 | chef?: AccountId; 43 | pot_name: String; 44 | pot_description: String; 45 | max_projects: number; 46 | application_start_ms: TimestampMs; 47 | application_end_ms: TimestampMs; 48 | public_round_start_ms: TimestampMs; 49 | public_round_end_ms: TimestampMs; 50 | registry_provider?: ProviderId; 51 | sybil_wrapper_provider?: ProviderId; 52 | custom_sybil_checks?: CustomSybilCheck[]; 53 | custom_min_threshold_score?: number; 54 | referral_fee_matching_pool_basis_points: number; 55 | referral_fee_public_round_basis_points: number; 56 | chef_fee_basis_points: number; 57 | } 58 | 59 | interface PotConfig extends PotArgs { 60 | deployed_by: AccountId; 61 | matching_pool_balance: string; 62 | donations_balance: string; 63 | cooldown_end_ms: TimestampMs | null; 64 | all_paid_out: boolean; 65 | } 66 | 67 | interface Pot { 68 | pot_id: AccountId; 69 | deployed_by: AccountId; 70 | } 71 | 72 | interface SBTRequirement { 73 | registry_id: AccountId; 74 | issuer_id: AccountId; 75 | class_id: number; 76 | } 77 | 78 | interface PotDeployerConfig { 79 | protocol_fee_basis_points: number; 80 | max_protocol_fee_basis_points: number; 81 | default_chef_fee_basis_points: number; 82 | max_chef_fee_basis_points: number; 83 | max_round_time: number; 84 | max_application_time: number; 85 | } 86 | 87 | interface Application { 88 | project_id: ProjectId; 89 | status: ApplicationStatus; 90 | submitted_at: TimestampMs; 91 | updated_at: TimestampMs | null; 92 | review_notes: string | null; 93 | } 94 | 95 | /// Patron donation; no application specified 96 | interface PatronDonation { 97 | id: number; 98 | donor_id: AccountId; 99 | total_amount: string; 100 | message: string | null; 101 | donated_at: TimestampMs; 102 | referrer_id: AccountId | null; 103 | referrer_fee: string | null; 104 | protocol_fee: string; 105 | amount_after_fees: string; 106 | } 107 | 108 | /// End-user donation; must specify application 109 | interface Donation { 110 | id: number; 111 | donor_id: AccountId; 112 | total_amount: string; 113 | message: string | null; 114 | donated_at: TimestampMs; 115 | project_id: ProjectId; 116 | protocol_fee: string; 117 | amount_after_fees: string; 118 | } 119 | 120 | /// Project payout 121 | interface Payout { 122 | id: PayoutId; 123 | project_id: ProjectId; 124 | matching_pool_amount: string; 125 | donations_amount: string; 126 | amount_total: string; 127 | paid_at: TimestampMs | null; 128 | } 129 | 130 | interface PayoutInput { 131 | project_id: ProjectId; 132 | matching_pool_amount: string; 133 | donations_amount: string; 134 | } 135 | -------------------------------------------------------------------------------- /contracts/test/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { utils } from "near-api-js"; 2 | 3 | export const DEFAULT_GAS = "300000000000000"; 4 | export const DEFAULT_DEPOSIT = utils.format.parseNearAmount("0.1") as string; 5 | export const NO_DEPOSIT = "0"; 6 | 7 | // export const functionCallBase = { 8 | // gas: DEFAULT_GAS, 9 | // attachedDeposit: utils.format.parseNearAmount("0.1"), 10 | // }; 11 | 12 | export const NO_CONTRACT_HASH = "1".repeat(32); 13 | 14 | // POT CONFIG 15 | export const DEFAULT_MAX_ROUND_TIME = 1000 * 60 * 60 * 24 * 7 * 8; // 8 weeks 16 | export const DEFAULT_ROUND_LENGTH = 1000 * 60 * 60 * 24 * 7 * 7; // 7 weeks 17 | export const DEFAULT_MAX_APPLICATION_TIME = 1000 * 60 * 60 * 24 * 7 * 2; // 2 weeks 18 | export const DEFAULT_APPLICATION_LENGTH = 1000 * 60 * 60 * 24 * 7 * 1; // 1 week 19 | export const DEFAULT_PROTOCOL_FEE_BASIS_POINTS = 700; // 7% 20 | export const DEFAULT_MAX_PROTOCOL_FEE_BASIS_POINTS = 1000; // 10% 21 | export const DEFAULT_DEFAULT_CHEF_FEE_BASIS_POINTS = 100; // 1% 22 | export const DEFAULT_MAX_CHEF_FEE_BASIS_POINTS = 500; // 5% 23 | export const DEFAULT_REFERRAL_FEE_BASIS_POINTS = 100; // 1% // TODO: clean this up 24 | export const DEFAULT_CHEF_FEE_BASIS_POINTS = 100; // 1% 25 | export const DEFAULT_MAX_PROJECTS = 10; 26 | export const DEFAULT_BASE_CURRENCY = "near"; 27 | export const DEFAULT_REGISTRY_ID = "registry-unstable.i-am-human.testnet"; 28 | export const DEFAULT_ISSUER_ID = "i-am-human-staging.testnet"; 29 | export const DEFAULT_CLASS_ID = 1; 30 | 31 | export const DEFAULT_PARENT_ACCOUNT_ID = "test-contracts.potlock.testnet"; // accounts created during testing, with the exception of near dev-deploy, will be subaccounts of this account. 32 | export const DEFAULT_NEW_ACCOUNT_AMOUNT = utils.format.parseNearAmount("10"); // 10 NEAR 33 | -------------------------------------------------------------------------------- /contracts/test/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import BN from "bn.js"; 3 | import assert from "assert"; 4 | 5 | import { Account, utils } from "near-api-js"; 6 | import { DEFAULT_DEPOSIT, DEFAULT_GAS } from "./constants"; 7 | 8 | export const loadCredentials = (networkId: string, contractId: string) => { 9 | let path = `${process.env.HOME}/.near-credentials/${networkId}/${contractId}.json`; 10 | if (!fs.existsSync(path)) { 11 | path = `./registry/neardev/${networkId}/${contractId}.json`; 12 | if (!fs.existsSync(path)) { 13 | console.warn("Credentials not found"); 14 | return null; 15 | } 16 | } 17 | return JSON.parse(fs.readFileSync(path, "utf-8")); 18 | }; 19 | 20 | export const _contractCall = async ({ 21 | contractId, 22 | callerAccount, 23 | methodName, 24 | args, 25 | gas, 26 | attachedDeposit, 27 | }: { 28 | contractId: AccountId; 29 | callerAccount: Account; 30 | methodName: string; 31 | args?: Record; 32 | gas?: string; 33 | attachedDeposit?: string; 34 | }) => { 35 | return await callerAccount.functionCall({ 36 | contractId, 37 | methodName, 38 | args, 39 | gas: new BN(gas || DEFAULT_GAS), 40 | attachedDeposit: 41 | !attachedDeposit || attachedDeposit === "0" 42 | ? undefined 43 | : new BN(attachedDeposit), 44 | }); 45 | }; 46 | 47 | export const _contractView = async ({ 48 | contractId, 49 | callerAccount, 50 | methodName, 51 | args, 52 | gas, 53 | attachedDeposit, 54 | }: { 55 | contractId: AccountId; 56 | callerAccount: Account; 57 | methodName: string; 58 | args?: Record; 59 | gas?: string; 60 | attachedDeposit?: string; 61 | }) => { 62 | return await callerAccount.viewFunction({ 63 | contractId, 64 | methodName, 65 | args, 66 | gas: new BN(gas || DEFAULT_GAS), 67 | attachedDeposit: 68 | !attachedDeposit || attachedDeposit === "0" 69 | ? undefined 70 | : new BN(attachedDeposit), 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /contracts/test/utils/patch-config.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { fileURLToPath } from "url"; 3 | 4 | const __filename = fileURLToPath(import.meta.url); 5 | const projectName = process.argv[process.argv.indexOf(__filename) + 1]; // get project name from command, e.g. node ../patch-config.js projectName 6 | if (!projectName) { 7 | console.error("Please provide a project name."); 8 | process.exit(1); 9 | } 10 | 11 | const contractId = fs 12 | .readFileSync(`./${projectName}/neardev/dev-account`) 13 | .toString() 14 | .trim(); // remove the newline 15 | 16 | const path = `./test/${projectName}/config.ts`; 17 | 18 | fs.readFile(path, "utf-8", function (err, data) { 19 | if (err) throw err; 20 | 21 | data = data.replace( 22 | /^export const contractId = .*;$/m, 23 | `export const contractId = "${contractId}";` 24 | ); 25 | 26 | fs.writeFile(path, data, "utf-8", function (err) { 27 | if (err) throw err; 28 | console.log("✅ Patched config for", projectName, "contract"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /contracts/test/utils/quadratics.ts: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/gitcoinco/quadratic-funding/blob/master/quadratic-funding/clr.py 2 | const BN = require("big.js"); 3 | 4 | type YoctoBN = typeof BN; 5 | type UserId = AccountId; 6 | 7 | type ProjectContribution = [ProjectId, UserId, YoctoBN]; 8 | 9 | let CLR_PERCENTAGE_DISTRIBUTED = 0; 10 | const PROJECT_CONTRIBUTIONS_EXAMPLE: ProjectContribution[] = [ 11 | ["project-1.testnet", "user-1.testnet", new BN("10000000000000000000000000")], 12 | ["project-1.testnet", "user-2.testnet", new BN("5000000000000000000000000")], 13 | ["project-1.testnet", "user-2.testnet", new BN("10000000000000000000000000")], 14 | ["project-1.testnet", "user-3.testnet", new BN("7000000000000000000000000")], 15 | ["project-1.testnet", "user-5.testnet", new BN("5000000000000000000000000")], 16 | ["project-1.testnet", "user-4.testnet", new BN("10000000000000000000000000")], 17 | ["project-1.testnet", "user-5.testnet", new BN("5000000000000000000000000")], 18 | ["project-1.testnet", "user-5.testnet", new BN("5000000000000000000000000")], 19 | ["project-2.testnet", "user-1.testnet", new BN("10000000000000000000000000")], 20 | ["project-2.testnet", "user-1.testnet", new BN("5000000000000000000000000")], 21 | ["project-2.testnet", "user-2.testnet", new BN("20000000000000000000000000")], 22 | ["project-2.testnet", "user-3.testnet", new BN("3000000000000000000000000")], 23 | ["project-2.testnet", "user-8.testnet", new BN("2000000000000000000000000")], 24 | ["project-2.testnet", "user-9.testnet", new BN("10000000000000000000000000")], 25 | ["project-2.testnet", "user-7.testnet", new BN("7000000000000000000000000")], 26 | ["project-2.testnet", "user-2.testnet", new BN("5000000000000000000000000")], 27 | ]; 28 | 29 | export function convertDonationsToProjectContributions( 30 | donations: Donation[] 31 | ): ProjectContribution[] { 32 | const projectContributionsList: ProjectContribution[] = []; 33 | for (const d of donations) { 34 | const val: [ProjectId, UserId, YoctoBN] = [ 35 | d.project_id, 36 | d.donor_id, 37 | d.amount_after_fees, 38 | ]; 39 | projectContributionsList.push(val); 40 | } 41 | return projectContributionsList; 42 | } 43 | 44 | // This function takes the flattened list of contributions and aggregates 45 | // the amounts contributed by each user to each project. 46 | // It returns a dictionary where each key is a projectId and its value 47 | // is another dictionary of userIds and their aggregated contribution amounts. 48 | type ContribDict = { [key: ProjectId]: { [key: UserId]: YoctoBN } }; 49 | function aggregateContributions( 50 | grantContributions: ProjectContribution[] 51 | ): ContribDict { 52 | const contribDict: { [key: ProjectId]: { [key: UserId]: YoctoBN } } = {}; 53 | for (const [proj, user, amount] of grantContributions) { 54 | if (!contribDict[proj]) { 55 | contribDict[proj] = {}; 56 | } 57 | contribDict[proj][user] = new BN(contribDict[proj][user] || 0).add(amount); 58 | } 59 | return contribDict; 60 | } 61 | 62 | // This function calculates the total overlapping contribution amounts between pairs of users for each project. 63 | // It returns a nested dictionary where the outer keys are userIds and the inner keys are also userIds, 64 | // and the inner values are the total overlap between these two users' contributions. 65 | type PairTotals = { [key: UserId]: { [key: UserId]: YoctoBN } }; 66 | function getTotalsByPair(contribDict: ContribDict): PairTotals { 67 | // console.log("contribDict: ", contribDict); 68 | const totOverlap: { [key: UserId]: { [key: UserId]: YoctoBN } } = {}; 69 | for (const contribz of Object.values(contribDict)) { 70 | for (const [k1, v1] of Object.entries(contribz)) { 71 | if (!totOverlap[k1]) { 72 | totOverlap[k1] = {}; 73 | } 74 | for (const [k2, v2] of Object.entries(contribz)) { 75 | if (!totOverlap[k1][k2]) { 76 | totOverlap[k1][k2] = new BN(0); 77 | } 78 | totOverlap[k1][k2] = totOverlap[k1][k2].add(v1.mul(v2).sqrt()); 79 | } 80 | } 81 | } 82 | return totOverlap; 83 | } 84 | 85 | // This function computes the CLR (Contribution Matching) amount for each project. 86 | // It takes the aggregated contributions, the total overlaps between user pairs, 87 | // a threshold value, and the total pot available for matching. 88 | // It then calculates the matching amount for each project using the quadratic formula 89 | // and returns a list of objects containing the projectId, the number of contributions, 90 | // the total contributed amount, and the matching amount. 91 | type ClrTotal = { 92 | id: ProjectId; 93 | number_contributions: number; 94 | contribution_amount_str: string; 95 | matching_amount_str: string; 96 | }; 97 | function calculateClr( 98 | aggregatedContributions: ContribDict, 99 | pairTotals: PairTotals, 100 | threshold: typeof BN, 101 | totalPot: YoctoBN 102 | ): ClrTotal[] { 103 | // console.log("aggregated contributions: ", aggregatedContributions); 104 | // console.log("pair totals: ", pairTotals); 105 | let bigtot = new BN(0); 106 | const totals: ClrTotal[] = []; 107 | 108 | for (const [proj, contribz] of Object.entries(aggregatedContributions)) { 109 | let tot = new BN(0); 110 | let _num = 0; 111 | let _sum = new BN(0); 112 | 113 | for (const [k1, v1] of Object.entries(contribz)) { 114 | _num += 1; 115 | _sum = _sum.add(v1); 116 | for (const [k2, v2] of Object.entries(contribz)) { 117 | if (k2 > k1) { 118 | const sqrt = v1.mul(v2).sqrt(); 119 | tot = tot.add(sqrt.div(pairTotals[k1][k2].div(threshold))); 120 | } 121 | } 122 | } 123 | bigtot = bigtot.add(tot); 124 | totals.push({ 125 | id: proj, 126 | number_contributions: _num, 127 | contribution_amount_str: _sum.round(0, 0).toFixed(), 128 | matching_amount_str: tot.round(0, 0).toFixed(), 129 | }); 130 | } 131 | 132 | // if we reach saturation, we need to normalize 133 | if (bigtot.gte(totalPot)) { 134 | console.log("NORMALIZING"); 135 | console.log("bigtot: ", bigtot.toString()); 136 | console.log("totalPot: ", totalPot.toString()); 137 | // Assuming CLR_PERCENTAGE_DISTRIBUTED is a mutable global variable 138 | CLR_PERCENTAGE_DISTRIBUTED = 100; 139 | for (const t of totals) { 140 | t.matching_amount_str = new BN(t.matching_amount_str) 141 | .div(bigtot) 142 | .mul(totalPot) 143 | .round(0, 0) 144 | .toFixed(); 145 | } 146 | } 147 | 148 | return totals; 149 | } 150 | 151 | // This is the main function that ties everything together. It translates the data, aggregates contributions, 152 | // calculates pairwise overlaps, and then calculates the CLR matching amounts. 153 | // It returns the final list of matching amounts for each project. 154 | export function calculateQuadraticPayouts( 155 | projectContribsCurr: ProjectContribution[], 156 | threshold: typeof BN, 157 | totalPot: YoctoBN 158 | ): PayoutInput[] { 159 | const contributions = aggregateContributions(projectContribsCurr); 160 | const pairTotals = getTotalsByPair(contributions); 161 | const totals = calculateClr(contributions, pairTotals, threshold, totalPot); 162 | const payouts: PayoutInput[] = totals.map((t) => { 163 | return { 164 | project_id: t.id, 165 | matching_pool_amount: t.matching_amount_str, 166 | donations_amount: t.contribution_amount_str, 167 | }; 168 | }); 169 | return payouts; 170 | } 171 | 172 | // // Sample call 173 | // const res = calculateQuadraticPayouts( 174 | // PROJECT_CONTRIBUTIONS_EXAMPLE, 175 | // new BN("25000000000000000000000000"), // 25 176 | // new BN("5000000000000000000000000000") // 5000 NEAR 177 | // ); 178 | // console.log("res: ", res); 179 | -------------------------------------------------------------------------------- /contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir": "./dist", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "typeRoots": ["./node_modules/@types", "./test"], 13 | "types": ["node", "mocha", "types"], 14 | "lib": ["ES2020"], 15 | "sourceMap": true 16 | }, 17 | "include": [ 18 | "**/*.ts", "test/**/contract.test.ts" 19 | ], 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /contracts/yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /usr/local/bin/node /usr/local/bin/yarn init 3 | 4 | PATH: 5 | /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Library/Apple/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/Users/lachlanglen/.cargo/bin:/Users/lachlanglen/Library/Python/3.10/bin:/Users/lachlanglen/.local/bin:/Users/lachlanglen/go/bin:/Users/lachlanglen/.npm-packages/bin:/Users/lachlanglen/Library/Python/3.10/bin:/Users/lachlanglen/.local/bin:/bin:/Users/lachlanglen/go/bin:/Users/lachlanglen/.npm-packages/bin:/Users/lachlanglen/Library/Python/3.10/bin:/Users/lachlanglen/.local/bin 6 | 7 | Yarn version: 8 | 1.22.19 9 | 10 | Node version: 11 | 18.17.0 12 | 13 | Platform: 14 | darwin arm64 15 | 16 | Trace: 17 | Error: canceled 18 | at Interface. (/usr/local/lib/node_modules/yarn/lib/cli.js:137150:13) 19 | at Interface.emit (node:events:514:28) 20 | at [_ttyWrite] [as _ttyWrite] (node:internal/readline/interface:1131:18) 21 | at ReadStream.onkeypress (node:internal/readline/interface:270:20) 22 | at ReadStream.emit (node:events:514:28) 23 | at emitKeys (node:internal/readline/utils:357:14) 24 | at emitKeys.next () 25 | at ReadStream.onData (node:internal/readline/emitKeypressEvents:64:36) 26 | at ReadStream.emit (node:events:514:28) 27 | at addChunk (node:internal/streams/readable:324:12) 28 | 29 | npm manifest: 30 | { 31 | "name": "potlock-core", 32 | "version": "1.0.0", 33 | "description": "PotLock Core Contracts", 34 | "main": "index.js", 35 | "repository": "git@github.com:PotLock/core.git", 36 | "author": "Lachlan Glen <54282009+lachlanglen@users.noreply.github.com>", 37 | "license": "MIT", 38 | "scripts": { 39 | "build:pot": "cd pot && ./scripts/build.sh && cd ..", 40 | "build:potdeployer": "cd pot_deployer && ./scripts/build.sh && cd ..", 41 | "build:registry": "cd registry && ./scripts/build.sh && cd ..", 42 | "dev:deploy:pot": "cd pot && ./scripts/deploy.sh && cd ..", 43 | "dev:deploy:pot:refresh": "cd pot && rm -rf neardev && ./scripts/deploy.sh && cd ..", 44 | "dev:deploy:potdeployer": "cd pot_deployer && ./scripts/deploy.sh && cd ..", 45 | "dev:deploy:potdeployer:refresh": "cd pot_deployer && rm -rf neardev && ./scripts/deploy.sh && cd ..", 46 | "dev:deploy:registry": "cd registry && ./scripts/deploy.sh && cd ..", 47 | "dev:deploy:registry:refresh": "cd registry && rm -rf neardev && ./scripts/deploy.sh && cd .." 48 | } 49 | } 50 | 51 | yarn manifest: 52 | No manifest 53 | 54 | Lockfile: 55 | No lockfile 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "potlock-core", 3 | "version": "1.0.0", 4 | "description": "PotLock Core Contracts", 5 | "repository": "git@github.com:PotLock/core.git", 6 | "author": "Lachlan Glen <54282009+lachlanglen@users.noreply.github.com>", 7 | "license": "MIT", 8 | "scripts": {} 9 | } 10 | --------------------------------------------------------------------------------