diff --git a/.github/workflows/npmtest.yml b/.github/workflows/npmtest.yml index 79f35d549..04052e83e 100644 --- a/.github/workflows/npmtest.yml +++ b/.github/workflows/npmtest.yml @@ -20,7 +20,17 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - run: yarn install --non-interactive --frozen-lockfile + - name: Install packages + run: yarn install --non-interactive --frozen-lockfile env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} - - run: yarn test + - name: Run tests + run: yarn test:coverage + - name: Upload coverage report + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.json + flags: unittests + name: graphprotocol-contracts + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 85b7e1e54..1fcf808ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -# Ignore zeppelin contracts node_modules/ # Ignore build stuff @@ -6,9 +5,6 @@ cache/ build/ dist/ -# Coverage tests -coverage/ - # Hardhat cache cached/ @@ -20,5 +16,6 @@ bin/ .DS_Store .vscode -# Sliter +# Coverage and other reports /reports +coverage.json diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index a863e106c..5b6dc9ed3 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,12 +1,10 @@ ## Deploying the Solidity Smart Contracts - ### Running -A CLI in `cli/cli.ts` deploys the contracts to the specified network when used with the `migrate` command. - -This script accepts multiple commands that you can print using: +Deploy functionality exists in `cli/cli.ts`. You can deploy the contracts to the specified network +when used with the `migrate` command. This script accepts multiple commands that you can print using: -``` +```bash cli/cli.ts --help ``` diff --git a/LICENSE b/LICENSE index 0cc620d5c..947d07ab5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,342 @@ -Copyright 2020 The Graph Foundation - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + +We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + +Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains + a notice placed by the copyright holder saying it may be distributed + under the terms of this General Public License. The "Program", below, + refers to any such program or work, and a "work based on the Program" + means either the Program or any derivative work under copyright law: + that is to say, a work containing the Program or a portion of it, + either verbatim or with modifications and/or translated into another + language. (Hereinafter, translation is included without limitation in + the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's + source code as you receive it, in any medium, provided that you + conspicuously and appropriately publish on each copy an appropriate + copyright notice and disclaimer of warranty; keep intact all the + notices that refer to this License and to the absence of any warranty; + and give any other recipients of the Program a copy of this License + along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion + of it, thus forming a work based on the Program, and copy and + distribute such modifications or work under the terms of Section 1 + above, provided that you also meet all of these conditions: + + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt + otherwise to copy, modify, sublicense or distribute the Program is + void, and will automatically terminate your rights under this License. + However, parties who have received copies, or rights, from you under + this License will not have their licenses terminated so long as such + parties remain in full compliance. + +5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and + all its terms and conditions for copying, distributing or modifying + the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further + restrictions on the recipients' exercise of the rights granted herein. + You are not responsible for enforcing compliance by third parties to + this License. + +7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot + distribute so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you + may not distribute the Program at all. For example, if a patent + license would not permit royalty-free redistribution of the Program by + all those who receive copies directly or indirectly through you, then + the only way you could satisfy both it and this License would be to + refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License + may add an explicit geographical distribution limitation excluding + those countries, so that distribution is permitted only in or among + countries not thus excluded. In such case, this License incorporates + the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions + of the General Public License from time to time. Such new versions will + be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + +10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the author + to ask for permission. For software which is copyrighted by the Free + Software Foundation, write to the Free Software Foundation; we sometimes + make exceptions for this. Our decision will be guided by the two goals + of preserving the free status of all derivatives of our free software and + of promoting the sharing and reuse of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED + OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS + TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE + PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, + REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, + INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING + OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED + TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY + YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program +`Gnomovision' (which makes passes at compilers) written by James Hacker. + +, 1 April 1989 +Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md index ac8a68f5e..0e2d0cf18 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,134 @@ -# Graph Protocol Solidity Smart Contracts +![License: GPL](https://img.shields.io/badge/license-GPL-blue) +![Version Badge](https://img.shields.io/badge/version-1.6.0-lightgrey.svg) +![CI Status](https://github.com/graphprotocol/contracts/actions/workflows/npmtest.yml/badge.svg) +[![codecov](https://codecov.io/gh/graphprotocol/contracts/branch/dev/graph/badge.svg?token=S8JWGR9SBN)](https://codecov.io/gh/graphprotocol/contracts) -![Version Badge](https://img.shields.io/badge/version-1.0.0-lightgrey.svg) +# Graph Protocol Contracts -## Contracts +[The Graph](https://thegraph.com/) is an indexing protocol for querying networks like Ethereum, IPFS, Polygon, and other blockchains. Anyone can build and Publish open APIs, called subgraphs, making data easily accessible. -This repository contains The Graph Protocol solidity contracts. It is based on the -[PRD outlined here](https://www.notion.so/thegraph/Public-Network-Contracts-PRD-5eb8466aa4b44a1da7f16a28acd6674f), -There are many other smaller, more detailed PRDs that these contracts implement, that can also be -found on notion. +The Graph Protocol Smart Contracts are a set of Solidity contracts that exist on the Ethereum Blockchain. The contracts enable an open and permissionless decentralized network that coordinates [Graph Nodes](https://github.com/graphprotocol/graph-node) to Index any subgraph that is added to the network. Graph Nodes then provide queries to users for those Subgraphs. Users pay for queries with the Graph Token (GRT). -The contracts enable a staking protocol build on top of Ethereum, using The Graph Network Token -(GRT). The network enables a decentralized network of Graph Nodes -to index and serve queries for subgraphs. -[Graph node's repository can be found here](https://github.com/graphprotocol/graph-node). +The protocol allows Indexers to Stake, Delegators to Delegate, and Curators to Signal on Subgraphs. The Signal informs Indexers which Subgraphs they should index. -The Graph Network enables smart contract development to happen alongside subgraph development. -It is a new and improved way to develop dapps. It allows developers to move some logic into the -subgraph for resolving data based on events, or past storage data on Ethereum. Therefore, -the contracts and the subgraph rely on each other, to show to end users the current data and state -of The Graph Network. +You can learn more by heading to [the documentation](https://thegraph.com/docs/about/introduction), or checking out some of the [blog posts on the protocol](https://thegraph.com/blog/the-graph-network-in-depth-part-1). -### Contracts Testing +# Contracts + +The contracts are upgradable, following the [Open Zeppelin Proxy Upgrade Pattern](https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies). Each contract will be explained in brief detail below. + +**_Curation_** + +> Allows Curators to Signal GRT towards a Subgraph Deployment they want indexed on The Graph. Curators are often Subgraph Developers, but anyone can participate. Curators also receive a portion of the query fees that are earned on the Subgraph. Signaled GRT goes into a bonding curve, which returns a Graph Curation Share (GCS) to the Curator. + +**_Graph Name Service (GNS)_** + +> Wraps around the Curation contract to provide pooling of Curator Signaled tokens towards a single Subgraph. This allows an owner to deploy a Subgraph, and upgrade their Subgraph to a new version. The upgrade will move all Curator tokens to a new Subgraph Deployment with a new bonding curve. + +**_Service Registry_** + +> Allows Indexers to tell the network the location of their node. This allows end users to choose a node close to themselves, lowering the latency for queries. + +**_Dispute Manager_** + +> Provides a way for Indexers to be slashed or incorrect or malicious behaviour. There are two types of disputes: _Query Disputes_ and _Indexing Disputes_. + +**_Epoch Manager_** + +> Keeps track of protocol Epochs. Epochs are configured to be a certain block length, which is configurable by The Governor. + +**_Controller_** + +> The Controller is a contract that has a registry of all protocol contract addresses. It also is the owner of all the contracts. The owner of the Controller is The Governor, which makes The Governor the address that can configure the whole protocol. The Governor is [The Graph Council](https://thegraph.com/blog/introducing-the-graph-council). + +**_Rewards Manager_** + +> Tracks how inflationary GRT rewards should be handed out. It relies on the Curation contract and the Staking contract. Signaled GRT in Curation determine what percentage of inflationary tokens go towards each subgraph. Each Subgraph can have multiple Indexers Staked on it. Thus, the total rewards for the Subgraph are split up for each Indexer based on much they have Staked on that Subgraph. + +**_Staking_** + +> The Staking contract allows Indexers to Stake on Subgraphs. Indexers Stake by creating Allocations on a Subgraph. It also allows Delegators to Delegate towards an Indexer. The contract also contains the slashing functionality. + +**_Graph Token_** + +> An ERC-20 token (GRT) that is used as a work token to power the network incentives. The token is inflationary. + +# NPM package + +The [NPM package](https://www.npmjs.com/package/@graphprotocol/contracts) contains contract interfaces and addresses for the testnet and mainnet. It also contains [typechain](https://github.com/ethereum-ts/TypeChain) generated objects to easily interact with the contracts. This allows for anyone to install the package in their repository and interact with the protocol. It is updated and released whenever a change to the contracts occurs. + +``` +yarn add @graphprotocol/contracts +``` + +# Contract Addresses + +The testnet runs on Rinkeby, while mainnet is on Ethereum Mainnet. The addresses for both of these can be found in `./addresses.json`. + +# Local Setup + +To setup the contracts locally, checkout the `dev` branch, then run: + +```bash +yarn +yarn build +``` + +# Testing Testing is done with the following stack: -- Waffle -- Hardhat -- Typescript -- Ethers +- [Waffle](https://getwaffle.io/) +- [Hardhat](https://hardhat.org/) +- [Typescript](https://www.typescriptlang.org/) +- [Ethers](https://docs.ethers.io/v5/) To test all files, use `yarn test`. To test a single file run: -`npx hardhat test test/.ts`. -### Contract addresses +```bash +npx hardhat test test/.ts +``` + +# Interacting with the contracts + +There are three ways to interact with the contracts through this repo: + +**Hardhat** -Currently we are running our testnet on Rinkeby. Contract addresses can be found in this repository at -`./addresses.json`. However, addresses should be obtained from the NPM Package. +The most straightforward way to interact with the contracts is through the hardhat console. We have extended the hardhat runtime environment to include all of the contracts. This makes it easy to run the console with autocomplete for all contracts and all functions. It is a quick and easy way to read and write to the contracts. -### Deploying Contracts +``` +# A console to interact with testnet contracts +npx hardhat console --network rinkeby +``` -In order to run deployments, see `./DEPLOYMENT.md`. We use a custom deployment script, which -allowed us to completely remove `truffle` as a dependency. +**Hardhat Tasks** -## Subgraph +There are hardhat tasks under the `/tasks` folder. Most tasks are for complex queries to get back data from the protocol. -The subgraph repository can be [found here](https://github.com/graphprotocol/graph-network-subgraph). +**cli** -Great care must be taken to ensure all the code and data the subgraph refers to is in sync with -the current contracts on the correct network. For tracking all of this, we have an NPM package. +There is a cli that can be used to read or write to the contracts. It includes scripts to help with deployment. -The addresses -for the subgraph need to be the most up to date. This includes grabbing the latest ABIs and -typechain bindings, as well as pointing the addresses in the subgraph manifest to the latest -addresses. You can find the latest subgraph addresses in `addresses.json`, and they are also -in the NPM package. +# Deploying Contracts -Currently the contracts are being tested on Rinkeby. We test on ganache as well. We used to use -Kovan, but it is somewhat deprecated. +In order to run deployments, see [`./DEPLOYMENT.md`](./DEPLOYMENT.md). -## NPM package +# Contributing -The NPM package will be release in versions, and the version will be coordinated to be the same -version as the contracts and the subgraph. Anyone wanting to tie into the graph network contracts -or subgraph should install the npm package into their repository, and refer to the same version -number for the contracts and subgraph. +Contributions are welcomed and encouraged! You can do so by: -**New development work on the contracts and subgraph will be merged to master. Thus, when developing** -**on the network, you should not rely on the master code as it might break between the subgraph repo** -**and the contracts repo. Please use a version that is tagged.** +- Creating an issue +- Opening a PR -The NPM package will contain the following files/information: +If you are opening a PR, it is a good idea to first go to [The Graph Discord](https://discord.com/invite/vtvv7FP) or [The Graph Forum](https://forum.thegraph.com/) and discuss your idea! Discussions on the forum or Discord are another great way to contribute. -- The contracts -- The ABIs for those contracts -- The typechain autogenerated functions. These are typescript functions that are created based off - the ABIs, and are very useful for their type checking and the fact they are tied to a version -- The deployed addresses for each network, the date of deployment, and the commit hash. -- Metadata JSON objects for Graph Account and Subgraph metadata - **This is the only place you should grab contract addresses from.** +# Security Disclosure -We will also release versions for specific releases, such as `@graphprotocol/contracts@beta`. +If you have found a bug / security issue, please go through the official channel, [The Graph Security Bounties on Immunefi](https://immunefi.com/bounty/thegraph/). Responsible disclosure procedures must be followed to receive bounties. # Copyright -Copyright © 2020 The Graph Foundation +Copyright © 2021 The Graph Foundation -Licensed under [MIT license](LICENSE). +Licensed under [GPL license](LICENSE). diff --git a/addresses.json b/addresses.json index ab0490f99..f0a7ea985 100644 --- a/addresses.json +++ b/addresses.json @@ -134,10 +134,10 @@ "txHash": "0x7ef90b0477e5c5d05bbd203af7d2bf15224640204e12abb07331df11425d2d00", "proxy": true, "implementation": { - "address": "0x28037b93702335E55fe6319e1C144B8A4d05DAEB", + "address": "0x8F0031C8A936e3f78Db1E0670135CCad27E5b689", "creationCodeHash": "0x94893fd38fff869e917edd9efecfbec10407d1484572b7dce9455fa88898c310", "runtimeCodeHash": "0xd3367702add6bae5bbe1d8048cb34975ca8b53b560603b07694f1679a1418b24", - "txHash": "0x2b997e8470285c174732616c9977b13ad99959a7ea83c39d2647675853389f23" + "txHash": "0xc0072042ee9b46a6f5b3d3846c6117353d7060e2b4a497c620a361e866d46244" } }, "Staking": { diff --git a/audits/ConsenSysDiligence/2021-05-graph-initial-review.pdf b/audits/ConsenSysDiligence/2021-05-graph-initial-review.pdf new file mode 100644 index 000000000..b1a4c0615 Binary files /dev/null and b/audits/ConsenSysDiligence/2021-05-graph-initial-review.pdf differ diff --git a/audits/OpenZeppelin/2021-04-graph-addresses-cache-audit.pdf b/audits/OpenZeppelin/2021-04-graph-addresses-cache-audit.pdf new file mode 100644 index 000000000..e74cf68e9 Binary files /dev/null and b/audits/OpenZeppelin/2021-04-graph-addresses-cache-audit.pdf differ diff --git a/audits/OpenZeppelin/2021-04-graph-governance-upgrade-audit.pdf b/audits/OpenZeppelin/2021-04-graph-governance-upgrade-audit.pdf new file mode 100644 index 000000000..4e5145470 Binary files /dev/null and b/audits/OpenZeppelin/2021-04-graph-governance-upgrade-audit.pdf differ diff --git a/audits/OpenZeppelin/2021-04-graph-rewardsmanager-upgrade-audit.pdf b/audits/OpenZeppelin/2021-04-graph-rewardsmanager-upgrade-audit.pdf new file mode 100644 index 000000000..d8e3d71d2 Binary files /dev/null and b/audits/OpenZeppelin/2021-04-graph-rewardsmanager-upgrade-audit.pdf differ diff --git a/audits/OpenZeppelin/2021-04-graph-slashing-upgrade-audit.pdf b/audits/OpenZeppelin/2021-04-graph-slashing-upgrade-audit.pdf new file mode 100644 index 000000000..aa0e8b90e Binary files /dev/null and b/audits/OpenZeppelin/2021-04-graph-slashing-upgrade-audit.pdf differ diff --git a/audits/OpenZeppelin/2021-04-graph-staking-bugfix-2-audit.pdf b/audits/OpenZeppelin/2021-04-graph-staking-bugfix-2-audit.pdf new file mode 100644 index 000000000..eaeeaa768 Binary files /dev/null and b/audits/OpenZeppelin/2021-04-graph-staking-bugfix-2-audit.pdf differ diff --git a/audits/OpenZeppelin/2021-04-graph-staking-bugfix-audit-1.pdf b/audits/OpenZeppelin/2021-04-graph-staking-bugfix-audit-1.pdf new file mode 100644 index 000000000..39693f29a Binary files /dev/null and b/audits/OpenZeppelin/2021-04-graph-staking-bugfix-audit-1.pdf differ diff --git a/audits/OpenZeppelin/2021-08-graph-gns-audit.pdf b/audits/OpenZeppelin/2021-08-graph-gns-audit.pdf new file mode 100644 index 000000000..ff358f78e Binary files /dev/null and b/audits/OpenZeppelin/2021-08-graph-gns-audit.pdf differ diff --git a/contracts/base/IMulticall.sol b/contracts/base/IMulticall.sol new file mode 100644 index 000000000..87daa0453 --- /dev/null +++ b/contracts/base/IMulticall.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.3; +pragma experimental ABIEncoderV2; + +/** + * @title Multicall interface + * @notice Enables calling multiple methods in a single call to the contract + */ +interface IMulticall { + /** + * @notice Call multiple functions in the current contract and return the data from all of them if they all succeed + * @param data The encoded function data for each of the calls to make to this contract + * @return results The results from each of the calls passed in via data + */ + function multicall(bytes[] calldata data) external returns (bytes[] memory results); +} diff --git a/contracts/base/Multicall.sol b/contracts/base/Multicall.sol new file mode 100644 index 000000000..d3ff3055b --- /dev/null +++ b/contracts/base/Multicall.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.3; +pragma experimental ABIEncoderV2; + +import "./IMulticall.sol"; + +// Inspired by https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/base/Multicall.sol +// Note: Removed payable from the multicall + +/** + * @title Multicall + * @notice Enables calling multiple methods in a single call to the contract + */ +abstract contract Multicall is IMulticall { + /// @inheritdoc IMulticall + function multicall(bytes[] calldata data) external override returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + + if (!success) { + // Next 5 lines from https://ethereum.stackexchange.com/a/83577 + if (result.length < 68) revert(); + assembly { + result := add(result, 0x04) + } + revert(abi.decode(result, (string))); + } + + results[i] = result; + } + } +} diff --git a/contracts/curation/Curation.sol b/contracts/curation/Curation.sol index ebad1afb0..c07bc6b2f 100644 --- a/contracts/curation/Curation.sol +++ b/contracts/curation/Curation.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; @@ -23,12 +23,16 @@ import "./GraphCurationToken.sol"; * Holders can burn GCS using this contract to get GRT tokens back according to the * bonding curve. */ -contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { +contract Curation is CurationV2Storage, GraphUpgradeable, ICuration { using SafeMath for uint256; + using SafeMath for uint32; // 100% in parts per million uint32 private constant MAX_PPM = 1000000; + // Precision for effective reserve ratio + uint256 private constant PRECISION = 10**6; + // Amount of signal you get with your minimum token deposit uint256 private constant SIGNAL_PER_MINIMUM_DEPOSIT = 1e18; // 1 signal as 18 decimal number @@ -72,7 +76,10 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { address _bondingCurve, uint32 _defaultReserveRatio, uint32 _curationTaxPercentage, - uint256 _minimumCurationDeposit + uint256 _minimumCurationDeposit, + uint256 _initializationDays, + uint256 _initializationExitDays, + uint256 _blocksPerDay ) external onlyImpl { Managed._initialize(_controller); @@ -83,6 +90,9 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { _setDefaultReserveRatio(_defaultReserveRatio); _setCurationTaxPercentage(_curationTaxPercentage); _setMinimumCurationDeposit(_minimumCurationDeposit); + _setBlocksPerDay(_blocksPerDay); + _setInitializationPeriod(_initializationDays); + _setInitializationExitPeriod(_initializationExitDays); } /** @@ -136,6 +146,76 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { emit ParameterUpdated("minimumCurationDeposit"); } + /** + * @dev Set the initialization period for a curation pool. + * @notice Update the initialization period to `_initializationDays` + * @param _initializationDays In days + */ + function setInitializationPeriod(uint256 _initializationDays) external override onlyGovernor { + _setInitializationPeriod(_initializationDays); + } + + /** + * @dev Internal: Set the initialization period for a curation pool. + * @notice Update the initialization period to `_initializationDays` + * @param _initializationDays In days + */ + function _setInitializationPeriod(uint256 _initializationDays) private { + // Initialization must be greater than 0 + require(_initializationDays > 0, "Initialization period must be > 0"); + + initializationPeriod = blocksPerDay * _initializationDays; + emit ParameterUpdated("initializationPeriod"); + } + + /** + * @dev Set the initialization period for a curation pool. + * @notice Update the initialization period to `_initializationExitDays` + * @param _initializationExitDays In days + */ + function setInitializationExitPeriod(uint256 _initializationExitDays) + external + override + onlyGovernor + { + _setInitializationExitPeriod(_initializationExitDays); + } + + /** + * @dev Internal: Set the initialization period for a curation pool. + * @notice Update the initialization period to `_initializationExitDays` + * @param _initializationExitDays In days + */ + function _setInitializationExitPeriod(uint256 _initializationExitDays) private { + // Initialization must be greater than 0 + require(_initializationExitDays > 0, "Initialization period must be > 0"); + + initializationExitPeriod = blocksPerDay * _initializationExitDays; + emit ParameterUpdated("initializationExitPeriod"); + } + + /** + * @dev Set blocks per day + * @notice Update blocks per day to `_blocksPerDay` + * @param _blocksPerDay In days + */ + function setBlocksPerDay(uint256 _blocksPerDay) external override onlyGovernor { + _setBlocksPerDay(_blocksPerDay); + } + + /** + * @dev Internal: Set blocks per day + * @notice Update blocks per day to `_blocksPerDay` + * @param _blocksPerDay In days + */ + function _setBlocksPerDay(uint256 _blocksPerDay) private { + // Initialization must be greater than 0 + require(_blocksPerDay > 0, "Blocks per day must be > 0"); + + blocksPerDay = _blocksPerDay; + emit ParameterUpdated("blocksPerDay"); + } + /** * @dev Set the curation tax percentage to charge when a curator deposits GRT tokens. * @param _percentage Curation tax percentage charged when depositing GRT tokens @@ -158,6 +238,13 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { emit ParameterUpdated("curationTaxPercentage"); } + function setCreatedAt(bytes32 _subgraphDeploymentID, uint256 _createdAt) external override { + require(msg.sender == address(gns()), "Only GNS contract can call this function"); + + CurationPool storage curationPool = pools[_subgraphDeploymentID]; + curationPool.createdAt = _createdAt; + } + /** * @dev Assign Graph Tokens collected as curation fees to the curation pool reserve. * This function can only be called by the Staking contract and will do the bookeeping of @@ -208,9 +295,6 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { // If it hasn't been curated before then initialize the curve if (!isCurated(_subgraphDeploymentID)) { - // Initialize - curationPool.reserveRatio = defaultReserveRatio; - // If no signal token for the pool - create one if (address(curationPool.gcs) == address(0)) { // TODO: Use a minimal proxy to reduce gas cost @@ -302,7 +386,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { * @param _subgraphDeploymentID SubgraphDeployment to check if curated * @return True if curated */ - function isCurated(bytes32 _subgraphDeploymentID) public override view returns (bool) { + function isCurated(bytes32 _subgraphDeploymentID) public view override returns (bool) { return pools[_subgraphDeploymentID].tokens > 0; } @@ -314,8 +398,8 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { */ function getCuratorSignal(address _curator, bytes32 _subgraphDeploymentID) public - override view + override returns (uint256) { if (address(pools[_subgraphDeploymentID].gcs) == address(0)) { @@ -331,8 +415,8 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { */ function getCurationPoolSignal(bytes32 _subgraphDeploymentID) public - override view + override returns (uint256) { if (address(pools[_subgraphDeploymentID].gcs) == address(0)) { @@ -348,8 +432,8 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { */ function getCurationPoolTokens(bytes32 _subgraphDeploymentID) external - override view + override returns (uint256) { return pools[_subgraphDeploymentID].tokens; @@ -359,7 +443,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { * @dev Get curation tax percentage * @return Amount the curation tax percentage in PPM */ - function curationTaxPercentage() external override view returns (uint32) { + function curationTaxPercentage() external view override returns (uint32) { return _curationTaxPercentage; } @@ -372,11 +456,12 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { */ function tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokensIn) public - override view + override returns (uint256, uint256) { uint256 curationTax = _tokensIn.mul(uint256(_curationTaxPercentage)).div(MAX_PPM); + uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, _tokensIn.sub(curationTax)); return (signalOut, curationTax); } @@ -395,6 +480,8 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { // Get curation pool tokens and signal CurationPool memory curationPool = pools[_subgraphDeploymentID]; + uint32 _effectiveReserveRatio = _getEffectiveReserveRatio(curationPool.createdAt); + // Init curation pool if (curationPool.tokens == 0) { require( @@ -404,11 +491,11 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { return BancorFormula(bondingCurve) .calculatePurchaseReturn( - SIGNAL_PER_MINIMUM_DEPOSIT, - minimumCurationDeposit, - defaultReserveRatio, - _tokensIn.sub(minimumCurationDeposit) - ) + SIGNAL_PER_MINIMUM_DEPOSIT, + minimumCurationDeposit, + _effectiveReserveRatio, + _tokensIn.sub(minimumCurationDeposit) + ) .add(SIGNAL_PER_MINIMUM_DEPOSIT); } @@ -416,7 +503,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { BancorFormula(bondingCurve).calculatePurchaseReturn( getCurationPoolSignal(_subgraphDeploymentID), curationPool.tokens, - curationPool.reserveRatio, + _effectiveReserveRatio, _tokensIn ); } @@ -429,12 +516,16 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { */ function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn) public - override view + override returns (uint256) { CurationPool memory curationPool = pools[_subgraphDeploymentID]; + + uint32 _effectiveReserveRatio = _getEffectiveReserveRatio(curationPool.createdAt); + uint256 curationPoolSignal = getCurationPoolSignal(_subgraphDeploymentID); + require( curationPool.tokens > 0, "Subgraph deployment must be curated to perform calculations" @@ -448,7 +539,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { BancorFormula(bondingCurve).calculateSaleReturn( curationPoolSignal, curationPool.tokens, - curationPool.reserveRatio, + _effectiveReserveRatio, _signalIn ); } @@ -463,4 +554,28 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration { rewardsManager.onSubgraphSignalUpdate(_subgraphDeploymentID); } } + + /** + * @dev Calculate reserve ratio based on initialization phase + * @param _createdAt When NameCurationPool was created + * @return Reserve ratio + */ + function _getEffectiveReserveRatio(uint256 _createdAt) private view returns (uint32) { + uint32 effectiveReserveRatio = defaultReserveRatio; + + if (block.number <= (_createdAt.add(initializationPeriod))) { + effectiveReserveRatio = MAX_PPM; + } else if ( + block.number <= (_createdAt.add(initializationPeriod).add(initializationExitPeriod)) + ) { + uint256 blockDiff = block.number.sub(_createdAt.add(initializationPeriod)); + uint256 exitRatio = blockDiff.mul(PRECISION).div(initializationExitPeriod); + uint256 reserve = MAX_PPM.sub(defaultReserveRatio).mul(PRECISION); + uint256 reserveRatio = reserve.div(exitRatio); + + effectiveReserveRatio = uint32(MAX_PPM.mul(PRECISION).sub(reserveRatio).div(PRECISION)); + } + + return effectiveReserveRatio; + } } diff --git a/contracts/curation/CurationStorage.sol b/contracts/curation/CurationStorage.sol index 0fbc60bae..698fc9edd 100644 --- a/contracts/curation/CurationStorage.sol +++ b/contracts/curation/CurationStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; @@ -27,3 +27,14 @@ contract CurationV1Storage is Managed { // There is only one CurationPool per SubgraphDeploymentID mapping(bytes32 => ICuration.CurationPool) public pools; } + +contract CurationV2Storage is CurationV1Storage { + // Block count for initialization period of the bonding curve + uint256 public initializationPeriod; + + // Block count for initialization exit period of the bonding curve + uint256 public initializationExitPeriod; + + // Average blocks per day + uint256 public blocksPerDay; +} diff --git a/contracts/curation/GraphCurationToken.sol b/contracts/curation/GraphCurationToken.sol index dbe9fee32..12a286213 100644 --- a/contracts/curation/GraphCurationToken.sol +++ b/contracts/curation/GraphCurationToken.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/curation/ICuration.sol b/contracts/curation/ICuration.sol index 829288766..18a54a10c 100644 --- a/contracts/curation/ICuration.sol +++ b/contracts/curation/ICuration.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; @@ -11,6 +11,7 @@ interface ICuration { uint256 tokens; // GRT Tokens stored as reserves for the subgraph deployment uint32 reserveRatio; // Ratio for the bonding curve IGraphCurationToken gcs; // Curation token contract for this curation pool + uint256 createdAt; } // -- Configuration -- @@ -21,8 +22,16 @@ interface ICuration { function setCurationTaxPercentage(uint32 _percentage) external; + function setInitializationPeriod(uint256 _initializationDays) external; + + function setInitializationExitPeriod(uint256 _initializationExitDays) external; + + function setBlocksPerDay(uint256 _blocksPerDay) external; + // -- Curation -- + function setCreatedAt(bytes32 _subgraphDeploymentID, uint256 _createdAt) external; + function mint( bytes32 _subgraphDeploymentID, uint256 _tokensIn, diff --git a/contracts/curation/IGraphCurationToken.sol b/contracts/curation/IGraphCurationToken.sol index 9421120b6..aeb2a6e1c 100644 --- a/contracts/curation/IGraphCurationToken.sol +++ b/contracts/curation/IGraphCurationToken.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/discovery/GNS.sol b/contracts/discovery/GNS.sol index 4cbdc768c..743a683fd 100644 --- a/contracts/discovery/GNS.sol +++ b/contracts/discovery/GNS.sol @@ -1,12 +1,14 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; import "@openzeppelin/contracts/math/SafeMath.sol"; +import "../base/Multicall.sol"; import "../bancor/BancorFormula.sol"; import "../upgrades/GraphUpgradeable.sol"; +import "../utils/TokenUtils.sol"; import "./IGNS.sol"; import "./GNSStorage.sol"; @@ -17,10 +19,14 @@ import "./GNSStorage.sol"; * used in the scope of the Graph Network. It translates subgraph names into subgraph versions. * Each version is associated with a Subgraph Deployment. The contract has no knowledge of * human-readable names. All human readable names emitted in events. + * The contract implements a multicall behaviour to support batching multiple calls in a single + * transaction. */ -contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { +contract GNS is GNSV1Storage, GraphUpgradeable, IGNS, Multicall { using SafeMath for uint256; + // -- Constants -- + uint256 private constant MAX_UINT256 = 2**256 - 1; // 100% in parts per million @@ -136,16 +142,28 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { uint256 withdrawnGRT ); + // -- Modifiers -- + /** - @dev Modifier that allows a function to be called by owner of a graph account - @param _graphAccount Address of the graph account - */ - modifier onlyGraphAccountOwner(address _graphAccount) { + * @dev Check if the owner is the graph account + * @param _graphAccount Address of the graph account + */ + function _isGraphAccountOwner(address _graphAccount) private view { address graphAccountOwner = erc1056Registry.identityOwner(_graphAccount); require(graphAccountOwner == msg.sender, "GNS: Only graph account owner can call"); + } + + /** + * @dev Modifier that allows a function to be called by owner of a graph account + * @param _graphAccount Address of the graph account + */ + modifier onlyGraphAccountOwner(address _graphAccount) { + _isGraphAccountOwner(_graphAccount); _; } + // -- Functions -- + /** * @dev Initialize this contract. */ @@ -239,8 +257,11 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { graphAccountSubgraphNumbers[_graphAccount] = graphAccountSubgraphNumbers[_graphAccount].add( 1 ); + + curation().setCreatedAt(_subgraphDeploymentID, block.number); + updateSubgraphMetadata(_graphAccount, subgraphNumber, _subgraphMetadata); - _enableNameSignal(_graphAccount, subgraphNumber); + _enableNameSignal(_graphAccount, subgraphNumber, block.number); } /** @@ -268,6 +289,10 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { "GNS: Cannot publish a new version with the same subgraph deployment ID" ); + NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber]; + + curation().setCreatedAt(_subgraphDeploymentID, namePool.createdAt); + _publishVersion(_graphAccount, _subgraphNumber, _subgraphDeploymentID, _versionMetadata); _upgradeNameSignal(_graphAccount, _subgraphNumber, _subgraphDeploymentID); } @@ -327,10 +352,15 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { * @param _graphAccount Graph account enabling name signal * @param _subgraphNumber Subgraph number being used */ - function _enableNameSignal(address _graphAccount, uint256 _subgraphNumber) private { + function _enableNameSignal( + address _graphAccount, + uint256 _subgraphNumber, + uint256 _blockNumber + ) private { NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber]; namePool.subgraphDeploymentID = subgraphs[_graphAccount][_subgraphNumber]; namePool.reserveRatio = defaultReserveRatio; + namePool.createdAt = _blockNumber; emit NameSignalEnabled( _graphAccount, @@ -408,10 +438,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { ); // Pull tokens from sender - require( - graphToken().transferFrom(msg.sender, address(this), _tokensIn), - "GNS: Cannot transfer tokens to mint n signal" - ); + TokenUtils.pullTokens(graphToken(), msg.sender, _tokensIn); // Get name signal to mint for tokens deposited (uint256 vSignal, ) = curation().mint(namePool.subgraphDeploymentID, _tokensIn, 0); @@ -461,11 +488,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { namePool.nSignal = namePool.nSignal.sub(_nSignal); namePool.curatorNSignal[msg.sender] = namePool.curatorNSignal[msg.sender].sub(_nSignal); - // Return the tokens to the nameCurator - require( - graphToken().transfer(msg.sender, tokens), - "GNS: Error sending nameCurators tokens" - ); + // Return the tokens to the curator + TokenUtils.pushTokens(graphToken(), msg.sender, tokens); emit NSignalBurned(_graphAccount, _subgraphNumber, msg.sender, _nSignal, vSignal, tokens); } @@ -482,6 +506,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { // If no nSignal, then no need to burn vSignal if (namePool.nSignal != 0) { + // Note: No slippage, burn at any cost namePool.withdrawableGRT = curation().burn( namePool.subgraphDeploymentID, namePool.vSignal, @@ -522,10 +547,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { namePool.nSignal = namePool.nSignal.sub(curatorNSignal); namePool.withdrawableGRT = namePool.withdrawableGRT.sub(tokensOut); - require( - graphToken().transfer(msg.sender, tokensOut), - "GNS: Error withdrawing tokens for nameCurator" - ); + // Return tokens to the curator + TokenUtils.pushTokens(graphToken(), msg.sender, tokensOut); emit GRTWithdrawn(_graphAccount, _subgraphNumber, msg.sender, curatorNSignal, tokensOut); } @@ -567,10 +590,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { uint256 ownerTaxAdjustedUp = totalAdjustedUp.sub(_tokens); // Get the owner of the subgraph to reimburse the curation tax - require( - graphToken().transferFrom(_owner, address(this), ownerTaxAdjustedUp), - "GNS: Error reimbursing curation tax" - ); + TokenUtils.pullTokens(graphToken(), _owner, ownerTaxAdjustedUp); + return totalAdjustedUp; } @@ -587,8 +608,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { uint256 _tokensIn ) public - override view + override returns ( uint256, uint256, @@ -615,7 +636,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { address _graphAccount, uint256 _subgraphNumber, uint256 _nSignalIn - ) public override view returns (uint256, uint256) { + ) public view override returns (uint256, uint256) { NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber]; uint256 vSignal = nSignalToVSignal(_graphAccount, _subgraphNumber, _nSignalIn); uint256 tokensOut = curation().signalToTokens(namePool.subgraphDeploymentID, vSignal); @@ -633,7 +654,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { address _graphAccount, uint256 _subgraphNumber, uint256 _vSignalIn - ) public override view returns (uint256) { + ) public view override returns (uint256) { NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber]; // Handle initialization by using 1:1 version to name signal @@ -661,7 +682,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { address _graphAccount, uint256 _subgraphNumber, uint256 _nSignalIn - ) public override view returns (uint256) { + ) public view override returns (uint256) { NameCurationPool storage namePool = nameSignals[_graphAccount][_subgraphNumber]; return BancorFormula(bondingCurve).calculateSaleReturn( @@ -683,7 +704,7 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { address _graphAccount, uint256 _subgraphNumber, address _curator - ) public override view returns (uint256) { + ) public view override returns (uint256) { return nameSignals[_graphAccount][_subgraphNumber].curatorNSignal[_curator]; } @@ -695,8 +716,8 @@ contract GNS is GNSV1Storage, GraphUpgradeable, IGNS { */ function isPublished(address _graphAccount, uint256 _subgraphNumber) public - override view + override returns (bool) { return subgraphs[_graphAccount][_subgraphNumber] != 0; diff --git a/contracts/discovery/GNSStorage.sol b/contracts/discovery/GNSStorage.sol index 0ad8514b2..a451fba13 100644 --- a/contracts/discovery/GNSStorage.sol +++ b/contracts/discovery/GNSStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; @@ -6,7 +6,6 @@ pragma experimental ABIEncoderV2; import "../governance/Managed.sol"; import "./erc1056/IEthereumDIDRegistry.sol"; - import "./IGNS.sol"; contract GNSV1Storage is Managed { diff --git a/contracts/discovery/IGNS.sol b/contracts/discovery/IGNS.sol index 3eb764993..71baca3fd 100644 --- a/contracts/discovery/IGNS.sol +++ b/contracts/discovery/IGNS.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; @@ -13,6 +13,7 @@ interface IGNS { uint32 reserveRatio; bool disabled; uint256 withdrawableGRT; + uint256 createdAt; } // -- Configuration -- diff --git a/contracts/discovery/IServiceRegistry.sol b/contracts/discovery/IServiceRegistry.sol index e1a9868c9..83199cd87 100644 --- a/contracts/discovery/IServiceRegistry.sol +++ b/contracts/discovery/IServiceRegistry.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/discovery/ServiceRegistry.sol b/contracts/discovery/ServiceRegistry.sol index 7fe93045b..468149447 100644 --- a/contracts/discovery/ServiceRegistry.sol +++ b/contracts/discovery/ServiceRegistry.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/discovery/ServiceRegistryStorage.sol b/contracts/discovery/ServiceRegistryStorage.sol index f0ad6f078..d14388f93 100644 --- a/contracts/discovery/ServiceRegistryStorage.sol +++ b/contracts/discovery/ServiceRegistryStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/disputes/DisputeManager.sol b/contracts/disputes/DisputeManager.sol index b2d83bb4a..0b0cf51da 100644 --- a/contracts/disputes/DisputeManager.sol +++ b/contracts/disputes/DisputeManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/disputes/DisputeManagerStorage.sol b/contracts/disputes/DisputeManagerStorage.sol index f9f328c19..c2a3eaeb7 100644 --- a/contracts/disputes/DisputeManagerStorage.sol +++ b/contracts/disputes/DisputeManagerStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/disputes/IDisputeManager.sol b/contracts/disputes/IDisputeManager.sol index 99a8b8d0d..d0fa19676 100644 --- a/contracts/disputes/IDisputeManager.sol +++ b/contracts/disputes/IDisputeManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/epochs/EpochManager.sol b/contracts/epochs/EpochManager.sol index b80d5bf2a..67c76fb3a 100644 --- a/contracts/epochs/EpochManager.sol +++ b/contracts/epochs/EpochManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/epochs/EpochManagerStorage.sol b/contracts/epochs/EpochManagerStorage.sol index 66ada49e8..2796dd0aa 100644 --- a/contracts/epochs/EpochManagerStorage.sol +++ b/contracts/epochs/EpochManagerStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/epochs/IEpochManager.sol b/contracts/epochs/IEpochManager.sol index 564f656f3..7b4be4f05 100644 --- a/contracts/epochs/IEpochManager.sol +++ b/contracts/epochs/IEpochManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/governance/Controller.sol b/contracts/governance/Controller.sol index 0f095eb5f..ebac30211 100644 --- a/contracts/governance/Controller.sol +++ b/contracts/governance/Controller.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/governance/Governed.sol b/contracts/governance/Governed.sol index fbd04bf36..cafc34f9f 100644 --- a/contracts/governance/Governed.sol +++ b/contracts/governance/Governed.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/governance/GraphGovernance.sol b/contracts/governance/GraphGovernance.sol index 89bc3a4bd..c2a842496 100644 --- a/contracts/governance/GraphGovernance.sol +++ b/contracts/governance/GraphGovernance.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/governance/GraphGovernanceStorage.sol b/contracts/governance/GraphGovernanceStorage.sol index f05a4c070..a4bf96df1 100644 --- a/contracts/governance/GraphGovernanceStorage.sol +++ b/contracts/governance/GraphGovernanceStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/governance/IController.sol b/contracts/governance/IController.sol index 8f6895ff6..7df3d94ee 100644 --- a/contracts/governance/IController.sol +++ b/contracts/governance/IController.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.6.12 <0.8.0; diff --git a/contracts/governance/IGraphGovernance.sol b/contracts/governance/IGraphGovernance.sol index 0205f1e0d..0c810ab48 100644 --- a/contracts/governance/IGraphGovernance.sol +++ b/contracts/governance/IGraphGovernance.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/governance/IManaged.sol b/contracts/governance/IManaged.sol index 4b6cb7175..24dd566d6 100644 --- a/contracts/governance/IManaged.sol +++ b/contracts/governance/IManaged.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index 9cba7ced1..2a207e79f 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -1,10 +1,11 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; import "./IController.sol"; import "../curation/ICuration.sol"; +import "../discovery/IGNS.sol"; import "../epochs/IEpochManager.sol"; import "../rewards/IRewardsManager.sol"; import "../staking/IStaking.sol"; @@ -56,12 +57,12 @@ contract Managed { require(msg.sender == address(controller), "Caller must be Controller"); } - modifier notPartialPaused { + modifier notPartialPaused() { _notPartialPaused(); _; } - modifier notPaused { + modifier notPaused() { _notPaused(); _; } @@ -113,6 +114,14 @@ contract Managed { return ICuration(_resolveContract(keccak256("Curation"))); } + /** + * @dev Return GNS interface. + * @return GNS contract registered with Controller + */ + function gns() internal view returns (IGNS) { + return IGNS(_resolveContract(keccak256("GNS"))); + } + /** * @dev Return EpochManager interface. * @return Epoch manager contract registered with Controller @@ -178,6 +187,7 @@ contract Managed { */ function syncAllContracts() external { _syncContract("Curation"); + _syncContract("GNS"); _syncContract("EpochManager"); _syncContract("RewardsManager"); _syncContract("Staking"); diff --git a/contracts/governance/Pausable.sol b/contracts/governance/Pausable.sol index 6235d51e3..96751f789 100644 --- a/contracts/governance/Pausable.sol +++ b/contracts/governance/Pausable.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index 2a3651c65..880cf739d 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index d8a11c7e9..2d3e01ca1 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; @@ -10,6 +10,14 @@ import "../upgrades/GraphUpgradeable.sol"; import "./RewardsManagerStorage.sol"; import "./IRewardsManager.sol"; +/** + * @title Rewards Manager Contract + * @dev Tracks how inflationary GRT rewards should be handed out. Relies on the Curation contract + * and the Staking contract. Signaled GRT in Curation determine what percentage of the tokens go + * towards each subgraph. Then each Subgraph can have multiple Indexers Staked on it. Thus, the + * total rewards for the Subgraph are split up for each Indexer based on much they have Staked on + * that Subgraph. + */ contract RewardsManager is RewardsManagerV1Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 1a411ec9b..4990383eb 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/staking/IStaking.sol b/contracts/staking/IStaking.sol index 78c431b2c..4304db8c7 100644 --- a/contracts/staking/IStaking.sol +++ b/contracts/staking/IStaking.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.6.12 <0.8.0; pragma experimental ABIEncoderV2; diff --git a/contracts/staking/IStakingData.sol b/contracts/staking/IStakingData.sol index a0bd2ab46..348a5a7f9 100644 --- a/contracts/staking/IStakingData.sol +++ b/contracts/staking/IStakingData.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.6.12 <0.8.0; diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 873bc35a9..1672d707c 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; @@ -16,6 +16,9 @@ import "./libs/Stakes.sol"; /** * @title Staking contract + * @dev The Staking contract allows Indexers to Stake on Subgraphs. Indexers Stake by creating + * Allocations on a Subgraph. It also allows Delegators to Delegate towards an Indexer. The + * contract also has the slashing functionality. */ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking { using SafeMath for uint256; @@ -704,27 +707,34 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking { /** * @dev Unstake tokens from the indexer stake, lock them until thawing period expires. + * NOTE: The function accepts an amount greater than the currently staked tokens. + * If that happens, it will try to unstake the max amount of tokens it can. + * The reason for this behaviour is to avoid time conditions while the transaction + * is in flight. * @param _tokens Amount of tokens to unstake */ function unstake(uint256 _tokens) external override notPartialPaused { address indexer = msg.sender; Stakes.Indexer storage indexerStake = stakes[indexer]; - require(_tokens > 0, "!tokens"); require(indexerStake.tokensStaked > 0, "!stake"); - require(indexerStake.tokensAvailable() >= _tokens, "!stake-avail"); + + // Tokens to lock is capped to the available tokens + uint256 tokensToLock = MathUtils.min(indexerStake.tokensAvailable(), _tokens); + require(tokensToLock > 0, "!stake-avail"); // Ensure minimum stake - uint256 newStake = indexerStake.tokensSecureStake().sub(_tokens); + uint256 newStake = indexerStake.tokensSecureStake().sub(tokensToLock); require(newStake == 0 || newStake >= minimumIndexerStake, "!minimumIndexerStake"); - // Before locking more tokens, withdraw any unlocked ones + // Before locking more tokens, withdraw any unlocked ones if possible uint256 tokensToWithdraw = indexerStake.tokensWithdrawable(); if (tokensToWithdraw > 0) { _withdraw(indexer); } - indexerStake.lockTokens(_tokens, thawingPeriod); + // Update the indexer stake locking tokens + indexerStake.lockTokens(tokensToLock, thawingPeriod); emit StakeLocked(indexer, indexerStake.tokensLocked, indexerStake.tokensLockedUntil); } @@ -1310,12 +1320,13 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking { uint256 shares = (pool.tokens == 0) ? delegatedTokens : delegatedTokens.mul(pool.shares).div(pool.tokens); + require(shares > 0, "!shares"); // Update the delegation pool pool.tokens = pool.tokens.add(delegatedTokens); pool.shares = pool.shares.add(shares); - // Update the delegation + // Update the individual delegation delegation.shares = delegation.shares.add(shares); emit StakeDelegated(_indexer, _delegator, delegatedTokens, shares); diff --git a/contracts/staking/StakingStorage.sol b/contracts/staking/StakingStorage.sol index 691cab9a6..721576de4 100644 --- a/contracts/staking/StakingStorage.sol +++ b/contracts/staking/StakingStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/staking/libs/MathUtils.sol b/contracts/staking/libs/MathUtils.sol index d0953ba2a..3c00ef839 100644 --- a/contracts/staking/libs/MathUtils.sol +++ b/contracts/staking/libs/MathUtils.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/staking/libs/Rebates.sol b/contracts/staking/libs/Rebates.sol index 4c5341ffa..a6b0770fe 100644 --- a/contracts/staking/libs/Rebates.sol +++ b/contracts/staking/libs/Rebates.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/staking/libs/Stakes.sol b/contracts/staking/libs/Stakes.sol index 9cfc99337..b911aad54 100644 --- a/contracts/staking/libs/Stakes.sol +++ b/contracts/staking/libs/Stakes.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/statechannels/AllocationExchange.sol b/contracts/statechannels/AllocationExchange.sol index dfcbe80f3..bfbbd9509 100644 --- a/contracts/statechannels/AllocationExchange.sol +++ b/contracts/statechannels/AllocationExchange.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/statechannels/GRTWithdrawHelper.sol b/contracts/statechannels/GRTWithdrawHelper.sol index c34b66558..6dd5b87ce 100644 --- a/contracts/statechannels/GRTWithdrawHelper.sol +++ b/contracts/statechannels/GRTWithdrawHelper.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/tests/GovernedMock.sol b/contracts/tests/GovernedMock.sol index 8f127983b..6afe5382d 100644 --- a/contracts/tests/GovernedMock.sol +++ b/contracts/tests/GovernedMock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/tests/RebatePoolMock.sol b/contracts/tests/RebatePoolMock.sol index 9c5fd7c99..da4a01f39 100644 --- a/contracts/tests/RebatePoolMock.sol +++ b/contracts/tests/RebatePoolMock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; diff --git a/contracts/tests/testnet/GDAI.sol b/contracts/tests/testnet/GDAI.sol index b972324ba..95b193690 100644 --- a/contracts/tests/testnet/GDAI.sol +++ b/contracts/tests/testnet/GDAI.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/tests/testnet/GSRManager.sol b/contracts/tests/testnet/GSRManager.sol index 39b7d73d0..d714efbdc 100644 --- a/contracts/tests/testnet/GSRManager.sol +++ b/contracts/tests/testnet/GSRManager.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/token/GraphToken.sol b/contracts/token/GraphToken.sol index fa9736724..e41557cfc 100644 --- a/contracts/token/GraphToken.sol +++ b/contracts/token/GraphToken.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/token/IGraphToken.sol b/contracts/token/IGraphToken.sol index 336202455..e8ea806c6 100644 --- a/contracts/token/IGraphToken.sol +++ b/contracts/token/IGraphToken.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/upgrades/GraphProxy.sol b/contracts/upgrades/GraphProxy.sol index 2e576d6a7..85baacdff 100644 --- a/contracts/upgrades/GraphProxy.sol +++ b/contracts/upgrades/GraphProxy.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/upgrades/GraphProxyAdmin.sol b/contracts/upgrades/GraphProxyAdmin.sol index f198871da..7d870fad6 100644 --- a/contracts/upgrades/GraphProxyAdmin.sol +++ b/contracts/upgrades/GraphProxyAdmin.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/upgrades/GraphProxyStorage.sol b/contracts/upgrades/GraphProxyStorage.sol index a4ed5494d..168400a20 100644 --- a/contracts/upgrades/GraphProxyStorage.sol +++ b/contracts/upgrades/GraphProxyStorage.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/upgrades/GraphUpgradeable.sol b/contracts/upgrades/GraphUpgradeable.sol index 580065e2c..254e364c2 100644 --- a/contracts/upgrades/GraphUpgradeable.sol +++ b/contracts/upgrades/GraphUpgradeable.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/upgrades/IGraphProxy.sol b/contracts/upgrades/IGraphProxy.sol index 7f1dcca13..3b52c9c8c 100644 --- a/contracts/upgrades/IGraphProxy.sol +++ b/contracts/upgrades/IGraphProxy.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/contracts/utils/TokenUtils.sol b/contracts/utils/TokenUtils.sol index a645d9422..a60afac76 100644 --- a/contracts/utils/TokenUtils.sol +++ b/contracts/utils/TokenUtils.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.3; diff --git a/graph.config.yml b/graph.config.yml index 7eb2faccd..e76e9b2b2 100644 --- a/graph.config.yml +++ b/graph.config.yml @@ -48,6 +48,9 @@ contracts: reserveRatio: 500000 # 50% (parts per million) curationTaxPercentage: 25000 # 2.5% (parts per million) minimumCurationDeposit: "1000000000000000000" # 1 GRT + initializationDays: 5 # Days + initializationExitDays: 30 # Days + blocksPerDay: 6400 DisputeManager: proxy: true init: @@ -89,4 +92,4 @@ contracts: proxy: true init: controller: "${{Controller.address}}" - issuanceRate: "1000000012184945188" # 3% annual rate (per block increase of total supply, blocks in a year = 365*60*60*24/13) \ No newline at end of file + issuanceRate: "1000000012184945188" # 3% annual rate (per block increase of total supply, blocks in a year = 365*60*60*24/13) diff --git a/hardhat.config.ts b/hardhat.config.ts index 61f044588..b35c1159e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -15,19 +15,19 @@ import '@nomiclabs/hardhat-waffle' import 'hardhat-abi-exporter' import 'hardhat-gas-reporter' import 'hardhat-contract-sizer' +import 'hardhat-tracer' import '@tenderly/hardhat-tenderly' import '@openzeppelin/hardhat-upgrades' import '@typechain/hardhat' - -// TODO: Not supported for now in hardhat -// usePlugin('solidity-coverage') +import 'solidity-coverage' // Tasks const SKIP_LOAD = process.env.SKIP_LOAD === 'true' if (!SKIP_LOAD) { - ;['contracts', 'misc', 'query', 'deployment', 'actions'].forEach((folder) => { + require('./tasks/gre.ts') + ;['contracts', 'misc', 'deployment', 'actions'].forEach((folder) => { const tasksPath = path.join(__dirname, 'tasks', folder) fs.readdirSync(tasksPath) .filter((pth) => pth.includes('.ts')) diff --git a/package.json b/package.json index 16f538633..1b1a8d010 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@graphprotocol/contracts", - "version": "1.6.0", + "version": "1.8.0", "description": "Contracts for the Graph Protocol", "directories": { "test": "test" @@ -62,6 +62,7 @@ "hardhat-contract-sizer": "^2.0.3", "hardhat-gas-reporter": "^1.0.4", "hardhat-storage-layout": "0.1.6", + "hardhat-tracer": "^1.0.0-alpha.6", "husky": "^4.3.8", "inquirer": "^8.0.0", "ipfs-http-client": "47.0.1", @@ -104,7 +105,8 @@ "prettier:ts": "prettier --write 'test/**/*.ts'", "prettier:sol": "prettier --write 'contracts/*.sol'", "analyze": "scripts/analyze", - "flatten": "scripts/flatten", + "myth": "scripts/myth", + "flatten": "scripts/flatten && scripts/clean", "typechain": "hardhat typechain", "verify": "hardhat verify", "size": "hardhat size-contracts" diff --git a/scripts/analyze b/scripts/analyze index 2daaa8ac2..a770fc750 100755 --- a/scripts/analyze +++ b/scripts/analyze @@ -3,8 +3,10 @@ ## Before running: # This tool requires to have solc installed. # Ensure that you have the binaries installed by pip3 in your path. -# Install: https://github.com/crytic/slither#how-to-install -# Usage: https://github.com/crytic/slither/wiki/Usage +# Install: +# - https://github.com/crytic/slither#how-to-install +# Usage: +# - https://github.com/crytic/slither/wiki/Usage mkdir -p reports @@ -13,8 +15,17 @@ yarn build && \ echo "Analyzing contracts..." slither . \ - --filter-paths "bancor/*" \ - &> reports/analyzer-report.log && \ + --hardhat-ignore-compile \ + --hardhat-artifacts-directory ./build/contracts \ + --sarif - \ + --filter-paths "contracts/bancor/.*|contracts/tests/.*|contracts/staking/libs/Cobbs.*|contracts/staking/libs/LibFixedMath.*|contracts/staking/libs/MathUtils.*" \ + --exclude-dependencies \ + --exclude similar-names,naming-convention \ + --disable-color \ + &> reports/analyzer-report.sarif && \ +echo "Slither report generated at ./reports/analyzer-report.sarif" +echo "Checking ERC compliance..." slither-check-erc build/flatten/GraphToken.sol GraphToken &> reports/analyzer-report-erc.log +echo "Compliance report generated at ./reports/analyzer-report-erc.log" echo "Done!" diff --git a/scripts/clean b/scripts/clean new file mode 100755 index 000000000..442a9e9ec --- /dev/null +++ b/scripts/clean @@ -0,0 +1,32 @@ +#!/bin/bash + +OUT_DIR="build/flatten" + +mkdir -p ${OUT_DIR} + +echo "Cleaning flattened contracts..." + +FLATTENED_FILES=( + "$OUT_DIR/Controller.sol" + "$OUT_DIR/GraphGovernance.sol" + "$OUT_DIR/GNS.sol" + "$OUT_DIR/ServiceRegistry.sol" + "$OUT_DIR/Curation.sol" + "$OUT_DIR/GraphCurationToken.sol" + "$OUT_DIR/Staking.sol" + "$OUT_DIR/RewardsManager.sol" + "$OUT_DIR/GraphToken.sol" + "$OUT_DIR/EpochManager.sol" + "$OUT_DIR/GraphProxy.sol" + "$OUT_DIR/GDAI.sol" + "$OUT_DIR/GSRManager.sol" +) + +for path in ${FLATTENED_FILES[@]}; do + echo "Clean > ${path}" + sed -i \ + -e "s|pragma solidity.*||g" \ + -e "s|// SPDX-License-Identifier:.*||g" \ + -e 's|pragma experimental ABIEncoderV2;|//pragma experimental ABIEncoderV2;|g' \ + -e '1s|^|pragma experimental ABIEncoderV2;\n|' $path +done \ No newline at end of file diff --git a/scripts/coverage b/scripts/coverage index ddf388331..ecfd40448 100755 --- a/scripts/coverage +++ b/scripts/coverage @@ -2,5 +2,5 @@ set -eo pipefail -yarn compile +yarn build npx hardhat coverage $@ diff --git a/scripts/myth b/scripts/myth new file mode 100755 index 000000000..60de5c94f --- /dev/null +++ b/scripts/myth @@ -0,0 +1,35 @@ +#!/bin/bash + +## Before running: +# This tool requires to have solc installed. +# Ensure that you have the binaries installed by pip3 in your path. +# Install: +# - https://github.com/ConsenSys/mythril#installation-and-setup +# Usage: +# - https://github.com/ConsenSys/mythril#usage + +pip3 install --user mythril && \ +yarn build && \ +mkdir -p reports/myth + +echo "Myth Analysis..." + +start_time="$(date -u +%s)" + +for filename in build/flatten/*.sol; do + step_start_time="$(date -u +%s)" + echo "Scanning $filename ..." + myth analyze \ + --parallel-solving \ + --execution-timeout 30 \ + --solver-timeout 6000 \ + -o markdown "$filename" \ + &> "reports/myth/$(basename "$filename" .sol)-report.md" && \ + + end_time="$(date -u +%s)" + total_elapsed="$(($end_time-$start_time))" + step_elapsed="$(($end_time-$step_start_time))" + echo "> Took $step_elapsed seconds. Total elapsed: $total_elapsed seconds." +done + +echo "Done!" \ No newline at end of file diff --git a/slither.config.json b/slither.config.json new file mode 100644 index 000000000..000ca97b3 --- /dev/null +++ b/slither.config.json @@ -0,0 +1,6 @@ +{ + "hardhat_artifacts_directory": "./build/contracts", + "filter_paths": "contracts/bancor/.*|contracts/tests/.*|contracts/staking/libs/Cobbs.*|contracts/staking/libs/LibFixedMath.*|contracts/staking/libs/MathUtils.*", + "detectors_to_exclude": "similar-names,naming-convention", + "exclude_dependencies": true +} \ No newline at end of file diff --git a/tasks/query/allocations.ts b/tasks/query/allocations.ts deleted file mode 100644 index 16c849d70..000000000 --- a/tasks/query/allocations.ts +++ /dev/null @@ -1,73 +0,0 @@ -import axios from 'axios' -import Table from 'cli-table' -import PQueue from 'p-queue' -import { utils, BigNumber } from 'ethers' -import { task } from 'hardhat/config' -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import '@nomiclabs/hardhat-ethers' - -import '../gre' - -const { formatEther } = utils - -task('query:allos', 'List allocations').setAction(async (_, hre: HardhatRuntimeEnvironment) => { - const { contracts } = hre - - // Get allocations from the subgraph - const query = `{ - allocations(where: { status: "Active" }, first: 1000) { - id - allocatedTokens - subgraphDeployment { id } - createdAt - createdAtEpoch - indexer { id stakedTokens } - } - } - ` - const url = 'https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet' - const res = await axios.post(url, { query }) - const allos = res.data.data.allocations - - const table = new Table({ - head: ['ID', 'Indexer', 'SID', 'Allocated', 'IdxRewards', 'IdxCut', 'Cooldown', 'Epoch'], - colWidths: [20, 20, 10, 20, 20, 10, 10, 10], - }) - - const currentBlock = await hre.ethers.provider.send('eth_blockNumber', []) - - let totalIndexingRewards = BigNumber.from(0) - let totalAllocated = BigNumber.from(0) - - // Get allocations - const queue = new PQueue({ concurrency: 4 }) - for (const allo of allos) { - queue.add(async () => { - console.log('coso') - const [pool, r] = await Promise.all([ - contracts.Staking.delegationPools(allo.indexer.id), - contracts.RewardsManager.getRewards(allo.id), - ]) - table.push([ - allo.id, - allo.indexer.id, - allo.subgraphDeployment.id, - formatEther(allo.allocatedTokens), - formatEther(r), - pool.indexingRewardCut / 10000, - pool.updatedAtBlock.add(pool.cooldownBlocks).toNumber() - currentBlock, - allo.createdAtEpoch, - ]) - - totalIndexingRewards = totalIndexingRewards.add(r) - totalAllocated = totalAllocated.add(allo.allocatedTokens) - }) - } - await queue.onIdle() - - // Display - console.log(table.toString()) - console.log('total entries: ', allos.length) - console.log('total pending idx-rewards: ', hre.ethers.utils.formatEther(totalIndexingRewards)) - console.log('total allocated: ', hre.ethers.utils.formatEther(totalAllocated)) -}) diff --git a/tasks/query/indexers.ts b/tasks/query/indexers.ts deleted file mode 100644 index c717e1483..000000000 --- a/tasks/query/indexers.ts +++ /dev/null @@ -1,62 +0,0 @@ -import axios from 'axios' -import Table from 'cli-table' -import { utils } from 'ethers' -import { task } from 'hardhat/config' -import { HardhatRuntimeEnvironment } from 'hardhat/types' - -import '../gre' - -const { formatEther } = utils - -task('query:indexers', 'List indexers').setAction(async (_, hre: HardhatRuntimeEnvironment) => { - // Get indexers from subgraph - const query = `{ - indexers(where: {stakedTokens_gt: "0"}, first: 1000) { - id - stakedTokens - delegatedTokens - allocatedTokens - allocationCount - } - }` - const url = 'https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet' - const res = await axios.post(url, { query }) - const indexers = res.data.data.indexers - - const table = new Table({ - head: ['ID', 'Stake', 'Delegated', 'Capacity Ratio', 'Allocated', 'Used', 'N'], - colWidths: [20, 20, 20, 20, 20, 10, 5], - }) - - // Calculate indexer data - let totalStaked = hre.ethers.BigNumber.from(0) - let totalDelegated = hre.ethers.BigNumber.from(0) - let totalAllocated = hre.ethers.BigNumber.from(0) - for (const indexer of indexers) { - const t = indexer.stakedTokens / 1e18 + indexer.delegatedTokens / 1e18 - const b = indexer.allocatedTokens / 1e18 / t - const maxCapacity = indexer.stakedTokens / 1e18 + (indexer.stakedTokens / 1e18) * 16 - const capacityRatio = - (indexer.stakedTokens / 1e18 + indexer.delegatedTokens / 1e18) / maxCapacity - - table.push([ - indexer.id, - formatEther(indexer.stakedTokens), - formatEther(indexer.delegatedTokens), - capacityRatio.toFixed(2), - formatEther(indexer.allocatedTokens), - b.toFixed(2), - indexer.allocationCount, - ]) - totalStaked = totalStaked.add(indexer.stakedTokens) - totalDelegated = totalDelegated.add(indexer.delegatedTokens) - totalAllocated = totalAllocated.add(indexer.allocatedTokens) - } - - // Display - console.log(table.toString()) - console.log('# indexers: ', indexers.length) - console.log('total staked: ', formatEther(totalStaked)) - console.log('total delegated: ', formatEther(totalDelegated)) - console.log('total allocated: ', formatEther(totalAllocated)) -}) diff --git a/tasks/query/rebates.ts b/tasks/query/rebates.ts deleted file mode 100644 index e6a85708c..000000000 --- a/tasks/query/rebates.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { BigNumber } from 'ethers' -import { parseEther } from 'ethers/lib/utils' -import Table from 'cli-table' -import PQueue from 'p-queue' -import { task } from 'hardhat/config' -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import '@nomiclabs/hardhat-ethers' - -import '../gre' - -task('query:rebates', 'List rebate pools') - .addParam('count', 'Number of pools to query') - .setAction(async ({ count }, hre: HardhatRuntimeEnvironment) => { - const { contracts } = hre - const { formatEther } = hre.ethers.utils - - const table = new Table({ - head: ['Epoch', 'Fees', 'Claimed', 'Allos', 'Done (%)'], - colWidths: [10, 40, 40, 10, 10], - }) - - // Get epoch data - const currentEpoch = await contracts.EpochManager.currentEpoch() - - // Summaries - let totalAllos = 0 - let totalUnclaimed = BigNumber.from(0) - - // Get rebates - const items = [] - const queue = new PQueue({ concurrency: 10 }) - for (let i = 0; i < count; i++) { - queue.add(async () => { - // Calculations - const epoch = currentEpoch.sub(i).toNumber() - const rebatePool = await contracts.Staking.rebates(epoch) - const shareClaimed = rebatePool.fees.gt(0) - ? formatEther(rebatePool.claimedRewards.mul(parseEther('1')).div(rebatePool.fees)) - : '1' - // Add to table - items.push([ - epoch, - formatEther(rebatePool.fees), - formatEther(rebatePool.claimedRewards), - rebatePool.unclaimedAllocationsCount, - Math.round(parseFloat(shareClaimed) * 100), - ]) - // Add to summaries - totalAllos += rebatePool.unclaimedAllocationsCount - totalUnclaimed = totalUnclaimed.add(rebatePool.fees.sub(rebatePool.claimedRewards)) - }) - } - await queue.onIdle() - - // Display - table.push(...items.sort((a, b) => b[0] - a[0])) - console.log(table.toString()) - console.log(`> Unclaimed Allos: ${totalAllos}`) - console.log(`> Unclaimed Fees: ${formatEther(totalUnclaimed)}`) - }) diff --git a/test/curation/curation.test.ts b/test/curation/curation.test.ts index 265c56ad1..da0265ef2 100644 --- a/test/curation/curation.test.ts +++ b/test/curation/curation.test.ts @@ -6,9 +6,19 @@ import { GraphToken } from '../../build/types/GraphToken' import { Controller } from '../../build/types/Controller' import { NetworkFixture } from '../lib/fixtures' -import { getAccounts, randomHexBytes, toBN, toGRT, formatGRT, Account } from '../lib/testHelpers' +import { + getAccounts, + randomHexBytes, + toBN, + toGRT, + formatGRT, + Account, + calcBondingCurve, + BIG_NUMBER_ZERO, +} from '../lib/testHelpers' const MAX_PPM = 1000000 +const DEFAULT_PPM = 500000 const chunkify = (total: BigNumber, maxChunks = 10): Array => { const chunks = [] @@ -48,37 +58,11 @@ describe('Curation', () => { const tokensToDeposit = toGRT('1000') const tokensToCollect = toGRT('2000') - async function calcBondingCurve( - supply: BigNumber, - reserveBalance: BigNumber, - reserveRatio: number, - depositAmount: BigNumber, - ) { - // Handle the initialization of the bonding curve - if (supply.eq(0)) { - const minDeposit = await curation.minimumCurationDeposit() - if (depositAmount.lt(minDeposit)) { - throw new Error('deposit must be above minimum') - } - const defaultReserveRatio = await curation.defaultReserveRatio() - const minSupply = toGRT('1') - return ( - (await calcBondingCurve( - minSupply, - minDeposit, - defaultReserveRatio, - depositAmount.sub(minDeposit), - )) + toFloat(minSupply) - ) - } - // Calculate bonding curve in the test - return ( - toFloat(supply) * - ((1 + toFloat(depositAmount) / toFloat(reserveBalance)) ** (reserveRatio / 1000000) - 1) - ) - } - - const shouldMint = async (tokensToDeposit: BigNumber, expectedSignal: BigNumber) => { + const shouldMint = async ( + tokensToDeposit: BigNumber, + expectedSignal: BigNumber, + expectedReserveRatio: number, + ) => { // Before state const beforeTokenTotalSupply = await grt.totalSupply() const beforeCuratorTokens = await grt.balanceOf(curator.address) @@ -117,7 +101,6 @@ describe('Curation', () => { // Allocated and balance updated expect(afterPool.tokens).eq(beforePool.tokens.add(tokensToDeposit.sub(curationTax))) expect(afterPoolSignal).eq(beforePoolSignal.add(expectedSignal)) - expect(afterPool.reserveRatio).eq(await curation.defaultReserveRatio()) // Contract balance updated expect(afterTotalTokens).eq(beforeTotalTokens.add(tokensToDeposit.sub(curationTax))) // Total supply is reduced to curation tax burning @@ -184,22 +167,6 @@ describe('Curation', () => { expect(afterTotalBalance).eq(beforeTotalBalance.add(tokensToCollect)) } - before(async function () { - // Use stakingMock so we can call collect - ;[me, governor, curator, stakingMock] = await getAccounts() - - fixture = new NetworkFixture() - ;({ controller, curation, grt } = await fixture.load(governor.signer)) - - // Give some funds to the curator and approve the curation contract - await grt.connect(governor.signer).mint(curator.address, curatorTokens) - await grt.connect(curator.signer).approve(curation.address, curatorTokens) - - // Give some funds to the staking contract and approve the curation contract - await grt.connect(governor.signer).mint(stakingMock.address, tokensToCollect) - await grt.connect(stakingMock.signer).approve(curation.address, tokensToCollect) - }) - beforeEach(async function () { await fixture.setUp() }) @@ -208,351 +175,1158 @@ describe('Curation', () => { await fixture.tearDown() }) - describe('bonding curve', function () { - const tokensToDeposit = curatorTokens - - it('reject convert signal to tokens if subgraph deployment not initted', async function () { - const tx = curation.signalToTokens(subgraphDeploymentID, toGRT('100')) - await expect(tx).revertedWith('Subgraph deployment must be curated to perform calculations') + // NOTE: Defaults set: + // initializationDays: 1 + // initializationExitDays: 2 + // blocksPerDay: 1 + // They are set in test/lib/deployments.ts + + describe('during initialization phase', function () { + before(async function () { + // Use stakingMock so we can call collect + ;[me, governor, curator, stakingMock] = await getAccounts() + + fixture = new NetworkFixture() + ;({ controller, curation, grt } = await fixture.load(governor.signer, { + curationOptions: { blocksPerDay: 6400 }, + })) + + // Give some funds to the curator and approve the curation contract + await grt.connect(governor.signer).mint(curator.address, curatorTokens) + await grt.connect(curator.signer).approve(curation.address, curatorTokens) + + // Give some funds to the staking contract and approve the curation contract + await grt.connect(governor.signer).mint(stakingMock.address, tokensToCollect) + await grt.connect(stakingMock.signer).approve(curation.address, tokensToCollect) }) - it('convert signal to tokens', async function () { - // Curate - await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + describe('bonding curve', function () { + const tokensToDeposit = curatorTokens - // Conversion - const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) - const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signal) - expect(expectedTokens).eq(tokensToDeposit) - }) + it('reject convert signal to tokens if subgraph deployment not initted', async function () { + const tx = curation.signalToTokens(subgraphDeploymentID, toGRT('100')) + await expect(tx).revertedWith('Subgraph deployment must be curated to perform calculations') + }) + + it('convert signal to tokens', async function () { + // Curate + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(tokensToDeposit).eq(expectedTokens) + }) - it('convert signal to tokens (with curation tax)', async function () { - // Set curation tax - const curationTaxPercentage = 50000 // 5% - await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) - - // Curate - const expectedCurationTax = tokensToDeposit.mul(curationTaxPercentage).div(MAX_PPM) - const { 1: curationTax } = await curation.tokensToSignal( - subgraphDeploymentID, - tokensToDeposit, - ) - await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) - - // Conversion - const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) - const tokens = await curation.signalToTokens(subgraphDeploymentID, signal) - expect(tokens).eq(tokensToDeposit.sub(expectedCurationTax)) - expect(expectedCurationTax).eq(curationTax) + it('convert signal to tokens (with curation tax)', async function () { + // Set curation tax + const curationTaxPercentage = 50000 // 5% + await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) + + // Curate + const expectedCurationTax = tokensToDeposit.mul(curationTaxPercentage).div(MAX_PPM) + const { 1: curationTax } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const tokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(tokens).eq(tokensToDeposit.sub(expectedCurationTax)) + expect(curationTax).eq(expectedCurationTax) + }) + + it('convert tokens to signal', async function () { + // Conversion + const tokens = toGRT('1000') + const { 0: signal } = await curation.tokensToSignal(subgraphDeploymentID, tokens) + expect(signal).eq(toGRT('10')) + }) + + it('convert tokens to signal if non-curated subgraph', async function () { + // Conversion + const nonCuratedSubgraphDeploymentID = randomHexBytes() + const tokens = toGRT('1') + const tx = curation.tokensToSignal(nonCuratedSubgraphDeploymentID, tokens) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) }) - it('convert tokens to signal', async function () { - // Conversion - const tokens = toGRT('1000') - const { 0: signal } = await curation.tokensToSignal(subgraphDeploymentID, tokens) - expect(signal).eq(signalAmountFor1000Tokens) + describe('curate', async function () { + it('reject deposit below minimum tokens required', async function () { + const tokensToDeposit = (await curation.minimumCurationDeposit()).sub(toBN(1)) + const tx = curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) + + it('should deposit on a subgraph deployment', async function () { + const tokensToDeposit = await curation.minimumCurationDeposit() + const expectedSignal = toGRT('1') + await shouldMint(tokensToDeposit, expectedSignal, MAX_PPM) + }) + + it('should get signal according to bonding curve', async function () { + const tokensToDeposit = toGRT('1000') + + await shouldMint(tokensToDeposit, toGRT('10'), MAX_PPM) + }) + + it('should get signal according to bonding curve (and account for curation tax)', async function () { + // Set curation tax + await curation.connect(governor.signer).setCurationTaxPercentage(50000) // 5% + + // Mint + const tokensToDeposit = toGRT('1000') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMint(tokensToDeposit, expectedSignal, MAX_PPM) + }) + + it('should revert curate if over slippage', async function () { + const tokensToDeposit = toGRT('1000') + const expectedSignal = toGRT('10') + const tx = curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, expectedSignal.add(1)) + + await expect(tx).revertedWith('Slippage protection') + }) }) - it('convert tokens to signal if non-curated subgraph', async function () { - // Conversion - const nonCuratedSubgraphDeploymentID = randomHexBytes() - const tokens = toGRT('1') - const tx = curation.tokensToSignal(nonCuratedSubgraphDeploymentID, tokens) - await expect(tx).revertedWith('Curation deposit is below minimum required') + describe('collect', async function () { + context('> not curated', async function () { + it('reject collect tokens distributed to the curation pool', async function () { + // Source of tokens must be the staking for this to work + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + const tx = curation + .connect(stakingMock.signer) + .collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Subgraph deployment must be curated to collect fees') + }) + }) + + context('> curated', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, toGRT('1000'), 0) + }) + + it('reject collect tokens distributed from invalid address', async function () { + const tx = curation.connect(me.signer).collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Caller must be the staking contract') + }) + + it('should collect tokens distributed to the curation pool', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + await shouldCollect(toGRT('1')) + await shouldCollect(toGRT('10')) + await shouldCollect(toGRT('100')) + await shouldCollect(toGRT('200')) + await shouldCollect(toGRT('500.25')) + }) + + it('should collect tokens and then unsignal all', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + await shouldCollect(toGRT('100')) + + // When we burn signal we should get more tokens than initially curated + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + await shouldBurn(signalToRedeem, toGRT('1100')) + }) + + it('should collect tokens and then unsignal multiple times', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + const tokensToCollect = toGRT('100') + await shouldCollect(tokensToCollect) + + // Unsignal partially + const signalOutRemainder = toGRT(1) + const signalOutPartial = ( + await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + ).sub(signalOutRemainder) + const tx1 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutPartial, 0) + const r1 = await tx1.wait() + const event1 = curation.interface.parseLog(r1.events[2]).args + const tokensOut1 = event1.tokens + + // Collect increase the pool reserves + await shouldCollect(tokensToCollect) + + // Unsignal the rest + const tx2 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutRemainder, 0) + const r2 = await tx2.wait() + const event2 = curation.interface.parseLog(r2.events[2]).args + const tokensOut2 = event2.tokens + + expect(tokensOut1.add(tokensOut2)).eq(toGRT('1000').add(tokensToCollect.mul(2))) + }) + }) }) - }) - describe('curate', async function () { - it('reject deposit below minimum tokens required', async function () { - const tokensToDeposit = (await curation.minimumCurationDeposit()).sub(toBN(1)) - const tx = curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) - await expect(tx).revertedWith('Curation deposit is below minimum required') + describe('burn', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + }) + + it('reject redeem more than a curator owns', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('1'), 0) + await expect(tx).revertedWith('Cannot burn more signal than you own') + }) + + it('reject redeem zero signal', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('0'), 0) + await expect(tx).revertedWith('Cannot burn zero signal') + }) + + it('should allow to redeem *partially*', async function () { + // Redeem just one signal + const signalToRedeem = toGRT('10') + const expectedTokens = toGRT('1000') + await shouldBurn(signalToRedeem, expectedTokens) + }) + + it('should allow to redeem *fully*', async function () { + // Get all signal of the curator + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const expectedTokens = tokensToDeposit + await shouldBurn(signalToRedeem, expectedTokens) + }) + + it('should allow to redeem back below minimum deposit', async function () { + // Redeem "almost" all signal + const signal = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + const signalToRedeem = signal.sub(toGRT('0.000001')) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signalToRedeem) + await shouldBurn(signalToRedeem, expectedTokens) + + // The pool should have less tokens that required by minimumCurationDeposit + const afterPool = await curation.pools(subgraphDeploymentID) + expect(afterPool.tokens).lt(await curation.minimumCurationDeposit()) + + // Should be able to deposit more after being under minimumCurationDeposit + const tokensToDeposit = toGRT('1') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMint(tokensToDeposit, expectedSignal, MAX_PPM) + }) + + it('should revert redeem if over slippage', async function () { + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const expectedTokens = tokensToDeposit + + const tx = curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalToRedeem, expectedTokens.add(1)) + await expect(tx).revertedWith('Slippage protection') + }) }) - it('should deposit on a subgraph deployment', async function () { - const tokensToDeposit = await curation.minimumCurationDeposit() - const expectedSignal = toGRT('1') - await shouldMint(tokensToDeposit, expectedSignal) + describe('conservation', async function () { + it('should match multiple deposits and redeems back to initial state', async function () { + this.timeout(60000) // increase timeout for test runner + + const totalDeposits = toGRT('1000000000') + + // Signal multiple times + let totalSignal = toGRT('0') + for (const tokensToDeposit of chunkify(totalDeposits, 10)) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + totalSignal = totalSignal.add(signal) + } + + // Redeem signal multiple times + let totalTokens = toGRT('0') + for (const signalToRedeem of chunkify(totalSignal, 10)) { + const tx = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalToRedeem, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const tokens = event.args['tokens'] + totalTokens = totalTokens.add(tokens) + // console.log('<', formatEther(signalToRedeem), '=', formatEther(tokens)) + } + + // Conservation of work + const afterPool = await curation.pools(subgraphDeploymentID) + const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + expect(afterPool.tokens).eq(toGRT('0')) + expect(afterPoolSignal).eq(toGRT('0')) + expect(await curation.isCurated(subgraphDeploymentID)).eq(false) + expect(totalTokens).eq(totalDeposits) + }) }) - it('should get signal according to bonding curve', async function () { - const tokensToDeposit = toGRT('1000') - const expectedSignal = signalAmountFor1000Tokens - await shouldMint(tokensToDeposit, expectedSignal) + describe('multiple minting', async function () { + it('should mint less signal every time due to the bonding curve', async function () { + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + + for (const tokensToDeposit of tokensToDepositMany) { + const expectedSignal = await calcBondingCurve( + await curation.getCurationPoolSignal(subgraphDeploymentID), + await curation.getCurationPoolTokens(subgraphDeploymentID), + tokensToDeposit, + BIG_NUMBER_ZERO, + BIG_NUMBER_ZERO, + await curation.initializationPeriod(), + await curation.initializationExitPeriod(), + await curation.defaultReserveRatio(), + await curation.minimumCurationDeposit(), + ) + + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + + expect(toRound(toFloat(signal))).eq(toRound(expectedSignal)) + } + }) + + it('should mint when using the edge case of linear function', async function () { + // Setup edge case like linear function: 1 GRT = 1 GCS + await curation.setMinimumCurationDeposit(toGRT('1')) + await curation.setDefaultReserveRatio(1000000) + + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + + // Mint multiple times + for (const tokensToDeposit of tokensToDepositMany) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + + expect(signal).eq(tokensToDeposit) // we compare 1:1 ratio + } + }) }) + }) + + describe('during initialization exit phase', function () { + before(async function () { + // Use stakingMock so we can call collect + ;[me, governor, curator, stakingMock] = await getAccounts() + + fixture = new NetworkFixture() + ;({ controller, curation, grt } = await fixture.load(governor.signer, { + curationOptions: { blocksPerDay: 6400 }, + })) + + // Give some funds to the curator and approve the curation contract + await grt.connect(governor.signer).mint(curator.address, curatorTokens) + await grt.connect(curator.signer).approve(curation.address, curatorTokens) - it('should get signal according to bonding curve (and account for curation tax)', async function () { - // Set curation tax - await curation.connect(governor.signer).setCurationTaxPercentage(50000) // 5% - - // Mint - const tokensToDeposit = toGRT('1000') - const { 0: expectedSignal } = await curation.tokensToSignal( - subgraphDeploymentID, - tokensToDeposit, - ) - await shouldMint(tokensToDeposit, expectedSignal) + // Give some funds to the staking contract and approve the curation contract + await grt.connect(governor.signer).mint(stakingMock.address, tokensToCollect) + await grt.connect(stakingMock.signer).approve(curation.address, tokensToCollect) }) - it('should revert curate if over slippage', async function () { - const tokensToDeposit = toGRT('1000') - const expectedSignal = signalAmountFor1000Tokens - const tx = curation - .connect(curator.signer) - .mint(subgraphDeploymentID, tokensToDeposit, expectedSignal.add(1)) - await expect(tx).revertedWith('Slippage protection') + describe('bonding curve', function () { + const tokensToDeposit = curatorTokens + + it('reject convert signal to tokens if subgraph deployment not initted', async function () { + const tx = curation.signalToTokens(subgraphDeploymentID, toGRT('100')) + await expect(tx).revertedWith('Subgraph deployment must be curated to perform calculations') + }) + + it('convert signal to tokens', async function () { + // Curate + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(tokensToDeposit).eq(expectedTokens) + }) + + it('convert signal to tokens (with curation tax)', async function () { + // Set curation tax + const curationTaxPercentage = 50000 // 5% + await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) + + // Curate + const expectedCurationTax = tokensToDeposit.mul(curationTaxPercentage).div(MAX_PPM) + const { 1: curationTax } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const tokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(tokens).eq(tokensToDeposit.sub(expectedCurationTax)) + expect(curationTax).eq(expectedCurationTax) + }) + + it('convert tokens to signal', async function () { + // Conversion + const tokens = toGRT('1000') + const { 0: signal } = await curation.tokensToSignal(subgraphDeploymentID, tokens) + expect(signal).eq(toGRT('10')) + }) + + it('convert tokens to signal if non-curated subgraph', async function () { + // Conversion + const nonCuratedSubgraphDeploymentID = randomHexBytes() + const tokens = toGRT('1') + const tx = curation.tokensToSignal(nonCuratedSubgraphDeploymentID, tokens) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) }) - }) - describe('collect', async function () { - context('> not curated', async function () { - it('reject collect tokens distributed to the curation pool', async function () { - // Source of tokens must be the staking for this to work - await controller - .connect(governor.signer) - .setContractProxy(utils.id('Staking'), stakingMock.address) - await curation.syncAllContracts() // call sync because we change the proxy for staking + describe('curate', async function () { + it('reject deposit below minimum tokens required', async function () { + const tokensToDeposit = (await curation.minimumCurationDeposit()).sub(toBN(1)) + const tx = curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) + + it('should deposit on a subgraph deployment', async function () { + const tokensToDeposit = await curation.minimumCurationDeposit() + const expectedSignal = toGRT('1') + await shouldMint(tokensToDeposit, expectedSignal, MAX_PPM) + }) + + it('should get signal according to bonding curve', async function () { + const tokensToDeposit = toGRT('1000') + await shouldMint(tokensToDeposit, toGRT('10'), MAX_PPM) + }) + + it('should get signal according to bonding curve (and account for curation tax)', async function () { + // Set curation tax + await curation.connect(governor.signer).setCurationTaxPercentage(50000) // 5% + + // Mint + const tokensToDeposit = toGRT('1000') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMint(tokensToDeposit, expectedSignal, MAX_PPM) + }) + + it('should revert curate if over slippage', async function () { + const tokensToDeposit = toGRT('1000') + const expectedSignal = toGRT('10') const tx = curation - .connect(stakingMock.signer) - .collect(subgraphDeploymentID, tokensToCollect) - await expect(tx).revertedWith('Subgraph deployment must be curated to collect fees') + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, expectedSignal.add(1)) + + await expect(tx).revertedWith('Slippage protection') }) }) - context('> curated', async function () { - beforeEach(async function () { - await curation.connect(curator.signer).mint(subgraphDeploymentID, toGRT('1000'), 0) + describe('collect', async function () { + context('> not curated', async function () { + it('reject collect tokens distributed to the curation pool', async function () { + // Source of tokens must be the staking for this to work + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + const tx = curation + .connect(stakingMock.signer) + .collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Subgraph deployment must be curated to collect fees') + }) }) - it('reject collect tokens distributed from invalid address', async function () { - const tx = curation.connect(me.signer).collect(subgraphDeploymentID, tokensToCollect) - await expect(tx).revertedWith('Caller must be the staking contract') + context('> curated', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, toGRT('1000'), 0) + }) + + it('reject collect tokens distributed from invalid address', async function () { + const tx = curation.connect(me.signer).collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Caller must be the staking contract') + }) + + it('should collect tokens distributed to the curation pool', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + await shouldCollect(toGRT('1')) + await shouldCollect(toGRT('10')) + await shouldCollect(toGRT('100')) + await shouldCollect(toGRT('200')) + await shouldCollect(toGRT('500.25')) + }) + + it('should collect tokens and then unsignal all', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + await shouldCollect(toGRT('100')) + + // When we burn signal we should get more tokens than initially curated + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + await shouldBurn(signalToRedeem, toGRT('1100')) + }) + + it('should collect tokens and then unsignal multiple times', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + const tokensToCollect = toGRT('100') + await shouldCollect(tokensToCollect) + + // Unsignal partially + const signalOutRemainder = toGRT(1) + const signalOutPartial = ( + await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + ).sub(signalOutRemainder) + const tx1 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutPartial, 0) + const r1 = await tx1.wait() + const event1 = curation.interface.parseLog(r1.events[2]).args + const tokensOut1 = event1.tokens + + // Collect increase the pool reserves + await shouldCollect(tokensToCollect) + + // Unsignal the rest + const tx2 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutRemainder, 0) + const r2 = await tx2.wait() + const event2 = curation.interface.parseLog(r2.events[2]).args + const tokensOut2 = event2.tokens + + expect(tokensOut1.add(tokensOut2)).eq(toGRT('1000').add(tokensToCollect.mul(2))) + }) }) + }) - it('should collect tokens distributed to the curation pool', async function () { - await controller - .connect(governor.signer) - .setContractProxy(utils.id('Staking'), stakingMock.address) - await curation.syncAllContracts() // call sync because we change the proxy for staking + describe('burn', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + }) - await shouldCollect(toGRT('1')) - await shouldCollect(toGRT('10')) - await shouldCollect(toGRT('100')) - await shouldCollect(toGRT('200')) - await shouldCollect(toGRT('500.25')) + it('reject redeem more than a curator owns', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('1'), 0) + await expect(tx).revertedWith('Cannot burn more signal than you own') }) - it('should collect tokens and then unsignal all', async function () { - await controller - .connect(governor.signer) - .setContractProxy(utils.id('Staking'), stakingMock.address) - await curation.syncAllContracts() // call sync because we change the proxy for staking + it('reject redeem zero signal', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('0'), 0) + await expect(tx).revertedWith('Cannot burn zero signal') + }) - // Collect increase the pool reserves - await shouldCollect(toGRT('100')) + it('should allow to redeem *partially*', async function () { + // Redeem just one signal + const signalToRedeem = toGRT('10') + const expectedTokens = toGRT('1000') + await shouldBurn(signalToRedeem, expectedTokens) + }) - // When we burn signal we should get more tokens than initially curated + it('should allow to redeem *fully*', async function () { + // Get all signal of the curator const signalToRedeem = await curation.getCuratorSignal( curator.address, subgraphDeploymentID, ) - await shouldBurn(signalToRedeem, toGRT('1100')) + const expectedTokens = tokensToDeposit + await shouldBurn(signalToRedeem, expectedTokens) }) - it('should collect tokens and then unsignal multiple times', async function () { - await controller - .connect(governor.signer) - .setContractProxy(utils.id('Staking'), stakingMock.address) - await curation.syncAllContracts() // call sync because we change the proxy for staking + it('should allow to redeem back below minimum deposit', async function () { + // Redeem "almost" all signal + const signal = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + const signalToRedeem = signal.sub(toGRT('0.000001')) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signalToRedeem) + await shouldBurn(signalToRedeem, expectedTokens) - // Collect increase the pool reserves - const tokensToCollect = toGRT('100') - await shouldCollect(tokensToCollect) + // The pool should have less tokens that required by minimumCurationDeposit + const afterPool = await curation.pools(subgraphDeploymentID) + expect(afterPool.tokens).lt(await curation.minimumCurationDeposit()) - // Unsignal partially - const signalOutRemainder = toGRT(1) - const signalOutPartial = ( - await curation.getCuratorSignal(curator.address, subgraphDeploymentID) - ).sub(signalOutRemainder) - const tx1 = await curation - .connect(curator.signer) - .burn(subgraphDeploymentID, signalOutPartial, 0) - const r1 = await tx1.wait() - const event1 = curation.interface.parseLog(r1.events[2]).args - const tokensOut1 = event1.tokens + // Should be able to deposit more after being under minimumCurationDeposit + const tokensToDeposit = toGRT('1') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMint(tokensToDeposit, expectedSignal, MAX_PPM) + }) - // Collect increase the pool reserves - await shouldCollect(tokensToCollect) + it('should revert redeem if over slippage', async function () { + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const expectedTokens = tokensToDeposit - // Unsignal the rest - const tx2 = await curation + const tx = curation .connect(curator.signer) - .burn(subgraphDeploymentID, signalOutRemainder, 0) - const r2 = await tx2.wait() - const event2 = curation.interface.parseLog(r2.events[2]).args - const tokensOut2 = event2.tokens - - expect(tokensOut1.add(tokensOut2)).eq(toGRT('1000').add(tokensToCollect.mul(2))) + .burn(subgraphDeploymentID, signalToRedeem, expectedTokens.add(1)) + await expect(tx).revertedWith('Slippage protection') }) }) - }) - describe('burn', async function () { - beforeEach(async function () { - await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + describe('conservation', async function () { + it('should match multiple deposits and redeems back to initial state', async function () { + this.timeout(60000) // increase timeout for test runner + + const totalDeposits = toGRT('1000000000') + + // Signal multiple times + let totalSignal = toGRT('0') + for (const tokensToDeposit of chunkify(totalDeposits, 10)) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + totalSignal = totalSignal.add(signal) + } + + // Redeem signal multiple times + let totalTokens = toGRT('0') + for (const signalToRedeem of chunkify(totalSignal, 10)) { + const tx = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalToRedeem, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const tokens = event.args['tokens'] + totalTokens = totalTokens.add(tokens) + // console.log('<', formatEther(signalToRedeem), '=', formatEther(tokens)) + } + + // Conservation of work + const afterPool = await curation.pools(subgraphDeploymentID) + const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + expect(afterPool.tokens).eq(toGRT('0')) + expect(afterPoolSignal).eq(toGRT('0')) + expect(await curation.isCurated(subgraphDeploymentID)).eq(false) + expect(totalTokens).eq(totalDeposits) + }) }) - it('reject redeem more than a curator owns', async function () { - const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('1'), 0) - await expect(tx).revertedWith('Cannot burn more signal than you own') - }) + describe('multiple minting', async function () { + it('should mint less signal every time due to the bonding curve', async function () { + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + + for (const tokensToDeposit of tokensToDepositMany) { + const expectedSignal = await calcBondingCurve( + await curation.getCurationPoolSignal(subgraphDeploymentID), + await curation.getCurationPoolTokens(subgraphDeploymentID), + tokensToDeposit, + BIG_NUMBER_ZERO, + BIG_NUMBER_ZERO, + await curation.initializationPeriod(), + await curation.initializationExitPeriod(), + await curation.defaultReserveRatio(), + await curation.minimumCurationDeposit(), + ) + + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + + expect(toRound(toFloat(signal))).eq(toRound(expectedSignal)) + } + }) - it('reject redeem zero signal', async function () { - const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('0'), 0) - await expect(tx).revertedWith('Cannot burn zero signal') + it('should mint when using the edge case of linear function', async function () { + // Setup edge case like linear function: 1 GRT = 1 GCS + await curation.setMinimumCurationDeposit(toGRT('1')) + await curation.setDefaultReserveRatio(1000000) + + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + + // Mint multiple times + for (const tokensToDeposit of tokensToDepositMany) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + + expect(signal).eq(tokensToDeposit) // we compare 1:1 ratio + } + }) }) + }) - it('should allow to redeem *partially*', async function () { - // Redeem just one signal - const signalToRedeem = toGRT('1') - const expectedTokens = toGRT('532.455532033675866536') - await shouldBurn(signalToRedeem, expectedTokens) - }) + describe('after initialization phase', function () { + before(async function () { + // Use stakingMock so we can call collect + ;[me, governor, curator, stakingMock] = await getAccounts() - it('should allow to redeem *fully*', async function () { - // Get all signal of the curator - const signalToRedeem = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) - const expectedTokens = tokensToDeposit - await shouldBurn(signalToRedeem, expectedTokens) - }) + fixture = new NetworkFixture() + ;({ controller, curation, grt } = await fixture.load(governor.signer)) + + // Give some funds to the curator and approve the curation contract + await grt.connect(governor.signer).mint(curator.address, curatorTokens) + await grt.connect(curator.signer).approve(curation.address, curatorTokens) - it('should allow to redeem back below minimum deposit', async function () { - // Redeem "almost" all signal - const signal = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) - const signalToRedeem = signal.sub(toGRT('0.000001')) - const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signalToRedeem) - await shouldBurn(signalToRedeem, expectedTokens) - - // The pool should have less tokens that required by minimumCurationDeposit - const afterPool = await curation.pools(subgraphDeploymentID) - expect(afterPool.tokens).lt(await curation.minimumCurationDeposit()) - - // Should be able to deposit more after being under minimumCurationDeposit - const tokensToDeposit = toGRT('1') - const { 0: expectedSignal } = await curation.tokensToSignal( - subgraphDeploymentID, - tokensToDeposit, - ) - await shouldMint(tokensToDeposit, expectedSignal) + // Give some funds to the staking contract and approve the curation contract + await grt.connect(governor.signer).mint(stakingMock.address, tokensToCollect) + await grt.connect(stakingMock.signer).approve(curation.address, tokensToCollect) }) - it('should revert redeem if over slippage', async function () { - const signalToRedeem = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) - const expectedTokens = tokensToDeposit + describe('bonding curve', function () { + const tokensToDeposit = curatorTokens + + it('reject convert signal to tokens if subgraph deployment not initted', async function () { + const tx = curation.signalToTokens(subgraphDeploymentID, toGRT('100')) + await expect(tx).revertedWith('Subgraph deployment must be curated to perform calculations') + }) + + it('convert signal to tokens', async function () { + // Curate + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(tokensToDeposit).eq(expectedTokens) + }) + + it('convert signal to tokens (with curation tax)', async function () { + // Set curation tax + const curationTaxPercentage = 50000 // 5% + await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) - const tx = curation - .connect(curator.signer) - .burn(subgraphDeploymentID, signalToRedeem, expectedTokens.add(1)) - await expect(tx).revertedWith('Slippage protection') + // Curate + const expectedCurationTax = tokensToDeposit.mul(curationTaxPercentage).div(MAX_PPM) + const { 1: curationTax } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + + // Conversion + const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) + const tokens = await curation.signalToTokens(subgraphDeploymentID, signal) + expect(tokens).eq(tokensToDeposit.sub(expectedCurationTax)) + expect(curationTax).eq(expectedCurationTax) + }) + + it('convert tokens to signal', async function () { + // Conversion + const tokens = toGRT('1000') + const { 0: signal } = await curation.tokensToSignal(subgraphDeploymentID, tokens) + expect(signal).eq(signalAmountFor1000Tokens) + }) + + it('convert tokens to signal if non-curated subgraph', async function () { + // Conversion + const nonCuratedSubgraphDeploymentID = randomHexBytes() + const tokens = toGRT('1') + const tx = curation.tokensToSignal(nonCuratedSubgraphDeploymentID, tokens) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) }) - }) - describe('conservation', async function () { - it('should match multiple deposits and redeems back to initial state', async function () { - const totalDeposits = toGRT('1000000000') + describe('curate', async function () { + it('reject deposit below minimum tokens required', async function () { + const tokensToDeposit = (await curation.minimumCurationDeposit()).sub(toBN(1)) + const tx = curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + await expect(tx).revertedWith('Curation deposit is below minimum required') + }) + + it('should deposit on a subgraph deployment', async function () { + const tokensToDeposit = await curation.minimumCurationDeposit() + const expectedSignal = toGRT('1') + await shouldMint(tokensToDeposit, expectedSignal, DEFAULT_PPM) + }) - // Signal multiple times - let totalSignal = toGRT('0') - for (const tokensToDeposit of chunkify(totalDeposits, 10)) { - const tx = await curation - .connect(curator.signer) - .mint(subgraphDeploymentID, tokensToDeposit, 0) - const receipt = await tx.wait() - const event: Event = receipt.events.pop() - const signal = event.args['signal'] - totalSignal = totalSignal.add(signal) - } - - // Redeem signal multiple times - let totalTokens = toGRT('0') - for (const signalToRedeem of chunkify(totalSignal, 10)) { - const tx = await curation + it('should get signal according to bonding curve', async function () { + const tokensToDeposit = toGRT('1000') + const expectedSignal = signalAmountFor1000Tokens + await shouldMint(tokensToDeposit, expectedSignal, DEFAULT_PPM) + }) + + it('should get signal according to bonding curve (and account for curation tax)', async function () { + // Set curation tax + await curation.connect(governor.signer).setCurationTaxPercentage(50000) // 5% + + // Mint + const tokensToDeposit = toGRT('1000') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, + tokensToDeposit, + ) + await shouldMint(tokensToDeposit, expectedSignal, DEFAULT_PPM) + }) + + it('should revert curate if over slippage', async function () { + const tokensToDeposit = toGRT('1000') + const expectedSignal = signalAmountFor1000Tokens + const tx = curation .connect(curator.signer) - .burn(subgraphDeploymentID, signalToRedeem, 0) - const receipt = await tx.wait() - const event: Event = receipt.events.pop() - const tokens = event.args['tokens'] - totalTokens = totalTokens.add(tokens) - // console.log('<', formatEther(signalToRedeem), '=', formatEther(tokens)) - } - - // Conservation of work - const afterPool = await curation.pools(subgraphDeploymentID) - const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) - expect(afterPool.tokens).eq(toGRT('0')) - expect(afterPoolSignal).eq(toGRT('0')) - expect(await curation.isCurated(subgraphDeploymentID)).eq(false) - expect(totalDeposits).eq(totalTokens) + .mint(subgraphDeploymentID, tokensToDeposit, expectedSignal.add(1)) + await expect(tx).revertedWith('Slippage protection') + }) }) - }) - describe('multiple minting', async function () { - it('should mint less signal every time due to the bonding curve', async function () { - const tokensToDepositMany = [ - toGRT('1000'), // should mint if we start with number above minimum deposit - toGRT('1000'), // every time it should mint less GCS due to bonding curve... - toGRT('1000'), - toGRT('1000'), - toGRT('2000'), - toGRT('2000'), - toGRT('123'), - toGRT('1'), // should mint below minimum deposit - ] - for (const tokensToDeposit of tokensToDepositMany) { - const expectedSignal = await calcBondingCurve( - await curation.getCurationPoolSignal(subgraphDeploymentID), - await curation.getCurationPoolTokens(subgraphDeploymentID), - await curation.defaultReserveRatio(), + describe('collect', async function () { + context('> not curated', async function () { + it('reject collect tokens distributed to the curation pool', async function () { + // Source of tokens must be the staking for this to work + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + const tx = curation + .connect(stakingMock.signer) + .collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Subgraph deployment must be curated to collect fees') + }) + }) + + context('> curated', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, toGRT('1000'), 0) + }) + + it('reject collect tokens distributed from invalid address', async function () { + const tx = curation.connect(me.signer).collect(subgraphDeploymentID, tokensToCollect) + await expect(tx).revertedWith('Caller must be the staking contract') + }) + + it('should collect tokens distributed to the curation pool', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + await shouldCollect(toGRT('1')) + await shouldCollect(toGRT('10')) + await shouldCollect(toGRT('100')) + await shouldCollect(toGRT('200')) + await shouldCollect(toGRT('500.25')) + }) + + it('should collect tokens and then unsignal all', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + await shouldCollect(toGRT('100')) + + // When we burn signal we should get more tokens than initially curated + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + await shouldBurn(signalToRedeem, toGRT('1100')) + }) + + it('should collect tokens and then unsignal multiple times', async function () { + await controller + .connect(governor.signer) + .setContractProxy(utils.id('Staking'), stakingMock.address) + await curation.syncAllContracts() // call sync because we change the proxy for staking + + // Collect increase the pool reserves + const tokensToCollect = toGRT('100') + await shouldCollect(tokensToCollect) + + // Unsignal partially + const signalOutRemainder = toGRT(1) + const signalOutPartial = ( + await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + ).sub(signalOutRemainder) + const tx1 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutPartial, 0) + const r1 = await tx1.wait() + const event1 = curation.interface.parseLog(r1.events[2]).args + const tokensOut1 = event1.tokens + + // Collect increase the pool reserves + await shouldCollect(tokensToCollect) + + // Unsignal the rest + const tx2 = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalOutRemainder, 0) + const r2 = await tx2.wait() + const event2 = curation.interface.parseLog(r2.events[2]).args + const tokensOut2 = event2.tokens + + expect(tokensOut1.add(tokensOut2)).eq(toGRT('1000').add(tokensToCollect.mul(2))) + }) + }) + }) + + describe('burn', async function () { + beforeEach(async function () { + await curation.connect(curator.signer).mint(subgraphDeploymentID, tokensToDeposit, 0) + }) + + it('reject redeem more than a curator owns', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('1'), 0) + await expect(tx).revertedWith('Cannot burn more signal than you own') + }) + + it('reject redeem zero signal', async function () { + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('0'), 0) + await expect(tx).revertedWith('Cannot burn zero signal') + }) + + it('should allow to redeem *partially*', async function () { + // Redeem just one signal + const signalToRedeem = toGRT('1') + const expectedTokens = toGRT('532.455532033675866536') + await shouldBurn(signalToRedeem, expectedTokens) + }) + + it('should allow to redeem *fully*', async function () { + // Get all signal of the curator + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const expectedTokens = tokensToDeposit + await shouldBurn(signalToRedeem, expectedTokens) + }) + + it('should allow to redeem back below minimum deposit', async function () { + // Redeem "almost" all signal + const signal = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) + const signalToRedeem = signal.sub(toGRT('0.000001')) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signalToRedeem) + await shouldBurn(signalToRedeem, expectedTokens) + + // The pool should have less tokens that required by minimumCurationDeposit + const afterPool = await curation.pools(subgraphDeploymentID) + expect(afterPool.tokens).lt(await curation.minimumCurationDeposit()) + + // Should be able to deposit more after being under minimumCurationDeposit + const tokensToDeposit = toGRT('1') + const { 0: expectedSignal } = await curation.tokensToSignal( + subgraphDeploymentID, tokensToDeposit, ) + await shouldMint(tokensToDeposit, expectedSignal, DEFAULT_PPM) + }) + + it('should revert redeem if over slippage', async function () { + const signalToRedeem = await curation.getCuratorSignal( + curator.address, + subgraphDeploymentID, + ) + const expectedTokens = tokensToDeposit - const tx = await curation + const tx = curation .connect(curator.signer) - .mint(subgraphDeploymentID, tokensToDeposit, 0) - const receipt = await tx.wait() - const event: Event = receipt.events.pop() - const signal = event.args['signal'] - expect(toRound(expectedSignal)).eq(toRound(toFloat(signal))) - } + .burn(subgraphDeploymentID, signalToRedeem, expectedTokens.add(1)) + await expect(tx).revertedWith('Slippage protection') + }) }) - it('should mint when using the edge case of linear function', async function () { - // Setup edge case like linear function: 1 GRT = 1 GCS - await curation.setMinimumCurationDeposit(toGRT('1')) - await curation.setDefaultReserveRatio(1000000) - - const tokensToDepositMany = [ - toGRT('1000'), // should mint if we start with number above minimum deposit - toGRT('1000'), // every time it should mint less GCS due to bonding curve... - toGRT('1000'), - toGRT('1000'), - toGRT('2000'), - toGRT('2000'), - toGRT('123'), - toGRT('1'), // should mint below minimum deposit - ] - - // Mint multiple times - for (const tokensToDeposit of tokensToDepositMany) { - const tx = await curation - .connect(curator.signer) - .mint(subgraphDeploymentID, tokensToDeposit, 0) - const receipt = await tx.wait() - const event: Event = receipt.events.pop() - const signal = event.args['signal'] - expect(tokensToDeposit).eq(signal) // we compare 1:1 ratio - } + describe('conservation', async function () { + it('should match multiple deposits and redeems back to initial state', async function () { + this.timeout(60000) // increase timeout for test runner + + const totalDeposits = toGRT('1000000000') + + // Signal multiple times + let totalSignal = toGRT('0') + for (const tokensToDeposit of chunkify(totalDeposits, 10)) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + totalSignal = totalSignal.add(signal) + } + + // Redeem signal multiple times + let totalTokens = toGRT('0') + for (const signalToRedeem of chunkify(totalSignal, 10)) { + const tx = await curation + .connect(curator.signer) + .burn(subgraphDeploymentID, signalToRedeem, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const tokens = event.args['tokens'] + totalTokens = totalTokens.add(tokens) + // console.log('<', formatEther(signalToRedeem), '=', formatEther(tokens)) + } + + // Conservation of work + const afterPool = await curation.pools(subgraphDeploymentID) + const afterPoolSignal = await curation.getCurationPoolSignal(subgraphDeploymentID) + expect(afterPool.tokens).eq(toGRT('0')) + expect(afterPoolSignal).eq(toGRT('0')) + expect(await curation.isCurated(subgraphDeploymentID)).eq(false) + expect(totalTokens).eq(totalDeposits) + }) + }) + + describe('multiple minting', async function () { + it('should mint equal signal', async function () { + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + for (const tokensToDeposit of tokensToDepositMany) { + const expectedSignal = await calcBondingCurve( + await curation.getCurationPoolSignal(subgraphDeploymentID), + await curation.getCurationPoolTokens(subgraphDeploymentID), + tokensToDeposit, + BIG_NUMBER_ZERO, + BigNumber.from(100), + await curation.initializationPeriod(), + await curation.initializationExitPeriod(), + await curation.defaultReserveRatio(), + await curation.minimumCurationDeposit(), + ) + + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + expect(toRound(toFloat(signal))).eq(toRound(expectedSignal)) + } + }) + + it('should mint when using the edge case of linear function', async function () { + // Setup edge case like linear function: 1 GRT = 1 GCS + await curation.setMinimumCurationDeposit(toGRT('1')) + await curation.setDefaultReserveRatio(1000000) + + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + + // Mint multiple times + for (const tokensToDeposit of tokensToDepositMany) { + const tx = await curation + .connect(curator.signer) + .mint(subgraphDeploymentID, tokensToDeposit, 0) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const signal = event.args['signal'] + expect(signal).eq(tokensToDeposit) // we compare 1:1 ratio + } + }) }) }) }) diff --git a/test/gns.test.ts b/test/gns.test.ts index 12986b21d..4bba1dda3 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -2,12 +2,19 @@ import { expect } from 'chai' import { ethers, ContractTransaction, BigNumber, Event } from 'ethers' import { GNS } from '../build/types/GNS' -import { getAccounts, randomHexBytes, Account, toGRT } from './lib/testHelpers' +import { + getAccounts, + randomHexBytes, + Account, + toGRT, + calcBondingCurve, + advanceBlockTo, +} from './lib/testHelpers' import { NetworkFixture } from './lib/fixtures' import { GraphToken } from '../build/types/GraphToken' import { Curation } from '../build/types/Curation' -import { toBN, formatGRT } from './lib/testHelpers' +import { toBN, formatGRT, BIG_NUMBER_ZERO } from './lib/testHelpers' interface Subgraph { graphAccount: Account @@ -71,18 +78,21 @@ describe('GNS', () => { async function calcGNSBondingCurve( gnsSupply: BigNumber, // nSignal gnsReserveBalance: BigNumber, // vSignal - gnsReserveRatio: number, // default reserve ratio of GNS depositAmount: BigNumber, // GRT deposited subgraphID: string, ): Promise { const signal = await curation.getCurationPoolSignal(subgraphID) const curationTokens = await curation.getCurationPoolTokens(subgraphID) - const curationReserveRatio = await curation.defaultReserveRatio() - const expectedSignal = await calcCurationBondingCurve( + const expectedSignal = await calcBondingCurve( signal, curationTokens, - curationReserveRatio, depositAmount, + BIG_NUMBER_ZERO, + BigNumber.from(100), + await curation.initializationPeriod(), + await curation.initializationExitPeriod(), + await curation.defaultReserveRatio(), + await curation.minimumCurationDeposit(), ) const expectedSignalBN = toGRT(String(expectedSignal.toFixed(18))) @@ -94,35 +104,6 @@ describe('GNS', () => { return (toFloat(gnsSupply) * toFloat(expectedSignalBN)) / toFloat(gnsReserveBalance) } - async function calcCurationBondingCurve( - supply: BigNumber, - reserveBalance: BigNumber, - reserveRatio: number, - depositAmount: BigNumber, - ): Promise { - // Handle the initialization of the bonding curve - const minSupply = toGRT('1') - if (supply.eq(0)) { - const minDeposit = await curation.minimumCurationDeposit() - if (depositAmount.lt(minDeposit)) { - throw new Error('deposit must be above minimum') - } - return ( - (await calcCurationBondingCurve( - minSupply, - minDeposit, - reserveRatio, - depositAmount.sub(minDeposit), - )) + toFloat(minSupply) - ) - } - // Calculate bonding curve in the test - return ( - toFloat(supply) * - ((1 + toFloat(depositAmount) / toFloat(reserveBalance)) ** (reserveRatio / 1000000) - 1) - ) - } - const publishNewSubgraph = async ( account: Account, graphAccount: string, @@ -529,458 +510,556 @@ describe('GNS', () => { await fixture.tearDown() }) - describe('Publishing names and versions', function () { - describe('setDefaultName', function () { - it('setDefaultName emits the event', async function () { - const tx = gns - .connect(me.signer) - .setDefaultName(me.address, 0, defaultName.nameIdentifier, defaultName.name) - await expect(tx) - .emit(gns, 'SetDefaultName') - .withArgs(subgraph0.graphAccount.address, 0, defaultName.nameIdentifier, defaultName.name) - }) + describe('when initialization phases exited', function () { + describe('Publishing names and versions', function () { + describe('setDefaultName', function () { + it('setDefaultName emits the event', async function () { + const tx = gns + .connect(me.signer) + .setDefaultName(me.address, 0, defaultName.nameIdentifier, defaultName.name) + await expect(tx) + .emit(gns, 'SetDefaultName') + .withArgs( + subgraph0.graphAccount.address, + 0, + defaultName.nameIdentifier, + defaultName.name, + ) + }) - it('setDefaultName fails if not owner', async function () { - const tx = gns - .connect(other.signer) - .setDefaultName(me.address, 0, defaultName.nameIdentifier, defaultName.name) - await expect(tx).revertedWith('GNS: Only graph account owner can call') + it('setDefaultName fails if not owner', async function () { + const tx = gns + .connect(other.signer) + .setDefaultName(me.address, 0, defaultName.nameIdentifier, defaultName.name) + await expect(tx).revertedWith('GNS: Only graph account owner can call') + }) }) - }) - describe('updateSubgraphMetadata', function () { - it('updateSubgraphMetadata emits the event', async function () { - const tx = gns - .connect(me.signer) - .updateSubgraphMetadata(me.address, 0, subgraph0.subgraphMetadata) - await expect(tx) - .emit(gns, 'SubgraphMetadataUpdated') - .withArgs(subgraph0.graphAccount.address, 0, subgraph0.subgraphMetadata) - }) + describe('updateSubgraphMetadata', function () { + it('updateSubgraphMetadata emits the event', async function () { + const tx = gns + .connect(me.signer) + .updateSubgraphMetadata(me.address, 0, subgraph0.subgraphMetadata) + await expect(tx) + .emit(gns, 'SubgraphMetadataUpdated') + .withArgs(subgraph0.graphAccount.address, 0, subgraph0.subgraphMetadata) + }) - it('updateSubgraphMetadata fails if not owner', async function () { - const tx = gns - .connect(other.signer) - .updateSubgraphMetadata(me.address, 0, subgraph0.subgraphMetadata) - await expect(tx).revertedWith('GNS: Only graph account owner can call') - }) - }) - describe('isPublished', function () { - it('should return if the subgraph is published', async function () { - expect(await gns.isPublished(subgraph0.graphAccount.address, 0)).eq(false) - await publishNewSubgraph(me, me.address, 0) - expect(await gns.isPublished(subgraph0.graphAccount.address, 0)).eq(true) + it('updateSubgraphMetadata fails if not owner', async function () { + const tx = gns + .connect(other.signer) + .updateSubgraphMetadata(me.address, 0, subgraph0.subgraphMetadata) + await expect(tx).revertedWith('GNS: Only graph account owner can call') + }) }) - }) - describe('publishNewSubgraph', async function () { - it('should publish a new subgraph and first version with it', async function () { - await publishNewSubgraph(me, me.address, 0) - // State updated - const deploymentID = await gns.subgraphs(subgraph0.graphAccount.address, 0) - expect(subgraph0.subgraphDeploymentID).eq(deploymentID) + describe('isPublished', function () { + it('should return if the subgraph is published', async function () { + expect(await gns.isPublished(subgraph0.graphAccount.address, 0)).eq(false) + await publishNewSubgraph(me, me.address, 0) + expect(await gns.isPublished(subgraph0.graphAccount.address, 0)).eq(true) + }) }) - it('should publish a new subgraph with an incremented value', async function () { - await publishNewSubgraph(me, me.address, 0) - await publishNewSubgraph(me, me.address, 1, subgraph1) - const deploymentID = await gns.subgraphs(subgraph1.graphAccount.address, 1) - expect(subgraph1.subgraphDeploymentID).eq(deploymentID) + describe('publishNewSubgraph', async function () { + it('should publish a new subgraph and first version with it', async function () { + await publishNewSubgraph(me, me.address, 0) + // State updated + const deploymentID = await gns.subgraphs(subgraph0.graphAccount.address, 0) + expect(subgraph0.subgraphDeploymentID).eq(deploymentID) + }) + + it('should publish a new subgraph with an incremented value', async function () { + await publishNewSubgraph(me, me.address, 0) + await publishNewSubgraph(me, me.address, 1, subgraph1) + const deploymentID = await gns.subgraphs(subgraph1.graphAccount.address, 1) + expect(subgraph1.subgraphDeploymentID).eq(deploymentID) + }) + + it('should reject publish if not sent from owner', async function () { + const tx = gns + .connect(other.signer) + .publishNewSubgraph( + subgraph0.graphAccount.address, + ethers.constants.HashZero, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) + await expect(tx).revertedWith('GNS: Only graph account owner can call') + }) + + it('should prevent subgraphDeploymentID of 0 to be used', async function () { + const tx = gns + .connect(me.signer) + .publishNewSubgraph( + subgraph0.graphAccount.address, + ethers.constants.HashZero, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) + await expect(tx).revertedWith('GNS: Cannot set deploymentID to 0 in publish') + }) }) - it('should reject publish if not sent from owner', async function () { - const tx = gns - .connect(other.signer) - .publishNewSubgraph( - subgraph0.graphAccount.address, - ethers.constants.HashZero, - subgraph0.versionMetadata, - subgraph0.subgraphMetadata, + describe('publishNewVersion', async function () { + beforeEach(async () => { + await publishNewSubgraph(me, me.address, 0) + await advanceBlockTo(100) + await mintNSignal(me, me.address, 0, tokens10000) + }) + + it('should publish a new version on an existing subgraph', async function () { + await publishNewVersion(me, me.address, 0, subgraph1) + }) + + it('should reject a new version with the same subgraph deployment ID', async function () { + const tx = gns + .connect(me.signer) + .publishNewVersion( + subgraph0.graphAccount.address, + 0, + subgraph0.subgraphDeploymentID, + subgraph0.versionMetadata, + ) + await expect(tx).revertedWith( + 'GNS: Cannot publish a new version with the same subgraph deployment ID', ) - await expect(tx).revertedWith('GNS: Only graph account owner can call') - }) + }) - it('should prevent subgraphDeploymentID of 0 to be used', async function () { - const tx = gns - .connect(me.signer) - .publishNewSubgraph( - subgraph0.graphAccount.address, - ethers.constants.HashZero, - subgraph0.versionMetadata, - subgraph0.subgraphMetadata, + it('should reject publishing a version to a numbered subgraph that does not exist', async function () { + const wrongNumberedSubgraph = 9999 + const tx = gns + .connect(me.signer) + .publishNewVersion( + subgraph1.graphAccount.address, + wrongNumberedSubgraph, + subgraph1.subgraphDeploymentID, + subgraph1.versionMetadata, + ) + await expect(tx).revertedWith( + 'GNS: Cannot update version if not published, or has been deprecated', ) - await expect(tx).revertedWith('GNS: Cannot set deploymentID to 0 in publish') - }) - }) + }) - describe('publishNewVersion', async function () { - beforeEach(async () => { - await publishNewSubgraph(me, me.address, 0) - await mintNSignal(me, me.address, 0, tokens10000) - }) + it('reject if not the owner', async function () { + const tx = gns + .connect(other.signer) + .publishNewVersion( + subgraph1.graphAccount.address, + 0, + subgraph1.subgraphDeploymentID, + subgraph1.versionMetadata, + ) + await expect(tx).revertedWith('GNS: Only graph account owner can call') + }) - it('should publish a new version on an existing subgraph', async function () { - await publishNewVersion(me, me.address, 0, subgraph1) - }) + it('should fail when upgrade tries to point to a pre-curated', async function () { + await curation.connect(me.signer).mint(subgraph1.subgraphDeploymentID, tokens1000, 0) + const tx = gns + .connect(me.signer) + .publishNewVersion( + me.address, + 0, + subgraph1.subgraphDeploymentID, + subgraph1.versionMetadata, + ) + await expect(tx).revertedWith( + 'GNS: Owner cannot point to a subgraphID that has been pre-curated', + ) + }) - it('should reject a new version with the same subgraph deployment ID', async function () { - const tx = gns - .connect(me.signer) - .publishNewVersion( - subgraph0.graphAccount.address, - 0, - subgraph0.subgraphDeploymentID, - subgraph0.versionMetadata, + it('should fail when trying to upgrade when there is no nSignal', async function () { + await burnNSignal(me, me.address, 0) + const tx = gns + .connect(me.signer) + .publishNewVersion( + me.address, + 0, + subgraph1.subgraphDeploymentID, + subgraph1.versionMetadata, + ) + await expect(tx).revertedWith( + 'GNS: There must be nSignal on this subgraph for curve math to work', ) - await expect(tx).revertedWith( - 'GNS: Cannot publish a new version with the same subgraph deployment ID', - ) - }) + }) - it('should reject publishing a version to a numbered subgraph that does not exist', async function () { - const wrongNumberedSubgraph = 9999 - const tx = gns - .connect(me.signer) - .publishNewVersion( - subgraph1.graphAccount.address, - wrongNumberedSubgraph, - subgraph1.subgraphDeploymentID, - subgraph1.versionMetadata, + it('should fail when subgraph is deprecated', async function () { + await deprecateSubgraph(me, me.address, 0) + const tx = gns + .connect(me.signer) + .publishNewVersion( + me.address, + 0, + subgraph1.subgraphDeploymentID, + subgraph1.versionMetadata, + ) + await expect(tx).revertedWith( + 'GNS: Cannot update version if not published, or has been deprecated', ) - await expect(tx).revertedWith( - 'GNS: Cannot update version if not published, or has been deprecated', - ) + }) }) - it('reject if not the owner', async function () { - const tx = gns - .connect(other.signer) - .publishNewVersion( - subgraph1.graphAccount.address, - 0, - subgraph1.subgraphDeploymentID, - subgraph1.versionMetadata, + describe('deprecateSubgraph', async function () { + beforeEach(async () => { + await publishNewSubgraph(me, me.address, 0) + await advanceBlockTo(100) + await mintNSignal(me, me.address, 0, tokens10000) + }) + + it('should deprecate a subgraph', async function () { + await deprecateSubgraph(me, me.address, 0) + }) + + it('should prevent a deprecated subgraph from being republished', async function () { + await deprecateSubgraph(me, me.address, 0) + const tx = gns + .connect(me.signer) + .publishNewVersion( + subgraph1.graphAccount.address, + 1, + subgraph1.subgraphDeploymentID, + subgraph1.versionMetadata, + ) + await expect(tx).revertedWith( + 'Cannot update version if not published, or has been deprecated', ) - await expect(tx).revertedWith('GNS: Only graph account owner can call') + }) + + it('reject if the subgraph does not exist', async function () { + const wrongNumberedSubgraph = 2340 + const tx = gns + .connect(me.signer) + .deprecateSubgraph(subgraph1.graphAccount.address, wrongNumberedSubgraph) + await expect(tx).revertedWith('GNS: Cannot deprecate a subgraph which does not exist') + }) + + it('reject deprecate if not the owner', async function () { + const tx = gns + .connect(other.signer) + .deprecateSubgraph(subgraph0.graphAccount.address, subgraph0.subgraphNumber) + await expect(tx).revertedWith('GNS: Only graph account owner can call') + }) }) + }) - it('should fail when upgrade tries to point to a pre-curated', async function () { - await curation.connect(me.signer).mint(subgraph1.subgraphDeploymentID, tokens1000, 0) - const tx = gns - .connect(me.signer) - .publishNewVersion( + describe('Curating on names', async function () { + const subgraphNumber0 = 0 + + describe('mintNSignal()', async function () { + it('should deposit into the name signal curve', async function () { + await publishNewSubgraph(me, me.address, subgraphNumber0) + await advanceBlockTo(100) + await mintNSignal(other, me.address, subgraphNumber0, tokens10000) + }) + + it('should fail when name signal is disabled', async function () { + await publishNewSubgraph(me, me.address, subgraphNumber0) + await advanceBlockTo(100) + await deprecateSubgraph(me, me.address, 0) + const tx = gns.connect(me.signer).mintNSignal(me.address, subgraphNumber0, tokens1000, 0) + await expect(tx).revertedWith('GNS: Cannot be disabled') + }) + + it('should fail if you try to deposit on a non existing name', async function () { + const tx = gns.connect(me.signer).mintNSignal(me.address, subgraphNumber0, tokens1000, 0) + await expect(tx).revertedWith('GNS: Must deposit on a name signal that exists') + }) + + it('reject minting if under slippage', async function () { + // First publish the subgraph + await publishNewSubgraph(me, me.address, subgraphNumber0) + await advanceBlockTo(100) + + // Set slippage to be 1 less than expected result to force reverting + const { 1: expectedNSignal } = await gns.tokensToNSignal( me.address, - 0, - subgraph1.subgraphDeploymentID, - subgraph1.versionMetadata, + subgraphNumber0, + tokens1000, ) - await expect(tx).revertedWith( - 'GNS: Owner cannot point to a subgraphID that has been pre-curated', - ) + const tx = gns + .connect(me.signer) + .mintNSignal(me.address, subgraphNumber0, tokens1000, expectedNSignal.add(1)) + await expect(tx).revertedWith('Slippage protection') + }) }) - it('should fail when trying to upgrade when there is no nSignal', async function () { - await burnNSignal(me, me.address, 0) - const tx = gns - .connect(me.signer) - .publishNewVersion( + describe('burnNSignal()', async function () { + beforeEach(async () => { + await publishNewSubgraph(me, me.address, subgraphNumber0) + await advanceBlockTo(100) + await mintNSignal(other, me.address, subgraphNumber0, tokens10000) + }) + + it('should withdraw from the name signal curve', async function () { + await burnNSignal(other, me.address, subgraphNumber0) + }) + + it('should fail when name signal is disabled', async function () { + await deprecateSubgraph(me, me.address, 0) + // just test 1 since it will fail + const tx = gns.connect(me.signer).burnNSignal(me.address, subgraphNumber0, 1, 0) + await expect(tx).revertedWith('GNS: Cannot be disabled') + }) + + it('should fail when the curator tries to withdraw more nSignal than they have', async function () { + const tx = gns.connect(me.signer).burnNSignal( me.address, + subgraphNumber0, + // 1000000 * 10^18 nSignal is a lot, and will cause fail + toBN('1000000000000000000000000'), 0, - subgraph1.subgraphDeploymentID, - subgraph1.versionMetadata, ) - await expect(tx).revertedWith( - 'GNS: There must be nSignal on this subgraph for curve math to work', - ) - }) + await expect(tx).revertedWith('GNS: Curator cannot withdraw more nSignal than they have') + }) - it('should fail when subgraph is deprecated', async function () { - await deprecateSubgraph(me, me.address, 0) - const tx = gns - .connect(me.signer) - .publishNewVersion( + it('reject burning if under slippage', async function () { + // Get current curator name signal + const curatorNSignal = await gns.getCuratorNSignal( me.address, - 0, - subgraph1.subgraphDeploymentID, - subgraph1.versionMetadata, + subgraphNumber0, + other.address, ) - await expect(tx).revertedWith( - 'GNS: Cannot update version if not published, or has been deprecated', - ) - }) - }) - - describe('deprecateSubgraph', async function () { - beforeEach(async () => { - await publishNewSubgraph(me, me.address, 0) - await mintNSignal(me, me.address, 0, tokens10000) - }) - - it('should deprecate a subgraph', async function () { - await deprecateSubgraph(me, me.address, 0) - }) - it('should prevent a deprecated subgraph from being republished', async function () { - await deprecateSubgraph(me, me.address, 0) - const tx = gns - .connect(me.signer) - .publishNewVersion( - subgraph1.graphAccount.address, - 1, - subgraph1.subgraphDeploymentID, - subgraph1.versionMetadata, + // Withdraw + const { 1: expectedTokens } = await gns.nSignalToTokens( + me.address, + subgraphNumber0, + curatorNSignal, ) - await expect(tx).revertedWith( - 'Cannot update version if not published, or has been deprecated', - ) - }) - it('reject if the subgraph does not exist', async function () { - const wrongNumberedSubgraph = 2340 - const tx = gns - .connect(me.signer) - .deprecateSubgraph(subgraph1.graphAccount.address, wrongNumberedSubgraph) - await expect(tx).revertedWith('GNS: Cannot deprecate a subgraph which does not exist') + // Force a revert by asking 1 more token than the function will return + const tx = gns + .connect(other.signer) + .burnNSignal(me.address, subgraphNumber0, curatorNSignal, expectedTokens.add(1)) + await expect(tx).revertedWith('Slippage protection') + }) }) - it('reject deprecate if not the owner', async function () { - const tx = gns - .connect(other.signer) - .deprecateSubgraph(subgraph0.graphAccount.address, subgraph0.subgraphNumber) - await expect(tx).revertedWith('GNS: Only graph account owner can call') - }) - }) - }) - describe('Curating on names', async function () { - const subgraphNumber0 = 0 + describe('withdraw()', async function () { + beforeEach(async () => { + await publishNewSubgraph(me, me.address, subgraphNumber0) + await advanceBlockTo(100) + await mintNSignal(other, me.address, subgraphNumber0, tokens10000) + }) - describe('mintNSignal()', async function () { - it('should deposit into the name signal curve', async function () { - await publishNewSubgraph(me, me.address, subgraphNumber0) - await mintNSignal(other, me.address, subgraphNumber0, tokens10000) - }) + it('should withdraw GRT from a disabled name signal', async function () { + await deprecateSubgraph(me, me.address, 0) + await withdraw(other, me.address, subgraphNumber0) + }) - it('should fail when name signal is disabled', async function () { - await publishNewSubgraph(me, me.address, subgraphNumber0) - await deprecateSubgraph(me, me.address, 0) - const tx = gns.connect(me.signer).mintNSignal(me.address, subgraphNumber0, tokens1000, 0) - await expect(tx).revertedWith('GNS: Cannot be disabled') - }) + it('should fail if not disabled', async function () { + const tx = gns.connect(other.signer).withdraw(me.address, subgraphNumber0) + await expect(tx).revertedWith('GNS: Name bonding curve must be disabled first') + }) - it('should fail if you try to deposit on a non existing name', async function () { - const tx = gns.connect(me.signer).mintNSignal(me.address, subgraphNumber0, tokens1000, 0) - await expect(tx).revertedWith('GNS: Must deposit on a name signal that exists') - }) + it('should fail when there is no more GRT to withdraw', async function () { + await deprecateSubgraph(me, me.address, 0) + await withdraw(other, me.address, subgraphNumber0) + const tx = gns.connect(other.signer).withdraw(me.address, subgraphNumber0) + await expect(tx).revertedWith('GNS: No more GRT to withdraw') + }) - it('reject minting if under slippage', async function () { - // First publish the subgraph - await publishNewSubgraph(me, me.address, subgraphNumber0) - - // Set slippage to be 1 less than expected result to force reverting - const { 1: expectedNSignal } = await gns.tokensToNSignal( - me.address, - subgraphNumber0, - tokens1000, - ) - const tx = gns - .connect(me.signer) - .mintNSignal(me.address, subgraphNumber0, tokens1000, expectedNSignal.add(1)) - await expect(tx).revertedWith('Slippage protection') + it('should fail if the curator has no nSignal', async function () { + await deprecateSubgraph(me, me.address, 0) + const tx = gns.connect(me.signer).withdraw(me.address, subgraphNumber0) + await expect(tx).revertedWith('GNS: Curator must have some nSignal to withdraw GRT') + }) }) - }) - describe('burnNSignal()', async function () { - beforeEach(async () => { - await publishNewSubgraph(me, me.address, subgraphNumber0) - await mintNSignal(other, me.address, subgraphNumber0, tokens10000) - }) + describe('multiple minting', async function () { + it('should mint less signal every time due to the bonding curve', async function () { + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1.06'), // should mint minimum deposit including tax + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + ] + await publishNewSubgraph(me, me.address, 0) + await advanceBlockTo(100) + + // State updated + const curationTaxPercentage = await curation.curationTaxPercentage() + + for (const tokensToDeposit of tokensToDepositMany) { + const poolOld = await gns.nameSignals(me.address, 0) + expect(subgraph0.subgraphDeploymentID).eq(poolOld.subgraphDeploymentID) + + const curationTax = toBN(curationTaxPercentage).mul(tokensToDeposit).div(toBN(1000000)) + const expectedNSignal = await calcGNSBondingCurve( + poolOld.nSignal, + poolOld.vSignal, + tokensToDeposit.sub(curationTax), + poolOld.subgraphDeploymentID, + ) + const tx = await mintNSignal(me, me.address, 0, tokensToDeposit) + const receipt = await tx.wait() + const event: Event = receipt.events.pop() + const nSignalCreated = event.args['nSignalCreated'] + expect(toRound(expectedNSignal)).eq(toRound(toFloat(nSignalCreated))) + } + }) - it('should withdraw from the name signal curve', async function () { - await burnNSignal(other, me.address, subgraphNumber0) - }) + it('should mint when using the edge case of linear function', async function () { + // Setup edge case like linear function: 1 vSignal = 1 nSignal = 1 token + await curation.setMinimumCurationDeposit(toGRT('1')) + await curation.setDefaultReserveRatio(1000000) + // note - reserve ratio is already set to 1000000 in GNS + + const tokensToDepositMany = [ + toGRT('1000'), // should mint if we start with number above minimum deposit + toGRT('1000'), // every time it should mint less GCS due to bonding curve... + toGRT('1000'), + toGRT('1000'), + toGRT('2000'), + toGRT('2000'), + toGRT('123'), + toGRT('1'), // should mint below minimum deposit + ] + + await publishNewSubgraph(me, me.address, 0) + await advanceBlockTo(100) + + // State updated + for (const tokensToDeposit of tokensToDepositMany) { + await mintNSignal(me, me.address, 0, tokensToDeposit) + } + }) - it('should fail when name signal is disabled', async function () { - await deprecateSubgraph(me, me.address, 0) - // just test 1 since it will fail - const tx = gns.connect(me.signer).burnNSignal(me.address, subgraphNumber0, 1, 0) - await expect(tx).revertedWith('GNS: Cannot be disabled') - }) + describe('setOwnerTaxPercentage', function () { + const newValue = 10 - it('should fail when the curator tries to withdraw more nSignal than they have', async function () { - const tx = gns.connect(me.signer).burnNSignal( - me.address, - subgraphNumber0, - // 1000000 * 10^18 nSignal is a lot, and will cause fail - toBN('1000000000000000000000000'), - 0, - ) - await expect(tx).revertedWith('GNS: Curator cannot withdraw more nSignal than they have') - }) + it('should set `ownerTaxPercentage`', async function () { + // Can set if allowed + await gns.connect(governor.signer).setOwnerTaxPercentage(newValue) + expect(await gns.ownerTaxPercentage()).eq(newValue) + }) + + it('reject set `ownerTaxPercentage` if out of bounds', async function () { + const tx = gns.connect(governor.signer).setOwnerTaxPercentage(1000001) + await expect(tx).revertedWith('Owner tax must be MAX_PPM or less') + }) - it('reject burning if under slippage', async function () { - // Get current curator name signal - const curatorNSignal = await gns.getCuratorNSignal( - me.address, - subgraphNumber0, - other.address, - ) - - // Withdraw - const { 1: expectedTokens } = await gns.nSignalToTokens( - me.address, - subgraphNumber0, - curatorNSignal, - ) - - // Force a revert by asking 1 more token than the function will return - const tx = gns - .connect(other.signer) - .burnNSignal(me.address, subgraphNumber0, curatorNSignal, expectedTokens.add(1)) - await expect(tx).revertedWith('Slippage protection') + it('reject set `ownerTaxPercentage` if not allowed', async function () { + const tx = gns.connect(me.signer).setOwnerTaxPercentage(newValue) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + }) }) }) - describe('withdraw()', async function () { - beforeEach(async () => { - await publishNewSubgraph(me, me.address, subgraphNumber0) - await mintNSignal(other, me.address, subgraphNumber0, tokens10000) - }) + describe('Two named subgraphs point to the same subgraph deployment ID', function () { + it('handle initialization under minimum signal values', async function () { + await curation.setMinimumCurationDeposit(toGRT('1')) - it('should withdraw GRT from a disabled name signal', async function () { - await deprecateSubgraph(me, me.address, 0) - await withdraw(other, me.address, subgraphNumber0) - }) + // Publish a named subgraph-0 -> subgraphDeployment0 + await gns + .connect(me.signer) + .publishNewSubgraph( + me.address, + subgraph0.subgraphDeploymentID, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) + await advanceBlockTo(100) + // Curate on the first subgraph + await gns.connect(me.signer).mintNSignal(me.address, 0, toGRT('90000'), 0) - it('should fail if not disabled', async function () { - const tx = gns.connect(other.signer).withdraw(me.address, subgraphNumber0) - await expect(tx).revertedWith('GNS: Name bonding curve must be disabled first') + // Publish a named subgraph-1 -> subgraphDeployment0 + await gns + .connect(me.signer) + .publishNewSubgraph( + me.address, + subgraph0.subgraphDeploymentID, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) + await advanceBlockTo(150) + // Curate on the second subgraph should work + await gns.connect(me.signer).mintNSignal(me.address, 1, toGRT('10'), 0) }) + }) + }) - it('should fail when there is no more GRT to withdraw', async function () { - await deprecateSubgraph(me, me.address, 0) - await withdraw(other, me.address, subgraphNumber0) - const tx = gns.connect(other.signer).withdraw(me.address, subgraphNumber0) - await expect(tx).revertedWith('GNS: No more GRT to withdraw') - }) + describe('batch calls', function () { + it('should publish new subgraph and mint signal in single transaction', async function () { + // Create a subgraph + const tx1 = await gns.populateTransaction.publishNewSubgraph( + me.address, + subgraph0.subgraphDeploymentID, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) + // Curate on the subgraph + const subgraphNumber = await gns.graphAccountSubgraphNumbers(me.address) + const tx2 = await gns.populateTransaction.mintNSignal( + me.address, + subgraphNumber, + toGRT('90000'), + 0, + ) - it('should fail if the curator has no nSignal', async function () { - await deprecateSubgraph(me, me.address, 0) - const tx = gns.connect(me.signer).withdraw(me.address, subgraphNumber0) - await expect(tx).revertedWith('GNS: Curator must have some nSignal to withdraw GRT') - }) + // Batch send transaction + await gns.connect(me.signer).multicall([tx1.data, tx2.data]) }) - describe('multiple minting', async function () { - it('should mint less signal every time due to the bonding curve', async function () { - const tokensToDepositMany = [ - toGRT('1000'), // should mint if we start with number above minimum deposit - toGRT('1000'), // every time it should mint less GCS due to bonding curve... - toGRT('1.06'), // should mint minimum deposit including tax - toGRT('1000'), - toGRT('1000'), - toGRT('2000'), - toGRT('2000'), - toGRT('123'), - ] - await publishNewSubgraph(me, me.address, 0) - - // State updated - const curationTaxPercentage = await curation.curationTaxPercentage() - - for (const tokensToDeposit of tokensToDepositMany) { - const poolOld = await gns.nameSignals(me.address, 0) - expect(subgraph0.subgraphDeploymentID).eq(poolOld.subgraphDeploymentID) - - const curationTax = toBN(curationTaxPercentage).mul(tokensToDeposit).div(toBN(1000000)) - const expectedNSignal = await calcGNSBondingCurve( - poolOld.nSignal, - poolOld.vSignal, - poolOld.reserveRatio, - tokensToDeposit.sub(curationTax), - poolOld.subgraphDeploymentID, - ) - const tx = await mintNSignal(me, me.address, 0, tokensToDeposit) - const receipt = await tx.wait() - const event: Event = receipt.events.pop() - const nSignalCreated = event.args['nSignalCreated'] - expect(toRound(expectedNSignal)).eq(toRound(toFloat(nSignalCreated))) - } - }) + it('should revert if batching a call to non-authorized function', async function () { + // Call a forbidden function + const tx1 = await gns.populateTransaction.setOwnerTaxPercentage(100) - it('should mint when using the edge case of linear function', async function () { - // Setup edge case like linear function: 1 vSignal = 1 nSignal = 1 token - await curation.setMinimumCurationDeposit(toGRT('1')) - await curation.setDefaultReserveRatio(1000000) - // note - reserve ratio is already set to 1000000 in GNS - - const tokensToDepositMany = [ - toGRT('1000'), // should mint if we start with number above minimum deposit - toGRT('1000'), // every time it should mint less GCS due to bonding curve... - toGRT('1000'), - toGRT('1000'), - toGRT('2000'), - toGRT('2000'), - toGRT('123'), - toGRT('1'), // should mint below minimum deposit - ] - - await publishNewSubgraph(me, me.address, 0) - - // State updated - for (const tokensToDeposit of tokensToDepositMany) { - await mintNSignal(me, me.address, 0, tokensToDeposit) - } - }) + // Create a subgraph + const tx2 = await gns.populateTransaction.publishNewSubgraph( + me.address, + subgraph0.subgraphDeploymentID, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) - describe('setOwnerTaxPercentage', function () { - const newValue = 10 + // Batch send transaction + const tx = gns.connect(me.signer).multicall([tx1.data, tx2.data]) + await expect(tx).revertedWith('Caller must be Controller governor') + }) - it('should set `ownerTaxPercentage`', async function () { - // Can set if allowed - await gns.connect(governor.signer).setOwnerTaxPercentage(newValue) - expect(await gns.ownerTaxPercentage()).eq(newValue) - }) + it('should revert if batching a call to initialize', async function () { + // Call a forbidden function + const tx1 = await gns.populateTransaction.initialize(me.address, me.address, me.address) - it('reject set `ownerTaxPercentage` if out of bounds', async function () { - const tx = gns.connect(governor.signer).setOwnerTaxPercentage(1000001) - await expect(tx).revertedWith('Owner tax must be MAX_PPM or less') - }) + // Create a subgraph + const tx2 = await gns.populateTransaction.publishNewSubgraph( + me.address, + subgraph0.subgraphDeploymentID, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) - it('reject set `ownerTaxPercentage` if not allowed', async function () { - const tx = gns.connect(me.signer).setOwnerTaxPercentage(newValue) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - }) + // Batch send transaction + const tx = gns.connect(me.signer).multicall([tx1.data, tx2.data]) + await expect(tx).revertedWith('Caller must be the implementation') }) - }) - describe('Two named subgraphs point to the same subgraph deployment ID', function () { - it('handle initialization under minimum signal values', async function () { - await curation.setMinimumCurationDeposit(toGRT('1')) - - // Publish a named subgraph-0 -> subgraphDeployment0 - await gns - .connect(me.signer) - .publishNewSubgraph( - me.address, - subgraph0.subgraphDeploymentID, - subgraph0.versionMetadata, - subgraph0.subgraphMetadata, - ) - // Curate on the first subgraph - await gns.connect(me.signer).mintNSignal(me.address, 0, toGRT('90000'), 0) - - // Publish a named subgraph-1 -> subgraphDeployment0 - await gns - .connect(me.signer) - .publishNewSubgraph( - me.address, - subgraph0.subgraphDeploymentID, - subgraph0.versionMetadata, - subgraph0.subgraphMetadata, - ) - // Curate on the second subgraph should work - await gns.connect(me.signer).mintNSignal(me.address, 1, toGRT('10'), 0) + it('should revert if trying to call a private function', async function () { + // Craft call a private function + const hash = ethers.utils.id('_setOwnerTaxPercentage(uint32)') + const functionHash = hash.slice(0, 10) + const calldata = ethers.utils.arrayify( + ethers.utils.defaultAbiCoder.encode(['uint32'], ['100']), + ) + const bogusPayload = ethers.utils.concat([functionHash, calldata]) + + // Create a subgraph + const tx2 = await gns.populateTransaction.publishNewSubgraph( + me.address, + subgraph0.subgraphDeploymentID, + subgraph0.versionMetadata, + subgraph0.subgraphMetadata, + ) + + // Batch send transaction + const tx = gns.connect(me.signer).multicall([bogusPayload, tx2.data]) + await expect(tx).revertedWith('') }) }) }) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index f04ba5b81..c73324709 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -26,11 +26,20 @@ logger.pause() // Default configuration used in tests +export interface CurationLoadOptions { + initializationDays?: number + initializationExitDays?: number + blocksPerDay?: number +} + export const defaults = { curation: { reserveRatio: toBN('500000'), minimumCurationDeposit: toGRT('100'), curationTaxPercentage: 0, + initializationDays: 1, + initializationExitDays: 2, + blocksPerDay: 1, }, dispute: { minimumDeposit: toGRT('100'), @@ -114,6 +123,7 @@ export async function deployCuration( deployer: Signer, controller: string, proxyAdmin: GraphProxyAdmin, + options?: CurationLoadOptions, ): Promise { // Dependency const bondingCurve = (await deployContract('BancorFormula', deployer)) as unknown as BancorFormula @@ -128,6 +138,9 @@ export async function deployCuration( defaults.curation.reserveRatio, defaults.curation.curationTaxPercentage, defaults.curation.minimumCurationDeposit, + options?.initializationDays || defaults.curation.initializationDays, + options?.initializationExitDays || defaults.curation.initializationExitDays, + options?.blocksPerDay || defaults.curation.blocksPerDay, ], deployer, ) as unknown as Curation diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 00b151107..340880e6c 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -4,6 +4,10 @@ import { utils, Wallet, Signer } from 'ethers' import * as deployment from './deployment' import { evmSnapshot, evmRevert } from './testHelpers' +interface loadOptions { + curationOptions?: deployment.CurationLoadOptions +} + export class NetworkFixture { lastSnapshotId: number @@ -13,6 +17,7 @@ export class NetworkFixture { async load( deployer: Signer, + options: loadOptions = {}, slasher: Signer = Wallet.createRandom() as Signer, arbitrator: Signer = Wallet.createRandom() as Signer, ): Promise { @@ -29,7 +34,12 @@ export class NetworkFixture { proxyAdmin, ) const grt = await deployment.deployGRT(deployer) - const curation = await deployment.deployCuration(deployer, controller.address, proxyAdmin) + const curation = await deployment.deployCuration( + deployer, + controller.address, + proxyAdmin, + options?.curationOptions, + ) const gns = await deployment.deployGNS(deployer, controller.address, proxyAdmin) const staking = await deployment.deployStaking(deployer, controller.address, proxyAdmin) const disputeManager = await deployment.deployDisputeManager( @@ -53,6 +63,7 @@ export class NetworkFixture { await controller.setContractProxy(utils.id('EpochManager'), epochManager.address) await controller.setContractProxy(utils.id('GraphToken'), grt.address) await controller.setContractProxy(utils.id('Curation'), curation.address) + await controller.setContractProxy(utils.id('GNS'), gns.address) await controller.setContractProxy(utils.id('Staking'), staking.address) await controller.setContractProxy(utils.id('DisputeManager'), staking.address) await controller.setContractProxy(utils.id('RewardsManager'), rewardsManager.address) diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index 7fd322719..9e546cc31 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -15,6 +15,9 @@ export const toGRT = (value: string | number): BigNumber => { export const formatGRT = (value: BigNumber): string => formatUnits(value, '18') export const randomHexBytes = (n = 32): string => hexlify(randomBytes(n)) export const randomAddress = (): string => getAddress(randomHexBytes(20)) +export const BIG_NUMBER_ZERO = BigNumber.from(0) + +const toFloat = (n: BigNumber) => parseFloat(formatGRT(n)) // Network @@ -84,6 +87,83 @@ export const advanceToNextEpoch = async (epochManager: EpochManager): Promise => { + const _blockNumber = blockNumber.toNumber() + const _createdAt = createdAt.toNumber() + const _initializationPeriod = initializationPeriod.toNumber() + const _initializationExitPeriod = initializationExitPeriod.toNumber() + + // Steady state reserve ratio + let effectiveReserveRatio = defaultReserveRatio + + // Initialization phase reserve ratio + if (_blockNumber <= _createdAt + _initializationPeriod) { + effectiveReserveRatio = 1000000 + + // Initialization exit phase reserve ratio + } else if (_blockNumber <= _createdAt + _initializationPeriod + _initializationExitPeriod) { + const percentExited = + (_blockNumber - (_createdAt + _initializationPeriod)) / _initializationExitPeriod + effectiveReserveRatio = 1 - (1 - defaultReserveRatio) / percentExited + } + + return effectiveReserveRatio +} + +export const calcBondingCurve = async ( + supply: BigNumber, + reserveBalance: BigNumber, + depositAmount: BigNumber, + curationCreatedAt: BigNumber, + currentBlockNumber: BigNumber, + initializationPeriod: BigNumber, + initializationExitPeriod: BigNumber, + defaultReserveRatio: number, + minimumCurationDeposit: BigNumber, +): Promise => { + const effectiveReserveRatio = await getEffectiveReserveRatio( + currentBlockNumber, + curationCreatedAt, + initializationPeriod, + initializationExitPeriod, + defaultReserveRatio, + ) + + // Handle the initialization of the bonding curve + if (supply.eq(0)) { + if (depositAmount.lt(minimumCurationDeposit)) { + throw new Error('deposit must be above minimum') + } + + const minSupply = toGRT('1') + return ( + (await calcBondingCurve( + minSupply, + minimumCurationDeposit, + depositAmount.sub(minimumCurationDeposit), + curationCreatedAt, + currentBlockNumber, + initializationPeriod, + initializationExitPeriod, + defaultReserveRatio, + minimumCurationDeposit, + )) + toFloat(minSupply) + ) + } + // Calculate bonding curve in the test + return ( + toFloat(supply) * + ((1 + toFloat(depositAmount) / toFloat(reserveBalance)) ** (effectiveReserveRatio / 1000000) - + 1) + ) +} + export const evmSnapshot = async (): Promise => provider().send('evm_snapshot', []) export const evmRevert = async (id: number): Promise => provider().send('evm_revert', [id]) diff --git a/test/staking/delegation.test.ts b/test/staking/delegation.test.ts index 15dd64181..130a38b5a 100644 --- a/test/staking/delegation.test.ts +++ b/test/staking/delegation.test.ts @@ -400,7 +400,7 @@ describe('Staking::Delegation', () => { await shouldDelegate(delegator2, toGRT('5000')) }) - it('should delegate a high number of tokens', async function () { + it('should delegate a high amount of tokens', async function () { await shouldDelegate(delegator, toGRT('100')) await shouldDelegate(delegator, toGRT('1000000000000000000')) }) @@ -416,9 +416,10 @@ describe('Staking::Delegation', () => { await shouldDelegate(delegator, toGRT('10000000')) }) - it('should delegate and burn delegation deposit tax (100%)', async function () { + it('reject delegate with delegation deposit tax (100%)', async function () { await staking.setDelegationTaxPercentage(1000000) - await shouldDelegate(delegator, toGRT('10000000')) + const tx = staking.connect(delegator.signer).delegate(indexer.address, toGRT('10000000')) + await expect(tx).revertedWith('!shares') }) }) }) @@ -649,5 +650,25 @@ describe('Staking::Delegation', () => { const afterDelegationPool = await staking.delegationPools(indexer.address) expect(afterDelegationPool.tokens).eq(beforeDelegationPool.tokens.add(delegationFees)) }) + + it('revert if it cannot assign the smallest amount of shares', async function () { + // Init the delegation pool + await shouldDelegate(delegator, tokensToDelegate) + + // Collect funds thru full allocation cycle + await staking.connect(governor.signer).setDelegationRatio(10) + await staking.connect(indexer.signer).setDelegationParameters(0, 0, 0) + await setupAllocation(tokensToAllocate) + await staking.connect(assetHolder.signer).collect(tokensToCollect, allocationID) + await advanceToNextEpoch(epochManager) + await staking.connect(indexer.signer).closeAllocation(allocationID, poi) + await advanceToNextEpoch(epochManager) + await staking.connect(indexer.signer).claim(allocationID, true) + + // Delegate with such small amount of tokens (1 wei) that we do not have enough precision + // to even assign 1 wei of shares + const tx = staking.connect(delegator.signer).delegate(indexer.address, toBN(1)) + await expect(tx).revertedWith('!shares') + }) }) }) diff --git a/test/staking/staking.test.ts b/test/staking/staking.test.ts index 5fc359f0d..8db3e530c 100644 --- a/test/staking/staking.test.ts +++ b/test/staking/staking.test.ts @@ -14,10 +14,11 @@ import { latestBlock, toBN, toGRT, + provider, Account, } from '../lib/testHelpers' -const { AddressZero } = constants +const { AddressZero, MaxUint256 } = constants function weightedAverage( valueA: BigNumber, @@ -269,14 +270,18 @@ describe('Staking:Stakes', () => { expect(afterIndexerBalance).eq(beforeIndexerBalance.add(tokensToUnstake)) }) - it('reject unstake zero tokens', async function () { - const tx = staking.connect(indexer.signer).unstake(toGRT('0')) - await expect(tx).revertedWith('!tokens') + it('should unstake available tokens even if passed a higher amount', async function () { + // Try to unstake a bit more than currently staked + const tokensOverCapacity = tokensToStake.add(toGRT('1')) + await staking.connect(indexer.signer).unstake(tokensOverCapacity) + + // Check state + const tokensLocked = (await staking.stakes(indexer.address)).tokensLocked + expect(tokensLocked).eq(tokensToStake) }) - it('reject unstake more than available tokens', async function () { - const tokensOverCapacity = tokensToStake.add(toGRT('1')) - const tx = staking.connect(indexer.signer).unstake(tokensOverCapacity) + it('reject unstake zero tokens', async function () { + const tx = staking.connect(indexer.signer).unstake(toGRT('0')) await expect(tx).revertedWith('!stake-avail') }) @@ -305,6 +310,28 @@ describe('Staking:Stakes', () => { await staking.connect(indexer.signer).unstake(tokensToStake) expect(await staking.getIndexerCapacity(indexer.address)).eq(0) }) + + it('should allow unstake of full amount with no upper limits', async function () { + // Use manual mining + await provider().send('evm_setAutomine', [false]) + + // Setup + const newTokens = toGRT('2') + const stakedTokens = await staking.getIndexerStakedTokens(indexer.address) + const tokensToUnstake = stakedTokens.add(newTokens) + + // StakeTo & Unstake + await staking.connect(indexer.signer).stakeTo(indexer.address, newTokens) + await staking.connect(indexer.signer).unstake(MaxUint256) + await provider().send('evm_mine', []) + + // Check state + const tokensLocked = (await staking.stakes(indexer.address)).tokensLocked + expect(tokensLocked).eq(tokensToUnstake) + + // Restore automine + await provider().send('evm_setAutomine', [true]) + }) }) describe('withdraw', function () { diff --git a/yarn.lock b/yarn.lock index 423a8a5c2..f45943cdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -243,6 +243,21 @@ "@ethersproject/properties" "^5.4.0" "@ethersproject/strings" "^5.4.0" +"@ethersproject/abi@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.4.1.tgz#6ac28fafc9ef6f5a7a37e30356a2eb31fa05d39b" + integrity sha512-9mhbjUk76BiSluiiW4BaYyI58KSbDMMQpCLdsAR+RsT2GyATiNYxVv+pGWRrekmsIdY3I+hOqsYQSTkc8L/mcg== + dependencies: + "@ethersproject/address" "^5.4.0" + "@ethersproject/bignumber" "^5.4.0" + "@ethersproject/bytes" "^5.4.0" + "@ethersproject/constants" "^5.4.0" + "@ethersproject/hash" "^5.4.0" + "@ethersproject/keccak256" "^5.4.0" + "@ethersproject/logger" "^5.4.0" + "@ethersproject/properties" "^5.4.0" + "@ethersproject/strings" "^5.4.0" + "@ethersproject/abstract-provider@5.4.1", "@ethersproject/abstract-provider@^5.4.0": version "5.4.1" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.4.1.tgz#e404309a29f771bd4d28dbafadcaa184668c2a6e" @@ -302,6 +317,15 @@ "@ethersproject/logger" "^5.4.0" bn.js "^4.11.9" +"@ethersproject/bignumber@5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.4.2.tgz#44232e015ae4ce82ac034de549eb3583c71283d8" + integrity sha512-oIBDhsKy5bs7j36JlaTzFgNPaZjiNDOXsdSgSpXRucUl+UA6L/1YLlFeI3cPAoodcenzF4nxNPV13pcy7XbWjA== + dependencies: + "@ethersproject/bytes" "^5.4.0" + "@ethersproject/logger" "^5.4.0" + bn.js "^4.11.9" + "@ethersproject/bytes@5.4.0", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.4.0.tgz#56fa32ce3bf67153756dbaefda921d1d4774404e" @@ -405,6 +429,11 @@ resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.0.tgz#f39adadf62ad610c420bcd156fd41270e91b3ca9" integrity sha512-xYdWGGQ9P2cxBayt64d8LC8aPFJk6yWCawQi/4eJ4+oJdMMjEBMrIcIMZ9AxhwpPVmnBPrsB10PcXGmGAqgUEQ== +"@ethersproject/logger@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.1.tgz#503bd33683538b923c578c07d1c2c0dd18672054" + integrity sha512-DZ+bRinnYLPw1yAC64oRl0QyVZj43QeHIhVKfD/+YwSz4wsv1pfwb5SOFjz+r710YEWzU6LrhuSjpSO+6PeE4A== + "@ethersproject/networks@5.4.2", "@ethersproject/networks@^5.4.0": version "5.4.2" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.4.2.tgz#2247d977626e97e2c3b8ee73cd2457babde0ce35" @@ -427,6 +456,13 @@ dependencies: "@ethersproject/logger" "^5.4.0" +"@ethersproject/properties@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.4.1.tgz#9f051f976ce790142c6261ccb7b826eaae1f2f36" + integrity sha512-cyCGlF8wWlIZyizsj2PpbJ9I7rIlUAfnHYwy/T90pdkSn/NFTa5YWZx2wTJBe9V7dD65dcrrEMisCRUJiq6n3w== + dependencies: + "@ethersproject/logger" "^5.4.0" + "@ethersproject/providers@5.4.3": version "5.4.3" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.3.tgz#4cd7ccd9e12bc3875b33df8b24abf735663958a5" @@ -452,6 +488,31 @@ bech32 "1.1.4" ws "7.4.6" +"@ethersproject/providers@5.4.5": + version "5.4.5" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.5.tgz#eb2ea2a743a8115f79604a8157233a3a2c832928" + integrity sha512-1GkrvkiAw3Fj28cwi1Sqm8ED1RtERtpdXmRfwIBGmqBSN5MoeRUHuwHPppMtbPayPgpFcvD7/Gdc9doO5fGYgw== + dependencies: + "@ethersproject/abstract-provider" "^5.4.0" + "@ethersproject/abstract-signer" "^5.4.0" + "@ethersproject/address" "^5.4.0" + "@ethersproject/basex" "^5.4.0" + "@ethersproject/bignumber" "^5.4.0" + "@ethersproject/bytes" "^5.4.0" + "@ethersproject/constants" "^5.4.0" + "@ethersproject/hash" "^5.4.0" + "@ethersproject/logger" "^5.4.0" + "@ethersproject/networks" "^5.4.0" + "@ethersproject/properties" "^5.4.0" + "@ethersproject/random" "^5.4.0" + "@ethersproject/rlp" "^5.4.0" + "@ethersproject/sha2" "^5.4.0" + "@ethersproject/strings" "^5.4.0" + "@ethersproject/transactions" "^5.4.0" + "@ethersproject/web" "^5.4.0" + bech32 "1.1.4" + ws "7.4.6" + "@ethersproject/random@5.4.0", "@ethersproject/random@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.4.0.tgz#9cdde60e160d024be39cc16f8de3b9ce39191e16" @@ -695,9 +756,9 @@ "@types/web3" "1.0.19" "@openzeppelin/contracts@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1.tgz#03c891fec7f93be0ae44ed74e57a122a38732ce7" - integrity sha512-cUriqMauq1ylzP2TxePNdPqkwI7Le3Annh4K9rrpvKfSBB/bdW+Iu1ihBaTIABTAAJ85LmKL5SSPPL9ry8d1gQ== + version "3.4.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" + integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== "@openzeppelin/hardhat-upgrades@^1.6.0": version "1.9.0" @@ -2803,7 +2864,7 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" -chownr@^1.1.1: +chownr@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -4571,6 +4632,42 @@ ethers@^5.0.0, ethers@^5.0.1, ethers@^5.0.2, ethers@^5.1.3, ethers@^5.4.0, ether "@ethersproject/web" "5.4.0" "@ethersproject/wordlists" "5.4.0" +ethers@^5.0.24: + version "5.4.7" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.4.7.tgz#0fd491a5da7c9793de2d6058d76b41b1e7efba8f" + integrity sha512-iZc5p2nqfWK1sj8RabwsPM28cr37Bpq7ehTQ5rWExBr2Y09Sn1lDKZOED26n+TsZMye7Y6mIgQ/1cwpSD8XZew== + dependencies: + "@ethersproject/abi" "5.4.1" + "@ethersproject/abstract-provider" "5.4.1" + "@ethersproject/abstract-signer" "5.4.1" + "@ethersproject/address" "5.4.0" + "@ethersproject/base64" "5.4.0" + "@ethersproject/basex" "5.4.0" + "@ethersproject/bignumber" "5.4.2" + "@ethersproject/bytes" "5.4.0" + "@ethersproject/constants" "5.4.0" + "@ethersproject/contracts" "5.4.1" + "@ethersproject/hash" "5.4.0" + "@ethersproject/hdnode" "5.4.0" + "@ethersproject/json-wallets" "5.4.0" + "@ethersproject/keccak256" "5.4.0" + "@ethersproject/logger" "5.4.1" + "@ethersproject/networks" "5.4.2" + "@ethersproject/pbkdf2" "5.4.0" + "@ethersproject/properties" "5.4.1" + "@ethersproject/providers" "5.4.5" + "@ethersproject/random" "5.4.0" + "@ethersproject/rlp" "5.4.0" + "@ethersproject/sha2" "5.4.0" + "@ethersproject/signing-key" "5.4.0" + "@ethersproject/solidity" "5.4.0" + "@ethersproject/strings" "5.4.0" + "@ethersproject/transactions" "5.4.0" + "@ethersproject/units" "5.4.0" + "@ethersproject/wallet" "5.4.0" + "@ethersproject/web" "5.4.0" + "@ethersproject/wordlists" "5.4.0" + ethjs-unit@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699" @@ -5183,7 +5280,7 @@ fs-extra@^9.0.1, fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-minipass@^1.2.5: +fs-minipass@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== @@ -5605,6 +5702,13 @@ hardhat-storage-layout@0.1.6: dependencies: console-table-printer "^2.9.0" +hardhat-tracer@^1.0.0-alpha.6: + version "1.0.0-alpha.6" + resolved "https://registry.yarnpkg.com/hardhat-tracer/-/hardhat-tracer-1.0.0-alpha.6.tgz#4545a772930567cad4620ee9448cb76e89b07b02" + integrity sha512-QXKEJPaCDU0P7ZNHvFuGQoKLZ9+uma3ASAoPjhHr4CYwgIHcronVPZ7zkztRc7LhDbKFffIuoh0jEQWGgR6Neg== + dependencies: + ethers "^5.0.24" + hardhat@^2.2.0: version "2.5.0" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.5.0.tgz#0a10bf85d9b2c3c7c12cfa4e35454296c670c054" @@ -7714,7 +7818,7 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: +minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== @@ -7722,7 +7826,7 @@ minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: safe-buffer "^5.1.2" yallist "^3.0.0" -minizlib@^1.2.1: +minizlib@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== @@ -7756,7 +7860,7 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" -mkdirp@0.5.5, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@0.5.5, mkdirp@0.5.x, mkdirp@^0.5.1, mkdirp@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -9726,7 +9830,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -9831,9 +9935,9 @@ semver-compare@^1.0.0: integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= semver-regex@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" - integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== + version "3.1.3" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.3.tgz#b2bcc6f97f63269f286994e297e229b6245d0dc3" + integrity sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ== "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0: version "5.7.1" @@ -10665,17 +10769,17 @@ tape@^4.6.3: through "~2.3.8" tar@^4.0.2: - version "4.4.15" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.15.tgz#3caced4f39ebd46ddda4d6203d48493a919697f8" - integrity sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" + version "4.4.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== + dependencies: + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" tdigest@^0.1.1: version "0.1.1" @@ -12096,7 +12200,7 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==