diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ab379faa49f6fedc157beed796dd69b06cbc411f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 TokenSPICE + +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. diff --git a/README-code-quality.md b/README-code-quality.md new file mode 100644 index 0000000000000000000000000000000000000000..c742c1a3498bc2270e2662cbb1ceba3a772887f5 --- /dev/null +++ b/README-code-quality.md @@ -0,0 +1,106 @@ +# Code Quality: Style Guide and Tests + +## Table of Contents + +- [Style Guide](#style-guide) +- [On Tests](#tests) +- [Remote Tests](#remote-tests) +- [Local Tests](#local-tests) +- [Local Fixes](#local-fixes) + +## Style Guide + +Code and checks in TokenSPICE should strive to follow: +- [PEP 8](https://www.python.org/dev/peps/pep-0008/) Style Guide, [PEP 20](https://www.python.org/dev/peps/pep-0020/) The Zen of Python, [PEP 484](https://www.python.org/dev/peps/pep-0484/) Type Hints, [PEP 257](https://www.python.org/dev/peps/pep-0257/) Docstring conventions +- And, most specifically, [`google` docstring convention](https://google.github.io/styleguide/pyguide.html) [[2](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)]. It's a good balance of compact, readable, and specific. Docstrings should include variable types if they are not explicitly type hints in code itself. +- pydocstyle v4.0.0 supports `google` docstring convention [[ref](http://www.pydocstyle.org/en/stable/error_codes.html#default-conventions)]. This means checks for all the errors except D203, D204, D213, D215, D400, D401, D404, D406, D407, D408, D409 and D413 . + + +```python +def myfunction(param1, param2): + """Example function with types documented in the docstring. + + `PEP 484`_ type annotations are supported. If attribute, parameter, and + return types are annotated according to `PEP 484`_, they do not need to be + included in the docstring: + + Args: + address: str -- Eth address + agents: set of Agent -- + agent_ages: dict of {agent_name:str : agent_age:int} -- agent's ages + completed: list of int -- + + Returns: + bool: Success if True + """ +``` + +## On Tests + +Ensure that the local tests are the same as remote; otherwise we'd end up fixing things we don't care about. + +Codacy supports testing across many tools at once. It's easy to run remotely. However to use locally, `codacy-cli` runs slowly and churns the CPU. There are so many unimportant tests that it obscures the important ones. + +Therefore, we _only_ use pylint. It's 80% of the benefit with 20% of the effort. Setup: +- Locally: just use `pylint`, not `codacy-cli` +- Remotely: use codacy, but only pylint is used +- For both, ignore the checks D203, D204, etc as specified in the Style Guide above + +## Remote Tests + +[Codacy](https://www.codacy.com) is automatically run _remotely_ for each commit of each PR. +- [Here's the Codacy TokenSPICE dashboard](https://app.codacy.com/gh/tokenspice/tokenspice/dashboard?branch=main), including links to tests. To access this, you need special permissions; ask Trent. +- [These "Code Patterns" settings](https://app.codacy.com/gh/tokenspice/tokenspice/patterns/list) specify what checks are run vs ignored. + +## Local Tests + +Use `pylint`. [Here's a pylint tutorial.](https://pylint.pycqa.org/en/latest/tutorial.html). + +Here's an example. In the terminal: +```console +pylint * +``` + +Pylint auto-loads `./pylintrc` and uses its options, such as ignoring D203, D204. + +Example pylint output: +```text +engine/SimEngine.py:9:0: W0611: Unused valuation imported from util (unused-import) +engine/AgentWallet.py:419:4: C0116: Missing function or method docstring (missing-function-docstring) +... +----------------------------------- +Your code has been rated at 8.07/10 +``` + +To learn more about a complaint, some examples: +```console +pylint --help-msg=unused-import +pylint --help-msg=missing-function-docstring +``` + +## Local Fixes + +There are a couple approaches to making fixes: +1. Use automated tools like [`black`](https://pypi.org/project/black/) +2. Manually + +We recommend to start with (1), then clean the rest with (2). + +Usage of black on one file: +```console +black netlists/simplepool/test/test_netlist.py +``` + +It will output: +```console +reformatted netlists/simplepool/test/test_netlist.py +All done! β¨ π° β¨ +1 file reformatted. +``` + +For maximum productivity, use black on everything: +```console +black ./ +``` + + diff --git a/README-tools.md b/README-tools.md new file mode 100644 index 0000000000000000000000000000000000000000..6baf89ab3d3a37a7139626378eedef201e324ad4 --- /dev/null +++ b/README-tools.md @@ -0,0 +1,54 @@ +# Higher-Level Tools + +TokenSPICE lends itself well to having tools built on top for design entry, verification, design space exploration, and more. + +Here are some such tools, largely inspired by tools from circuit land. +- Each tool can have just a backend, or both a backend and frontend. A frontend with good UX can make a huge difference. +- Each tool can be applied for the initial design, before deployment. And, they can be used in real-time against *live chains*, for real-time verification, optimization, etc. How: grab the latest chainβs snapshot into ganache, run a local verification etc for a few seconds or minutes, then do transaction(s) on the live chain. This can lead to trading systems, failure monitoring, more. + +## Design entry tools + +- Schematic editor - be able to input netlists by editing a schematic. Examples: + - [Circuits](https://www.google.com/search?q=spice+schematic+editor&sxsrf=ALeKk01lEnlL27hPsWzA_sBTOoxQLAlUJg:1626253838940&source=lnms&tbm=isch&sa=X&ved=2ahUKEwinnInTm-LxAhUNnYsKHe0mA1wQ_AUoAXoECAEQAw&biw=1600&bih=721#imgrc=TUv3yDrlTDLNVM) + - [Videogames: machinations.io](https://machinations.io/) + - [System Dynamics](https://systemdynamics.org/what-is-system-dynamics/): [stock and flow diagrams](https://www.google.com/search?q=stock+and+flow+diagram&sxsrf=AOaemvK9W36reEyVfw54RGs3JmLiwZbTDQ:1643101186893&source=lnms&tbm=isch&sa=X&ved=2ahUKEwj358v0xMz1AhUXSPEDHZU7CwkQ_AUoAXoECAEQAw&cshid=1643101212647476&biw=1514&bih=934&dpr=1) in [Stella](https://www.iseesystems.com/store/products/stella-professional.aspx) and [Vensim](https://vensim.com/) (commercial); [full sw list](https://en.wikipedia.org/wiki/Comparison_of_system_dynamics_software) + - [Data pipelines via StreamSets](https://streamsets.com/learn/data-pipelines/) +- Auto schematic import - input a netlist, auto-generate a schematic visually. Uses optimization etc. +- Waveform viewer - build on `tsp.do_plot` with more a interactive tool. [Examples from circuits](https://www.google.com/search?q=spice+waveform+viewer&sxsrf=ALeKk02nY3U9rkOZuz58PNXWjoFY4MCcUQ:1626255741604&source=lnms&tbm=isch&sa=X&ved=2ahUKEwj9zareouLxAhXm-ioKHW0MCtgQ_AUoAXoECAEQAw&biw=1600&bih=721). + +## Verification tools + +Base tools: +- Corner simulation tool - simulate at pre-specified "corners". Corners are a small handful of points in random variable or worst-case variable space to simulate against. Enables rapid design-space exploration, because each you only simulate each design on corners +- Worst-case verification - verification across worst-case variables. E.g. via global optimization across worst-case variables. E.g. global synthesis across worst-case variables and agent python structures. Include corner extraction feature. +- 3-sigma verification - verification across random variables. E.g. via Monte Carlo analysis where all samples are drawn from the random variables' pdf, and each sample is simulated. Be able to simulate across >=1 worst-case corners; include stastitical extraction feature. + +Advanced tools: +- High-sigma verification - verification across random variables, where catastrophic failure happens rarely, e.g. 1 in a million times (4.5 sigma) or 1 in a billion (6 sigma). E.g. using tricks from "rare event estimation" literature or [this book](https://www.amazon.com/Variation-Aware-Design-Custom-Integrated-Circuits/dp/146142268X). Be able to simulate across >=1 worst-case corners; include stastitical extraction feature. +- System identification - auto-extract a TokenSPICE netlist from raw blockchain info. To make it more tractable, use a parameterized netlist and Solidity code where possible. Ie "whitebox" not "blackbox". +- Behavioral modeling - auto-extract a lower-fidelity / faster-simulating TokenSPICE netlist from a higher-fidelity TokenSPICE netlist & simulation run, with minimal loss of error. +- Mixed-signal verification - include digital verification in the loop. Example: replace ganache with [hevm](https://fv.ethereum.org/2020/07/28/symbolic-hevm-release), which does fuzzing etc on Solidity. + +## Design Space Exploration tools + +Base tools: +- Sensitivity analysis - locally perterb design variables. +- Sweep analysis - sweep design variables one at a time. Ignore interactions. + +Advanced tools: +- Fast sweep analysis - sweep across all design variables including interactions, but without combinatorial explosion via model-in-the-loop +- Local optimization - search across design variables to optimize for objectives & constraints +- Global optimization - search across all design variables, with affordances to not get stuck + +Extra-advanced tools: +- Synthesis - search design variables *and structure*. E.g. Evolve solidity or EVM bytecode. AI DAOs that own themselves. Go nuts:) +- Variation-aware synthesis - all of the above at once. This isnβt easy! But itβs possible. Example: use MOJITO (http://trent.st/mojito/), but use TokenSPICE (not SPICE) and Solidity building blocks (not circuit ones) + +## More "Larger" Ideas for TokenSPICE + +- "tsp publish" - publish sim results to Ocean +- Use Ocean to manage IP: contracts, agents, netlists, results. Or, using brownie pm? Result: TokenSPICE itself becomes super small, just an engine +- TokenSPICE inputs SPICE netlists. Pros: well-defined, battle-tested, built-in hierarchy, IP mgmt hooks, specifications of time series, compact, interoperability with SPICE tools. +- How to build / test the "SPICE format" and IP ideas: Make MOJITO work at the same time +- Deploy to pypi. Make it easy to install separately +- Trading system using TokenSPICE predictions diff --git a/README.md b/README.md index acb4089bbbb8a7252c8566bfea9aed7f0b9ea2cf..3f82600ebd8bfee51fc50f9225677b21708bba05 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,343 @@ -# FOSS2023-2-final +<img src="images/tokenspice-banner-thin.png" width="100%"> +# TokenSPICE: EVM Agent-Based Token Simulator +<div align="center"> -## Getting started +<!-- Pytest and MyPy Badges --> +<img alt="Pytest Unit Testing" src="https://github.com/tokenspice/tokenspice/actions/workflows/pytest.yml/badge.svg"> +<img alt="MyPy Static Type Checking" src="https://github.com/tokenspice/tokenspice/actions/workflows/mypy.yml/badge.svg"> -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +<!-- Codacy Badges --> +[](https://www.codacy.com/gh/tokenspice/tokenspice/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tokenspice/tokenspice&utm_campaign=Badge_Coverage) +[](https://www.codacy.com/gh/tokenspice/tokenspice/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tokenspice/tokenspice&utm_campaign=Badge_Grade) + +</div> -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +β _Note: as of mid 2023, this codebase is not being maintained. It might work, it might not. If you find a bug, feel free to report it, but do not expect it to be fixed. If you do a PR where tests pass, we're happy to merge it. And feel free to fork this repo and change it as you wish (including bug fixes)._ -## Add your files +TokenSPICE simulates tokenized ecosystems via an agent-based approach, with EVM in-the-loop. -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +It can help in [Token](https://blog.oceanprotocol.com/towards-a-practice-of-token-engineering-b02feeeff7ca) [Engineering](https://www.tokenengineering.org) flows, to design, tune, and verify tokenized ecosystems. It's young but promising. We welcome you to contribute! π +- TokenSPICE simulates by simply running a loop. At each iteration, each _agent_ in the _netlist_ takes a step. That's it! [Simple is good.](https://www.goodreads.com/quotes/7144975-i-apologize-for-such-a-long-letter---i-didn-t) +- A netlist wires up a collection of agents to interact in a given way. Each agent is a class. It has an Ethereum wallet, and does work to earn money. Agents may be written in pure Python, or with an EVM-based backend. +- One models a system by writing a netlist and tracking metrics (KPIs). One can write their own netlists and agents to simulate whatever they like. The [netlists](https://github.com/tokenspice/tokenspice/tree/main/netlists) directory has examples. + +# Contents + +- [π Initial Setup](#-initial-setup) +- [π Running, Debugging](#-running-debugging) +- [π¦ Agents and Netlists](#-agents-and-netlists) +- [π Updating Envt](#-updating-envt) +- [π‘ Backlog](#-backlog) +- [π Benefits of EVM Agent Simulation](#-benefits-of-evm-agent-simulation) +- [π¦ Resources](#-resources) +- [π License](#-license) + +# π Initial Setup + +## Prerequisites + +- Linux/MacOS +- Python 3.8.5+ +- solc 0.8.0+ [[Instructions](https://docs.soliditylang.org/en/v0.8.9/installing-solidity.html)] +- ganache. To install: `npm install ganache --global` +- nvm 16.13.2, _not_ nvm 17. To install: `nvm install 16.13.2; nvm use 16.13.2`. [[Details](https://github.com/tokenspice/tokenspice/issues/165)] + +## Install TokenSPICE + +Open a new terminal and: +```console +#clone repo +git clone https://github.com/tokenspice/tokenspice +cd tokenspice + +#create a virtual environment +python3 -m venv venv + +#activate env +source venv/bin/activate + +#install dependencies +pip install -r requirements.txt + +#install brownie packages (you can ignore FileExistsErrors) +./brownie-install.sh +``` + +**Potential issues & workarounds** + +- Issue: Brownie doesn't support Python 3.11 yet. Workaround: before "install dependencies" step above, run `pip install vyper==0.3.7 --ignore-requires-python` and `sudo apt-get install python3.11-dev` +- Issue: MacOS might flag "Unsupported architecture". Workaround: install including ARCHFLAGS: `ARCHFLAGS="-arch x86_64" pip install -r requirements.txt` + +## Run Ganache + +From "Prerequisites", you should have Ganache installed. + +Open a new console and go to tokenspice directory. Then: +```console +source venv/bin/activate +./ganache.py +``` + +This will start a Ganache chain, and populate 9 accounts. + + +## TokenSPICE CLI + +`tsp` is the command-line interface for TokenSPICE. + +Open a new console and go to tokenspice directory. Then: +```console +source venv/bin/activate + +#add pwd to bash path +export PATH=$PATH:. + +#see tsp help +tsp +``` + +## Compile the contracts + +NOTE: if you have a directory named `contracts` from before, which is side-by-side with your `tokenspice` directory, you'll get [issues](https://github.com/tokenspice/tokenspice/issues/160). To avoid this, rename or move that contracts directory. + +From the same terminal: +```console +#install 3rd party libs, then call "brownie compile" in sol057/ and sol080/ +tsp compile +``` + +TokenSPICE sees smart contracts as classes. How: +- When it starts, it calls `brownie.project.load('./sol057', name="MyProject")` to load the ABIs in `./sol057/build/`. Similar for `sol080`. +- That's enough info to treat each contract in `sol057/contracts/` as a _class_. Then, call `deploy()` on it to create a new _object_. + + +# π Running, Debugging + +## Testing + +From terminal: +```console +#run single test. It uses brownie, which auto-starts Ganache local blockchain node. +pytest sol057/contracts/simpletoken/test/test_Simpletoken.py::test_transfer + +#run all of a directory's tests +pytest sol057/contracts/simpletoken/test + +#run all unit tests +pytest + +#run static type-checking. By default, uses config mypy.ini. Note: pytest does dynamic type-checking. +mypy ./ + +#run linting on code style +pylint * + +#auto-fix some pylint complaints +black ./ +``` + +**[Go here](README-code-quality.md)** for details on linting / style. + +## Simulating with TokenSPICE + +From terminal: +```console +#run simulation, sending results to 'outdir_csv' (clear dir first, to be sure) +rm -rf outdir_csv; tsp run netlists/scheduler/netlist.py outdir_csv +``` + +You'll see an output like: +```text +Arguments: NETLIST=netlists/... +Launching 'ganache-cli --accounts 10 --hardfork ... +mnemonic: 'sausage bunker giant drum ... +INFO:master:Begin. +INFO:master:SimStrategy={OCEAN_funded=5.0, duration_seconds=157680000, ...} +INFO:master:Tick=0 (0.0 h, 0.0 d, 0.0 mo, 0.0 y); timestamp=1642844072; OCEAN_vested=0, ... +INFO:master:Tick=3 (2160.0 h, 90.0 d, 3.0 mo, 0.2 y); timestamp=1650620073; OCEAN_vested=0.0, ... +INFO:master:Tick=6 (4320.0 h, 180.0 d, 6.0 mo, 0.5 y); timestamp=1658396073; OCEAN_vested=0.0, ... +INFO:master:Tick=9 (6480.0 h, 270.0 d, 9.0 mo, 0.7 y); timestamp=1666172074; OCEAN_vested=0.0, ... +INFO:master:Tick=12 (8640.0 h, 360.0 d, 12.0 mo, 1.0 y); timestamp=1673948074; OCEAN_vested=0.0, ... +INFO:master:Tick=15 (10800.0 h, 450.0 d, 15.0 mo, 1.2 y); timestamp=1681724074; OCEAN_vested=0.232876 ... +``` + +Now, let's view the results visually. In the same terminal: +```console +#create output plots in 'outdir_png' (clear dir first, to be sure) +rm -rf outdir_png; tsp plot netlists/scheduler/netlist.py outdir_csv outdir_png + +#view plots +eog outdir_png +``` + +To see the blockchain txs apart from the other logs: open a _new_ terminal and: +```console +#activate env't +cd tokenspice +source venv/bin/activate + +#run ganache +export PATH=$PATH:. +tsp ganache +``` + +Now, from your original terminal: +```console +#run the sim. It will auto-connect to ganache +rm -rf outdir_csv; tsp run netlists/scheduler/netlist.py outdir_csv +``` + +For longer runs (eg wsloop), we can log to a file while watching the console in real-time: + +```console +#run the sim in the background, logging to out.txt +rm -rf outdir_csv; tsp run netlists/wsloop/netlist.py outdir_csv > out.txt 2>&1 & + +#monitor in real-time +tail -f out.txt +``` + +To kill a sim in the background: +```console +#find the background process +ps ax |grep "tsp run" + +#example result: +#223429 pts/4 Rl 0:02 python ./tsp run netlists/wsloop/netlist.py outdir_csv + +#to kill it: +kill 223429 +``` + +## Debugging from Brownie Console + +Brownie console is a Python console, with some extra Brownie goodness, so that we can interactively play with Solidity contracts as Python classes, and deployed Solidity contracts as Python objects. + +From terminal: ``` -cd existing_repo -git remote add origin https://git.ajou.ac.kr/chipkkang9/foss2023-2-final.git -git branch -M main -git push -uf origin main +#brownie needs a directory with ./contracts/. Go to one. +cd sol057/ + +#start console +brownie console ``` -## Integrate with your tools +In brownie console: +```python +>>> st = Simpletoken.deploy("DT1", "Simpletoken 1", 18, Wei('100 ether'), {'from': accounts[0], "priority_fee": chain.priority_fee, "max_fee": chain.base_fee + 2 * + chain.priority_fee}) +Transaction sent: 0x9d20d3239d5c8b8a029f037fe573c343efd9361efd4d99307e0f5be7499367ab + Gas price: 0.0 gwei Gas limit: 6721975 + Simpletoken.constructor confirmed - Block: 1 Gas used: 601010 (8.94%) + Simpletoken deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87 + +>>> st.symbol() +'DT1' + +>>> st.balanceOf(accounts[0])/1e18 + +>>> dir(st) +[abi, address, allowance, approve, balance, balanceOf, bytecode, decimals, decode_input, get_method, get_method_object, info, name, selectors, signatures, symbol, topics, totalSupply, transfer, transferFrom, tx] +``` + +# π¦ Agents and Netlists + +## Agents Basics + +Agents are defined at `agents/`. Agents are in a separate directory than netlists, to facilitate reuse across many netlists. + +All agents are written in Python. Some may include EVM behavior (more on this later). + +Each Agent has an [`AgentWallet`](https://github.com/tokenspice/tokenspice/blob/main/engine/AgentWallet.py), which holds a [`Web3Wallet`](https://github.com/tokenspice/tokenspice/blob/main/web3tools/web3wallet.py). The `Web3Wallet` holds a private key and creates transactions (txs). + +## Netlists Basics + +The netlist defines what you simulate, and how. + +Netlists are defined at `netlists/`. You can reuse existing netlists or create your own. + +## What A Netlist Definition Must Hold + +TokenSPICE expects a netlist module (in a netlist.py file) that defines these specific classes and functions: + +- `SimStrategy` class: simulation run parameters +- `KPIs` class and `netlist_createLogData()` function: what metrics to log during the run +- `netlist_plotInstructions()` function: how to plot the metrics after the run +- `SimState` class: system-level structure & parameters, i.e. how agents are instantiated and connected. It imports agents defined in `agents/*Agent.py`. Some agents use EVM. You can add and edit Agents to suit your needs. + +## How to Implement Netlists + +There are two practical ways to specify `SimStrategy`, `KPIs`, and so on for netlist.py: -- [ ] [Set up project integrations](https://git.ajou.ac.kr/chipkkang9/foss2023-2-final/-/settings/integrations) +1. **For simple netlists.** Have just one file (`netlist.py`) to hold all the code for each class and method given above. This is appropriate for simple netlists, like [simplegrant](https://github.com/tokenspice/tokenspice/blob/main/netlists/simplegrant/about.md) (just Python) and [simplepool](https://github.com/tokenspice/tokenspice/blob/main/netlists/simplepool/about.md) (Python+EVM). -## Collaborate with your team +2. **For complex netlists.** Have one or more _separate files_ for each class and method given above, such as `netlists/NETLISTX/SimStrategy.py`. Then, import them all into `netlist.py` file to unify their scope to a single module (`netlist`). This allows for arbitrary levels of netlist complexity. The [wsloop](https://github.com/tokenspice/tokenspice/blob/main/netlists/wsloop/about.md) netlist is a good example. It models the [Web3 Sustainability Loop](https://blog.oceanprotocol.com/the-web3-sustainability-loop-b2a4097a36e), which is inspired by the Amazon flywheel and used by [Ocean](https://www.oceanprotocol.com), [Boson](https://www.bosonprotocol.io/) and others as their system-level token design. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +## Agent.takeStep() method -## Test and Deploy +The class `SimState` defines which agents are used. Some agents even spawn other agents. Each agent object is stored in the `SimState.agents` object, a dict with some added querying abilities. Key `SimState` methods to access this object are `addAgent(agent)`, `getAgent(name:str)`, `allAgents()`, and `numAgents()`. [`SimStateBase`](https://github.com/tokenspice/tokenspice/blob/main/engine/SimStateBase.py) has details. -Use the built-in continuous integration in GitLab. +Every iteration of the engine make a call to each agent's `takeStep()` method. The implementation of [`GrantGivingAgent.takeStep()`](https://github.com/tokenspice/tokenspice/blob/main/agents/GrantGivingAgent.py) is shown below. Lines 26β33 determine whether it should disburse funds on this tick. Lines 35β37 do the disbursal if appropriate. +There are no real constraints on how an agent's `takeStep()` is implemented. This which gives great TokenSPICE flexibility in agent-based simulation. For example, it can loop in EVM, like we show later. -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +<img src="images/takestep.png" width="100%"> -*** +## Netlist Examples +Here are some existing netlists. -# Editing this README +- [simplegrant](netlists/simplegrant/about.md) - granter plus receiver, that's all. No EVM. +- [simplepool](netlists/simplepool/about.md) - publisher that periodically creates new pools. EVM. +- [scheduler](netlists/scheduler/about.md) - scheduled vesting from a wallet. EVM. +- [wsloop](netlists/wsloop/about.md) - Web3 Sustainability Loop. No EVM. +- [oceanv3](netlists/oceanv3/about.md) - Ocean Market V3 - initial design. EVM. +- [oceanv4](netlists/oceanv4/about.md) - Ocean Market V4 - solves rug pulls. EVM. -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. +To learn more about how TokenSPICE netlists are structured, we refer you to the [simplegrant](netlists/simplegrant/about.md) (pure Python) and [simplepool](netlists/simplepool/about.md) (Python+EVM) netlists, which each have more thorough explainers. -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +# π‘ Backlog -## Name -Choose a self-explaining name for your project. +Larger things we'd like to see: +- **[Higher-level tools](README-tools.md)** that use TokenSPICE, including design entry, verification, design space exploration, and more. +- Improvements to TokenSPICE itself in the form of faster simulation speed, improved UX, and more. -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +See this board: https://github.com/orgs/tokenspice/projects/1/views/1 -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +# π Benefits of EVM Agent Simulation -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +TokenSPICE and other EVM agent simulators have these benefits: +- Faster and less error prone, because the model = the Solidity code. Donβt have to port any existing Solidity code into Python, just wrap it. Donβt have to write lower-fidelity equations. +- Enables rapid iterations of writing Solidity code -> simulating -> changing Solidity code -> simulating. +- Super high fidelity simulations, since it uses the actual code itself. Enables modeling of design, random and worst-case variables. +- Mental model is general enough to extend to Vyper, LLL, and direct EVM bytecode. Can extend to non-EVM blockchain, and multi-chain scenarios. +- Opportunity for real-time analysis / optimization / etc against *live chains*: grab the latest chainβs snapshot into ganache, run a local analysis / optimization etc for a few seconds or minutes, then do transaction(s) on the live chain. This can lead to trading systems, failure monitoring, more. -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +# π¦ Resources -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +Here are further resources. -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +* [TokenSPICE medium posts](https://medium.com/tokenspice), starting with ["Introducing TokenSPICE"](https://medium.com/tokenspice/introducing-tokenspice-fb4dac98bcf9) +* Intro to SPICE & TokenSPICE [[Gslides - short](https://docs.google.com/presentation/d/167nbvrQyr6vdvTE6exC1zEA3LktrPzbR08Cg5S1sVDs)] [[Gslides - long](https://docs.google.com/presentation/d/1yUrU7AI702zpRZve6CCR830JSXrpPmfg00M5x9ndhvE)] +* TE for Ocean V3 [[GSlides](https://docs.google.com/presentation/d/1DmC6wfyl7ZMjuB-h3Zbfy--xFuYSt3tGACpgfJH9ZFk/edit)] [[video](https://www.youtube.com/watch?v=ztnIf9gCsNI&ab_channel=TokenEngineering)] , TE Community Workshop, Dec 9, 2020 +* TE for Ocean V4 [[GSlides](https://docs.google.com/presentation/d/1JfFi9hT4Lf3UQKfCXGDhA27YPpPcWsXU7YArfRGAmMQ/edit#slide=id.p1)] [[slides](http://trent.st/content/20210521%20Ocean%20Market%20Balancer%20Simulations%20For%20TE%20Academy.pdf)] [[video](https://www.youtube.com/watch?v=TDG53PTbqhQ&ab_channel=TokenEngineering)] , TE Academy, May 21, 2021 -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +History: +- TokenSPICE was [initially built to model](https://github.com/tokenspice/tokenspice0.1) the [Web3 Sustainability Loop](https://blog.oceanprotocol.com/the-web3-sustainability-loop-b2a4097a36e). It's now been generalized to support EVM, on arbitary netlists. +- Most initial work was by [trentmc](https://github.com/trentmc) ([Ocean Protocol](https://www.oceanprotocol.com)); [several more contributors](https://github.com/tokenspice/tokenspice/graphs/contributors) have joined since πͺπ¨βπ©βπ§βπ§ -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +Art: +- [TokenSPICE logos](https://github.com/tokenspice/art/blob/main/README.md) +- Fishnado image sources (CC): [[1](https://www.flickr.com/photos/robinhughes/404457553)] [[2](https://commons.wikimedia.org/wiki/File:Fish_Tornado_(226274841).jpeg)] -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +Other links: +- Twitter: [@tokenspice](https://twitter.com/tokenspice) +- Medium: [@tokenspice](https://medium.com/tokenspice) -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +# π License -## License -For open source projects, say how it is licensed. +The license is MIT. [Details](LICENSE) -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +<img src="images/fishnado2-crop.jpeg" width="100%"> diff --git a/agents/DataconsumerAgent.py b/agents/DataconsumerAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..c25bd820a916654ef0d24f17e7948d87e699d59a --- /dev/null +++ b/agents/DataconsumerAgent.py @@ -0,0 +1,225 @@ +import random +from typing import List + +from enforce_typing import enforce_types + +from agents.PoolAgent import PoolAgent +from engine import AgentBase +from util import globaltokens +from util.base18 import toBase18 +from util import constants + +# magic numbers +DEFAULT_s_between_buys = 3 * constants.S_PER_DAY +DEFAULT_profit_margin_on_consume = 0.2 + + +@enforce_types +class DataconsumerAgent(AgentBase.AgentBaseEvm): + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + s_between_buys: int = DEFAULT_s_between_buys, + profit_margin_on_consume: float = DEFAULT_profit_margin_on_consume, + ): # pylint: disable=too-many-arguments + super().__init__(name, USD, OCEAN) + + self._s_since_buy = 0 + self._s_between_buys = s_between_buys + self._profit_margin_on_consume = profit_margin_on_consume + + def takeStep(self, state) -> None: + self._s_since_buy += state.ss.time_step + if self._doBuyAndConsumeDT(state): + self._s_since_buy = 0 + self._buyAndConsumeDT(state) + + def _doBuyAndConsumeDT(self, state): + cand_pool_agents = self._candPoolAgents(state) + if not cand_pool_agents: + return False + return self._s_since_buy >= self._s_between_buys + + def _candPoolAgents( # pylint: disable=too-many-locals + self, state + ) -> List[PoolAgent]: + """Pools that this agent can afford to buy 1.0 datatokens from, + at least based on a first approximation. + """ + OCEAN_address = globaltokens.OCEAN_address() + OCEAN_base = toBase18(self.OCEAN()) + all_pool_agents = state.agents.filterToPool() + + cand_pool_agents = [] + for pool_name, pool_agent in all_pool_agents.items(): + # filter 1: pool rugged? + if hasattr(state, "rugged_pools") and pool_name in state.rugged_pools: + continue + + # filter 2: agent has enough funds? + pool = pool_agent.pool + DT_address = pool_agent.datatoken_address + + tokenBalanceIn = pool.getBalance(OCEAN_address) + tokenWeightIn = pool.getDenormalizedWeight(OCEAN_address) + tokenBalanceOut = pool.getBalance(DT_address) + tokenWeightOut = pool.getDenormalizedWeight(DT_address) + tokenAmountOut = toBase18(1.0) # number of DTs + swapFee = pool.getSwapFee() + + OCEANamountIn_base = pool.calcInGivenOut( + tokenBalanceIn, + tokenWeightIn, + tokenBalanceOut, + tokenWeightOut, + tokenAmountOut, + swapFee, + ) + + if OCEANamountIn_base >= OCEAN_base: + continue + + # passed all filters! Add this agent + cand_pool_agents.append(pool_agent) + + return cand_pool_agents + + def _buyAndConsumeDT(self, state): + """Buy dataset, then consume it""" + DT_buy_amt = 1.0 # buy just enough to consume once + max_OCEAN_allow = self.OCEAN() + + cand_pool_agents = self._candPoolAgents(state) + assert cand_pool_agents + pool_agent = random.choice(cand_pool_agents) + + pool = pool_agent.pool + DT = pool_agent.datatoken + + DT_before = self.DT(DT) + OCEAN_before = self.OCEAN() + + # buy + self._wallet.buyDT(pool, DT, DT_buy_amt, max_OCEAN_allow) + + DT_after = self.DT(DT) + OCEAN_after = self.OCEAN() + + assert DT_after == (DT_before + DT_buy_amt) + assert OCEAN_after < OCEAN_before + + OCEAN_spend = OCEAN_before - OCEAN_after + + # consume + publisher_agent = state.agents.agentByAddress(pool_agent.controller_address) + self._wallet.transferDT(publisher_agent._wallet, DT, DT_buy_amt) + + # get business value due to consume + OCEAN_returned = OCEAN_spend * (1.0 + self._profit_margin_on_consume) + self.receiveOCEAN(OCEAN_returned) + + return OCEAN_spend + + +@enforce_types +class DataconsumerAgentV4(AgentBase.AgentBaseEvm): + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + s_between_buys: int = DEFAULT_s_between_buys, + profit_margin_on_consume: float = DEFAULT_profit_margin_on_consume, + ): # pylint: disable=too-many-arguments + super().__init__(name, USD, OCEAN) + + self._s_since_buy = 0 + self._s_between_buys = s_between_buys + self._profit_margin_on_consume = profit_margin_on_consume + + def takeStep(self, state) -> None: + self._s_since_buy += state.ss.time_step + if self._doBuyAndConsumeDT(state): + self._s_since_buy = 0 + self._buyAndConsumeDT(state) + + def _doBuyAndConsumeDT(self, state): + cand_pool_agents = self._candPoolAgents(state) + if not cand_pool_agents: + return False + return self._s_since_buy >= self._s_between_buys + + def _candPoolAgents( # pylint: disable=too-many-locals + self, state + ) -> List[PoolAgent]: + """Pools that this agent can afford to buy 1.0 datatokens from, + at least based on a first approximation. + """ + OCEAN_address = globaltokens.OCEAN_address() + OCEAN_base = toBase18(self.OCEAN()) + all_pool_agents = state.agents.filterToPoolV4() + + cand_pool_agents = [] + for pool_name, pool_agent in all_pool_agents.items(): + # filter 1: pool rugged? + if hasattr(state, "rugged_pools") and pool_name in state.rugged_pools: + continue + + # filter 2: agent has enough funds? + pool = pool_agent.pool + DT_address = pool_agent.datatoken_address + + tokenAmountOut = toBase18(1.0) # number of DTs + swapFee = pool.getSwapFee() + + OCEANamountIn_base = pool.getAmountInExactOut( + OCEAN_address, DT_address, tokenAmountOut, swapFee + ) + + if OCEANamountIn_base >= OCEAN_base: + continue + + # passed all filters! Add this agent + cand_pool_agents.append(pool_agent) + + return cand_pool_agents + + def _buyAndConsumeDT(self, state): + """Buy dataset, then consume it""" + DT_buy_amt = 1.0 # buy just enough to consume once + max_OCEAN_allow = self.OCEAN() + + cand_pool_agents = self._candPoolAgents(state) + assert cand_pool_agents + pool_agent = random.choice(cand_pool_agents) + + pool = pool_agent.pool + DT = pool_agent.datatoken + + DT_before = self.DT(DT) + OCEAN_before = self.OCEAN() + + # buy + self._wallet.buyDTV4(pool, DT, DT_buy_amt, max_OCEAN_allow) + + DT_after = self.DT(DT) + OCEAN_after = self.OCEAN() + + assert DT_after == (DT_before + DT_buy_amt) + assert OCEAN_after < OCEAN_before + + OCEAN_spend = OCEAN_before - OCEAN_after + + # consume + # import ipdb + # ipdb.set_trace() + publisher_agent = state.agents.agentByAddress(pool_agent.minter_address) + self._wallet.transferDT(publisher_agent._wallet, DT, DT_buy_amt) + + # get business value due to consume + OCEAN_returned = OCEAN_spend * (1.0 + self._profit_margin_on_consume) + self.receiveOCEAN(OCEAN_returned) + + return OCEAN_spend diff --git a/agents/DataecosystemAgent.py b/agents/DataecosystemAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..ebe20c8d166507d55c62ffdd12dc86850235a94a --- /dev/null +++ b/agents/DataecosystemAgent.py @@ -0,0 +1,58 @@ +from enforce_typing import enforce_types + +from engine import AgentBase +from agents.PublisherAgent import PublisherAgent +from agents.SpeculatorAgent import StakerspeculatorAgent +from agents.DataconsumerAgent import DataconsumerAgent + + +@enforce_types +class DataecosystemAgent(AgentBase.AgentBaseNoEvm): + """Will operate as a high-fidelity replacement for MarketplacesAgents, + when it's ready.""" + + def takeStep(self, state): + if self._doCreatePublisherAgent(state): + self._createPublisherAgent(state) + + if self._doCreateStakerspeculatorAgent(state): + self._createStakerspeculatorAgent(state) + + if self._doCreateDataconsumerAgent(state): + self._createDataconsumerAgent(state) + + @staticmethod + def _doCreatePublisherAgent(state) -> bool: + # magic number: rule - only create if no agents so far + return not state.publisherAgents() + + def _createPublisherAgent(self, state) -> None: + name = "foo_publisher" + USD = 0.0 # magic number + OCEAN = 1000.0 # magic number + new_agent = PublisherAgent(name=name, USD=USD, OCEAN=OCEAN) + state.addAgent(new_agent) + + @staticmethod + def _doCreateStakerspeculatorAgent(state) -> bool: + # magic number: rule - only create if no agents so far + return not state.stakerspeculatorAgents() + + def _createStakerspeculatorAgent(self, state) -> None: + name = "foo_stakerspeculator" + USD = 0.0 # magic number + OCEAN = 1000.0 # magic number + new_agent = StakerspeculatorAgent(name=name, USD=USD, OCEAN=OCEAN) + state.addAgent(new_agent) + + @staticmethod + def _doCreateDataconsumerAgent(state) -> bool: + # magic number: rule - only create if no agents so far + return not state.dataconumerAgents() + + def _createDataconsumerAgent(self, state) -> None: + name = "foo_dataconsumer" + USD = 0.0 # magic number + OCEAN = 1000.0 # magic number + new_agent = DataconsumerAgent(name=name, USD=USD, OCEAN=OCEAN) + state.addAgent(new_agent) diff --git a/agents/GrantGivingAgent.py b/agents/GrantGivingAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..faf0da5f2fd4e9c2e68952bca03ee37ec7127bf1 --- /dev/null +++ b/agents/GrantGivingAgent.py @@ -0,0 +1,52 @@ +from enforce_typing import enforce_types + +from engine import AgentBase + + +@enforce_types +class GrantGivingAgent(AgentBase.AgentBaseNoEvm): + """ + Disburses funds at a fixed # evenly-spaced intervals. + Same amount each time. + """ + + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + receiving_agent_name: str, + s_between_grants: int, + n_actions: int, + ): # pylint: disable=too-many-arguments + super().__init__(name, USD, OCEAN) + self._receiving_agent_name: str = receiving_agent_name + self._s_between_grants: int = s_between_grants + self._USD_per_grant: float = USD / float(n_actions) + self._OCEAN_per_grant: float = OCEAN / float(n_actions) + + self._tick_last_disburse = None + + def takeStep(self, state): + do_disburse = False + if self._tick_last_disburse is None: + do_disburse = True + else: + n_ticks_since = state.tick - self._tick_last_disburse + n_s_since = n_ticks_since * state.ss.time_step + n_s_thr = self._s_between_grants + do_disburse = n_s_since >= n_s_thr + + if do_disburse: + self._disburseFunds(state) + self._tick_last_disburse = state.tick + + def _disburseFunds(self, state): + # same amount each time + receiving_agent = state.getAgent(self._receiving_agent_name) + + USD = min(self.USD(), self._USD_per_grant) + self._transferUSD(receiving_agent, USD) + + OCEAN = min(self.OCEAN(), self._OCEAN_per_grant) + self._transferOCEAN(receiving_agent, OCEAN) diff --git a/agents/GrantTakingAgent.py b/agents/GrantTakingAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..455004daee3ba76103d8ac5ba8fc3307f7cba5fe --- /dev/null +++ b/agents/GrantTakingAgent.py @@ -0,0 +1,20 @@ +from enforce_typing import enforce_types + +from engine import AgentBase + + +@enforce_types +class GrantTakingAgent(AgentBase.AgentBaseNoEvm): + def __init__(self, name: str, USD: float, OCEAN: float): + super().__init__(name, USD, OCEAN) + self._spent_at_tick = 0.0 # USD and OCEAN (in USD) spent + + def takeStep(self, state): + self._spent_at_tick = self.USD() + self.OCEAN() * state.OCEANprice() + + # spend it all, as soon as agent has it + self._transferUSD(None, self.USD()) + self._transferOCEAN(None, self.OCEAN()) + + def spentAtTick(self) -> float: + return self._spent_at_tick diff --git a/agents/MarketplacesAgent.py b/agents/MarketplacesAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..5f96a0a93cd7ebc61845e303673ed6891b4d87f4 --- /dev/null +++ b/agents/MarketplacesAgent.py @@ -0,0 +1,65 @@ +import math +from enforce_typing import enforce_types + +from engine import AgentBase +from util.constants import S_PER_YEAR + + +@enforce_types +class MarketplacesAgent(AgentBase.AgentBaseNoEvm): + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + toll_agent_name: str, + n_marketplaces: float, + sales_per_marketplace_per_s: float, + time_step: int, + ): # pylint: disable=too-many-arguments + super().__init__(name, USD, OCEAN) + self._toll_agent_name: str = toll_agent_name + + # set initial values. These grow over time. + self._n_marketplaces: float = n_marketplaces + self._consume_sales_per_marketplace_per_s: float = sales_per_marketplace_per_s + self._time_step: int = time_step + + def numMarketplaces(self) -> float: + return self._n_marketplaces + + def consumeSalesPerMarketplacePerSecond(self) -> float: + return self._consume_sales_per_marketplace_per_s + + def takeStep(self, state): + ratio = state.kpis.mktsRNDToSalesRatio() + mkts_growth_rate_per_year = state.ss.annualMktsGrowthRate(ratio) + mkts_growth_rate_per_tick = self._growthRatePerTick(mkts_growth_rate_per_year) + + # *grow* the number of marketplaces, and revenue per marketplace + self._n_marketplaces *= 1.0 + mkts_growth_rate_per_tick + self._consume_sales_per_marketplace_per_s *= 1.0 + mkts_growth_rate_per_tick + + # compute sales -> toll -> send funds accordingly + consume_sales = self._consumeSalesPerTick() + toll = state.ss.networkRevenue(consume_sales) + toll_agent = state.getAgent(self._toll_agent_name) + toll_agent.receiveUSD(toll) + + # NOTE: we don't bother modeling marketplace profits or tracking mkt wallet $ + + def _consumeSalesPerTick(self) -> float: + return ( + self._n_marketplaces + * self._consume_sales_per_marketplace_per_s + * self._time_step + ) + + def _growthRatePerTick(self, g_per_year: float) -> float: + """ + @arguments + g_per_year -- growth rate per year, e.g. 0.05 for 5% annual rate + """ + ticks_per_year = S_PER_YEAR / float(self._time_step) + g_per_tick = math.pow(g_per_year + 1, 1.0 / ticks_per_year) - 1.0 + return g_per_tick diff --git a/agents/MinterAgents.py b/agents/MinterAgents.py new file mode 100644 index 0000000000000000000000000000000000000000..830b26f0be83b5f25199f33356faa69686cad10b --- /dev/null +++ b/agents/MinterAgents.py @@ -0,0 +1,296 @@ +import math + +from enforce_typing import enforce_types + +from engine import AgentBase +from util.constants import S_PER_YEAR, BITCOIN_NUM_HALF_LIVES + +# ==================================================================== +# Linear minting +@enforce_types +class OCEANLinearMinterAgent(AgentBase.AgentBaseNoEvm): + """ + Mints OCEAN according to a schedule, then send to receiving agent. + + Minting schedule is linear (flat): same # OCEAN tokens each time, n times. + """ + + def __init__( + self, + name: str, + receiving_agent_name: str, + total_OCEAN_to_mint: float, + s_between_mints: int, + n_mints: int, + ): # pylint: disable=too-many-arguments + assert total_OCEAN_to_mint >= 0.0 + if total_OCEAN_to_mint > 0.0: + assert n_mints > 0 + + super().__init__(name, USD=0.0, OCEAN=0.0) + self._receiving_agent_name: str = receiving_agent_name + self._s_between_mints: int = s_between_mints + self._OCEAN_per_mint: float = total_OCEAN_to_mint / float(n_mints) + + self._tick_previous_mint = None + self._n_mints_left: int = n_mints + + def takeStep(self, state): + if self._doMint(state): + self._mintAndDisburseFunds(state) + + def _doMint(self, state) -> bool: + assert self._n_mints_left >= 0.0 + + if self._n_mints_left == 0: + return False + if self._tick_previous_mint is None: + return True + + n_ticks_since = state.tick - self._tick_previous_mint + n_s_since = n_ticks_since * state.ss.time_step + n_s_thr = self._s_between_mints + return n_s_since >= n_s_thr + + def _mintAndDisburseFunds(self, state): + assert self._n_mints_left > 0, "only call if mints are left" + + # mint! + OCEAN: float = self._OCEAN_per_mint # type:ignore + + state._total_OCEAN_minted += OCEAN + self.receiveOCEAN(OCEAN) + + receiving_agent = state.getAgent(self._receiving_agent_name) + self._transferOCEAN(receiving_agent, OCEAN) + + self._tick_previous_mint = state.tick + self._n_mints_left -= 1 + + +# ==================================================================== +# Minting funcs: Exponential, exponential with ratchet +@enforce_types +class ExpFunc: + """ + F(H,t) = 1 - (0.5*t/H). i.e. like Bitcoin. + + Where + -t = time passed (years) (=4 for Bitcoin) + -H = half life. By H=t, 50% of tokens are dispensed. By H=2t, 75%. Etc. + """ + + def __init__(self, H: float): + assert H > 0.0 + self._H: float = H + + def __call__(self, t: float) -> float: + H = self._H + return 1.0 - math.pow(0.5, t / H) + + def keepMinting(self, t: float) -> bool: + num_half_lives = t / self._H + return num_half_lives <= BITCOIN_NUM_HALF_LIVES + + +@enforce_types +class RampedExpFunc: # pylint: disable=too-many-instance-attributes + """ + Bitcoin follows the following formula for supply of tokens + F(H,t) = 1 - (0.5*t/H). + + Where + -t = time passed (years) + -H = half life. By H=t, 50% of tokens are dispensed. By H=2t, 75%. Etc. + + So, minting is aggressive in the first 4 years. That's ok for Bitcoin. + + Challenges: + -Minting aggressively in first few years + low liquidity --> downwards + price pressure + -Bumpy funding in first few years: lots from $ from BDB + OPF + minting, + then drop when less from BDB + OPF. Better: avoid a dropoff, such that + flat or increasing. + -OceanDAO will take months or years to stabilize. Dangerous to give too + much $ to OceanDAO when it's still unstable. + + To address these challenges: + -We ratchet up the multiplier of rewards from small initially (10%), + then 25% after interval 0 (eg 0.5 years), then 50% after another interval + (eg 0.5 years), and finally 100% after a final interval (e.g. 1 year). + + We considered having ratchets based on milestones other than time. + However, non time-based milestones add complexity and are harder to govern. + + = Equations = + g(t) is the overall network rewards schedule. Itβs a piecewise model of + four exponential curves. + + g(t) = { 0 t < T0 + { g1(t) T0 <= t < T1 + { g2(t) T1 <= t < T2 + { g3(t) T2 <= t < T3 + { g4(t) otherwise + + Where gi(t) are pieces of the piecewise model chosen depending on t, + and Gi are the values of gi(t) at the inflection points. + + G1=g1(t=T1); G2=g2(t=T2); G3=g3(t=T3) + g1(t) = M1*f(t-T0) + g2(t) = M2*f(t-T0) - M2*f(T1-T0) + G1 + g3(t) = M3*f(t-T0) - M3*f(T2-T0) + G2 + g4(t) = (1-G3)*f(t-T3) + G3 + + And f(t) is the value of F(H,t) assuming a constant H. F(H,t) is the + base exponential curve. The units of H and t are years. + f(t) = F(H,t) + F(H,t) = 1 - (0.5*t/H). This is the Bitcoin formula (if H=4) + + The pieces of the model are a function of t, which are parameterized + with Ti. The units of Ti are years. + + Example parameter settings: + H=4 years + T0=0.0 + T1=0.5 + T2=1.0 + T3=2.0 + M1=0.10 + M2=0.25 + M3=0.50 + """ + + def __init__( + self, H, T0, T1, T2, T3, M1, M2, M3 + ): # pylint: disable=too-many-arguments + assert H > 0.0 + assert T0 <= T1 <= T2 <= T3 + assert M1 <= M2 <= M3 + + self._H = H + self._T0, self._T1, self._T2, self._T3 = T0, T1, T2, T3 + self._M1, self._M2, self._M3 = M1, M2, M3 + + def __call__(self, t): + return self._MYG(t) + + def keepMinting(self, t: float) -> bool: + num_half_lives = t / self._H + return num_half_lives <= BITCOIN_NUM_HALF_LIVES + + def _MYG(self, t): + MYG1, MYG2, MYG3, MYG4 = self._MYG1, self._MYG2, self._MYG3, self._MYG4 + T0, T1, T2, T3 = self._T0, self._T1, self._T2, self._T3 + G1 = MYG1(t) + G2 = MYG2(t, G1) + G3 = MYG3(t, G1, G2) + + if t < T0: + return 0.0 + if t < T1: + return MYG1(t) + if t < T2: + return MYG2(t, G1) + if t < T3: + return MYG3(t, G1, G2) + return MYG4(t, G1, G2, G3) + + def _MYG1(self, t): + MYF = self._MYF + M1 = self._M1 + T0 = self._T0 + return M1 * MYF(t - T0) + + def _MYG2(self, t, G1): # pylint: disable=unused-argument + MYF = self._MYF + M2 = self._M2 + T0, T1 = self._T0, self._T1 + return M2 * MYF(t - T0) - M2 * MYF(T1 - T0) + G1 + + def _MYG3(self, t, G1, G2): # pylint: disable=unused-argument + MYF = self._MYF + M3 = self._M3 + T0, T2 = self._T0, self._T2 + return M3 * MYF(t - T0) - M3 * MYF(T2 - T0) + G2 + + def _MYG4(self, t, G1, G2, G3): # pylint: disable=unused-argument + MYF = self._MYF + M4 = 1.0 - G3 + T3 = self._T3 + return M4 * MYF(t - T3) + G3 + + def _MYF(self, t): + H = self._H + return 1.0 - math.pow(0.5, t / H) + + +@enforce_types +class OCEANFuncMinterAgent(AgentBase.AgentBaseNoEvm): + def __init__( + self, + name: str, + receiving_agent_name: str, + total_OCEAN_to_mint: float, + s_between_mints: int, + func, + ): # pylint: disable=too-many-arguments + assert total_OCEAN_to_mint >= 0.0 + + super().__init__(name, USD=0.0, OCEAN=0.0) + self._receiving_agent_name: str = receiving_agent_name + self._total_OCEAN_to_mint: float = total_OCEAN_to_mint + self._s_between_mints: int = s_between_mints + self._func = func # e.g. RampedExpFunc + + self._tick_previous_mint = None + self._OCEAN_left_to_mint: float = total_OCEAN_to_mint + + def takeStep(self, state): + if self._doMint(state): + self._mintAndDisburseFunds(state) + + def OCEANminted(self): + return self._total_OCEAN_to_mint - self._OCEAN_left_to_mint + + def _doMint(self, state) -> bool: + if self._OCEAN_left_to_mint == 0.0: + return False + if state.tick == 0: + return False + if self._tick_previous_mint is None: + return True + + n_ticks_since = state.tick - self._tick_previous_mint + n_s_since = n_ticks_since * state.ss.time_step + n_s_thr = self._s_between_mints + return n_s_since >= n_s_thr + + def _mintAndDisburseFunds(self, state): + assert self._OCEAN_left_to_mint > 0.0, "only call if mints are left" + + t = state.tick * state.ss.time_step / S_PER_YEAR + G_t = self._func(t) + + if self._tick_previous_mint is None: + tprev = 0.0 + else: + tprev = self._tick_previous_mint * state.ss.time_step / float(S_PER_YEAR) + G_tprev = self._func(tprev) + + percent_to_mint = G_t - G_tprev + + # mint! + OCEAN: float = percent_to_mint * self._total_OCEAN_to_mint # type:ignore + + if not self._func.keepMinting(t): + OCEAN = self._OCEAN_left_to_mint + OCEAN = min(OCEAN, self._OCEAN_left_to_mint) # don't mint > OCEAN left + + state._total_OCEAN_minted += OCEAN + self.receiveOCEAN(OCEAN) + + receiving_agent = state.getAgent(self._receiving_agent_name) + self._transferOCEAN(receiving_agent, OCEAN) + + self._tick_previous_mint = state.tick + self._OCEAN_left_to_mint -= OCEAN diff --git a/agents/OCEANBurnerAgent.py b/agents/OCEANBurnerAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..2be8bafaec220bef2eef1e17fdee90587376dcee --- /dev/null +++ b/agents/OCEANBurnerAgent.py @@ -0,0 +1,19 @@ +from enforce_typing import enforce_types + +from engine import AgentBase + + +@enforce_types +class OCEANBurnerAgent(AgentBase.AgentBaseNoEvm): + def takeStep(self, state): + if self.USD() > 0.0: + # OCEAN price will go up as we buy. Reflect it here. + nloops = 10 + USD_spend_per_loop = self.USD() / float(nloops) + for _ in range(nloops): + price = state.OCEANprice() # a func of supply + OCEAN = USD_spend_per_loop / price + state._total_OCEAN_burned += OCEAN + state._total_OCEAN_burned_USD += USD_spend_per_loop + + self._wallet.withdrawUSD(self.USD()) # we've spent the USD! diff --git a/agents/PoolAgent.py b/agents/PoolAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..7ca20ec7fabb7ac4ca1218d7b30daac6ecbbb820 --- /dev/null +++ b/agents/PoolAgent.py @@ -0,0 +1,102 @@ +from enforce_typing import enforce_types + +from engine import AgentBase +from util import globaltokens +from util.constants import BROWNIE_PROJECT057, BROWNIE_PROJECT080 + + +@enforce_types +class PoolAgent(AgentBase.AgentBaseEvm): + def __init__(self, name: str, pool): + super().__init__(name, USD=0.0, OCEAN=0.0) + self._pool = pool + + self._dt_address = self._datatokenAddress() + self._dt = BROWNIE_PROJECT057.DataTokenTemplate.at(self._dt_address) + self._controller_address = self._controllerAddress() + + @property + def pool(self): + return self._pool + + @property + def datatoken_address(self) -> str: + return self._dt_address + + @property + def datatoken(self): + return self._dt + + def takeStep(self, state): + # it's a smart contract robot, it doesn't initiate anything itself + pass + + def _datatokenAddress(self): + addrs = self._pool.getCurrentTokens() + assert len(addrs) == 2 + OCEAN_addr = globaltokens.OCEAN_address() + for addr in addrs: + if addr != OCEAN_addr: + return addr + raise AssertionError("should never get here") + + @property + def controller_address(self) -> str: + return self._controller_address + + def _controllerAddress(self): + return self._pool.getController() + + +@enforce_types +class PoolAgentV4(AgentBase.AgentBaseEvm): + def __init__(self, name: str, pool): + super().__init__(name, USD=0.0, OCEAN=0.0) + self._pool = pool + + self._dt_address = self._datatokenAddress() + self._dt = BROWNIE_PROJECT080.ERC20Template.at(self._dt_address) + self._controller_address = self._controllerAddress() + + self._minter_address = self._minterAddress() + + @property + def pool(self): + return self._pool + + @property + def datatoken_address(self) -> str: + return self._dt_address + + @property + def datatoken(self): + return self._dt + + def takeStep(self, state): + # it's a smart contract robot, it doesn't initiate anything itself + pass + + def _datatokenAddress(self): + addrs = self._pool.getCurrentTokens() + assert len(addrs) == 2 + OCEAN_addr = globaltokens.OCEAN_address() + for addr in addrs: + if addr != OCEAN_addr: + return addr + raise AssertionError("should never get here") + + @property + def controller_address(self) -> str: + return self._controller_address + + def _controllerAddress(self): + return self._pool.getController() + + @property + def minter_address(self) -> str: + return self._minterAddress() + + def _minterAddress(self): + nft_address = self.datatoken.getERC721Address() + nft = BROWNIE_PROJECT080.ERC721Template.at(nft_address) + return nft.ownerOf(1) diff --git a/agents/PublisherAgent.py b/agents/PublisherAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..b5c3496164f138885ba0d97ca1f632645a751ba0 --- /dev/null +++ b/agents/PublisherAgent.py @@ -0,0 +1,506 @@ +import random +from typing import List, Optional + +from enforce_typing import enforce_types +import brownie + +from sol057.contracts.oceanv3 import oceanv3util +from sol080.contracts.oceanv4 import oceanv4util +from engine import AgentBase +from util import globaltokens +from util.base18 import toBase18 +from util.constants import S_PER_DAY, S_PER_HOUR, BROWNIE_PROJECT080 +from util.tx import txdict +from agents.PoolAgent import PoolAgent, PoolAgentV4 + + +# magic numbers +DEFAULT_DT_init = 1000.0 +DEFAULT_DT_stake = 20.0 +DEFAULT_pool_weight_DT = 3.0 +DEFAULT_pool_weight_OCEAN = 7.0 +DEFAULT_s_between_create = 7 * S_PER_DAY +DEFAULT_s_between_unstake = 3 * S_PER_DAY +DEFAULT_s_between_sellDT = 15 * S_PER_DAY +PERCENT_UNSTAKE = 0.10 + +DEFAULT_is_malicious = False +DEFAULT_s_wait_to_rug = int(DEFAULT_s_between_create / 2) +DEFAULT_s_rug_time = int(DEFAULT_s_wait_to_rug / 5) + +# magic numbers specific to oceanv4 +DEFAULT_DT_CAP = 1000.0 +DEFAULT_vested_amount = 100.0 +DEFAULT_s_between_getVesting = 12 * S_PER_HOUR +ss_DT_vested_blocks = 600 + + +class PublisherStrategy: # pylint: disable=too-many-instance-attributes + def __init__( + self, + DT_init: float = DEFAULT_DT_init, + DT_stake: float = DEFAULT_DT_stake, + pool_weight_DT: float = DEFAULT_pool_weight_DT, + pool_weight_OCEAN: float = DEFAULT_pool_weight_OCEAN, + s_between_create: int = DEFAULT_s_between_create, + s_between_unstake: int = DEFAULT_s_between_unstake, + s_between_sellDT: int = DEFAULT_s_between_sellDT, + is_malicious: bool = DEFAULT_is_malicious, + s_wait_to_rug: int = DEFAULT_s_wait_to_rug, + s_rug_time: int = DEFAULT_s_rug_time, + ): # pylint: disable=too-many-arguments + self.DT_init: float = DT_init + self.DT_stake: float = DT_stake + self.pool_weight_DT: float = pool_weight_DT + self.pool_weight_OCEAN: float = pool_weight_OCEAN + self.s_between_create: int = s_between_create + self.s_between_unstake: int = s_between_unstake + self.s_between_sellDT: int = s_between_sellDT + + self.is_malicious: bool = is_malicious + self.s_wait_to_rug: int = s_wait_to_rug + self.s_rug_time: int = s_rug_time + + +@enforce_types +class PublisherAgent(AgentBase.AgentBaseEvm): + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + pub_ss: Optional[PublisherStrategy] = None, # type:ignore + ): + super().__init__(name, USD, OCEAN) + if pub_ss is None: + pub_ss = PublisherStrategy() + + self.pub_ss: PublisherStrategy = pub_ss + + self._s_since_create: int = 0 + self._s_since_unstake: int = 0 + self._s_since_sellDT: int = 0 + + self.pools: List[str] = [] # pools created by this agent + + def takeStep(self, state) -> None: + self._s_since_create += state.ss.time_step + self._s_since_unstake += state.ss.time_step + self._s_since_sellDT += state.ss.time_step + + if self._doCreatePool(): + self._s_since_create = 0 + self._createPoolAgent(state) + + if self._doUnstakeOCEAN(state): + self._s_since_unstake = 0 + self._unstakeOCEANsomewhere(state) + + if self._doSellDT(state): + self._s_since_sellDT = 0 + self._sellDTsomewhere(state) + + if self._doRug(): + if (len(self.pools) > 0) & (self.pools[-1] not in state.rugged_pools): + state.rugged_pools.append(self.pools[-1]) + + def _doCreatePool(self) -> bool: + if self.OCEAN() < 200.0: # magic number + return False + return self._s_since_create >= self.pub_ss.s_between_create + + def _createPoolAgent(self, state) -> PoolAgent: + assert self.OCEAN() > 0.0, "should not call if no OCEAN" + account = self._wallet._account + OCEAN = globaltokens.OCEANtoken() + + # name + pool_i = len(state.agents.filterToPool()) + dt_name = f"DT{pool_i}" + pool_agent_name = f"pool{pool_i}" + + # new DT + DT = self._createDatatoken(dt_name, mint_amt=self.pub_ss.DT_init) + + # new pool + pool = oceanv3util.newBPool(account) + + # bind tokens & add initial liquidity + OCEAN_bind_amt = max(0, self.OCEAN() - 1.0) # magic number: use most OCEAN + DT_bind_amt = self.pub_ss.DT_stake + + DT.approve(pool.address, toBase18(DT_bind_amt), txdict(account)) + OCEAN.approve(pool.address, toBase18(OCEAN_bind_amt), txdict(account)) + + pool.bind( + DT.address, + toBase18(DT_bind_amt), + toBase18(self.pub_ss.pool_weight_DT), + txdict(account), + ) + pool.bind( + OCEAN.address, + toBase18(OCEAN_bind_amt), + toBase18(self.pub_ss.pool_weight_OCEAN), + txdict(account), + ) + + pool.finalize(txdict(account)) + + # create agent + pool_agent = PoolAgent(pool_agent_name, pool) + state.addAgent(pool_agent) + self._wallet.resetCachedInfo() + + # update self.pools + self.pools.append(pool_agent.name) + + return pool_agent + + def _doUnstakeOCEAN(self, state) -> bool: + if not state.agents.filterByNonzeroStake(self): + return False + + if self.pub_ss.is_malicious: + return ( + (self._s_since_unstake >= self.pub_ss.s_between_unstake) + & (self._s_since_create >= self.pub_ss.s_wait_to_rug) + & ( + self._s_since_create + <= self.pub_ss.s_wait_to_rug + self.pub_ss.s_rug_time + ) + ) + + return self._s_since_unstake >= self.pub_ss.s_between_unstake + + def _unstakeOCEANsomewhere(self, state): + """Choose what pool to unstake and by how much. Then do the action.""" + pool_agents = state.agents.filterByNonzeroStake(self) + pool_agent = random.choice(list(pool_agents.values())) + BPT = self.BPT(pool_agent.pool) + BPT_unstake = PERCENT_UNSTAKE * BPT + self.unstakeOCEAN(BPT_unstake, pool_agent.pool) + + def _doSellDT(self, state) -> bool: + if not self._DTsWithNonzeroBalance(state): + return False + + if self.pub_ss.is_malicious: + return ( + (self._s_since_sellDT >= self.pub_ss.s_between_sellDT) + & (self._s_since_create >= self.pub_ss.s_wait_to_rug) + & ( + self._s_since_create + <= self.pub_ss.s_wait_to_rug + self.pub_ss.s_rug_time + ) + ) + + return self._s_since_sellDT >= self.pub_ss.s_between_sellDT + + def _sellDTsomewhere(self, state, perc_sell: float = 0.01): + """Choose what DT to sell and by how much. Then do the action.""" + if self.pub_ss.is_malicious: + # unstakes the newest pool + pool_agent = state.getAgent(self.pools[-1]) + DT = pool_agent.datatoken + pool = pool_agent.pool + else: + # random by DT, then pool (could be something else) + cand_DTs = self._DTsWithNonzeroBalance(state) + DT = random.choice(cand_DTs) + cand_pools = self._poolsWithDT(state, DT) + pool = random.choice(cand_pools) + + DT_balance_amt = self.DT(DT) + assert DT_balance_amt > 0.0 + DT_sell_amt = perc_sell * DT_balance_amt + self._wallet.sellDT(pool, DT, DT_sell_amt) + + def _doRug(self): + if not self.pub_ss.is_malicious: + return False + if not self.pools: + return False + return self._s_since_create >= self.pub_ss.s_wait_to_rug + + def _rug(self, state): + assert self.pub_ss.is_malicious, "should only call if malicious" + assert self.pools, "can't rug if no pools" + assert hasattr(state, "rugged_pools"), "state needs 'rugged_pools attr" + # state.rugged_pools.append(self.pools[-1]) + + @staticmethod + def _poolsWithDT(state, DT) -> list: + """Return a list of pools that have this DT. Typically exactly 1 pool""" + return [ + pool_agent.pool + for pool_agent in state.agents.filterToPool().values() + if pool_agent.datatoken.address == DT.address + ] + + def _DTsWithNonzeroBalance(self, state) -> list: + """Return a list of Datatokens that this agent has >0 balance of""" + pool_agents = state.agents.filterToPool().values() + DTs = [pool_agent.datatoken for pool_agent in pool_agents] + return [DT for DT in DTs if self.DT(DT) > 0.0] + + def _createDatatoken(self, dt_name: str, mint_amt: float): + """Create datatoken contract and mint DTs to self.""" + account = self._wallet._account + DT = oceanv3util.newDatatoken("", dt_name, dt_name, toBase18(mint_amt), account) + DT.mint(account.address, toBase18(mint_amt), txdict(account)) + self._wallet.resetCachedInfo() + return DT + + +class PublisherStrategyV4: # pylint: disable=too-many-instance-attributes + def __init__( + self, + DT_cap: float = DEFAULT_DT_CAP, + vested_amount: float = DEFAULT_vested_amount, + s_between_create: int = DEFAULT_s_between_create, + s_between_unstake: int = DEFAULT_s_between_unstake, + s_between_sellDT: int = DEFAULT_s_between_sellDT, + is_malicious: bool = DEFAULT_is_malicious, + s_wait_to_rug: int = DEFAULT_s_wait_to_rug, + s_rug_time: int = DEFAULT_s_rug_time, + s_between_getVesting=DEFAULT_s_between_getVesting, + ): # pylint: disable=too-many-arguments + self.DT_cap: float = DT_cap + self.vested_amount: float = vested_amount + self.s_between_create: int = s_between_create + self.s_between_unstake: int = s_between_unstake + self.s_between_sellDT: int = s_between_sellDT + self.is_malicious: bool = is_malicious + self.s_wait_to_rug: int = s_wait_to_rug + self.s_rug_time: int = s_rug_time + self.s_between_getVesting = s_between_getVesting + + +@enforce_types +class PublisherAgentV4(AgentBase.AgentBaseEvm): + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + pub_ss: Optional[PublisherStrategyV4] = None, # type:ignore + ): + super().__init__(name, USD, OCEAN) + if pub_ss is None: + pub_ss = PublisherStrategyV4() + + self.pub_ss: PublisherStrategyV4 = pub_ss + + self._s_since_create: int = 0 + self._s_since_unstake: int = 0 + self._s_since_sellDT: int = 0 + + self._s_since_getVesing: int = 0 + + self.pools: List[str] = [] # pools created by this agent + + def takeStep(self, state) -> None: + self._s_since_create += state.ss.time_step + self._s_since_unstake += state.ss.time_step + self._s_since_sellDT += state.ss.time_step + self._s_since_getVesing += state.ss.time_step + + if (self._doCreatePool()) & (len(self.pools) < 1): + self._s_since_create = 0 + self._createPoolAgent(state) + + if self._doUnstakeOCEAN(state): + self._s_since_unstake = 0 + self._unstakeOCEANsomewhere(state) + + if self._doSellDT(state): + self._s_since_sellDT = 0 + self._sellDTsomewhere(state) + + if self._doRug(): + if (len(self.pools) > 0) & (self.pools[-1] not in state.rugged_pools): + state.rugged_pools.append(self.pools[-1]) + + if self._doGetVesting(): + self._s_since_getVesing = 0 + self._vest(state) + + def _doCreatePool(self) -> bool: + if self.OCEAN() < 200.0: # magic number + return False + return self._s_since_create >= self.pub_ss.s_between_create + + def _createPoolAgent(self, state) -> PoolAgentV4: + assert self.OCEAN() > 0.0, "should not call if no OCEAN" + + # name + pool_i = len(state.agents.filterToPoolV4()) + dt_name = f"DT{pool_i}" + pool_agent_name = f"pool{pool_i}" + + # Router + router = self._deployRouter() + + # new dataNFT + createDataNFT = self._creatDataNFT_withRouter(dt_name, dt_name, router) + dataNFT = createDataNFT[0] + erc721_factory = createDataNFT[1] + + # new DT + DT = self._createDataToken(dt_name, dt_name, self.pub_ss.DT_cap, dataNFT) + + # new pool + OCEAN_bind_amt = self.OCEAN() - 1 # magic number: use all the OCEAN + + pool = self._createBpool( + DT, self.pub_ss.vested_amount, OCEAN_bind_amt, erc721_factory + ) + + # create agent + pool_agent = PoolAgentV4(pool_agent_name, pool) + state.addAgent(pool_agent) + self._wallet.resetCachedInfo() + + # update self.pools + self.pools.append(pool_agent.name) + + return pool_agent + + def _doUnstakeOCEAN(self, state) -> bool: + if not state.agents.filterByNonzeroStakeV4(self): + return False + + if self.pub_ss.is_malicious: + return ( + (self._s_since_unstake >= self.pub_ss.s_between_unstake) + & (self._s_since_create >= self.pub_ss.s_wait_to_rug) + & ( + self._s_since_create + <= self.pub_ss.s_wait_to_rug + self.pub_ss.s_rug_time + ) + ) + + return self._s_since_unstake >= self.pub_ss.s_between_unstake + + def _unstakeOCEANsomewhere(self, state): + """Choose what pool to unstake and by how much. Then do the action.""" + pool_agents = state.agents.filterByNonzeroStakeV4(self) + pool_agent = random.choice(list(pool_agents.values())) + BPT = self.BPT(pool_agent.pool) + BPT_unstake = PERCENT_UNSTAKE * BPT + self.unstakeOCEAN(BPT_unstake, pool_agent.pool) + + def _doSellDT(self, state) -> bool: + if not self._DTsWithNonzeroBalance(state): + return False + + if self.pub_ss.is_malicious: + return ( + (self._s_since_sellDT >= self.pub_ss.s_between_sellDT) + & (self._s_since_create >= self.pub_ss.s_wait_to_rug) + & ( + self._s_since_create + <= self.pub_ss.s_wait_to_rug + self.pub_ss.s_rug_time + ) + ) + + return self._s_since_sellDT >= self.pub_ss.s_between_sellDT + + def _sellDTsomewhere(self, state, perc_sell: float = 0.1): + """Choose what DT to sell and by how much. Then do the action.""" + if self.pub_ss.is_malicious: + # unstakes the newest pool + pool_agent = state.getAgent(self.pools[-1]) + DT = pool_agent.datatoken + pool = pool_agent.pool + else: + # random by DT, then pool (could be something else) + cand_DTs = self._DTsWithNonzeroBalance(state) + DT = random.choice(cand_DTs) + cand_pools = self._poolsWithDT(state, DT) + pool = random.choice(cand_pools) + + DT_balance_amt = self.DT(DT) + if DT_balance_amt > 0.0: + DT_sell_amt = perc_sell * DT_balance_amt + self._wallet.sellDTV4(pool, DT, DT_sell_amt) + + def _doRug(self): + if not self.pub_ss.is_malicious: + return False + if not self.pools: + return False + return self._s_since_create >= self.pub_ss.s_wait_to_rug + + def _rug(self, state): + assert self.pub_ss.is_malicious, "should only call if malicious" + assert self.pools, "can't rug if no pools" + assert hasattr(state, "rugged_pools"), "state needs 'rugged_pools attr" + + @staticmethod + def _poolsWithDT(state, DT) -> list: + """Return a list of pools that have this DT. Typically exactly 1 pool""" + return [ + pool_agent.pool + for pool_agent in state.agents.filterToPoolV4().values() + if pool_agent.datatoken.address == DT.address + ] + + def _DTsWithNonzeroBalance(self, state) -> list: + """Return a list of Datatokens that this agent has >0 balance of""" + pool_agents = state.agents.filterToPoolV4().values() + DTs = [pool_agent.datatoken for pool_agent in pool_agents] + return [DT for DT in DTs if self.DT(DT) > 0.0] + + def _deployRouter(self): + account = self._wallet._account + return oceanv4util.deployRouter(account) + + def _creatDataNFT_withRouter(self, dataNFT_name: str, dataNFT_symbol: str, router): + account = self._wallet._account + createDataNFT = oceanv4util.createDataNFT( + dataNFT_name, dataNFT_symbol, account, router + ) + return createDataNFT + + def _createDataToken(self, DT_name, DT_symbol, DT_cap, dataNFT): + account = self._wallet._account + DT = oceanv4util.createDatatokenFromDataNFT( + DT_name, DT_symbol, DT_cap, dataNFT, account + ) + return DT + + def _createBpool( + self, datatoken, DT_vest_amount, OCEAN_init_liquidity, erc721_factory + ): + account = self._wallet._account + DT_vest_num_blocks = 600 + pool = oceanv4util.createBPoolFromDatatoken( + datatoken, + erc721_factory, + account, + OCEAN_init_liquidity, + DT_vest_amount, + DT_vest_num_blocks, + ) + return pool + + def _doGetVesting(self): + return (brownie.chain.height >= ss_DT_vested_blocks) & ( + self._s_since_getVesing == self._s_since_getVesing + ) + + def _vest(self, state): + pool_agents = state.agents.filterByNonzeroStakeV4(self).values() + for pool_agent in pool_agents: + pool = pool_agent.pool + DT = pool_agent._dt + oneSSContractAddress = pool.getController() + oneSSContract = BROWNIE_PROJECT080.SideStaking.at(oneSSContractAddress) + + get_vest = oneSSContract.getvestingAmountSoFar( + DT.address + ) < oneSSContract.getvestingAmount(DT.address) + if get_vest: + oneSSContract.getVesting(DT.address) diff --git a/agents/RouterAgent.py b/agents/RouterAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..8feb269d69cda0c01b48ac0a5fa5331568912eb7 --- /dev/null +++ b/agents/RouterAgent.py @@ -0,0 +1,73 @@ +import math +from typing import List + +from enforce_typing import enforce_types + +from engine import AgentBase +from util.constants import S_PER_MONTH + + +@enforce_types +class RouterAgent(AgentBase.AgentBaseNoEvm): + def __init__(self, name: str, USD: float, OCEAN: float, receiving_agents: dict): + """Constructor. + + Args: + name: str -- agent name + USD: float -- initial USD + OCEAN: float -- initial OCEAN + receiving_agents: dict of {agent_n_name:str : method_for_percent_going_to_agent_n:method + + Note: + The dict values are methods, not floats, so that the return value + can change over time. E.g. percent_burn changes. + + """ + super().__init__(name, USD, OCEAN) + self._receiving_agents = receiving_agents + + # track amounts over time + self._USD_per_tick: List[float] = [] # the next tick will record what's in self + self._OCEAN_per_tick: List[float] = [] # "" + + def takeStep(self, state) -> None: + # record what we had up until this point + self._USD_per_tick.append(self.USD()) + self._OCEAN_per_tick.append(self.OCEAN()) + + # disburse it all, as soon as agent has it + if self.USD() > 0: + self._disburseUSD(state) + if self.OCEAN() > 0: + self._disburseOCEAN(state) + + def _disburseUSD(self, state) -> None: + USD = self.USD() + for name, computePercent in self._receiving_agents.items(): + self._transferUSD(state.getAgent(name), computePercent() * USD) + + def _disburseOCEAN(self, state) -> None: + OCEAN = self.OCEAN() + for name, computePercent in self._receiving_agents.items(): + self._transferOCEAN(state.getAgent(name), computePercent() * OCEAN) + + def monthlyUSDreceived(self, state) -> float: + """USD received in the past month. Disburses immediately on receipt.""" + tick1 = self._tickOneMonthAgo(state) + tick2 = state.tick + return float(sum(self._USD_per_tick[tick1 : tick2 + 1])) + + def monthlyOCEANreceived(self, state) -> float: + """OCEAN received in past month. Disburses immediately on receipt.""" + tick1 = self._tickOneMonthAgo(state) + tick2 = state.tick + return float(sum(self._OCEAN_per_tick[tick1 : tick2 + 1])) + + @staticmethod + def _tickOneMonthAgo(state) -> int: + t2 = state.tick * state.ss.time_step + t1 = t2 - S_PER_MONTH + if t1 < 0: + return 0 + tick1 = int(max(0, math.floor(t1 / float(state.ss.time_step)))) + return tick1 diff --git a/agents/SpeculatorAgent.py b/agents/SpeculatorAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..7abccb159176974a2a1d883a0336bd82bfb93ee3 --- /dev/null +++ b/agents/SpeculatorAgent.py @@ -0,0 +1,194 @@ +"""Implement SpeculatorAgent *and* StakerspeculatorAgent.py +-parent: SpeculatorAgentBase +-child: SpeculatorAgent - speculate via buying & selling DT +-child: StakerspeculatorAgent - speculate via staking & unstaking +""" + +from abc import abstractmethod +import random +from typing import List + +from enforce_typing import enforce_types + +from engine import AgentBase +from util import constants + +# magic numbers +DEFAULT_s_between_speculates = 1 * constants.S_PER_DAY + + +@enforce_types +class SpeculatorAgentBase(AgentBase.AgentBaseEvm): + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + s_between_speculates: int = DEFAULT_s_between_speculates, + ): + super().__init__(name, USD, OCEAN) + + self._s_since_speculate: int = 0 + self._s_between_speculates: int = s_between_speculates + + def takeStep(self, state): + self._s_since_speculate += state.ss.time_step + + if self._doSpeculateAction(state): + self._s_since_speculate = 0 + self._speculateAction(state) + + def _doSpeculateAction(self, state): + pool_agents = self._poolsForSpeculate(state) + if not pool_agents: + return False + + return self._s_since_speculate >= self._s_between_speculates + + @abstractmethod + def _speculateAction(self, state): + pass + + @staticmethod + def _poolsForSpeculate(state) -> List[AgentBase.AgentBaseAbstract]: + pool_agents = state.agents.filterToPool() + + if hasattr(state, "rugged_pools"): + for pool_name in state.rugged_pools: + if pool_name in pool_agents: + del pool_agents[pool_name] + + pool_agents = pool_agents.values() + return pool_agents + + +class SpeculatorAgent(SpeculatorAgentBase): + """Speculates by buying and selling DT""" + + def _speculateAction(self, state): + pool_agents = self._poolsForSpeculate(state) + assert pool_agents, "need pools to be able to speculate" + + pool_agent = random.choice(list(pool_agents)) + pool = pool_agent.pool + + DT = self.DT(pool_agent.datatoken) + datatoken = pool_agent.datatoken + + max_OCEAN_allow = self.OCEAN() + if DT > 0.0 and random.random() < 0.50: # magic number + DT_sell_amt = 1.0 * DT # magic number + self._wallet.sellDT(pool, datatoken, DT_sell_amt) + + else: + DT_buy_amt = 1.0 # magic number + self._wallet.buyDT(pool, datatoken, DT_buy_amt, max_OCEAN_allow) + + +@enforce_types +class StakerspeculatorAgent(SpeculatorAgentBase): + """Speculates by staking and unstaking""" + + def _speculateAction(self, state): + pool_agents = self._poolsForSpeculate(state) + assert pool_agents, "need pools to be able to speculate" + + pool = random.choice(list(pool_agents)).pool + BPT = self.BPT(pool) + + if BPT > 0.0 and random.random() < 0.50: # magic number + BPT_sell = 0.10 * BPT # magic number + self.unstakeOCEAN(BPT_sell, pool) + + else: + OCEAN_stake = 0.10 * self.OCEAN() # magic number + self.stakeOCEAN(OCEAN_stake, pool) + + +@enforce_types +class SpeculatorAgentBaseV4(AgentBase.AgentBaseEvm): + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + s_between_speculates: int = DEFAULT_s_between_speculates, + ): + super().__init__(name, USD, OCEAN) + + self._s_since_speculate: int = 0 + self._s_between_speculates: int = s_between_speculates + + def takeStep(self, state): + self._s_since_speculate += state.ss.time_step + + if self._doSpeculateAction(state): + self._s_since_speculate = 0 + self._speculateAction(state) + + def _doSpeculateAction(self, state): + pool_agents = self._poolsForSpeculate(state) + if not pool_agents: + return False + + return self._s_since_speculate >= self._s_between_speculates + + @abstractmethod + def _speculateAction(self, state): + pass + + @staticmethod + def _poolsForSpeculate(state) -> List[AgentBase.AgentBaseAbstract]: + pool_agents = state.agents.filterToPoolV4() + + if hasattr(state, "rugged_pools"): + for pool_name in state.rugged_pools: + del pool_agents[pool_name] + + pool_agents = pool_agents.values() + return pool_agents + + +class SpeculatorAgentV4(SpeculatorAgentBaseV4): + """Speculates by buying and selling DT""" + + def _speculateAction(self, state): + pool_agents = self._poolsForSpeculate(state) + assert pool_agents, "need pools to be able to speculate" + + pool_agent = random.choice(list(pool_agents)) + pool = pool_agent.pool + + DT = self.DT(pool_agent.datatoken) + datatoken = pool_agent.datatoken + + max_OCEAN_allow = self.OCEAN() + if DT > 0.0 and random.random() < 0.50: # magic number + DT_sell_amt = 0.5 * DT # magic number + self._wallet.sellDTV4(pool, datatoken, DT_sell_amt) + + else: + DT_buy_amt = 5.0 # magic number + self._wallet.buyDTV4(pool, datatoken, DT_buy_amt, max_OCEAN_allow) + + +@enforce_types +class StakerspeculatorAgentV4(SpeculatorAgentBaseV4): + """Speculates by staking and unstaking""" + + def _speculateAction(self, state): + pool_agents = self._poolsForSpeculate(state) + assert pool_agents, "need pools to be able to speculate" + + pool = random.choice(list(pool_agents)).pool + BPT = self.BPT(pool) + + if BPT > 0.0 and random.random() < 0.50: # magic number + BPT_sell = 0.1 * BPT # magic number + self.unstakeOCEAN(BPT_sell, pool) + + else: + OCEAN_stake = 0.05 * self.OCEAN() # magic number + # OCEAN_stake = 200 + self.joinPoolAddOCEAN(OCEAN_stake, pool) + # potentially other actions diff --git a/agents/VestingBeneficiaryAgent.py b/agents/VestingBeneficiaryAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..9c27f5dc99fa956f40d8b125b41fde5fae2d4a10 --- /dev/null +++ b/agents/VestingBeneficiaryAgent.py @@ -0,0 +1,24 @@ +from enforce_typing import enforce_types + +from engine import AgentBase +from util.globaltokens import OCEAN_address +from util.tx import txdict + + +@enforce_types +class VestingBeneficiaryAgent(AgentBase.AgentBaseEvm): + def __init__( + self, name: str, USD: float, OCEAN: float, vesting_wallet_agent_name: str + ): + super().__init__(name, USD, OCEAN) + self._vesting_wallet_agent_name = vesting_wallet_agent_name + + def takeStep(self, state): + # ping the vesting wallet agent to release OCEAN + # (anyone can ping the vesting wallet; if this agent is + # the beneficiary then the vesting wallet will send OCEAN) + vw = state.getAgent(self._vesting_wallet_agent_name).vesting_wallet + vw.release(OCEAN_address(), txdict(self._wallet.account)) + + # ensure that self.OCEAN() is accurate + self._wallet.resetCachedInfo() diff --git a/agents/VestingFunderAgent.py b/agents/VestingFunderAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..bf591c355470f079edefa1770d8fb6f2f235ad85 --- /dev/null +++ b/agents/VestingFunderAgent.py @@ -0,0 +1,59 @@ +from enforce_typing import enforce_types + +from agents.VestingWalletAgent import VestingWalletAgent +from engine import AgentBase +from util.constants import BROWNIE_PROJECT057 +from util import globaltokens +from util.base18 import toBase18 +from util.tx import txdict + + +@enforce_types +class VestingFunderAgent(AgentBase.AgentBaseEvm): + """Will create and fund a VestingWalletAgent with specified name. + The vesting wallet will vest over time; when its funds are released, + they will go to the specified beneficiary agent.""" + + def __init__( + self, + name: str, + USD: float, + OCEAN: float, + vesting_wallet_agent_name: str, + beneficiary_agent_name: str, + start_timestamp: int, + duration_seconds: int, + ): # pylint: disable=too-many-arguments + super().__init__(name, USD, OCEAN) + + self._vesting_wallet_agent_name: str = vesting_wallet_agent_name + self._beneficiary_agent_name: str = beneficiary_agent_name + self._start_timestamp: int = start_timestamp + self._duration_seconds: int = duration_seconds + + self._did_funding = False + + def takeStep(self, state) -> None: + if self._did_funding: + return + self._did_funding = True + + # create vesting wallet + beneficiary_agent = state.getAgent(self._beneficiary_agent_name) + vw_contract = BROWNIE_PROJECT057.VestingWallet057.deploy( + beneficiary_agent.address, + self._start_timestamp, + self._duration_seconds, + txdict(self.account), + ) + + # fund the vesting wallet with all of self's OCEAN + token = globaltokens.OCEANtoken() + token.transfer( + vw_contract.address, toBase18(self.OCEAN()), txdict(self.account) + ) + self._wallet.resetCachedInfo() + + # create vesting wallet agent, add to state + vw_agent = VestingWalletAgent(self._vesting_wallet_agent_name, vw_contract) + state.addAgent(vw_agent) diff --git a/agents/VestingWalletAgent.py b/agents/VestingWalletAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..6c2a1413be194272363963f780749d0bd2188c2d --- /dev/null +++ b/agents/VestingWalletAgent.py @@ -0,0 +1,19 @@ +from enforce_typing import enforce_types + +from engine import AgentBase + + +@enforce_types +class VestingWalletAgent(AgentBase.AgentBaseEvm): + # this vesting wallet never owns stuff itself, therefore USD=OCEAN=0 + def __init__(self, name: str, vesting_wallet): + super().__init__(name, USD=0.0, OCEAN=0.0) + self._vesting_wallet = vesting_wallet # brownie smart contract + + @property + def vesting_wallet(self): + return self._vesting_wallet + + def takeStep(self, state): + # it's a smart contract robot, it doesn't initiate anything itself + pass diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/agents/test/__init__.py b/agents/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/agents/test/conftest.py b/agents/test/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..4decbbf5b9aa432c3005566d7b280f783259a5c8 --- /dev/null +++ b/agents/test/conftest.py @@ -0,0 +1,115 @@ +# account0 --> GOD_ACCOUNT, deploys factories, controls OCEAN +# account9 --> alice_info, gets some OCEAN in a pool below +# account2, 3, .., 8 --> not set here, but could use in other tests + +import brownie +from enforce_typing import enforce_types +import pytest + +from engine import AgentBase +from sol057.contracts.oceanv3 import oceanv3util +from util import globaltokens +from util.base18 import toBase18, fromBase18 +from util.constants import GOD_ACCOUNT +from util.tx import txdict + +accounts = brownie.network.accounts +account0, account1 = accounts[0], accounts[1] + +_OCEAN_INIT = 1000.0 +_OCEAN_STAKE = 200.0 +_DT_INIT = 100.0 +_DT_STAKE = 20.0 + +_POOL_WEIGHT_DT = 3.0 +_POOL_WEIGHT_OCEAN = 7.0 + + +@pytest.fixture +def alice_info(): + return _make_info(account0) + + +@enforce_types +def _make_info(account): + assert account.address != GOD_ACCOUNT.address + + OCEAN = globaltokens.OCEANtoken() + + # reset OCEAN balances on-chain, to avoid relying on brownie chain reverts + # -assumes that DT and BPT in each test are new tokens each time, and + # therefore don't need re-setting + for a in accounts: + if a.address != GOD_ACCOUNT.address: + OCEAN.transfer(GOD_ACCOUNT, OCEAN.balanceOf(a), txdict(a)) + + class Info: + def __init__(self): + self.account = None + self.agent = None + self.DT = None + self.pool = None + + info = Info() + info.account = account + + class SimpleAgent(AgentBase.AgentBaseEvm): + def takeStep(self, state): + pass + + info.agent = SimpleAgent("agent1", USD=0.0, OCEAN=0.0) + info.agent._wallet._account = account # force agent to use this account + info.agent._wallet.resetCachedInfo() # because account changed wallet + + info.DT = _createDT(account) + info.agent._wallet.resetCachedInfo() # because DT was deposited to account + assert info.DT.balanceOf(account) == toBase18(_DT_INIT) + + globaltokens.fundOCEANFromAbove(account.address, toBase18(_OCEAN_INIT)) + info.agent._wallet.resetCachedInfo() # because OCEAN was deposited to account + + info.pool = _createPool(info.DT, account) # create pool, stake DT & OCEAN + info.agent._wallet.resetCachedInfo() # because OCEAN & DT was staked + + # postconditions + w = info.agent._wallet + OCEAN1 = w.OCEAN() + assert w._cached_OCEAN_base is not None + OCEAN2 = fromBase18(int(w._cached_OCEAN_base)) + OCEAN3 = fromBase18(OCEAN.balanceOf(account)) + assert OCEAN1 == OCEAN2 == OCEAN3, (OCEAN1, OCEAN2, OCEAN3) + + return info + + +@enforce_types +def _createDT(account): + DT = oceanv3util.newDatatoken("foo", "DT1", "DT1", toBase18(_DT_INIT), account) + DT.mint(account.address, toBase18(_DT_INIT), txdict(account)) + return DT + + +@enforce_types +def _createPool(DT, account): + # Create OCEAN-DT pool + OCEAN = globaltokens.OCEANtoken() + pool = oceanv3util.newBPool(account) + + DT.approve(pool.address, toBase18(_DT_STAKE), txdict(account)) + OCEAN.approve(pool.address, toBase18(_OCEAN_STAKE), txdict(account)) + + assert OCEAN.balanceOf(account) >= toBase18(_DT_STAKE) + assert OCEAN.balanceOf(account) >= toBase18(_OCEAN_STAKE) + pool.bind( + DT.address, toBase18(_DT_STAKE), toBase18(_POOL_WEIGHT_DT), txdict(account) + ) + pool.bind( + OCEAN.address, + toBase18(_OCEAN_STAKE), + toBase18(_POOL_WEIGHT_OCEAN), + txdict(account), + ) + + pool.finalize(txdict(account)) + + return pool diff --git a/agents/test/test_DataconsumerAgent.py b/agents/test/test_DataconsumerAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..d6746514448d3a853ac38be503b551c47a4b3aaa --- /dev/null +++ b/agents/test/test_DataconsumerAgent.py @@ -0,0 +1,121 @@ +from enforce_typing import enforce_types +from pytest import approx + +from agents.PoolAgent import PoolAgent +from agents.DataconsumerAgent import DataconsumerAgent +from engine import AgentBase +from engine.AgentDict import AgentDict +from util import constants + + +class MockSS: + def __init__(self): + # seconds per tick + self.time_step: int = constants.S_PER_HOUR # type:ignore + self.pool_weight_DT: float = 1.0 # type:ignore + self.pool_weight_OCEAN: float = 1.0 # type:ignore + + +class MockState: + def __init__(self): + self.agents = AgentDict({}) + self.ss = MockSS() + + def addAgent(self, agent): + self.agents[agent.name] = agent + + +class SimpleAgent(AgentBase.AgentBaseEvm): + def takeStep(self, state): + pass + + +@enforce_types +def test_constructor1(): + agent = DataconsumerAgent("agent1", 0.1, 0.2) + assert agent.USD() == 0.1 + assert agent.OCEAN() == 0.2 + assert agent._s_between_buys > 0 + assert agent._profit_margin_on_consume > 0.0 + + +@enforce_types +def test_constructor2(): + agent = DataconsumerAgent("agent1", 0.1, 0.2, 3, 0.4) + assert agent._s_between_buys == 3 + assert agent._profit_margin_on_consume == 0.4 + + +@enforce_types +def test_doBuyAndConsumeDT_happy_path(alice_info): + alice_pool = alice_info.pool + state = MockState() + + agent = DataconsumerAgent("agent1", USD=0.0, OCEAN=1000.0) + + assert agent._s_since_buy == 0 + assert agent._s_between_buys > 0 + + assert not agent._doBuyAndConsumeDT(state) + + agent._s_since_buy += agent._s_between_buys + assert not state.agents.filterToPool().values() + assert not agent._doBuyAndConsumeDT(state) # still no, since no pools + + state.agents["pool1"] = PoolAgent("pool1", alice_pool) + assert state.agents.filterToPool().values() # have pools + assert agent._candPoolAgents(state) # have useful pools + assert agent._doBuyAndConsumeDT(state) + + +@enforce_types +def test_doBuyAndConsumeDT_have_rugged_pools(alice_info): + alice_pool = alice_info.pool + state = MockState() + agent = DataconsumerAgent("agent1", USD=0.0, OCEAN=1000.0) + + agent._s_since_buy += agent._s_between_buys + + state.agents["pool1"] = PoolAgent("pool1", alice_pool) + assert agent._candPoolAgents(state) # have useful pools + + state.rugged_pools = ["pool1"] # pylint: disable=attribute-defined-outside-init + assert not agent._candPoolAgents(state) # do _not_ have useful pools + + +@enforce_types +def test_buyAndConsumeDT(alice_info): + state = MockState() + + publisher_agent = SimpleAgent("agent1", USD=0.0, OCEAN=0.0) + publisher_agent._wallet = alice_info.agent._wallet + state.addAgent(publisher_agent) + + OCEAN_before = 1000.0 + consumer_agent = DataconsumerAgent("consumer1", USD=0.0, OCEAN=OCEAN_before) + consumer_agent._s_since_buy += consumer_agent._s_between_buys + state.addAgent(consumer_agent) + + pool_agent = PoolAgent("pool1", alice_info.pool) + state.addAgent(pool_agent) + + assert state.agents.filterToPool().values() # have pools + assert consumer_agent._candPoolAgents(state) # have useful pools + assert consumer_agent._doBuyAndConsumeDT(state) + + # buyAndConsumeDT + dt = state.agents["pool1"].datatoken + + assert consumer_agent.OCEAN() == OCEAN_before + assert consumer_agent.DT(dt) == 0.0 + + OCEAN_spend = consumer_agent._buyAndConsumeDT(state) + + OCEAN_after = consumer_agent.OCEAN() + OCEAN_gained = OCEAN_spend * (1.0 + consumer_agent._profit_margin_on_consume) + assert OCEAN_after == approx(OCEAN_before - OCEAN_spend + OCEAN_gained) + + assert consumer_agent.DT(dt) == 0.0 # bought 1.0, then consumed it + + # consumeDT + assert state.agents.agentByAddress(pool_agent.controller_address) diff --git a/agents/test/test_DataecosystemAgent.py b/agents/test/test_DataecosystemAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..b2bd20da446e11d0d19e9f3430f36d82d70c1e93 --- /dev/null +++ b/agents/test/test_DataecosystemAgent.py @@ -0,0 +1,25 @@ +from agents.DataecosystemAgent import DataecosystemAgent + + +def test_createPublisherAgent(): + a = DataecosystemAgent("a", 0.0, 0.0) # pylint: disable=unused-variable + + +def test_createStakerspeculatorAgent(): + pass + + +def test_createDataconsumerAgent(): + pass + + +def test_doCreatePublisherAgent(): + pass + + +def test_doCreateStakerspeculatorAgent(): + pass + + +def test_doCreateDataconsumerAgent(): + pass diff --git a/agents/test/test_GrantGivingAgent.py b/agents/test/test_GrantGivingAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..d0dec32486768779cc445c049c98b1ade417f74e --- /dev/null +++ b/agents/test/test_GrantGivingAgent.py @@ -0,0 +1,92 @@ +from enforce_typing import enforce_types + +from agents.GrantGivingAgent import GrantGivingAgent +from engine import AgentBase, SimStateBase, SimStrategyBase +from util.constants import S_PER_DAY + +SimStrategy = SimStrategyBase.SimStrategyBase +SimState = SimStateBase.SimStateBase + + +@enforce_types +def test1(): # pylint: disable=too-many-statements + ss = SimStrategy() + ss.time_step = S_PER_DAY + state = SimState(ss) + + class SimpleAgent(AgentBase.AgentBaseNoEvm): + def takeStep(self, state): + pass + + state.agents["a1"] = a1 = SimpleAgent("a1", 0.0, 0.0) + assert a1.OCEAN() == 0.0 + + g1 = GrantGivingAgent( + "g1", + USD=0.0, + OCEAN=1.0, + receiving_agent_name="a1", + s_between_grants=S_PER_DAY * 3, + n_actions=4, + ) + assert g1.OCEAN() == 1.0 + + g1.takeStep(state) + state.tick += 1 # tick = 1 #disperse here + assert g1.OCEAN() == (1.0 - 1.0 * 1 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 1 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 2 + assert g1.OCEAN() == (1.0 - 1.0 * 1 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 1 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 3 + assert g1.OCEAN() == (1.0 - 1.0 * 1 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 1 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 4 #disperse here + assert g1.OCEAN() == (1.0 - 1.0 * 2 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 2 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 5 + assert g1.OCEAN() == (1.0 - 1.0 * 2 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 2 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 6 + assert g1.OCEAN() == (1.0 - 1.0 * 2 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 2 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 7 #disperse here + assert g1.OCEAN() == (1.0 - 1.0 * 3 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 3 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 8 + assert g1.OCEAN() == (1.0 - 1.0 * 3 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 3 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 9 + assert g1.OCEAN() == (1.0 - 1.0 * 3 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 3 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 10 #disperse here + assert g1.OCEAN() == (1.0 - 1.0 * 4 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 4 / 4) + + g1.takeStep(state) + state.tick += 1 # tick = 11 + g1.takeStep(state) + state.tick += 1 # tick = 12 + g1.takeStep(state) + state.tick += 1 # tick = 13 #don't disperse, 0 left + + assert g1.OCEAN() == (1.0 - 1.0 * 4 / 4) + assert a1.OCEAN() == (0.0 + 1.0 * 4 / 4) diff --git a/agents/test/test_GrantTakingAgent.py b/agents/test/test_GrantTakingAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..8497d5e5ca9592eb456de8dea767f2b5adffe0c2 --- /dev/null +++ b/agents/test/test_GrantTakingAgent.py @@ -0,0 +1,32 @@ +from enforce_typing import enforce_types + +from agents.GrantTakingAgent import GrantTakingAgent + + +@enforce_types +def test1(): + class DummySimState: + def __init__(self): + pass + + def OCEANprice(self) -> float: + return 3.0 + + state = DummySimState() + a = GrantTakingAgent("foo", USD=10.0, OCEAN=20.0) + assert a._spent_at_tick == 0.0 + + a.takeStep(state) + assert a.USD() == 0.0 + assert a.OCEAN() == 0.0 + assert a._spent_at_tick == (10.0 + 20.0 * 3.0) + + a.takeStep(state) + assert a._spent_at_tick == 0.0 + + a.receiveUSD(5.0) + a.takeStep(state) + assert a._spent_at_tick == 5.0 + + a.takeStep(state) + assert a._spent_at_tick == 0.0 diff --git a/agents/test/test_MarketplacesAgent.py b/agents/test/test_MarketplacesAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..e23b4d96fb9ed32fe00a4f6852b7b3be21ad7013 --- /dev/null +++ b/agents/test/test_MarketplacesAgent.py @@ -0,0 +1,99 @@ +import math +import pytest + +from enforce_typing import enforce_types + +from agents.MarketplacesAgent import MarketplacesAgent +from util.constants import S_PER_DAY, S_PER_YEAR + + +@enforce_types +def test1_basic(): + a = MarketplacesAgent("mkts", 0.0, 0.0, "toll", 10.0, 0.1, 1) + assert a.numMarketplaces() == 10.0 + assert a.consumeSalesPerMarketplacePerSecond() == 0.1 + + +@enforce_types +def test2_growthRatePerTick_000(): + a = MarketplacesAgent("mkts", 0.0, 0.0, "toll", 10.0, 0.1, S_PER_YEAR) + assert a._growthRatePerTick(0.0) == 0.0 + assert a._growthRatePerTick(0.25) == 0.25 + + +@enforce_types +def test3_growthRatePerTick_025(): + a = MarketplacesAgent("mkts", 0.0, 0.0, "toll", 10.0, 0.1, S_PER_DAY) + assert a._growthRatePerTick(0.0) == 0.0 + assert a._growthRatePerTick(0.25) == _annualToDailyGrowthRate(0.25) + + +@enforce_types +def test4_takeStep(): + class DummyTollAgent: + def __init__(self): + self.USD = 3.0 + + def receiveUSD(self, USD): + self.USD += USD + + class DummyKpis: + def mktsRNDToSalesRatio(self): + return 0.0 + + class DummySS: + def __init__(self): + self.time_step = S_PER_DAY + self._percent_consume_sales_for_network = 0.05 + + def annualMktsGrowthRate(self, dummy_ratio): # pylint: disable=unused-argument + return 0.25 + + def networkRevenue(self, consume_sales: float) -> float: + return self._percent_consume_sales_for_network * consume_sales + + class DummySimState: + def __init__(self): + self.kpis, self.ss = DummyKpis(), DummySS() + self._toll_agent = DummyTollAgent() + + def getAgent(self, name: str): + assert name == "toll_agent" + return self._toll_agent + + state = DummySimState() + a = MarketplacesAgent( + name="marketplaces", + USD=10.0, + OCEAN=20.0, + toll_agent_name="toll_agent", + n_marketplaces=100.0, + sales_per_marketplace_per_s=2.0, + time_step=state.ss.time_step, + ) + g = _annualToDailyGrowthRate(0.25) + + a.takeStep(state) + assert a._n_marketplaces == (100.0 * (1.0 + g)) + assert a._consume_sales_per_marketplace_per_s == (2.0 * (1.0 + g)) + assert a._consumeSalesPerTick() == ( + a._n_marketplaces * a._consume_sales_per_marketplace_per_s * S_PER_DAY + ) + expected_toll = 0.05 * a._consumeSalesPerTick() + assert state._toll_agent.USD == (3.0 + expected_toll) + + a.takeStep(state) + assert a._n_marketplaces == (100.0 * (1.0 + g) * (1.0 + g)) + assert a._consume_sales_per_marketplace_per_s == (2.0 * (1.0 + g) * (1.0 + g)) + + for _ in range(10): + a.takeStep(state) + assert pytest.approx(a._n_marketplaces) == 100.0 * math.pow(1.0 + g, 1 + 1 + 10) + assert pytest.approx(a._consume_sales_per_marketplace_per_s) == 2.0 * math.pow( + 1.0 + g, 1 + 1 + 10 + ) + + +@enforce_types +def _annualToDailyGrowthRate(annual_growth_rate: float) -> float: + return math.pow(1.0 + annual_growth_rate, 1 / 365.0) - 1.0 diff --git a/agents/test/test_MinterAgents.py b/agents/test/test_MinterAgents.py new file mode 100644 index 0000000000000000000000000000000000000000..2c6bd455f86dc8beb7eb2e462c6f71a66fc9695e --- /dev/null +++ b/agents/test/test_MinterAgents.py @@ -0,0 +1,199 @@ +from enforce_typing import enforce_types +from matplotlib import pyplot, ticker + +from agents.MinterAgents import ( + OCEANLinearMinterAgent, + ExpFunc, + RampedExpFunc, + OCEANFuncMinterAgent, +) +from engine import AgentBase +from netlists.wsloop import SimState, SimStrategy +from util.constants import S_PER_DAY, S_PER_MONTH, S_PER_YEAR + + +@enforce_types +def testOCEANLinearMinterAgent(): + ss = SimStrategy.SimStrategy() + assert hasattr(ss, "time_step") + ss.time_step = 2 + + state = SimState.SimState(ss) + + class SimpleAgent(AgentBase.AgentBaseNoEvm): + def takeStep(self, state): + pass + + state.agents["a1"] = a1 = SimpleAgent("a1", 0.0, 0.0) + + # default + minter = OCEANLinearMinterAgent( + "minter", + receiving_agent_name="a1", + total_OCEAN_to_mint=20.0, + s_between_mints=4, + n_mints=2, + ) + assert minter.USD() == 0.0 + assert minter.OCEAN() == 0.0 + assert state._total_OCEAN_minted == 0.0 + + minter.takeStep(state) + state.tick += 1 # tick=1 (2 s elapsed), 1st mint + assert minter.OCEAN() == 0.0 + assert a1.OCEAN() == 10.0 + assert state._total_OCEAN_minted == 10.0 + + minter.takeStep(state) + state.tick += 1 # tick=2 (4 s elapsed), noop + assert minter.OCEAN() == 0.0 + assert a1.OCEAN() == 10.0 + assert state._total_OCEAN_minted == 10.0 + + minter.takeStep(state) + state.tick += 1 # tick=3 (6 s elapsed), 2nd mint + assert minter.OCEAN() == 0.0 + assert a1.OCEAN() == 20.0 + assert state._total_OCEAN_minted == 20.0 + + minter.takeStep(state) + state.tick += 1 # tick=4 (8 s elapsed), noop + assert minter.OCEAN() == 0.0 + assert a1.OCEAN() == 20.0 + assert state._total_OCEAN_minted == 20.0 + + for _ in range(10): + minter.takeStep(state) + state.tick += 1 # tick=14 (28 s elapsed), noop + assert minter.OCEAN() == 0.0 + assert a1.OCEAN() == 20.0 + assert state._total_OCEAN_minted == 20.0 + + +@enforce_types +def test_funcMinter_exp(): + func = ExpFunc(H=4.0) + _test_funcMinter(func) + + +@enforce_types +def test_funcMinter_rampedExp(): + func = RampedExpFunc( + H=4.0, T0=0.0, T1=0.5, T2=1.0, T3=2.0, M1=0.10, M2=0.25, M3=0.50 + ) + _test_funcMinter(func) + + +@enforce_types +def _test_funcMinter(func): # pylint: disable=too-many-statements, too-many-locals + # Simulate with realistic conditions: half-life, OCEAN to mint, + # target num half-lives (34, like Bitcoin). + # Main check is to see whether we hit expected # years passed. + # Not realistic here: time step and s_between_mints. Both are set higher + # for unittest speed. They shouldn't affect the main check. + + manual_test = False # HACK if True + do_log_plot = False # if manual_test, linear or log plot? + max_year = 5 # if manual_test, stop earlier? + + ss = SimStrategy.SimStrategy() + assert hasattr(ss, "time_step") + if manual_test: + ss.time_step = S_PER_DAY + s_between_mints = S_PER_DAY + else: + ss.time_step = 100 * S_PER_DAY + s_between_mints = 100 * S_PER_YEAR + + state = SimState.SimState(ss) + + class SimpleAgent2(AgentBase.AgentBaseNoEvm): + def takeStep(self, state): + pass + + state.agents["a1"] = SimpleAgent2("a1", 0.0, 0.0) + + # base + minter = OCEANFuncMinterAgent( + "minter", + receiving_agent_name="a1", + total_OCEAN_to_mint=700e6, + s_between_mints=s_between_mints, + func=func, + ) + + assert minter.USD() == 0.0 + assert minter.OCEAN() == 0.0 + assert minter._receiving_agent_name == "a1" + assert minter._total_OCEAN_to_mint == 700e6 + assert minter._tick_previous_mint is None + assert minter._OCEAN_left_to_mint == 700e6 + assert minter._func._H == 4.0 + + assert state._total_OCEAN_minted == 0.0 + + # run for full length + years, OCEAN_left, OCEAN_minted = [], [], [] + + years.append(0.0) + OCEAN_left.append(minter._OCEAN_left_to_mint) + OCEAN_minted.append(minter.OCEANminted()) + + stopped_bc_inf_loop = False + while True: + minter.takeStep(state) + state.tick += 1 + + year = state.tick * ss.time_step / S_PER_YEAR + + years.append(year) + OCEAN_left.append(minter._OCEAN_left_to_mint) + OCEAN_minted.append(minter.OCEANminted()) + + mo = state.tick * ss.time_step / S_PER_MONTH + if manual_test: + print( + "tick=%d (mo=%.2f,yr=%.3f), OCEAN_left=%.4g,minted=%.4f", + state.tick, + mo, + year, + minter._OCEAN_left_to_mint, + minter.OCEANminted(), + ) + + if minter._OCEAN_left_to_mint == 0.0: + break + if manual_test and year > max_year: + break + if year > 1000: # avoid infinite loop + stopped_bc_inf_loop = True + break + assert not stopped_bc_inf_loop + if not (manual_test and year > max_year): + assert minter._OCEAN_left_to_mint == 0.0 + + # main check: did we hit target # years? + # HACK assert 130.0 <= year <= 140.0 + + if not manual_test: + return + + # plot + _, ax = pyplot.subplots() + ax.set_xlabel("Year") + + # ax.plot(years, OCEAN_left, label="OCEAN left") + # ax.set_ylabel("# OCEAN left") + + ax.plot(years, OCEAN_minted, label="OCEAN minted") + ax.set_ylabel("# OCEAN minted") + + if do_log_plot: + pyplot.yscale("log") + ax.get_yaxis().set_major_formatter( + ticker.ScalarFormatter() + ) # turn off exponential notation + + ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2g")) + + pyplot.show() # popup diff --git a/agents/test/test_OCEANBurnerAgent.py b/agents/test/test_OCEANBurnerAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..5b55bb49d6278ffeb3cbfb3432913c8388547d68 --- /dev/null +++ b/agents/test/test_OCEANBurnerAgent.py @@ -0,0 +1,68 @@ +from enforce_typing import enforce_types +from pytest import approx + +from agents.OCEANBurnerAgent import OCEANBurnerAgent + + +@enforce_types +def test_fixedOCEANprice(): + class DummySimState: + def __init__(self): + self._total_OCEAN_burned: float = 0.0 # type:ignore + self._total_OCEAN_burned_USD: float = 0.0 # type:ignore + + def OCEANprice(self) -> float: # type:ignore + return 2.0 # type:ignore + + state = DummySimState() + a = OCEANBurnerAgent("foo", USD=10.0, OCEAN=0.0) + + a.takeStep(state) + assert a.USD() == 0.0 + assert state._total_OCEAN_burned_USD, 10.0 + assert state._total_OCEAN_burned == 5.0 + assert a.OCEAN() == 0.0 + + a.receiveUSD(20.0) + a.takeStep(state) + assert a.USD() == 0.0 + assert state._total_OCEAN_burned_USD == 30.0 + assert state._total_OCEAN_burned == 15.0 + + +@enforce_types +def test_changingOCEANprice(): + class DummySimState: + def __init__(self): + self._total_OCEAN_burned: float = 0.0 # type:ignore + self._total_OCEAN_burned_USD: float = 0.0 # type:ignore + self._initial_OCEAN: float = 100.0 # type:ignore + + def OCEANprice(self) -> float: + return self.overallValuation() / self.OCEANsupply() + + def overallValuation(self) -> float: + return 200.0 + + def OCEANsupply(self) -> float: + return self._initial_OCEAN - self._total_OCEAN_burned + + state = DummySimState() + a = OCEANBurnerAgent("foo", USD=10.0, OCEAN=0.0) + assert state.OCEANprice() == 2.0 # (val 200)/(supply 100) + + a.takeStep(state) + assert 0.0 < state.OCEANsupply() < 100.0 + assert state.OCEANprice() > 2.0 + assert state.OCEANprice() == 200.0 / state.OCEANsupply() + assert a.USD() == 0.0 + assert approx(state._total_OCEAN_burned_USD) == 10.0 + assert 4.0 < state._total_OCEAN_burned < 5.0 + + price1 = state.OCEANprice() + a.receiveUSD(20.0) + a.takeStep(state) + assert a.USD() == 0.0 + assert state.OCEANprice() > price1 + assert approx(state._total_OCEAN_burned_USD) == 30.0 + assert 13.0 < state._total_OCEAN_burned < 15.0 diff --git a/agents/test/test_PoolAgent.py b/agents/test/test_PoolAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..3b6c2d435a74255bd5efeb4d4451c0d316295bbb --- /dev/null +++ b/agents/test/test_PoolAgent.py @@ -0,0 +1,17 @@ +from enforce_typing import enforce_types + +from agents import PoolAgent + + +@enforce_types +def test1(alice_info): + alice_pool, alice_DT = alice_info.pool, alice_info.DT + alice_pool_agent = PoolAgent.PoolAgent("pool_agent", alice_pool) + assert alice_pool_agent.pool.address == alice_pool.address + assert alice_pool_agent.datatoken_address == alice_DT.address + + class MockState: + pass + + state = MockState() + alice_pool_agent.takeStep(state) diff --git a/agents/test/test_PublisherAgent.py b/agents/test/test_PublisherAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..b9068f78f3276b860ed843a471b563453169fca0 --- /dev/null +++ b/agents/test/test_PublisherAgent.py @@ -0,0 +1,150 @@ +from enforce_typing import enforce_types +import pytest + +from agents.PoolAgent import PoolAgent +from agents.PublisherAgent import PublisherStrategy, PublisherAgent, PERCENT_UNSTAKE +from engine import AgentDict + + +class MockSS: + def __init__(self): + self.pool_weight_DT = 3.0 + self.pool_weight_OCEAN = 7.0 + + +class MockState: + def __init__(self): + self.agents = AgentDict.AgentDict({}) + self.ss = MockSS() + + def addAgent(self, agent): + self.agents[agent.name] = agent + + def getAgent(self, name): + return self.agents[name] + + +@enforce_types +def test_PublisherStrategy(): + pub_ss = PublisherStrategy( + DT_init=1.1, + DT_stake=2.2, + pool_weight_DT=3.3, + pool_weight_OCEAN=4.4, + s_between_create=50, + s_between_unstake=60, + s_between_sellDT=70, + is_malicious=True, + s_wait_to_rug=80, + s_rug_time=90, + ) + + assert pub_ss.DT_init == 1.1 + assert pub_ss.DT_stake == 2.2 + assert pub_ss.pool_weight_DT == 3.3 + assert pub_ss.pool_weight_OCEAN == 4.4 + assert pub_ss.s_between_create == 50 + assert pub_ss.s_between_unstake == 60 + assert pub_ss.s_between_sellDT == 70 + + assert pub_ss.is_malicious + assert pub_ss.s_wait_to_rug == 80 + assert pub_ss.s_rug_time == 90 + + +@enforce_types +@pytest.mark.parametrize("is_malicious", [False, True]) +def test_doCreatePool(is_malicious): + pub_ss = PublisherStrategy(is_malicious=is_malicious) + agent = PublisherAgent(name="a", USD=0.0, OCEAN=1000.0, pub_ss=pub_ss) + c = agent._doCreatePool() + assert c in [False, True] + + +@enforce_types +def test_constructor(): + pub_ss = PublisherStrategy() + agent = PublisherAgent("pub1", USD=1.0, OCEAN=2.0, pub_ss=pub_ss) + assert agent.USD() == 1.0 + assert agent.OCEAN() == 2.0 + assert id(agent.pub_ss) == id(pub_ss) + assert agent.pub_ss.s_between_create == pub_ss.s_between_create + + assert agent._s_since_create == 0 + assert agent._s_since_unstake == 0 + assert agent._s_since_sellDT == 0 + + assert not agent.pools + + +@enforce_types +@pytest.mark.parametrize("is_malicious", [False, True]) +def test_createPoolAgent(is_malicious): + state = MockState() + assert len(state.agents) == 0 + + pub_ss = PublisherStrategy(is_malicious=is_malicious) + pub_agent = PublisherAgent(name="a", USD=0.0, OCEAN=1000.0, pub_ss=pub_ss) + + state.addAgent(pub_agent) + assert len(state.agents) == 1 + assert len(state.agents.filterToPool()) == 0 + + pool_agent = pub_agent._createPoolAgent(state) + assert len(state.agents) == 2 + assert len(state.agents.filterToPool()) == 1 + pool_agent2 = state.agents[pool_agent.name] + assert isinstance(pool_agent2, PoolAgent) + + +@enforce_types +@pytest.mark.parametrize("is_malicious", [False, True]) +def test_unstakeOCEANsomewhere(is_malicious): + state = MockState() + + pub_ss = PublisherStrategy(is_malicious=is_malicious) + pub_agent = PublisherAgent(name="a", USD=0.0, OCEAN=1000.0, pub_ss=pub_ss) + + state.addAgent(pub_agent) + assert len(state.agents.filterByNonzeroStake(pub_agent)) == 0 + assert not pub_agent._doUnstakeOCEAN(state) + + pool_agent = pub_agent._createPoolAgent(state) + assert len(state.agents.filterByNonzeroStake(pub_agent)) == 1 + assert not pub_agent._doUnstakeOCEAN(state) + + pub_agent._s_since_unstake += pub_agent.pub_ss.s_between_unstake # force unstake + pub_agent._s_since_create += pub_agent.pub_ss.s_wait_to_rug # "" + assert pub_agent._doUnstakeOCEAN(state) + + BPT_before = pub_agent.BPT(pool_agent.pool) + pub_agent._unstakeOCEANsomewhere(state) + BPT_after = pub_agent.BPT(pool_agent.pool) + assert BPT_after == (1.0 - PERCENT_UNSTAKE) * BPT_before + + +@enforce_types +@pytest.mark.parametrize("is_malicious", [False, True]) +def test_sellDTsomewhere(is_malicious): + state = MockState() + + pub_ss = PublisherStrategy(is_malicious=is_malicious) + pub_agent = PublisherAgent(name="a", USD=0.0, OCEAN=1000.0, pub_ss=pub_ss) + + state.addAgent(pub_agent) + assert len(state.agents.filterByNonzeroStake(pub_agent)) == 0 + assert not pub_agent._doSellDT(state) + + pool_agent = pub_agent._createPoolAgent(state) + assert len(pub_agent._DTsWithNonzeroBalance(state)) == 1 + assert not pub_agent._doSellDT(state) + + pub_agent._s_since_sellDT += pub_agent.pub_ss.s_between_sellDT # force sell + pub_agent._s_since_create += pub_agent.pub_ss.s_wait_to_rug # "" + assert pub_agent._doSellDT(state) + + DT_before = pub_agent.DT(pool_agent.datatoken) + perc_sell = 0.01 + pub_agent._sellDTsomewhere(state, perc_sell=perc_sell) + DT_after = pub_agent.DT(pool_agent.datatoken) + assert DT_after == (1.0 - perc_sell) * DT_before diff --git a/agents/test/test_RouterAgent.py b/agents/test/test_RouterAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..c335693d07851cabbd58c50179553204aed6dfc0 --- /dev/null +++ b/agents/test/test_RouterAgent.py @@ -0,0 +1,79 @@ +from enforce_typing import enforce_types + +from agents.RouterAgent import RouterAgent +from engine import AgentBase, SimStateBase, SimStrategyBase +from util.constants import S_PER_DAY, S_PER_MONTH + +SimState = SimStateBase.SimStateBase +SimStrategy = SimStrategyBase.SimStrategyBase + + +@enforce_types +def test_RouterAgent(): + ss = SimStrategy() + ss.time_step = S_PER_DAY + state = SimState(ss) + + class FooAgent(AgentBase.AgentBaseNoEvm): + def takeStep(self, state): + pass + + state.agents["a1"] = a1 = FooAgent("a1", 0.0, 0.0) + state.agents["a2"] = a2 = FooAgent("a2", 0.0, 0.0) + + def perc_f1(): + return 0.2 + + def perc_f2(): + return 0.8 + + am = RouterAgent("moneyrouter", 1.0, 10.0, {"a1": perc_f1, "a2": perc_f2}) + + assert not am._USD_per_tick + assert not am._OCEAN_per_tick + assert am._tickOneMonthAgo(state) == (0) + assert am.monthlyUSDreceived(state) == (0.0) + assert am.monthlyOCEANreceived(state) == (0.0) + + am.takeStep(state) + assert a1.USD() == (0.2 * 1.0) + assert a2.USD() == (0.8 * 1.0) + + assert a1.OCEAN() == (0.2 * 10.0) + assert a2.OCEAN() == (0.8 * 10.0) + + assert am._USD_per_tick == ([1.0]) + assert am._OCEAN_per_tick == ([10.0]) + assert am._tickOneMonthAgo(state) == (0) + assert am.monthlyUSDreceived(state) == (1.0) + assert am.monthlyOCEANreceived(state) == (10.0) + + am.takeStep(state) + state.tick += 1 + am.takeStep(state) + state.tick += 1 + + assert am._USD_per_tick == ([1.0, 0.0, 0.0]) + assert am._OCEAN_per_tick == ([10.0, 0.0, 0.0]) + assert am._tickOneMonthAgo(state) == (0) + assert am.monthlyUSDreceived(state) == (1.0) + assert am.monthlyOCEANreceived(state) == (10.0) + + # make a month pass, give $ + ticks_per_mo = int(S_PER_MONTH / float(state.ss.time_step)) + for _ in range(ticks_per_mo): + am.receiveUSD(2.0) + am.receiveOCEAN(3.0) + am.takeStep(state) + state.tick += 1 + assert am._tickOneMonthAgo(state) > 1 # should be 2 + assert am.monthlyUSDreceived(state) == (2.0 * ticks_per_mo) + assert am.monthlyOCEANreceived(state) == (3.0 * ticks_per_mo) + + # make another month pass, don't give $ this time + for _ in range(ticks_per_mo + 1): + am.takeStep(state) + state.tick += 1 + assert am._tickOneMonthAgo(state) > (1 + ticks_per_mo) + assert am.monthlyUSDreceived(state) == (0.0) + assert am.monthlyOCEANreceived(state) == (0.0) diff --git a/agents/test/test_SpeculatorAgent.py b/agents/test/test_SpeculatorAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..e9a418a5303662f23c2ae19eedcd3bd5da1a363e --- /dev/null +++ b/agents/test/test_SpeculatorAgent.py @@ -0,0 +1,138 @@ +"""Test SpeculatorAgent *and* StakerspeculatorAgent.py""" +from enforce_typing import enforce_types +import pytest + +from agents.PoolAgent import PoolAgent +from agents.SpeculatorAgent import SpeculatorAgent, StakerspeculatorAgent +from engine.AgentDict import AgentDict +from util.constants import S_PER_HOUR + + +class MockSS: + def __init__(self): + # seconds per tick + self.time_step: int = S_PER_HOUR # type:ignore + + +class MockState: + def __init__(self): + self.ss = MockSS() + self.agents = AgentDict({}) + + def addAgent(self, agent): + self.agents[agent.name] = agent + + +@enforce_types +@pytest.mark.parametrize("_AgentClass", [SpeculatorAgent, StakerspeculatorAgent]) +def test_constructor1(_AgentClass): + agent = _AgentClass("agent1", 0.1, 0.2) + assert agent.USD() == 0.1 + assert agent.OCEAN() == 0.2 + assert agent._s_between_speculates > 0 + + +@enforce_types +@pytest.mark.parametrize("_AgentClass", [SpeculatorAgent, StakerspeculatorAgent]) +def test_constructor2(_AgentClass): + agent = _AgentClass("agent1", 0.1, 0.2, 3) + assert agent._s_between_speculates == 3 + + +@enforce_types +@pytest.mark.parametrize("_AgentClass", [SpeculatorAgent, StakerspeculatorAgent]) +def test_doSpeculateAction(alice_info, _AgentClass): + alice_pool = alice_info.pool + state = MockState() + + agent = _AgentClass("agent1", USD=0.0, OCEAN=0.0, s_between_speculates=1000) + + assert agent._s_since_speculate == 0 + assert agent._s_between_speculates == 1000 + + assert not agent._doSpeculateAction(state) + + agent._s_since_speculate += agent._s_between_speculates + assert not state.agents.filterToPool().values() + assert not agent._doSpeculateAction(state) # still no, since no pools + + state.agents["pool1"] = PoolAgent("pool1", alice_pool) + assert state.agents.filterToPool().values() + + assert agent._doSpeculateAction(state) + + +@enforce_types +@pytest.mark.parametrize("_AgentClass", [SpeculatorAgent, StakerspeculatorAgent]) +def test_speculateAction_nopools(_AgentClass): + state = MockState() + agent = _AgentClass("agent1", USD=0.0, OCEAN=1000.0) + assert not agent._poolsForSpeculate(state) + assert not agent._doSpeculateAction(state) + with pytest.raises(AssertionError): + agent._speculateAction(state) # error because no pools + + +@enforce_types +@pytest.mark.parametrize("_AgentClass", [SpeculatorAgent, StakerspeculatorAgent]) +def test_speculateAction_with_rugged_pools(alice_info, _AgentClass): + alice_pool = alice_info.pool + state = MockState() + state.agents["pool1"] = PoolAgent("pool1", alice_pool) + state.rugged_pools = ["pool1"] # pylint: disable=attribute-defined-outside-init + + agent = _AgentClass("agent1", USD=0.0, OCEAN=500.0) + + assert not agent._poolsForSpeculate(state) + assert not agent._doSpeculateAction(state) + with pytest.raises(AssertionError): + agent._speculateAction(state) # error because no good pools + + +@enforce_types +@pytest.mark.parametrize("_AgentClass", [SpeculatorAgent, StakerspeculatorAgent]) +def test_speculateAction_with_good_pools(alice_info, _AgentClass): + alice_pool = alice_info.pool + state = MockState() + state.agents["pool1"] = PoolAgent("pool1", alice_pool) + + agent = _AgentClass("agent1", USD=0.0, OCEAN=500.0) + + assert agent._poolsForSpeculate(state) + + assert not agent._doSpeculateAction(state) # not enough time passsed + agent._s_since_speculate += agent._s_between_speculates # make time pass + assert agent._doSpeculateAction(state) # now, enough time passed + + dt = state.agents["pool1"].datatoken + assert agent.OCEAN() == 500.0 + if _AgentClass == SpeculatorAgent: + assert agent.DT(dt) == 0.0 + else: + assert agent.BPT(alice_pool) == 0.0 + + agent._speculateAction(state) + assert agent.OCEAN() < 500.0 + if _AgentClass == SpeculatorAgent: + assert agent.DT(dt) > 0.0 + else: + assert agent.BPT(alice_pool) > 0.0 + + +@enforce_types +@pytest.mark.parametrize("_AgentClass", [SpeculatorAgent, StakerspeculatorAgent]) +def test_take_step(alice_info, _AgentClass): + alice_pool = alice_info.pool + state = MockState() + state.agents["pool1"] = PoolAgent("pool1", alice_pool) + + agent = _AgentClass("agent1", USD=0.0, OCEAN=500.0) + + num_speculates = num_loops = 0 + while num_speculates < 5 and num_loops < 10000: + agent.takeStep(state) + if num_loops > 0 and agent._s_since_speculate == 0: + num_speculates += 1 + num_loops += 1 + + assert num_speculates > 0, "should have had at least one speculate" diff --git a/agents/test/test_VestingBeneficiaryAgent.py b/agents/test/test_VestingBeneficiaryAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..62559636d26aac0969596195d8b4635322ef670e --- /dev/null +++ b/agents/test/test_VestingBeneficiaryAgent.py @@ -0,0 +1,44 @@ +from enforce_typing import enforce_types + +from agents.VestingBeneficiaryAgent import VestingBeneficiaryAgent +from util.base18 import toBase18 +from util.globaltokens import fundOCEANFromAbove + + +@enforce_types +def test1(): + beneficiary_agent = VestingBeneficiaryAgent( + "beneficiary1", USD=10.0, OCEAN=20.0, vesting_wallet_agent_name="vw1" + ) + assert beneficiary_agent.USD() == 10.0 + assert beneficiary_agent.OCEAN() == 20.0 + + class MockVestingWallet: + def __init__(self, beneficiary_address: str): + self._released = False + self._beneficiary_address = beneficiary_address + + def release(self, token_address, from_dict): # pylint: disable=unused-argument + fundOCEANFromAbove(self._beneficiary_address, toBase18(100.0)) + self._released = True + + vw = MockVestingWallet(beneficiary_agent.address) + + class MockVestingWalletAgent: + def __init__(self, vw): + self.vesting_wallet = vw + + vw_agent = MockVestingWalletAgent(vw) + + class MockSimState: + def __init__(self, vw_agent): + self.vw_agent = vw_agent + + def getAgent(self, name): # pylint: disable=unused-argument + return self.vw_agent + + state = MockSimState(vw_agent) + + beneficiary_agent.takeStep(state) + assert beneficiary_agent.USD() == 10.0 + assert beneficiary_agent.OCEAN() == (20.0 + 100.0) diff --git a/agents/test/test_VestingFunderAgent.py b/agents/test/test_VestingFunderAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..1c2d8459883a53ef2c186ea68a3b2a4e438f6caa --- /dev/null +++ b/agents/test/test_VestingFunderAgent.py @@ -0,0 +1,79 @@ +import brownie +from enforce_typing import enforce_types + +from agents.VestingFunderAgent import VestingFunderAgent +from util import globaltokens +from util.base18 import toBase18 +from util.tx import txdict + +# don't use account0, it's GOD_ACCOUNT. don't use account9, it's conftest pool +account1 = brownie.network.accounts[1] +chain = brownie.network.chain + + +@enforce_types +def test1(): + OCEAN = globaltokens.OCEANtoken() + assert OCEAN.balanceOf(account1) == 0 + + # initialize beneficiary agent + class MockBeneficiaryAgent: + def __init__(self, account): + self.account = account # brownie account + self.address = account.address + + beneficiary_agent = MockBeneficiaryAgent(account1) + assert OCEAN.balanceOf(beneficiary_agent.address) == 0 + + # initialize state + class MockState: + def __init__(self, beneficiary_agent): + self.agents = {"beneficiary1": beneficiary_agent, "vw1": None} + + def getAgent(self, name): + return self.agents[name] + + def addAgent(self, agent): + self.agents[agent.name] = agent + + def takeStep(self): + for agent in self.agents.values(): + agent.takeStep(self) + + state = MockState(beneficiary_agent) + + # initialize funder agent + funder_agent = VestingFunderAgent( + name="funder1", + USD=0.0, + OCEAN=100.0, + vesting_wallet_agent_name="vw1", + beneficiary_agent_name="beneficiary1", + start_timestamp=chain[-1].timestamp, + duration_seconds=30, + ) + assert not funder_agent._did_funding + assert funder_agent.OCEAN() == 100.0 + assert state.getAgent("vw1") is None + + # create vw agent and send OCEAN to it + funder_agent.takeStep(state) + assert state.getAgent("vw1") is not None + vw = state.getAgent("vw1").vesting_wallet + assert vw.beneficiary() == beneficiary_agent.address + assert 0 <= vw.vestedAmount(OCEAN.address, chain[-1].timestamp) < toBase18(100.0) + assert vw.released(OCEAN.address) == 0 + assert funder_agent._did_funding + assert funder_agent.OCEAN() == 0.0 + assert OCEAN.balanceOf(beneficiary_agent.address) == 0 + + # OCEAN vests + chain.mine(blocks=1, timedelta=60) + assert vw.vestedAmount(OCEAN.address, chain[-1].timestamp) == toBase18(100.0) + assert vw.released(OCEAN.address) == 0 + assert OCEAN.balanceOf(beneficiary_agent.address) == 0 + + # release OCEAN + vw.release(OCEAN.address, txdict(account1)) + assert vw.released(OCEAN.address) == toBase18(100.0) + assert OCEAN.balanceOf(beneficiary_agent.address) == toBase18(100.0) diff --git a/agents/test/test_VestingWalletAgent.py b/agents/test/test_VestingWalletAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..cec39cd47208bb240b0fbf91462374c479d35f5e --- /dev/null +++ b/agents/test/test_VestingWalletAgent.py @@ -0,0 +1,50 @@ +import brownie +from enforce_typing import enforce_types +from pytest import approx + +from agents import VestingWalletAgent +from util import globaltokens +from util.constants import BROWNIE_PROJECT057 +from util.base18 import toBase18 +from util.tx import txdict + +# don't use account0, it's GOD_ACCOUNT. don't use account9, it's conftest pool +accounts = brownie.network.accounts +account1, account2, account3 = accounts[1], accounts[2], accounts[3] +chain = brownie.network.chain + + +@enforce_types +def test1(): + token = globaltokens.OCEANtoken() + + start_timestamp = chain[-1].timestamp + 5 + duration_seconds = 30 + vw_orig = BROWNIE_PROJECT057.VestingWallet057.deploy( + account2.address, start_timestamp, duration_seconds, txdict(account1) + ) + + vw_agent = VestingWalletAgent.VestingWalletAgent("vw_agent", vw_orig) + vw = vw_agent.vesting_wallet + assert id(vw) == id(vw_orig) + globaltokens.fundOCEANFromAbove(vw.address, toBase18(5.0)) + + class MockState: + @staticmethod + def takeStep(): + chain.mine(blocks=3, timedelta=10) + + state = MockState() + state.takeStep() # pass enough time (10 s) for _some_ vesting + assert 0 < vw.vestedAmount(token.address, chain[-1].timestamp) < toBase18(5.0) + + for _ in range(6): # pass enough time for _all_ vesting + state.takeStep() + assert vw.vestedAmount(token.address, chain[-1].timestamp) == toBase18(5.0) + assert vw.released() == 0 + assert token.balanceOf(account2.address) == 0 + + # release the OCEAN. Anyone can call it. Beneficiary receives it + vw.release(token.address, txdict(account3)) + assert vw.released(token.address) / 1e18 == approx(5.0) # now it's released! + assert token.balanceOf(account2.address) == toBase18(5.0) # beneficiary is richer diff --git a/brownie-install.sh b/brownie-install.sh new file mode 100755 index 0000000000000000000000000000000000000000..588202242474c435c9529e5e45950a8850089901 --- /dev/null +++ b/brownie-install.sh @@ -0,0 +1,8 @@ +echo "brownie-install.sh: Install 3rd party libs" +brownie pm install OpenZeppelin/openzeppelin-contracts@2.1.1 +brownie pm install OpenZeppelin/openzeppelin-contracts@4.0.0 +brownie pm install OpenZeppelin/openzeppelin-contracts@4.2.0 +brownie pm install GNSPS/solidity-bytes-utils@0.8.0 + +echo "brownie-install.sh: Done" + diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000000000000000000000000000000000000..7360c554d2fcc98da910cbb8c5408803a1e8be05 --- /dev/null +++ b/compile.sh @@ -0,0 +1,24 @@ +echo "compile.sh: Compile sol057/..." +cd sol057 +echo """compiler: + solc: + version: 0.5.7""" > brownie-config.yaml +brownie compile +rm brownie-config.yaml +cd .. + +echo "" + +echo "compile.sh: Compile sol080/..." +cd sol080 +echo """compiler: + solc: + version: 0.8.10""" > brownie-config.yaml +brownie compile +rm brownie-config.yaml +cd .. + +echo "" + +echo "compile.sh: Done" + diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..17b975e74950864b5db8391f11c8b0921bd30539 --- /dev/null +++ b/conftest.py @@ -0,0 +1,20 @@ +""" +Hooks to run commands after the entire testsuite or at_exit in unittest / pytest + +https://stackoverflow.com/questions/62628825/hooks-to-run-commands-after-the-entire-testsuite-or-at-exit-in-unittest-pytest +""" +import pytest + +import brownie + + +@pytest.fixture(scope="session", autouse=True) +def session_setup_teardown(): + # setup code goes here if needed + yield + cleanup_testsuite() + + +def cleanup_testsuite(): + if brownie.network.is_connected(): + brownie.network.disconnect() diff --git a/engine/AgentBase.py b/engine/AgentBase.py new file mode 100644 index 0000000000000000000000000000000000000000..107d818a662bd7dc9b63a9220ed8659746d85af4 --- /dev/null +++ b/engine/AgentBase.py @@ -0,0 +1,109 @@ +""" +Main classes in this module: +-AgentBaseAbstract - abstract interface +-AgentBaseNoEvm - hold AgentWalletNoEvm +-AgentBaseEvm - hold AgentWalletEvm +-Sub-class AgentBase{NoEvm,Evm} for specific agents (buyers, publishers, ..) +""" + +from abc import ABC, abstractmethod +import logging + +from enforce_typing import enforce_types + +from engine.AgentWallet import AgentWalletAbstract, AgentWalletEvm, AgentWalletNoEvm +from util.constants import SAFETY +from util.strutil import StrMixin + +log = logging.getLogger("baseagent") + + +@enforce_types +class AgentBaseAbstract(ABC): + def __init__(self, name: str): + self.name = name + self._wallet: AgentWalletAbstract + + @abstractmethod + def takeStep(self, state): # this is where the Agent does *work* + pass + + # USD-related + def USD(self) -> float: + return self._wallet.USD() + + def receiveUSD(self, amount: float) -> None: + self._wallet.depositUSD(amount) + + def _transferUSD(self, receiving_agent, amount: float) -> None: + if SAFETY: + assert isinstance(receiving_agent, AgentBaseAbstract) or ( + receiving_agent is None + ) + if receiving_agent is not None: + self._wallet.transferUSD(receiving_agent._wallet, amount) + else: + self._wallet.withdrawUSD(amount) + + # OCEAN-related + def OCEAN(self) -> float: + return self._wallet.OCEAN() + + def receiveOCEAN(self, amount: float) -> None: + self._wallet.depositOCEAN(amount) + + def _transferOCEAN(self, receiving_agent, amount: float) -> None: + if SAFETY: + assert isinstance(receiving_agent, AgentBaseAbstract) or ( + receiving_agent is None + ) + if receiving_agent is not None: + self._wallet.transferOCEAN(receiving_agent._wallet, amount) + else: + self._wallet.withdrawOCEAN(amount) + + +@enforce_types +class AgentBaseNoEvm(StrMixin, AgentBaseAbstract): + def __init__(self, name: str, USD: float, OCEAN: float): + AgentBaseAbstract.__init__(self, name) + self._wallet: AgentWalletNoEvm = AgentWalletNoEvm(USD, OCEAN) + + # postconditions + assert self.USD() == USD + assert self.OCEAN() == OCEAN + + +@enforce_types +class AgentBaseEvm(StrMixin, AgentBaseAbstract): + def __init__(self, name: str, USD: float, OCEAN: float): + AgentBaseAbstract.__init__(self, name) + self._wallet: AgentWalletEvm = AgentWalletEvm(USD, OCEAN) + + # postconditions + assert self.USD() == USD + assert self.OCEAN() == OCEAN + + @property + def address(self) -> str: + return self._wallet.address + + @property + def account(self) -> str: + return self._wallet.account + + # datatoken and pool-related + def DT(self, dt) -> float: + return self._wallet.DT(dt) + + def BPT(self, pool) -> float: + return self._wallet.BPT(pool) + + def stakeOCEAN(self, OCEAN_stake: float, pool): + self._wallet.stakeOCEAN(OCEAN_stake, pool) + + def unstakeOCEAN(self, BPT_unstake: float, pool): + self._wallet.unstakeOCEAN(BPT_unstake, pool) + + def joinPoolAddOCEAN(self, OCEAN_stake: float, pool): # oceanv4 + self._wallet.joinPoolAddOCEAN(OCEAN_stake, pool) diff --git a/engine/AgentDict.py b/engine/AgentDict.py new file mode 100644 index 0000000000000000000000000000000000000000..5e3a3c6e00cdfe482d016606f834292121c02ebe --- /dev/null +++ b/engine/AgentDict.py @@ -0,0 +1,62 @@ +from enforce_typing import enforce_types + +from agents.PublisherAgent import PublisherAgent +from agents.PoolAgent import PoolAgent, PoolAgentV4 +from agents.DataconsumerAgent import DataconsumerAgent +from agents.SpeculatorAgent import SpeculatorAgent, StakerspeculatorAgent + + +@enforce_types +class AgentDict(dict): + def __init__(self, *arg, **kw): # pylint: disable=useless-super-delegation + """Extend the dict object to get the best of both worlds (object/dict)""" + super().__init__(*arg, **kw) + + def filterByNonzeroStake(self, agent): + """Which pools has 'agent' staked on?""" + return AgentDict( + { + pool_agent.name: pool_agent + for pool_agent in self.filterToPool().values() + if agent.BPT(pool_agent.pool) > 0.0 + } + ) + + def filterToPool(self): + return self.filterByClass(PoolAgent) + + def filterToPublisher(self): + return self.filterByClass(PublisherAgent) + + def filterToStakerspeculator(self): + return self.filterByClass(StakerspeculatorAgent) + + def filterToDataconsumer(self): + return self.filterByClass(DataconsumerAgent) + + def filterToSpeculator(self): + return self.filterByClass(SpeculatorAgent) + + def filterToPoolV4(self): + return self.filterByClass(PoolAgentV4) + + def filterByNonzeroStakeV4(self, agent): + """Which pools has 'agent' staked on?""" + return AgentDict( + { + pool_agent.name: pool_agent + for pool_agent in self.filterToPoolV4().values() + if agent.BPT(pool_agent.pool) > 0.0 + } + ) + + def filterByClass(self, _class): + return AgentDict( + {agent.name: agent for agent in self.values() if isinstance(agent, _class)} + ) + + def agentByAddress(self, address): + for agent in self.values(): + if agent.address == address: + return agent + return None diff --git a/engine/AgentWallet.py b/engine/AgentWallet.py new file mode 100644 index 0000000000000000000000000000000000000000..afcaa6a06dad148d1085958b2b51e177c133edb4 --- /dev/null +++ b/engine/AgentWallet.py @@ -0,0 +1,541 @@ +""" +Main classes in this module: +-AgentWalletAbstract - abstract interface +-AgentWalletNoEvm - no-EVM wallet +-AgentWalletEvm - EVM wallet + +Support classes include: +-UsdNoEvmWalletMixIn - no-EVM implement USD deposit/withdraw/.. +-OceanNoEvmWalletMixIn - no-EVM implement OCEAN deposit/withdraw/.. +-StrMixIn - for __str__() +-BurnWallet - special wallet to burn to +""" + +from abc import abstractmethod, ABC +import logging +import typing + +import brownie +from brownie.network.account import Account # pylint: disable=no-name-in-module +from enforce_typing import enforce_types + +from util import constants +from util import globaltokens +from util.base18 import toBase18, fromBase18 +from util.constants import GOD_ACCOUNT, OPF_ADDRESS +from util.strutil import asCurrency +from util.tx import txdict, transferETH + +log = logging.getLogger("wallet") + + +@enforce_types +class AgentWalletAbstract(ABC): + """ + An AgentWallet holds balances of USD and OCEAN for a given Agent. + + This is an abstract class. It has children that (a) do (b) don't use Evm. + """ + + @abstractmethod + def __init__(self, USD: float = 0.0, OCEAN: float = 0.0, private_key=None): + pass + + # =================================================================== + # OCEAN-related + @abstractmethod + def OCEAN(self) -> float: + pass + + @abstractmethod + def depositOCEAN(self, amt: float) -> None: + pass + + @abstractmethod + def withdrawOCEAN(self, amt: float) -> None: + pass + + @abstractmethod + def transferOCEAN(self, dst_wallet, amt: float) -> None: + pass + + @abstractmethod + def totalOCEANin(self) -> float: + pass + + # =================================================================== + # USD-related + @abstractmethod + def USD(self) -> float: + pass + + @abstractmethod + def depositUSD(self, amt: float) -> None: + pass + + @abstractmethod + def withdrawUSD(self, amt: float) -> None: + pass + + @abstractmethod + def transferUSD(self, dst_wallet, amt: float) -> None: + pass + + @abstractmethod + def totalUSDin(self) -> float: + pass + + +@enforce_types +class UsdNoEvmWalletMixIn: + def __init__(self, USD: float): + self._USD = USD + self._total_USD_in: float = USD + + def USD(self) -> float: + return self._USD + + def depositUSD(self, amt: float) -> None: + assert amt >= 0.0 + self._USD += amt + self._total_USD_in += amt + + def withdrawUSD(self, amt: float) -> None: + assert amt >= 0.0 + if amt > 0.0 and self._USD > 0.0: + tol = 1e-12 + if (1.0 - tol) <= amt / self._USD <= (1.0 + tol): + self._USD = amt # avoid floating point roundoff + if amt > self._USD: + amt = round(amt, 12) + if amt > self._USD: + raise ValueError( + f"USD withdraw amount ({amt}) exceeds holdings ({self._USD})" + ) + self._USD -= amt + + def transferUSD(self, dst_wallet, amt: float) -> None: + assert isinstance(dst_wallet, AgentWalletAbstract) + + self.withdrawUSD(amt) + dst_wallet.depositUSD(amt) + + def totalUSDin(self) -> float: + return self._total_USD_in + + +@enforce_types +class OceanNoEvmWalletMixIn: + def __init__(self, OCEAN: float): + self._OCEAN = OCEAN + self._total_OCEAN_in: float = OCEAN + + def OCEAN(self) -> float: + return self._OCEAN + + def depositOCEAN(self, amt: float) -> None: + assert amt >= 0.0 + self._OCEAN += amt + self._total_OCEAN_in += amt + + def withdrawOCEAN(self, amt: float) -> None: + assert amt >= 0.0 + if amt > 0.0 and self._OCEAN > 0.0: + tol = 1e-12 + if (1.0 - tol) <= amt / self._OCEAN <= (1.0 + tol): + self._OCEAN = amt # avoid floating point roundoff + if amt > self._OCEAN: + amt = round(amt, 12) + if amt > self._OCEAN: + raise ValueError( + f"OCEAN withdraw amount ({amt}) exceeds holdings ({self._OCEAN})" + ) + self._OCEAN -= amt + + def transferOCEAN(self, dst_wallet, amt: float) -> None: + assert isinstance(dst_wallet, AgentWalletAbstract) + self.withdrawOCEAN(amt) + dst_wallet.depositOCEAN(amt) + + def totalOCEANin(self) -> float: + return self._total_OCEAN_in + + +@enforce_types +class StrMixIn: + def __str__(self) -> str: + s = [] + s += ["AgentWallet={\n"] + + USD = self.USD() # type:ignore # pylint: disable=E1101 + OCEAN = self.OCEAN() # type:ignore # pylint: disable=E1101 + totalUSDin = self.totalUSDin() # type:ignore # pylint: disable=E1101 + totalOCEANin = self.totalOCEANin() # type:ignore # pylint: disable=E1101 + + s += [f"USD={asCurrency(USD)}"] + s += [f"; OCEAN={OCEAN:.6f}"] + s += [f"; total_USD_in={asCurrency(totalUSDin)}"] + s += [f"; total_OCEAN_in={totalOCEANin:.6f}"] + + s += [" /AgentWallet}"] + return "".join(s) + + +@enforce_types +class AgentWalletNoEvm( + UsdNoEvmWalletMixIn, + OceanNoEvmWalletMixIn, + StrMixIn, + AgentWalletAbstract, +): + """ + In this wallet subclass, USD and OCEAN are stored in pure Python. No Evm. + """ + + def __init__(self, USD: float = 0.0, OCEAN: float = 0.0, private_key=None): + assert private_key is None, "if no evm, no private key" + AgentWalletAbstract.__init__(self, USD, OCEAN, private_key) + UsdNoEvmWalletMixIn.__init__(self, USD) + OceanNoEvmWalletMixIn.__init__(self, OCEAN) + + # postconditions + assert self.USD() == USD + assert self.OCEAN() == OCEAN + + +@enforce_types +class AgentWalletEvm( + UsdNoEvmWalletMixIn, + StrMixIn, + AgentWalletAbstract, +): + """ + In this wallet subclass, OCEAN is on Evm. USD is stored in Python. + + It also has functionality for ETH (for gas), DTs, and BPTs / staking. + + It also serves as a thin-layer conversion interface between + -the top-level system which operates in floats + -the Evm system which operates in base18-value ints + """ + + def __init__(self, USD: float = 0.0, OCEAN: float = 0.0, private_key=None): + AgentWalletAbstract.__init__(self, USD, OCEAN, private_key) + UsdNoEvmWalletMixIn.__init__(self, USD) + + self._account: Account = None + + accounts = brownie.network.accounts + if private_key is None: + self._account = accounts.add() + else: + self._account = accounts.add(private_key=private_key) + + # Give the new wallet ETH to pay gas fees (but don't track otherwise) + transferETH(GOD_ACCOUNT, self._account, "0.01 ether") + + # OCEAN is tracked in EVM, not here. But we cache here for speed + self._burnOCEAN_nocache() # ensure 0 OCEAN (eg >1 unit tests) + self._cached_OCEAN_base: typing.Union[int, None] = None + self._total_OCEAN_in: float = OCEAN + assert self.OCEAN() == 0.0 + + globaltokens.fundOCEANFromAbove(self._account.address, toBase18(OCEAN)) + self._cached_OCEAN_base = None + + # postconditions + assert self.USD() == USD + assert self.OCEAN() == OCEAN + + @property + def account(self): + """Returns self's brownie account""" + return self._account + + @property + def address(self) -> str: + return self._account.address + + def _burnOCEAN_nocache(self): + """ + If this agent has any OCEAN, burn it. + Explicitly don't use caching, so __init__ can safely call this + """ + OCEAN_token = globaltokens.OCEANtoken() + OCEAN_balance_base = OCEAN_token.balanceOf(self._account) + if OCEAN_balance_base > 0: + OCEAN_token.transfer( + _BURN_WALLET.address, OCEAN_balance_base, txdict(self._account) + ) + + def resetCachedInfo(self): + self._cached_OCEAN_base = None + + # =================================================================== + # USD-related + def _USD_base(self) -> int: + return 0 + + # =================================================================== + # OCEAN-related + def OCEAN(self) -> float: + return fromBase18(self._OCEAN_base()) + + def _OCEAN_base(self) -> int: + OCEAN_token = globaltokens.OCEANtoken() + if self._cached_OCEAN_base is None: + self._cached_OCEAN_base = OCEAN_token.balanceOf(self.address) + + assert self._cached_OCEAN_base == OCEAN_token.balanceOf(self.address), ( + self._cached_OCEAN_base, + OCEAN_token.balanceOf(self.address), + self._account.address, + ) + + return self._cached_OCEAN_base + + def depositOCEAN(self, amt: float) -> None: + assert amt >= 0.0 + globaltokens.fundOCEANFromAbove(self._account.address, toBase18(amt)) + self._total_OCEAN_in += amt + self.resetCachedInfo() + + def withdrawOCEAN(self, amt: float) -> None: + self.transferOCEAN(_BURN_WALLET, amt) + + def transferOCEAN(self, dst_wallet, amt: float) -> None: + assert isinstance(dst_wallet, (AgentWalletEvm, BurnWallet)) + dst_address = dst_wallet.address + + amt_base = toBase18(amt) + assert amt_base >= 0 + if amt_base == 0: + return + + OCEAN_base = self._OCEAN_base() + if OCEAN_base == 0: + raise ValueError("no funds to transfer from") + + tol = 1e-12 + if (1.0 - tol) <= amt / fromBase18(OCEAN_base) <= (1.0 + tol): + amt_base = OCEAN_base + + if amt_base > OCEAN_base: + raise ValueError( + "transfer amt ({fromBase18(amt_base)})" + " exceeds OCEAN holdings ({fromBase18(OCEAN_base)})" + ) + + globaltokens.OCEANtoken().transfer(dst_address, amt_base, txdict(self._account)) + + dst_wallet._total_OCEAN_in += amt + self.resetCachedInfo() + dst_wallet.resetCachedInfo() + + def totalOCEANin(self) -> float: + return self._total_OCEAN_in + + # =================================================================== + # ETH-related. Not much here because we use it little, just for gas + def ETH(self) -> float: + return fromBase18(self._ETH_base()) + + def _ETH_base(self) -> int: # i.e. num wei + return self._account.balance() + + # =================================================================== + # datatoken and pool-related + def DT(self, dt) -> float: + return fromBase18(self._DT_base(dt)) + + def _DT_base(self, dt) -> int: + return dt.balanceOf(self.address) + + def BPT(self, pool) -> float: + return fromBase18(self._BPT_base(pool)) + + def _BPT_base(self, pool) -> int: + return pool.balanceOf(self.address) + + def sellDT(self, pool, DT, DT_sell_amt: float, min_OCEAN_amt: float = 0.0): + """Swap DT for OCEAN. min_OCEAN_amt>0 protects from slippage.""" + DT.approve(pool.address, toBase18(DT_sell_amt), txdict(self._account)) + + tokenIn_address = DT.address # entering pool + tokenAmountIn_base = toBase18(DT_sell_amt) # "" + tokenOut_address = globaltokens.OCEAN_address() # leaving pool + minAmountOut_base = toBase18(min_OCEAN_amt) # "" + maxPrice_base = 2**255 # limit by min_OCEAN_amt, not price + pool.swapExactAmountIn( + tokenIn_address, + tokenAmountIn_base, + tokenOut_address, + minAmountOut_base, + maxPrice_base, + txdict(self._account), + ) + self.resetCachedInfo() + + def buyDT(self, pool, DT, DT_buy_amt: float, max_OCEAN_allow: float): + """Swap OCEAN for DT""" + OCEAN = globaltokens.OCEANtoken() + OCEAN.approve(pool.address, toBase18(max_OCEAN_allow), txdict(self._account)) + + tokenIn_address = globaltokens.OCEAN_address() + maxAmountIn_base = toBase18(max_OCEAN_allow) + tokenOut_address = DT.address + tokenAmountOut_base = toBase18(DT_buy_amt) + maxPrice_base = 2**255 + pool.swapExactAmountOut( + tokenIn_address, + maxAmountIn_base, + tokenOut_address, + tokenAmountOut_base, + maxPrice_base, + txdict(self._account), + ) + self.resetCachedInfo() + + def stakeOCEAN(self, OCEAN_stake: float, pool): + """Convert some OCEAN to DT, then add both as liquidity.""" + OCEAN = globaltokens.OCEANtoken() + OCEAN.approve(pool.address, toBase18(OCEAN_stake), txdict(self._account)) + tokenIn_address = globaltokens.OCEAN_address() + tokenAmountIn_base = toBase18(OCEAN_stake) + minPoolAmountOut_base = toBase18(0.0) + pool.joinswapExternAmountIn( + tokenIn_address, + tokenAmountIn_base, + minPoolAmountOut_base, + txdict(self._account), + ) + self.resetCachedInfo() + + def unstakeOCEAN(self, BPT_unstake: float, pool): + tokenOut_address = globaltokens.OCEAN_address() + poolAmountIn_base = toBase18(BPT_unstake) + minAmountOut_base = toBase18(0.0) + pool.exitswapPoolAmountIn( + tokenOut_address, + poolAmountIn_base, + minAmountOut_base, + txdict(self._account), + ) + self.resetCachedInfo() + + def transferDT(self, dst_wallet, DT, amt: float) -> None: + assert isinstance(dst_wallet, (AgentWalletEvm, BurnWallet)) + dst_address = dst_wallet.address + + amt_base = toBase18(amt) + assert amt_base >= 0 + if amt_base == 0: + return + + DT_base = self._DT_base(DT) + if DT_base == 0: + raise ValueError("no data tokens to transfer") + + tol = 1e-12 + if (1.0 - tol) <= amt / fromBase18(DT_base) <= (1.0 + tol): + amt_base = DT_base + + if amt_base > DT_base: + raise ValueError( + f"transfer amt ({fromBase18(amt_base)})" + " exceeds DT holdings ({fromBase18(DT_base)})" + ) + + DT.transfer(dst_address, amt_base, txdict(self._account)) + + def joinPoolAddOCEAN(self, OCEAN_stake: float, pool): + """adds more liquidity with joinswapExternAmountIn (only OCEAN), oceanv4 contracts""" + OCEAN = globaltokens.OCEANtoken() + OCEAN.approve(pool.address, toBase18(OCEAN_stake), txdict(self._account)) + tokenAmountIn_base = toBase18(OCEAN_stake) + minPoolAmountOut_base = toBase18(0.1) + + pool.joinswapExternAmountIn( + OCEAN.address, + tokenAmountIn_base, + minPoolAmountOut_base, + txdict(self._account), + ) + self.resetCachedInfo() + + def buyDTV4(self, pool, DT, DT_buy_amt: float, max_OCEAN_allow: float): + """Swap OCEAN for DT, oceanv4 contracts""" + OCEAN = globaltokens.OCEANtoken() + OCEAN.approve(pool.address, toBase18(max_OCEAN_allow), txdict(self._account)) + + tokenIn_address = globaltokens.OCEAN_address() + tokenOut_address = DT.address + marketFeeAddress = OPF_ADDRESS + + maxAmountIn_base = toBase18(max_OCEAN_allow) + tokenAmountOut_base = toBase18(DT_buy_amt) + maxPrice_base = 2**255 + + tokenInOutMarket = [ + tokenIn_address, + tokenOut_address, + marketFeeAddress, + ] # // [tokenIn,tokenOut,marketFeeAddress] + amountsInOutMaxFee = [ + maxAmountIn_base, + tokenAmountOut_base, + maxPrice_base, + 0, + ] # [maxAmountIn,exactAmountOut,maxPrice,_swapMarketFee] + pool.swapExactAmountOut( + tokenInOutMarket, amountsInOutMaxFee, txdict(self._account) + ) + self.resetCachedInfo() + + def sellDTV4(self, pool, DT, DT_sell_amt: float, min_OCEAN_amt: float = 0.0): + """Swap DT for OCEAN. min_OCEAN_amt>0 protects from slippage.""" + DT.approve(pool.address, toBase18(DT_sell_amt), txdict(self._account)) + + tokenIn_address = DT.address # entering pool + tokenAmountIn_base = toBase18(DT_sell_amt) # "" + tokenOut_address = globaltokens.OCEAN_address() # leaving pool + minAmountOut_base = toBase18(min_OCEAN_amt) # "" + maxPrice_base = 2**255 # limit by min_OCEAN_amt, not price + marketFeeAddress = OPF_ADDRESS + tokenInOutMarket = [ + tokenIn_address, + tokenOut_address, + marketFeeAddress, + ] # [tokenIn,tokenOut,marketFeeAddress] + amountsInOutMaxFee = [ + tokenAmountIn_base, + minAmountOut_base, + maxPrice_base, + 0, + ] # [exactAmountIn,minAmountOut,maxPrice,_swapMarketFee] + + pool.swapExactAmountIn( + tokenInOutMarket, + amountsInOutMaxFee, + txdict(self._account), + ) + self.resetCachedInfo() + + +# ======================================================================== +# burn-related +@enforce_types +class BurnWallet: + """This is a wallet-level interface to send funds-to-burn to. + This is *not* a burner wallet, that's a completely different concept. + """ + + def __init__(self): + self.address = constants.BURN_ADDRESS + self._total_OCEAN_in: float = 0.0 # type:ignore + + def resetCachedInfo(self): + pass + + +_BURN_WALLET = BurnWallet() diff --git a/engine/KPIsBase.py b/engine/KPIsBase.py new file mode 100644 index 0000000000000000000000000000000000000000..c6c8ec92f20822c3f8e1400656d490eb96f0c641 --- /dev/null +++ b/engine/KPIsBase.py @@ -0,0 +1,19 @@ +from enforce_typing import enforce_types + + +@enforce_types +class KPIsBase: + def __init__(self, time_step: int): + self._time_step = time_step # seconds per tick + self._tick = 0 + + def takeStep(self, state): # pylint: disable=unused-argument + self._tick += 1 + + def tick(self) -> int: + """# ticks since start of run""" + return self._tick + + def elapsedTime(self) -> int: + """Elapsed time (seconds) since start of run""" + return self._tick * self._time_step diff --git a/engine/SimEngine.py b/engine/SimEngine.py new file mode 100644 index 0000000000000000000000000000000000000000..a936e22da8049391349ad88ad6056c96499c2c2c --- /dev/null +++ b/engine/SimEngine.py @@ -0,0 +1,126 @@ +import logging +import os + +from brownie.network import chain # pylint: disable=no-name-in-module +from enforce_typing import enforce_types + +from util.constants import S_PER_MIN, S_PER_HOUR, S_PER_DAY, S_PER_MONTH, S_PER_YEAR + +log = logging.getLogger("master") + + +@enforce_types +class SimEngine: + """ + @description + Runs a simulation. + + @attributes + state - child of SimState + output_dir -- directory of where results are stored + """ + + def __init__(self, state, output_dir: str, netlist_log_func=None): + self.state = state + self.output_dir = output_dir + self.output_csv = "data.csv" # magic number + self.netlist_log_func = netlist_log_func + + def run(self): + """ + @description + Runs the simulation! This is the main work routine. + + @return + <<none>> but it continually generates an output csv output_dir + """ + log.info("Begin.") + log.info(str(self.state.ss) + "\n") # pylint: disable=logging-not-lazy + + while True: + self.takeStep() + if self.doStop(): + break + self.state.tick += 1 + chain.mine(blocks=1, timedelta=self.state.ss.time_step) + log.info("Done") + + def takeStep(self) -> None: + """Run one tick, updates self.state""" + log.debug("=============================================") + log.debug("Tick=%d: begin", (self.state.tick)) + + if (self.elapsedSeconds() % self.state.ss.log_interval) == 0: + s, dataheader, datarow = self.createLogData() + log.info("".join(s)) + self.logToCsv(dataheader, datarow) + + # main work + self.state.takeStep() + + log.debug("=============================================") + log.debug("Tick=%d: done", self.state.tick) + + def createLogData(self): + """Compute this iter's status, and output in forms ready + for console logging and csv logging.""" + state = self.state + + s = [] # for console logging + dataheader = [] # for csv logging: list of string + datarow = [] # for csv logging: list of float + + # columns always logged: Tick, Second, Min, Hour, Day, Month, Year + s += [f"Tick={state.tick}"] + dataheader += ["Tick"] + datarow += [state.tick] + + es = float(self.elapsedSeconds()) + emi, eh, ed, emo, ey = ( + es / S_PER_MIN, + es / S_PER_HOUR, + es / S_PER_DAY, + es / S_PER_MONTH, + es / S_PER_YEAR, + ) + s += [f" ({eh:.1f} h, {ed:.1f} d, {emo:.1f} mo, {ey:.1f} y)"] + dataheader += ["Second", "Min", "Hour", "Day", "Month", "Year"] + datarow += [es, emi, eh, ed, emo, ey] + + # other columns to log + if self.netlist_log_func is not None: + s2, dataheader2, datarow2 = self.netlist_log_func(state) + s += s2 + dataheader += dataheader2 + datarow += datarow2 + + return s, dataheader, datarow + + def logToCsv(self, dataheader, datarow) -> None: + if self.output_dir is None: + return + + if not os.path.exists(self.output_dir): + os.mkdir(self.output_dir) + + full_filename = os.path.join(self.output_dir, self.output_csv) + + # if needed, create file and add header + if not os.path.exists(full_filename): + with open(full_filename, mode="w+", encoding="UTF-8") as f: + f.write(", ".join(dataheader) + "\n") + + # add in row + datarow_s = [f"{dataval}" for dataval in datarow] + with open(full_filename, mode="a+", encoding="UTF-8") as f: + f.write(", ".join(datarow_s) + "\n") + + def elapsedSeconds(self) -> int: + return self.state.tick * self.state.ss.time_step + + def doStop(self) -> bool: + if self.state.tick >= self.state.ss.max_ticks: + log.info("Stop: tick (%d) >= max", self.state.tick) + return True + + return False diff --git a/engine/SimStateBase.py b/engine/SimStateBase.py new file mode 100644 index 0000000000000000000000000000000000000000..db49ef10763954f52f8b29b133ea76e0e1bca3b2 --- /dev/null +++ b/engine/SimStateBase.py @@ -0,0 +1,42 @@ +from enforce_typing import enforce_types +from engine.AgentDict import AgentDict + + +@enforce_types +class SimStateBase: + def __init__(self, ss=None): + # number of ticks elapsed in the simulation + self.tick = 0 + + # child of SimStrategy. Holds max num ticks, etc. + self.ss = ss + + # agent instances. Holds state for each agent + self.agents = AgentDict() # agent_name : Agent instance + + # extra-agenent state variables, to track metrics. Child of Kpis + self.kpis = None + + def takeStep(self) -> None: + """This happens once per tick""" + # update agents + for agent in list(self.agents.values()): + agent.takeStep(self) + + # update global state values + self.kpis.takeStep(self) + + # ============================================================== + # basic agent management + def getAgent(self, name: str): + return self.agents[name] + + def allAgents(self): + return set(self.agents.values()) + + def numAgents(self) -> int: + return len(self.agents) + + def addAgent(self, agent): + assert agent.name not in self.agents, "have an agent with this name" + self.agents[agent.name] = agent diff --git a/engine/SimStrategyBase.py b/engine/SimStrategyBase.py new file mode 100644 index 0000000000000000000000000000000000000000..3012981cae9101098b367386be52cc0975709125 --- /dev/null +++ b/engine/SimStrategyBase.py @@ -0,0 +1,62 @@ +"""Strategies hold 'magic numbers' related to running sim engine""" +import logging + +from enforce_typing import enforce_types + +from util.constants import S_PER_MIN, S_PER_HOUR, S_PER_DAY, S_PER_MONTH, S_PER_YEAR +from util.strutil import StrMixin + +log = logging.getLogger("simstrategy") + + +@enforce_types +class SimStrategyBase(StrMixin): + def __init__(self): + # seconds per time step (tick) + self.time_step: int = S_PER_HOUR # type:ignore + + # max # time steps (ticks) to run until + self.max_ticks = 1 + + # how often to log to stdout and to csv (seconds) + self.log_interval = S_PER_DAY + + def setTimeStep(self, time_step: int): + """How many seconds are there in each time step (tick)?""" + self.time_step = time_step + + def setMaxTicks(self, max_ticks: int): + """What's the max # time steps (ticks) to run until?""" + self.max_ticks = max_ticks + + def setMaxTime(self, val: int, unit: str): + """Convenience function set max_ticks according to a time unit. + Examples: + val = 4, unit = 'hours' --> max time is 4 hours + val = 10, unit = 'years' --> max time is 10 years + Units may be: ticks, hours, days, months, years + """ + if unit == "ticks": + max_ticks = val + self.max_ticks = max_ticks + elif unit in ["min", "minutes"]: + max_hours = val + self.max_ticks = max_hours * S_PER_MIN / self.time_step + 1 + elif unit == "hours": + max_hours = val + self.max_ticks = max_hours * S_PER_HOUR / self.time_step + 1 + elif unit == "days": + max_days = val + self.max_ticks = max_days * S_PER_DAY / self.time_step + 1 + elif unit == "months": + max_months = val + self.max_ticks = max_months * S_PER_MONTH / self.time_step + 1 + elif unit == "years": + max_years = val + self.max_ticks = max_years * S_PER_YEAR / self.time_step + 1 + else: + raise ValueError(unit) + + def setLogInterval(self, log_interval: int): + """How often to log to stdout and to csv? (seconds)""" + self.log_interval = log_interval diff --git a/engine/__init__.py b/engine/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3af8d15f8847b08d37952f0df6f8c02cba56c1b3 --- /dev/null +++ b/engine/__init__.py @@ -0,0 +1,2 @@ +"""empty (and it should be for clean namespaces) +""" diff --git a/engine/test/__init__.py b/engine/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/engine/test/conftest.py b/engine/test/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..fbe3a6626fae1a33237d3ee6949dfd88f0c68da4 --- /dev/null +++ b/engine/test/conftest.py @@ -0,0 +1 @@ +from agents.test.conftest import * # pylint: disable=wildcard-import, W0614 diff --git a/engine/test/test_AgentBase.py b/engine/test/test_AgentBase.py new file mode 100644 index 0000000000000000000000000000000000000000..826385a2c6f212b2be99c6212edf1de66343767b --- /dev/null +++ b/engine/test/test_AgentBase.py @@ -0,0 +1,113 @@ +from enforce_typing import enforce_types +import pytest + +from engine.AgentBase import AgentBaseEvm, AgentBaseNoEvm +from agents.test.conftest import _DT_INIT, _DT_STAKE + + +@enforce_types +class MyTestAgentEvm(AgentBaseEvm): + def takeStep(self, state): + pass + + +@enforce_types +class MyTestAgentNoEvm(AgentBaseNoEvm): + def takeStep(self, state): + pass + + +@enforce_types +def _MyTestAgent(use_EVM): + if use_EVM: + return MyTestAgentEvm + return MyTestAgentNoEvm + + +@enforce_types +def testInitEvm(): + agent = MyTestAgentEvm("agent1", USD=1.1, OCEAN=1.2) + assert agent.name == "agent1" + assert agent.USD() == 1.1 + assert agent.OCEAN() == 1.2 + assert "MyTestAgent" in str(agent) + assert isinstance(agent.address, str) + assert agent.address == agent._wallet.address + assert id(agent.account) == id(agent._wallet.account) + + +@enforce_types +def testInitNoEvm(): + agent = MyTestAgentNoEvm("agent1", USD=1.1, OCEAN=1.2) + assert agent.name == "agent1" + assert agent.USD() == 1.1 + assert agent.OCEAN() == 1.2 + assert "MyTestAgent" in str(agent) + + +@enforce_types +@pytest.mark.parametrize("use_EVM", [True, False]) +def testReceiveAndSend(use_EVM): + # agents are of arbitary classes + agent = _MyTestAgent(use_EVM)("agent1", USD=0.0, OCEAN=3.30) + agent2 = _MyTestAgent(use_EVM)("agent2", USD=0.0, OCEAN=3.30) + + # USD + assert pytest.approx(agent.USD()) == 0.00 + agent.receiveUSD(13.25) + assert pytest.approx(agent.USD()) == 13.25 + + agent._transferUSD(None, 1.10) + assert pytest.approx(agent.USD()) == 12.15 + + assert pytest.approx(agent2.USD()) == 0.00 + agent._transferUSD(agent2, 1.00) + assert pytest.approx(agent.USD()) == (12.15 - 1.00) + assert pytest.approx(agent2.USD()) == (0.00 + 1.00) + + # OCEAN + assert pytest.approx(agent.OCEAN()) == 3.30 + agent.receiveOCEAN(2.01) + assert pytest.approx(agent.OCEAN()) == 5.31 + + agent._transferOCEAN(None, 0.20) + assert pytest.approx(agent.OCEAN()) == 5.11 + + assert pytest.approx(agent2.OCEAN()) == 3.30 + agent._transferOCEAN(agent2, 0.10) + assert pytest.approx(agent.OCEAN()) == (5.11 - 0.10) + assert pytest.approx(agent2.OCEAN()) == (3.30 + 0.10) + + +# =================================================================== +# datatoken and pool-related +@enforce_types +def test_DT(alice_info): + agent, DT = alice_info.agent, alice_info.DT + DT_amt = agent._wallet.DT(DT) + assert DT_amt == (_DT_INIT - _DT_STAKE) + + +@enforce_types +def test_BPT(alice_info): + agent, pool = alice_info.agent, alice_info.pool + assert agent.BPT(pool) == 100.0 + + +@enforce_types +def test_stakeOCEAN(alice_info): + agent, pool = alice_info.agent, alice_info.pool + OCEAN_before, BPT_before = agent.OCEAN(), agent.BPT(pool) + agent.stakeOCEAN(OCEAN_stake=20.0, pool=pool) + OCEAN_after, BPT_after = agent.OCEAN(), agent.BPT(pool) + assert OCEAN_after == (OCEAN_before - 20.0) + assert BPT_after > BPT_before + + +@enforce_types +def test_unstakeOCEAN(alice_info): + agent, pool = alice_info.agent, alice_info.pool + BPT_before = agent.BPT(pool) + agent.unstakeOCEAN(BPT_unstake=20.0, pool=pool) + BPT_after = agent.BPT(pool) + assert BPT_after == (BPT_before - 20.0) diff --git a/engine/test/test_AgentDict.py b/engine/test/test_AgentDict.py new file mode 100644 index 0000000000000000000000000000000000000000..5e0724b228de9509cf97591450f05af766d2a827 --- /dev/null +++ b/engine/test/test_AgentDict.py @@ -0,0 +1,74 @@ +from enforce_typing import enforce_types + +from engine.AgentDict import AgentDict + + +@enforce_types +def test1(): + class AgentBase: + def __init__(self, name): + self.name = name + + class FooAgent(AgentBase): + pass + + class BarAgent(AgentBase): + pass + + class BahAgent(AgentBase): + pass + + d = AgentDict( + {"foo1": FooAgent("foo1"), "foo2": FooAgent("foo2"), "bar1": BarAgent("bar1")} + ) + + foo_d = d.filterByClass(FooAgent) + assert sorted(foo_d.keys()) == ["foo1", "foo2"] + + bar_d = d.filterByClass(BarAgent) + assert sorted(bar_d.keys()) == ["bar1"] + + bah_d = d.filterByClass(BahAgent) + assert sorted(bah_d.keys()) == [] + + +@enforce_types +def test2(): + class FooAgent: + def __init__(self, name): + self.name = name + + def BPT(self, pool): # pylint: disable=unused-argument + return 0.0 + + d = AgentDict({"foo1": FooAgent("foo1")}) + assert not d.filterByNonzeroStake(FooAgent("foo2")) + + assert not d.filterToPool() + + assert not d.filterToPublisher() + + assert not d.filterToStakerspeculator() + + assert not d.filterToDataconsumer() + + assert len(d.filterByClass(FooAgent)) == 1 + + +@enforce_types +def test_agentByAddress(): + class FooAgent: + def __init__(self, name, address): + self.name = name + self.address = address + + d = AgentDict( + { + "foo1": FooAgent("foo1", "address 111"), + "foo2": FooAgent("foo2", "address 222"), + } + ) + + assert d.agentByAddress("address 111").address == "address 111" + assert d.agentByAddress("address 222").address == "address 222" + assert d.agentByAddress("address foo") is None diff --git a/engine/test/test_AgentWallet.py b/engine/test/test_AgentWallet.py new file mode 100644 index 0000000000000000000000000000000000000000..1556932216ddcf2c5a2e702cc4639a58f13fc94b --- /dev/null +++ b/engine/test/test_AgentWallet.py @@ -0,0 +1,365 @@ +from enforce_typing import enforce_types +import pytest +from pytest import approx + +from agents.test.conftest import _DT_INIT, _DT_STAKE +from engine.AgentWallet import ( + AgentWalletNoEvm, + AgentWalletEvm, + BurnWallet, + UsdNoEvmWalletMixIn, + OceanNoEvmWalletMixIn, +) +from util import constants, globaltokens +from util.base18 import fromBase18 + + +@enforce_types +def testUsdNoEvmWalletMixIn(): + m = UsdNoEvmWalletMixIn(USD=3.2) + + assert m._USD == 3.2 + assert m.USD() == 3.2 + assert m._total_USD_in == 3.2 + assert m.totalUSDin() == 3.2 + + m.depositUSD(1.1) + assert m._USD == approx(4.3) + assert m.USD() == approx(4.3) + assert m._total_USD_in == approx(4.3) + assert m.totalUSDin() == approx(4.3) + + m.withdrawUSD(0.1) + assert m._USD == approx(4.2) + assert m.USD() == approx(4.2) + assert m._total_USD_in == approx(4.3) # don't shrink; it tracks total income + assert m.totalUSDin() == approx(4.3) # "" + + w2 = AgentWalletNoEvm() + m.transferUSD(w2, 0.2) + assert m._USD == approx(4.0) + assert m.USD() == approx(4.0) + assert m._total_USD_in == approx(4.3) # don't shrink; it tracks total income + assert m.totalUSDin() == approx(4.3) # "" + + +# ======================================================================= +@enforce_types +def testOceanNoEvmWalletMixIn(): + m = OceanNoEvmWalletMixIn(OCEAN=3.2) + + assert m._OCEAN == 3.2 + assert m.OCEAN() == 3.2 + assert m._total_OCEAN_in == 3.2 + assert m.totalOCEANin() == 3.2 + + m.depositOCEAN(1.1) + assert m._OCEAN == approx(4.3) + assert m.OCEAN() == approx(4.3) + assert m._total_OCEAN_in == approx(4.3) + assert m.totalOCEANin() == approx(4.3) + + m.withdrawOCEAN(0.1) + assert m._OCEAN == approx(4.2) + assert m.OCEAN() == approx(4.2) + assert m._total_OCEAN_in == approx(4.3) # don't shrink; it tracks total income + assert m.totalOCEANin() == approx(4.3) # "" + + w2 = AgentWalletNoEvm() + m.transferOCEAN(w2, 0.2) + assert m._OCEAN == approx(4.0) + assert m.OCEAN() == approx(4.0) + assert m._total_OCEAN_in == approx(4.3) # don't shrink; it tracks total income + assert m.totalOCEANin() == approx(4.3) # "" + + +# ======================================================================= +# ======================================================================= +# BOTH AgentWalletEvm *and* AgentWalletNoEvm +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def testInitiallyEmpty(_AgentWallet): + w = _AgentWallet() + + assert w.USD() == 0.0 + assert w.OCEAN() == 0.0 + + +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def testInitiallyFilled(_AgentWallet): + w = _AgentWallet(USD=1.2, OCEAN=3.4) + + assert w.USD() == 1.2 + assert w.OCEAN() == 3.4 + + +# ======================================================================= +# USD-related +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def testUSD(_AgentWallet): + w = _AgentWallet() + + w.depositUSD(13.25) + assert w.USD() == 13.25 + + w.depositUSD(1.00) + assert w.USD() == 14.25 + + w.withdrawUSD(2.10) + assert w.USD() == 12.15 + + w.depositUSD(0.0) + w.withdrawUSD(0.0) + assert w.USD() == 12.15 + + assert w.totalUSDin() == (13.25 + 1.0) + + with pytest.raises(AssertionError): + w.depositUSD(-5.0) + + with pytest.raises(AssertionError): + w.withdrawUSD(-5.0) + + with pytest.raises(ValueError): + w.withdrawUSD(1000.0) + + +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def testFloatingPointRoundoff_USD(_AgentWallet): + w = _AgentWallet(USD=2.4) + w.withdrawUSD(2.4000000000000004) # should not get ValueError + assert w.USD() == 0.0 + assert w.OCEAN() == 0.0 + + +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def test_transferUSD(_AgentWallet): + w1 = _AgentWallet(USD=10.0) + w2 = _AgentWallet(USD=1.0) + + w1.transferUSD(w2, 2.0) + + assert w1.USD() == 10.0 - 2.0 + assert w2.USD() == 1.0 + 2.0 + + +# ======================================================================= +# OCEAN-related +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def testOCEAN(_AgentWallet): + w = _AgentWallet() + + w.depositOCEAN(13.25) + assert w.OCEAN() == 13.25 + + w.depositOCEAN(1.00) + assert w.OCEAN() == 14.25 + + w.withdrawOCEAN(2.10) + assert w.OCEAN() == 12.15 + + w.depositOCEAN(0.0) + w.withdrawOCEAN(0.0) + assert w.OCEAN() == 12.15 + + assert w.totalOCEANin() == (13.25 + 1.0) + + with pytest.raises(AssertionError): + w.depositOCEAN(-5.0) + + with pytest.raises(AssertionError): + w.withdrawOCEAN(-5.0) + + with pytest.raises(ValueError): + w.withdrawOCEAN(1000.0) + + +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def testFloatingPointRoundoff_OCEAN(_AgentWallet): + w = _AgentWallet(OCEAN=2.4) + w.withdrawOCEAN(2.4000000000000004) # should not get ValueError + assert w.USD() == 0.0 + assert w.OCEAN() == 0.0 + + +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def test_transferOCEAN(_AgentWallet): + w1 = _AgentWallet(OCEAN=10.0) + w2 = _AgentWallet(OCEAN=1.0) + + w1.transferOCEAN(w2, 2.0) + + assert w1.OCEAN() == (10.0 - 2.0) + assert w2.OCEAN() == (1.0 + 2.0) + + +# =================================================================== +# str-related +@enforce_types +@pytest.mark.parametrize("_AgentWallet", [AgentWalletNoEvm, AgentWalletEvm]) +def testStr(_AgentWallet): + w = _AgentWallet() + assert "AgentWallet" in str(w) + + +# ======================================================================= +# ======================================================================= +# ======================================================================= +# AgentWalletNoEvm +# (ADD TO ME) + +# ======================================================================= +# ======================================================================= +# ======================================================================= +# AgentWalletEvm +# __init__ related +@enforce_types +def test_initFromPrivateKey(): + private_key = "0xbbfbee4961061d506ffbb11dfea64eba16355cbf1d9c29613126ba7fec0aed5d" + target_address = "0x66aB6D9362d4F35596279692F0251Db635165871" + + w = AgentWalletEvm(private_key=private_key) + assert w.address == target_address + assert w.account is not None + + +@enforce_types +def test_initFromRandom(): + w1 = AgentWalletEvm() + w2 = AgentWalletEvm() + assert w1.address != w2.address + + +@enforce_types +def test_gotSomeETHforGas(): + w = AgentWalletEvm() + assert w.ETH() > 0.0 + + +# =================================================================== +# ETH-related +@enforce_types +def testETH1(): + # super-basic test for ETH + w = AgentWalletEvm() + assert isinstance(w.ETH(), float) + + +# =================================================================== +# =================================================================== +# =================================================================== +# burn-related +@enforce_types +def testBurnWallet(): + w = BurnWallet() + assert w.address == constants.BURN_ADDRESS + + +# =================================================================== +# datatoken and pool-related +@enforce_types +def test_DT(alice_info): + agent_wallet, DT = alice_info.agent._wallet, alice_info.DT + DT_amt = agent_wallet.DT(DT) + assert DT_amt == (_DT_INIT - _DT_STAKE) + + +@enforce_types +def test_BPT(alice_info): + agent_wallet, pool = alice_info.agent._wallet, alice_info.pool + assert agent_wallet.BPT(pool) == 100.0 + + +@enforce_types +def test_poolToDTaddress(alice_info): + DT, pool = alice_info.DT, alice_info.pool + assert _poolToDTaddress(pool) == DT.address + + +@enforce_types +def test_sellDT(alice_info): + agent_wallet, DT, pool = alice_info.agent._wallet, alice_info.DT, alice_info.pool + assert _poolToDTaddress(pool) == DT.address + + DT_before, OCEAN_before = agent_wallet.DT(DT), agent_wallet.OCEAN() + + DT_sell_amt = 10.0 + agent_wallet.sellDT(pool, DT, DT_sell_amt=DT_sell_amt) + + DT_after, OCEAN_after = agent_wallet.DT(DT), agent_wallet.OCEAN() + + assert OCEAN_after > OCEAN_before + assert DT_after == (DT_before - DT_sell_amt) + + +@enforce_types +def test_buyDT(alice_info): + alice_info.agent._wallet.resetCachedInfo() + agent_wallet, DT, pool = alice_info.agent._wallet, alice_info.DT, alice_info.pool + assert _poolToDTaddress(pool) == DT.address + + DT_before = agent_wallet.DT(DT) + OCEAN_before = agent_wallet.OCEAN() + + DT_buy_amt = 1.0 + agent_wallet.buyDT(pool, DT, DT_buy_amt=DT_buy_amt, max_OCEAN_allow=OCEAN_before) + + DT_after, OCEAN_after = agent_wallet.DT(DT), agent_wallet.OCEAN() + + assert OCEAN_after < OCEAN_before + assert DT_after == (DT_before + DT_buy_amt) + + +@enforce_types +def test_stakeOCEAN(alice_info): + alice_info.agent._wallet.resetCachedInfo() + agent_wallet, pool = alice_info.agent._wallet, alice_info.pool + OCEAN = globaltokens.OCEANtoken() + + BPT_before = agent_wallet.BPT(pool) + OCEAN1 = agent_wallet.OCEAN() + OCEAN2 = fromBase18(agent_wallet._cached_OCEAN_base) + OCEAN3 = fromBase18(OCEAN.balanceOf(agent_wallet.address)) + assert OCEAN1 == OCEAN2 == OCEAN3 + + agent_wallet.stakeOCEAN(OCEAN_stake=20.0, pool=pool) + + OCEAN_after, BPT_after = agent_wallet.OCEAN(), agent_wallet.BPT(pool) + assert OCEAN_after == (OCEAN1 - 20.0) + assert BPT_after > BPT_before + + +@enforce_types +def test_unstakeOCEAN(alice_info): + agent_wallet, pool = alice_info.agent._wallet, alice_info.pool + + BPT_before: float = agent_wallet.BPT(pool) # type:ignore + + agent_wallet.unstakeOCEAN(BPT_unstake=20.0, pool=pool) + + BPT_after = agent_wallet.BPT(pool) + assert BPT_after == (BPT_before - 20.0) + + +# =================================================================== +# helps testing + + +def _poolToDTaddress(pool) -> str: + """Return the address of the datatoken of this pool. + Don't make this public because it has strong assumptions: + assumes 2 tokens; assumes one token is OCEAN; assumes other is DT + """ + cur_addrs = pool.getCurrentTokens() + assert len(cur_addrs) == 2, "this method currently assumes 2 tokens" + OCEAN_addr = globaltokens.OCEAN_address() + assert OCEAN_addr in cur_addrs, "this method expects one token is OCEAN" + DT_addr = [addr for addr in cur_addrs if addr != OCEAN_addr][0] + return DT_addr diff --git a/engine/test/test_KPIsBase.py b/engine/test/test_KPIsBase.py new file mode 100644 index 0000000000000000000000000000000000000000..83380667822ffe8a7b5044c0874678a202c80411 --- /dev/null +++ b/engine/test/test_KPIsBase.py @@ -0,0 +1,24 @@ +from enforce_typing import enforce_types + +from engine.KPIsBase import KPIsBase + + +@enforce_types +class MyKPIs(KPIsBase): + pass + + +@enforce_types +def test1(): + kpis = MyKPIs(time_step=12) + assert kpis._time_step == 12 + assert kpis.tick() == 0 + assert kpis.elapsedTime() == 0 + + state = None + kpis.takeStep(state) + kpis.takeStep(state) + + assert kpis._tick == 2 + assert kpis.tick() == 2 + assert kpis.elapsedTime() == 2 * 12 diff --git a/engine/test/test_SimEngine.py b/engine/test/test_SimEngine.py new file mode 100644 index 0000000000000000000000000000000000000000..e3820daf03848ffd35615c2fda2d5f03217bbdf7 --- /dev/null +++ b/engine/test/test_SimEngine.py @@ -0,0 +1,83 @@ +import os +import shutil + +from brownie.network import chain # pylint: disable=no-name-in-module +from enforce_typing import enforce_types + +from engine import AgentBase +from engine import SimEngine, SimStateBase, SimStrategyBase, KPIsBase + +PATH1 = "/tmp/test_outpath1" + +# ================================================================== +# testing stubs +class SimStrategy(SimStrategyBase.SimStrategyBase): + pass + + +class KPIs(KPIsBase.KPIsBase): + def takeStep(self, state): + pass + + def tick(self): + pass + + +class SimpleAgent(AgentBase.AgentBaseNoEvm): + def takeStep(self, state): + pass + + +class SimState(SimStateBase.SimStateBase): + def __init__(self, time_step: int): + super().__init__() + self.ss = SimStrategy() + self.ss.setTimeStep(time_step) + self.ss.setLogInterval(time_step * 10) + self.kpis = KPIs(time_step) + + +# ================================================================== +# actual tests + + +@enforce_types +def setUp(): + # possible cleanup from prev run + if os.path.exists(PATH1): + shutil.rmtree(PATH1) + + +@enforce_types +def testRunLonger(): + _testRunLonger(15) + + +@enforce_types +def _testRunLonger(max_ticks): + state = SimState(10) + state.ss.setMaxTicks(max_ticks) + engine = SimEngine.SimEngine(state, PATH1) + engine.run() + + +@enforce_types +def testRunEngine(): + init_time = chain[-1].timestamp + + state = SimState(time_step=10) + state.ss.setMaxTicks(3) + engine = SimEngine.SimEngine(state, PATH1) + engine.run() + assert os.path.exists(PATH1) + assert engine.state.numAgents() >= 0 + assert engine.state.tick == 3 + + elapsed_time = chain[-1].timestamp - init_time + assert elapsed_time in [30, 31] # 3 ticks * 10 s/tick + + +@enforce_types +def tearDown(): + if os.path.exists(PATH1): + shutil.rmtree(PATH1) diff --git a/engine/test/test_SimStateBase.py b/engine/test/test_SimStateBase.py new file mode 100644 index 0000000000000000000000000000000000000000..d79ade8539f6c5209392745cc148351e92f40d31 --- /dev/null +++ b/engine/test/test_SimStateBase.py @@ -0,0 +1,58 @@ +from enforce_typing import enforce_types + +from engine import SimStateBase, SimStrategyBase, KPIsBase +from engine import AgentBase + +# ================================================================== +# testing stubs + +SimStrategy = SimStrategyBase.SimStrategyBase + + +@enforce_types +class MockKPIs(KPIsBase.KPIsBase): + def takeStep(self, state): + pass + + def tick(self): + pass + + +@enforce_types +class SimpleAgent(AgentBase.AgentBaseEvm): + def takeStep(self, state): + pass + + +@enforce_types +class MockSimState(SimStateBase.SimStateBase): + def __init__(self): + super().__init__() + self.ss = SimStrategy() + self.kpis = MockKPIs(time_step=3) + + self.addAgent(SimpleAgent("agent1", 0.0, 0.0)) + self.addAgent(SimpleAgent("agent2", 0.0, 0.0)) + + +# ================================================================== +# actual tests + + +@enforce_types +def test1(): + state = MockSimState() + assert state.tick == 0 + assert state.numAgents() == 2 + assert isinstance(state.kpis, MockKPIs) + + state.takeStep() + + assert id(state.getAgent("agent1")) == id(state.agents["agent1"]) + + assert len(state.allAgents()) == 2 + assert state.numAgents() == 2 + + agent3 = SimpleAgent("agent3", 0.0, 0.0) + state.addAgent(agent3) + assert state.numAgents() == 3 diff --git a/engine/test/test_SimStrategyBase.py b/engine/test/test_SimStrategyBase.py new file mode 100644 index 0000000000000000000000000000000000000000..112cef390cae5b52c54252f3a702987095a4e332 --- /dev/null +++ b/engine/test/test_SimStrategyBase.py @@ -0,0 +1,42 @@ +from enforce_typing import enforce_types +import pytest + +from engine.SimStrategyBase import SimStrategyBase +from util.constants import S_PER_HOUR, S_PER_DAY, S_PER_MONTH, S_PER_YEAR + + +@enforce_types +def test1(): + ss = SimStrategyBase() + assert ss.time_step > 0 + assert ss.max_ticks > 0 + assert ss.log_interval > 0 + + ss.setTimeStep(7) + assert ss.time_step == 7 + + assert "SimStrategy" in str(ss) + + ss.setMaxTicks(1000) + assert ss.max_ticks == 1000 + + ss.setMaxTime(100, "ticks") + assert ss.max_ticks == 100 + + ss.setMaxTime(10, "hours") + assert ss.max_ticks == (10 * S_PER_HOUR / ss.time_step + 1) + + ss.setMaxTime(10, "days") + assert ss.max_ticks == (10 * S_PER_DAY / ss.time_step + 1) + + ss.setMaxTime(10, "months") + assert ss.max_ticks == (10 * S_PER_MONTH / ss.time_step + 1) + + ss.setMaxTime(10, "years") + assert ss.max_ticks == (10 * S_PER_YEAR / ss.time_step + 1) + + ss.setLogInterval(32) + assert ss.log_interval == 32 + + with pytest.raises(ValueError): + ss.setMaxTime(10, "foo_unit") diff --git a/ganache.py b/ganache.py new file mode 100755 index 0000000000000000000000000000000000000000..162d189a1bb31df1462e03f2024d253ea5045fb7 --- /dev/null +++ b/ganache.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +import argparse +import os + +import eth_utils + +from util.configutil import confFileValue + +# Argument parsing for hiding ganache.py in background +parser = argparse.ArgumentParser() +parser.add_argument( + "--run-in-background", + action="store_true", + help="Run ganache on background for further use of current terminal.", +) +args = parser.parse_args() + +# Port in ganache_url must match ganache call +port_number = "8545" +ganache_url = confFileValue("general", "GANACHE_URL") +assert port_number in ganache_url + +# get ganache going, populating accounts found in conf file + +# grab private keys +alice_private_key = confFileValue("ganache", "TEST_PRIVATE_KEY1") +bob_private_key = confFileValue("ganache", "TEST_PRIVATE_KEY2") +factory_deployer_private_key = confFileValue("ganache", "FACTORY_DEPLOYER_PRIVATE_KEY") + +# launch ganache and give each private account 100 eth. +amount_eth = 100 +amount_wei = eth_utils.to_wei(amount_eth, "ether") + +ganache_command = f'ganache --port {port_number} --gasLimit 10000000000 --gasPrice 1 ---hardfork istanbul --account="{alice_private_key},{amount_wei}" --account="{bob_private_key},{amount_wei}" --account="{factory_deployer_private_key},{amount_wei}"' # pylint: disable=line-too-long + +# add 7 more accounts, for 10 total. Private keys were chosen arbitrarily. +for private_key in [ + "0x69a0315f0a6932ca52d5b1ad9ce31b2fef7de658f8da625a6d97f4dbb3ba22c1", + "0x2c06b48c205efccc3506430212630a11bcc99cad1994452898e5df63985eda10", + "0xd890fedd404c6f49daf4be91cd720df22786e1d35d579b8372cc531eed80a267", + "0x9da43e9603043299cd6c5aecec69b7713342496f3465caaadbee5db955f18010", + "0x53a2a4124387132ceae955edb80f13aa549f2e956d3f8aae0383412a3c765a93", + "0x1803ea57835da6f03f8b43458482f65f280600c278947fe0eaf78c5d7d260c81", + "0xb13f2706716d269a9639f2eb99d38ba8aaef0e210d1b35a2e40e3e8b62ab76f9", +]: + ganache_command += f' --account="{private_key},{amount_wei}"' + +if args.run_in_background: + os.system(ganache_command + "&") +else: + os.system(ganache_command) diff --git a/images/fishnado2-crop.jpeg b/images/fishnado2-crop.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..682503e79c6e2889dd536a37e161017f47b47848 Binary files /dev/null and b/images/fishnado2-crop.jpeg differ diff --git a/images/plots-crop.PNG b/images/plots-crop.PNG new file mode 100644 index 0000000000000000000000000000000000000000..9e29efd836f246fe7ad4a082196eeb638bfab783 Binary files /dev/null and b/images/plots-crop.PNG differ diff --git a/images/takestep.png b/images/takestep.png new file mode 100644 index 0000000000000000000000000000000000000000..3c95020ead6f2dc7484e23ee23e129c5b85e5564 Binary files /dev/null and b/images/takestep.png differ diff --git a/images/tokenspice-banner-thin.png b/images/tokenspice-banner-thin.png new file mode 100644 index 0000000000000000000000000000000000000000..986802e8cc1ff6d23404d247fe2bba178df286da Binary files /dev/null and b/images/tokenspice-banner-thin.png differ diff --git a/images/wsloop-example-small.png b/images/wsloop-example-small.png new file mode 100644 index 0000000000000000000000000000000000000000..26535c36ad5440c4b03b72fed2b9fc37bf015e23 Binary files /dev/null and b/images/wsloop-example-small.png differ diff --git a/logging.conf b/logging.conf new file mode 100644 index 0000000000000000000000000000000000000000..1c1714e5f15c45182acdcb3f0c9f7f1f95c206f7 --- /dev/null +++ b/logging.conf @@ -0,0 +1,116 @@ +# logging.conf: Configure log levels and more. +# Tutorial: https://docs.python.org/3/howto/logging.html + +# -------------------------------------------------------------------------- +# Custom loggers for this project. + +# On parameters: +# -"level": one of INFO, DEBUG, WARNING, ERROR, CRITICAL. Default=WARNING. +# -"handlers": one of consoleHandler (send to console), streamHandler (to file-like objects), fileHandler (to disk files), socketHandler (to TCP/IP sockets), SMTPHandler (to email), HTTPHandler (to an HTTP server), NullHandler (do nothing) +# -"qualname": A py module uses this to get its logger. Example: log = logging.getLogger('mathutil') +# -"propagate": Set to 0 to avoid double-logging. Or get fancy w/ hierarchy. + +[loggers] +keys=root,agents,baseagent,constants,kpis,master,mathutil,marketagents,minteragents,simstate,simstrategy,strutil,valuation,wallet + +[logger_agents] +level=WARNING +handlers=consoleHandler +qualname=agent +propagate=0 + +[logger_baseagent] +level=WARNING +handlers=consoleHandler +qualname=agent +propagate=0 + +[logger_constants] +level=WARNING +handlers=consoleHandler +qualname=constants +propagate=0 + +[logger_kpis] +level=WARNING +handlers=consoleHandler +qualname=kpis +propagate=0 + +[logger_master] +level=WARNING +handlers=consoleHandler +qualname=master +propagate=0 + +[logger_mathutil] +level=WARNING +handlers=consoleHandler +qualname=mathutil +propagate=0 + +[logger_marketagents] +level=WARNING +handlers=consoleHandler +qualname=marketagents +propagate=0 + +[logger_minteragents] +level=WARNING +handlers=consoleHandler +qualname=minteragents +propagate=0 + +[logger_simstate] +level=INFO +handlers=consoleHandler +qualname=simstate +propagate=0 + +[logger_simstrategy] +level=WARNING +handlers=consoleHandler +qualname=simstrategy +propagate=0 + +[logger_strutil] +level=WARNING +handlers=consoleHandler +qualname=strutil +propagate=0 + +[logger_valuation] +level=WARNING +handlers=consoleHandler +qualname=wallet +propagate=0 + +[logger_wallet] +level=WARNING +handlers=consoleHandler +qualname=wallet +propagate=0 + +# -------------------------------------------------------------------------- +# Standard setup +[formatters] +keys=simpleFormatter + +[formatter_simpleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stdout,) + +[handlers] +keys=consoleHandler + +[logger_root] +level=DEBUG +handlers=consoleHandler +qualname=root +propagate=0 diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000000000000000000000000000000000..8deea3079e6e63ee1c94269faed2bfd36d040b0a --- /dev/null +++ b/mypy.ini @@ -0,0 +1,23 @@ +[mypy] +exclude = venv|codacy-analysis-cli-master + +[mypy-eth_account.*] +ignore_missing_imports = True + +[mypy-eth_keys.*] +ignore_missing_imports = True + +[mypy-enforce_typing.*] +ignore_missing_imports = True + +[mypy-matplotlib.*] +ignore_missing_imports = True + +[mypy-numpy.*] +ignore_missing_imports = True + +[mypy-pylab.*] +ignore_missing_imports = True + +[mypy-brownie.*] +ignore_missing_imports = True diff --git a/netlists/__init__.py b/netlists/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/oceanv3/KPIs.py b/netlists/oceanv3/KPIs.py new file mode 100644 index 0000000000000000000000000000000000000000..bf83bb66b852e051422c6a3a114849a04e4ac060 --- /dev/null +++ b/netlists/oceanv3/KPIs.py @@ -0,0 +1,181 @@ +from typing import List + +from enforce_typing import enforce_types + +from engine import KPIsBase +from util import globaltokens +from util.base18 import fromBase18 +from util.plotutil import YParam, arrayToFloatList, LINEAR, MULT1, COUNT, DOLLAR + + +@enforce_types +class KPIs(KPIsBase.KPIsBase): + pass + + +@enforce_types +def get_OCEAN_in_DTs(state, agent) -> float: + """Value of DT that this agent staked across all pools, denominated in OCEAN + + Args: + state: SimState -- SimState, holds all pool agents (& their pools) + agent: AgentBase -- agent of interest + + Returns: + value_held: float -- value staked, denominated in OCEAN + """ + OCEAN_address = globaltokens.OCEAN_address() + value_held = 0.0 + + for pool_agent in state.agents.filterToPool().values(): + pool = pool_agent._pool + DT = pool_agent._dt + price = fromBase18(pool.getSpotPrice(OCEAN_address, DT.address)) + amt_DT = agent.DT(DT) + value_held += amt_DT * price + + return value_held + + +@enforce_types +@enforce_types +def get_OCEAN_in_BPTs(state, agent): + """Value of BPTs that this agent owns across all pools, denominated in OCEAN + + Args: + state: SimState -- SimState, holds all pool agents (& their pools) + agent: AgentBase -- agent of interest + + Returns: + value_held: float -- value of BPTs, denominated in OCEAN + """ + OCEAN_address = globaltokens.OCEAN_address() + value_held = 0 + + for pool_agent in state.agents.filterToPool().values(): + pool = pool_agent._pool + DT = pool_agent._dt + + price = fromBase18(pool.getSpotPrice(OCEAN_address, DT.address)) + pool_value_DT = price * fromBase18(pool.getBalance(DT.address)) + pool_value_OCEAN = fromBase18(pool.getBalance(OCEAN_address)) + pool_value = pool_value_DT + pool_value_OCEAN + + amt_pool_BPTs = fromBase18(pool.totalSupply()) + agent_percent_pool = agent.BPT(pool) / amt_pool_BPTs + + value_held += agent_percent_pool * pool_value + + return value_held + + +@enforce_types +def netlist_createLogData(state): + """SimEngine constructor uses this""" + s = [] # for console logging + dataheader = [] # for csv logging: list of string + datarow = [] # for csv logging: list of float + + # SimEngine already logs: Tick, Second, Min, Hour, Day, Month, Year + # So we log other things... + agents_names = [ + "publisher", + "consumer", + "stakerSpeculator", + "speculator", + "maliciousPublisher", + ] + + # tracking OCEAN + for name in agents_names: + agent = state.getAgent(name) + + # in wallet + dataheader += [f"{name}_OCEAN"] + datarow += [agent.OCEAN()] + + # in DTs + dataheader += [f"{name}_OCEAN_in_DTs"] + datarow += [get_OCEAN_in_DTs(state, agent)] + + # in BPTs + dataheader += [f"{name}_OCEAN_in_BPTs"] + datarow += [get_OCEAN_in_BPTs(state, agent)] + + # networth + dataheader += [f"{name}_OCEAN_networth"] + datarow += [ + agent.OCEAN() + + get_OCEAN_in_DTs(state, agent) + + get_OCEAN_in_BPTs(state, agent) + ] + + pool_agents = state.agents.filterToPool() + n_pools = len(pool_agents) + s += [f"; # pools={n_pools}"] + dataheader += ["n_pools"] + datarow += [n_pools] + + rugged_pool = state.rugged_pools + n_rugged = len(rugged_pool) + s += [f"; # rugged pools={n_rugged}"] + dataheader += ["n_rugged"] + datarow += [n_rugged] + + return s, dataheader, datarow + + +@enforce_types +def netlist_plotInstructions(header: List[str], values): + """ + Describe how to plot the information. + tsp.do_plot() calls this + + :param: header: List[str] holding 'Tick', 'Second', ... + :param: values: 2d array of float [tick_i, valuetype_i] + :return: x_label: str -- e.g. "Day", "Month", "Year" + :return: x: List[float] -- x-axis info on how to plot + :return: y_params: List[YParam] -- y-axis info on how to plot + """ + x_label = "Day" + x = arrayToFloatList(values[:, header.index(x_label)]) + + y_params = [ + YParam( + [ + "publisher_OCEAN_networth", + "consumer_OCEAN_networth", + "stakerSpeculator_OCEAN_networth", + "speculator_OCEAN_networth", + "maliciousPublisher_OCEAN_networth", + ], + [ + "publisher", + "consumer", + "stakerSpeculator", + "speculator", + "maliciousPublisher", + ], + "Agents OCEAN networth", + LINEAR, + MULT1, + COUNT, + ), + YParam( + [ + "publisher_OCEAN", + "consumer_OCEAN", + "stakerSpeculator_OCEAN", + "speculator_OCEAN", + "maliciousPublisher_OCEAN", + ], + ["publisher", "consumer", "staker", "speculator", "maliciousPublisher"], + "Agents OCEAN wallet", + LINEAR, + MULT1, + DOLLAR, + ), + YParam(["n_pools"], ["# pools"], "n_pools", LINEAR, MULT1, COUNT), + ] + + return (x_label, x, y_params) diff --git a/netlists/oceanv3/SimState.py b/netlists/oceanv3/SimState.py new file mode 100644 index 0000000000000000000000000000000000000000..4b58cf67afc5a575c9998e746f8cfdffc00e4fd8 --- /dev/null +++ b/netlists/oceanv3/SimState.py @@ -0,0 +1,104 @@ +from typing import Set, List + +from enforce_typing import enforce_types + +from agents.DataconsumerAgent import DataconsumerAgent +from agents.PublisherAgent import PublisherAgent, PublisherStrategy +from agents.SpeculatorAgent import SpeculatorAgent, StakerspeculatorAgent + +from engine import SimStateBase, AgentBase +from .KPIs import KPIs +from .SimStrategy import SimStrategy + + +@enforce_types +class SimState(SimStateBase.SimStateBase): + def __init__(self, ss=None): + # initialize self.tick, ss, agents, kpis + super().__init__(ss) + + # now, fill in actual values for ss, agents, kpis + if self.ss is None: + self.ss = SimStrategy() + ss = self.ss # for convenience as we go forward + + # wire up the circuit + new_agents: Set[AgentBase.AgentBaseAbstract] = set() # type:ignore + + pub_ss = PublisherStrategy( # type:ignore + DT_init=self.ss.publisher_DT_init, + DT_stake=self.ss.publisher_DT_stake, + pool_weight_DT=self.ss.publisher_pool_weight_DT, + pool_weight_OCEAN=self.ss.publisher_pool_weight_OCEAN, + s_between_create=self.ss.publisher_s_between_create, + s_between_unstake=self.ss.publisher_s_between_unstake, + s_between_sellDT=self.ss.publisher_s_between_sellDT, + is_malicious=False, + ) + new_agents.add( + PublisherAgent( + name="publisher", + USD=0.0, + OCEAN=self.ss.publisher_init_OCEAN, + pub_ss=pub_ss, + ) + ) + + new_agents.add( + DataconsumerAgent( + name="consumer", + USD=0.0, + OCEAN=self.ss.consumer_init_OCEAN, + s_between_buys=self.ss.consumer_s_between_buys, + profit_margin_on_consume=self.ss.consumer_profit_margin_on_consume, + ) + ) + + new_agents.add( + StakerspeculatorAgent( + name="stakerSpeculator", + USD=0.0, + OCEAN=self.ss.staker_init_OCEAN, + s_between_speculates=self.ss.staker_s_between_speculates, + ) + ) + + new_agents.add( + SpeculatorAgent( + name="speculator", + USD=0.0, + OCEAN=self.ss.speculator_init_OCEAN, + s_between_speculates=self.ss.speculator_s_between_speculates, + ) + ) + + mal_pub_ss = PublisherStrategy( + DT_init=self.ss.mal_DT_init, + DT_stake=self.ss.mal_DT_stake, + pool_weight_DT=self.ss.mal_pool_weight_DT, + pool_weight_OCEAN=self.ss.mal_pool_weight_OCEAN, + s_between_create=self.ss.mal_s_between_create, + s_between_unstake=self.ss.mal_s_between_unstake, + s_between_sellDT=self.ss.mal_s_between_sellDT, + is_malicious=True, + s_wait_to_rug=self.ss.mal_s_wait_to_rug, + s_rug_time=self.ss.mal_s_rug_time, + ) + new_agents.add( + PublisherAgent( + name="maliciousPublisher", + USD=0.0, + OCEAN=self.ss.mal_init_OCEAN, + pub_ss=mal_pub_ss, + ) + ) + + for agent in new_agents: + self.agents[agent.name] = agent + + # kpis is defined in this netlist module + self.kpis = KPIs(self.ss.time_step) + + # pools that were rug-pulled by a malicious publisher. Some agents + # watch for 'state.rugged_pools' and act accordingly. + self.rugged_pools: List[str] = [] # type:ignore diff --git a/netlists/oceanv3/SimStrategy.py b/netlists/oceanv3/SimStrategy.py new file mode 100644 index 0000000000000000000000000000000000000000..1ba91107bedf65fb969228ca5cf38d697f834777 --- /dev/null +++ b/netlists/oceanv3/SimStrategy.py @@ -0,0 +1,56 @@ +from enforce_typing import enforce_types + +from engine import SimStrategyBase +from util.constants import S_PER_HOUR, S_PER_DAY + + +@enforce_types +class SimStrategy( + SimStrategyBase.SimStrategyBase +): # pylint: disable=too-many-instance-attributes + def __init__(self): + super().__init__() + + # ==baseline + self.setTimeStep(S_PER_HOUR) + self.setMaxTime(19, "days") + self.setLogInterval(10 * S_PER_HOUR) + + # data publisher + self.publisher_init_OCEAN = 5000.0 + self.publisher_DT_init = 100.0 + self.publisher_DT_stake = 50.0 + self.publisher_pool_weight_DT = 3.0 + self.publisher_pool_weight_OCEAN = 7.0 + assert ( + self.publisher_pool_weight_DT + self.publisher_pool_weight_OCEAN + ) == 10.0 + self.publisher_s_between_create = 7 * S_PER_DAY + self.publisher_s_between_unstake = 3 * S_PER_DAY + self.publisher_s_between_sellDT = 15 * S_PER_DAY + + # data consumer + self.consumer_init_OCEAN = 5000.0 + self.consumer_s_between_buys = 3 * S_PER_DAY + self.consumer_profit_margin_on_consume = 0.2 + + # staker-speculator + self.staker_init_OCEAN = 5000.0 + self.staker_s_between_speculates = 8 * S_PER_HOUR + + # speculator + self.speculator_init_OCEAN = 5000.0 + self.speculator_s_between_speculates = 1 * S_PER_DAY + + # malicious publisher + self.mal_init_OCEAN = 5000.0 + self.mal_DT_init = 100.0 + self.mal_DT_stake = 50.0 + self.mal_pool_weight_DT = 3.0 + self.mal_pool_weight_OCEAN = 7.0 + assert (self.mal_pool_weight_DT + self.mal_pool_weight_OCEAN) == 10.0 + self.mal_s_between_create = 10 * S_PER_DAY + self.mal_s_between_unstake = 1 * S_PER_HOUR + self.mal_s_between_sellDT = 1 * S_PER_HOUR + self.mal_s_wait_to_rug = 5 * S_PER_DAY + self.mal_s_rug_time = 1 * S_PER_DAY diff --git a/netlists/oceanv3/__init__.py b/netlists/oceanv3/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/oceanv3/about.md b/netlists/oceanv3/about.md new file mode 100644 index 0000000000000000000000000000000000000000..c6bdb761d4e0b8e07184a4e528fb7725eb577f76 --- /dev/null +++ b/netlists/oceanv3/about.md @@ -0,0 +1,9 @@ +## About oceanv3 Netlist + +System-level design is W3SL. But now higher fidelity is added to the "data ecosystem" part. This is in order to better [Ocean Market](https://market.oceanprotocol.com) publishing, pool creation, staking, and data consumption. + +The actual netlist is WIP. + +<img src="images/model-status-quo.png" width="100%"> + +[GSlides for the above image](https://docs.google.com/presentation/d/14BB50dkGXTcPjlbrZilQ3WYnFLDetgfMS1BKGuMX8Q0/edit#slide=id.gac81e1e848_0_8) diff --git a/netlists/oceanv3/images/model-status-quo.png b/netlists/oceanv3/images/model-status-quo.png new file mode 100644 index 0000000000000000000000000000000000000000..09a02948b8a3ab4672ea0a5235b88198bbcabc29 Binary files /dev/null and b/netlists/oceanv3/images/model-status-quo.png differ diff --git a/netlists/oceanv3/netlist.py b/netlists/oceanv3/netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..2cbab40c4ac974330f88d81869bb50db870868fa --- /dev/null +++ b/netlists/oceanv3/netlist.py @@ -0,0 +1,8 @@ +# pylint: disable=unused-import +""" +Netlist to simulate oceanV3 +""" + +from .SimStrategy import SimStrategy +from .SimState import SimState +from .KPIs import KPIs, netlist_createLogData, netlist_plotInstructions diff --git a/netlists/oceanv3/test/__init__.py b/netlists/oceanv3/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/oceanv3/test/conftest.py b/netlists/oceanv3/test/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..fbe3a6626fae1a33237d3ee6949dfd88f0c68da4 --- /dev/null +++ b/netlists/oceanv3/test/conftest.py @@ -0,0 +1 @@ +from agents.test.conftest import * # pylint: disable=wildcard-import, W0614 diff --git a/netlists/oceanv3/test/test_KPIs.py b/netlists/oceanv3/test/test_KPIs.py new file mode 100644 index 0000000000000000000000000000000000000000..26866a0b8f123fe8954815ad0c0160c42fba6324 --- /dev/null +++ b/netlists/oceanv3/test/test_KPIs.py @@ -0,0 +1,96 @@ +from pytest import approx + +from enforce_typing import enforce_types + +from agents.PoolAgent import PoolAgent +from util import globaltokens +from util.base18 import fromBase18 +from .. import KPIs + + +@enforce_types +class MockAgentDict(dict): # subset of engine.AgentDict + def __init__(self, *arg, **kw): # pylint: disable=useless-super-delegation + super().__init__(*arg, **kw) + + def filterToPool(self): + return self.filterByClass(PoolAgent) + + def filterByClass(self, _class): + return MockAgentDict( + {agent.name: agent for agent in self.values() if isinstance(agent, _class)} + ) + + +@enforce_types +class MockSimState: + def __init__(self): + self.agents = MockAgentDict() + + def getAgent(self, name: str): + return self.agents[name] + + +@enforce_types +class FooAgent: + def __init__(self, name: str): + self.name = name + self._DT = 3.0 # magic number + self._BPT = 5.0 # "" + + def DT(self, dt) -> float: # pylint: disable=unused-argument + return self._DT + + def BPT(self, pool) -> float: # pylint: disable=unused-argument + return self._BPT + + +@enforce_types +def test_get_OCEAN_in_DTs(alice_info): + state = MockSimState() + + pool, DT = alice_info.pool, alice_info.DT + pool_agent = PoolAgent("pool_agent", pool) + state.agents["agent1"] = pool_agent + + foo_agent = FooAgent("foo_agent") + + OCEAN_address = globaltokens.OCEAN_address() + price = fromBase18(pool.getSpotPrice(OCEAN_address, DT.address)) + + amt_DT = foo_agent.DT("bar") + assert amt_DT == 3.0 + + value_held = KPIs.get_OCEAN_in_DTs(state, foo_agent) + assert value_held == amt_DT * price + + +@enforce_types +def test_get_OCEAN_in_BPTs(alice_info): + state = MockSimState() + + pool, DT = alice_info.pool, alice_info.DT + pool_agent = PoolAgent("pool_agent", pool) + state.agents["agent1"] = pool_agent + + foo_agent = FooAgent("foo_agent") + + OCEAN_address = globaltokens.OCEAN_address() + price = fromBase18(pool.getSpotPrice(OCEAN_address, DT.address)) + pool_value_DT = price * fromBase18(pool.getBalance(DT.address)) + pool_value_OCEAN = fromBase18(pool.getBalance(OCEAN_address)) + pool_value = pool_value_DT + pool_value_OCEAN + + # case: foo_agent no BPTs + value_held = KPIs.get_OCEAN_in_BPTs(state, foo_agent) + foo_agent_pool_shape = foo_agent._BPT / fromBase18(pool.totalSupply()) + + assert value_held == approx(foo_agent_pool_shape * pool_value) + + # case: foo_agent has all BPTs + foo_agent._BPT = fromBase18( + pool.totalSupply() + ) # make pool think agent has 100% of BPTs + value_held = KPIs.get_OCEAN_in_BPTs(state, foo_agent) + + assert value_held == 1.0 * pool_value diff --git a/netlists/oceanv3/test/test_SimState.py b/netlists/oceanv3/test/test_SimState.py new file mode 100644 index 0000000000000000000000000000000000000000..058a98598b8f0e63393ba5791dd41ef831f7c97e --- /dev/null +++ b/netlists/oceanv3/test/test_SimState.py @@ -0,0 +1,23 @@ +from enforce_typing import enforce_types + +from agents.PublisherAgent import PublisherAgent +from agents.DataconsumerAgent import DataconsumerAgent +from agents.SpeculatorAgent import SpeculatorAgent, StakerspeculatorAgent +from ..SimState import KPIs +from ..SimState import SimState + + +@enforce_types +def test1(): + state = SimState() + + assert hasattr(state.ss, "publisher_init_OCEAN") + assert isinstance(state.getAgent("publisher"), PublisherAgent) + assert isinstance(state.getAgent("consumer"), DataconsumerAgent) + assert isinstance(state.getAgent("speculator"), SpeculatorAgent) + assert isinstance(state.getAgent("stakerSpeculator"), StakerspeculatorAgent) + assert isinstance(state.getAgent("maliciousPublisher"), PublisherAgent) + + assert isinstance(state.kpis, KPIs) + + assert not state.rugged_pools diff --git a/netlists/oceanv3/test/test_SimStrategy.py b/netlists/oceanv3/test/test_SimStrategy.py new file mode 100644 index 0000000000000000000000000000000000000000..206bae0b18d21ec2a83c76e3fe5d3d228e01f431 --- /dev/null +++ b/netlists/oceanv3/test/test_SimStrategy.py @@ -0,0 +1,47 @@ +from enforce_typing import enforce_types + +from ..SimStrategy import SimStrategy + + +@enforce_types +def test1(): + # very lightweight testing: just on attribute names and basic (>0) + ss = SimStrategy() + + assert ss.time_step > 0 + assert ss.max_ticks > 0 + assert ss.log_interval > 0 + + assert ss.publisher_init_OCEAN > 0.0 + assert ss.publisher_DT_init > 0.0 + assert ss.publisher_DT_stake > 0.0 + assert ss.publisher_pool_weight_DT > 0.0 + assert ss.publisher_pool_weight_OCEAN > 0.0 + assert ss.publisher_s_between_create > 0 + assert ss.publisher_s_between_unstake > 0 + assert ss.publisher_s_between_sellDT > 0 + + # data consumer + assert ss.consumer_init_OCEAN > 0.0 + assert ss.consumer_s_between_buys > 0 + assert ss.consumer_profit_margin_on_consume > 0.0 + + # staker-speculator + assert ss.staker_init_OCEAN > 0.0 + assert ss.staker_s_between_speculates > 0 + + # speculator + assert ss.speculator_init_OCEAN > 0.0 + assert ss.speculator_s_between_speculates > 0 + + # malicious publisher + assert ss.mal_init_OCEAN > 0.0 + assert ss.mal_DT_init > 0.0 + assert ss.mal_DT_stake > 0.0 + assert ss.mal_pool_weight_DT > 0.0 + assert ss.mal_pool_weight_OCEAN > 0.0 + assert ss.mal_s_between_create > 0 + assert ss.mal_s_between_unstake > 0 + assert ss.mal_s_between_sellDT > 0 + assert ss.mal_s_wait_to_rug > 0 + assert ss.mal_s_rug_time > 0 diff --git a/netlists/oceanv3/test/test_netlist.py b/netlists/oceanv3/test/test_netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..0ba39ceaf0b886c92df622cdf6528f0d3591d08f --- /dev/null +++ b/netlists/oceanv3/test/test_netlist.py @@ -0,0 +1,14 @@ +"""simply test for scope""" +import inspect +from .. import netlist + + +def test1(): + netlist.SimStrategy() + netlist.SimState() + + assert inspect.isclass(netlist.SimStrategy) + assert inspect.isclass(netlist.SimState) + assert inspect.isclass(netlist.KPIs) + assert callable(netlist.netlist_createLogData) + assert callable(netlist.netlist_plotInstructions) diff --git a/netlists/oceanv4/KPIs.py b/netlists/oceanv4/KPIs.py new file mode 100644 index 0000000000000000000000000000000000000000..2e5e8dee3f2aadc508ea621e6ca928f1d2157c44 --- /dev/null +++ b/netlists/oceanv4/KPIs.py @@ -0,0 +1,304 @@ +from typing import List +import brownie + +from enforce_typing import enforce_types + +from engine import KPIsBase +from util import globaltokens +from util.base18 import fromBase18 +from util.plotutil import YParam, arrayToFloatList, LINEAR, MULT1, COUNT, DOLLAR + + +@enforce_types +class KPIs(KPIsBase.KPIsBase): + pass + + +@enforce_types +def get_OCEAN_in_DTs(state, agent) -> float: + """Value of DT that this agent staked across all pools, denominated in OCEAN + Args: + state: SimState -- SimState, holds all pool agents (& their pools) + agent: AgentBase -- agent of interest + Returns: + value_held: float -- value staked, denominated in OCEAN + """ + OCEAN_address = globaltokens.OCEAN_address() + value_held = 0.0 + + for pool_agent in state.agents.filterToPoolV4().values(): + pool = pool_agent._pool + DT = pool_agent._dt + swapFee = pool.getSwapFee() + price = fromBase18(pool.getSpotPrice(OCEAN_address, DT.address, swapFee)) + amt_DT = agent.DT(DT) + value_held += amt_DT * price + + return value_held + + +@enforce_types +@enforce_types +def get_OCEAN_in_BPTs(state, agent): + """Value of BPTs that this agent owns across all pools, denominated in OCEAN + Args: + state: SimState -- SimState, holds all pool agents (& their pools) + agent: AgentBase -- agent of interest + Returns: + value_held: float -- value of BPTs, denominated in OCEAN + """ + OCEAN_address = globaltokens.OCEAN_address() + value_held = 0 + + for pool_agent in state.agents.filterToPoolV4().values(): + pool = pool_agent._pool + DT = pool_agent._dt + + swapFee = pool.getSwapFee() + price = fromBase18(pool.getSpotPrice(OCEAN_address, DT.address, swapFee)) + pool_value_DT = price * fromBase18(pool.getBalance(DT.address)) + pool_value_OCEAN = fromBase18(pool.getBalance(OCEAN_address)) + pool_value = pool_value_DT + pool_value_OCEAN + + amt_pool_BPTs = fromBase18( + pool.totalSupply() + ) # from BPool, inheriting from BToken + agent_percent_pool = agent.BPT(pool) / amt_pool_BPTs + + value_held += agent_percent_pool * pool_value + # import ipdb + # ipdb.set_trace() + + return value_held + + +@enforce_types +def netlist_createLogData(state): # pylint: disable=too-many-statements + """SimEngine constructor uses this""" + s = [] # for console logging + dataheader = [] # for csv logging: list of string + datarow = [] # for csv logging: list of float + + # SimEngine already logs: Tick, Second, Min, Hour, Day, Month, Year + # So we log other things... + agents_names = [ + "publisher", + # "consumer", + "stakerSpeculator", + "speculator", + # "buySellRobot", + # "maliciousPublisher" + # "erc721" + ] + # tracking OCEAN + OCEANtoken = globaltokens.OCEANtoken() + OCEAN_address = globaltokens.OCEAN_address() + for name in agents_names: + agent = state.getAgent(name) + + # in wallet + dataheader += [f"{name}_OCEAN"] + datarow += [agent.OCEAN()] + + # in DTs + dataheader += [f"{name}_OCEAN_in_DTs"] + datarow += [get_OCEAN_in_DTs(state, agent)] + + # in BPTs + dataheader += [f"{name}_OCEAN_in_BPTs"] + datarow += [get_OCEAN_in_BPTs(state, agent)] + + # networth + dataheader += [f"{name}_OCEAN_networth"] + datarow += [ + agent.OCEAN() + + get_OCEAN_in_DTs(state, agent) + + get_OCEAN_in_BPTs(state, agent) + ] + + dataheader += [f"DT_{name}"] + dataheader += [f"BPT_{name}"] + + # Tracking DT and BPT balances of agents + if any(state.agents.filterToPoolV4().values()): + poolAgent_0 = list(state.agents.filterToPoolV4().values())[0] + DT = poolAgent_0._dt + amt_DT = agent.DT(DT) + datarow += [amt_DT] + datarow += [agent.BPT(poolAgent_0._pool)] # agent.BPT(pool) + else: + datarow += [0, 0] + + # Track pool0 + # 1. DT in pool, + # 2. DT balance of 1ss contract + # 3. BPT total supply, + # 4. BPT balance of 1ss contract, + # 5. DT spot price + # 6. OCEAN in pool + dataheader += ["DT_pool"] + dataheader += ["DT_1ss_contract"] + dataheader += ["BPT_total"] + dataheader += ["BPT_1ss_contract"] + dataheader += ["DT_price"] + dataheader += ["pool_OCEAN"] + if any(state.agents.filterToPoolV4().values()): + poolAgent_0 = list(state.agents.filterToPoolV4().values())[0] + pool0 = poolAgent_0._pool + DT = poolAgent_0._dt + oneSSContractAddress = pool0.getController() + + datarow += [fromBase18(DT.balanceOf(pool0.address))] # 1 + datarow += [fromBase18(DT.balanceOf(oneSSContractAddress))] # 2 + datarow += [fromBase18(pool0.totalSupply())] # 3 + datarow += [fromBase18(pool0.balanceOf(oneSSContractAddress))] # 4 + swapFee = pool0.getSwapFee() + datarow += [ + fromBase18(pool0.getSpotPrice(OCEAN_address, DT.address, swapFee)) + ] # 5 + datarow += [fromBase18(OCEANtoken.balanceOf(pool0.address))] + else: + datarow += [0, 0, 0, 0, 0, 0] + + pool_agents = state.agents.filterToPoolV4() + n_pools = len(pool_agents) + s += [f"; # pools={n_pools}"] + dataheader += ["n_pools"] + datarow += [n_pools] + + rugged_pool = state.rugged_pools + n_rugged = len(rugged_pool) + # s += [f"; # rugged pools={n_rugged}"] + s += [f"; # block height={brownie.chain.height}"] + dataheader += ["n_rugged"] + datarow += [n_rugged] + + return s, dataheader, datarow + + +@enforce_types +def netlist_plotInstructions(header: List[str], values): + """ + Describe how to plot the information. + tsp.do_plot() calls this + :param: header: List[str] holding 'Tick', 'Second', ... + :param: values: 2d array of float [tick_i, valuetype_i] + :return: x_label: str -- e.g. "Day", "Month", "Year" + :return: x: List[float] -- x-axis info on how to plot + :return: y_params: List[YParam] -- y-axis info on how to plot + """ + x_label = "Day" + x = arrayToFloatList(values[:, header.index(x_label)]) + + y_params = [ + YParam( + [ + "publisher_OCEAN_networth", + "consumer_OCEAN_networth", + "stakerSpeculator_OCEAN_networth", + "speculator_OCEAN_networth", + # "buySellRobot_OCEAN_networth", + # "maliciousPublisher_OCEAN_networth" + ], + [ + "publisher", + "consumer", + "stakerSpeculator", + "speculator", + # "buySellRobot", + # "maliciousPublisher" + ], + "Agents OCEAN networth", + LINEAR, + MULT1, + COUNT, + ), + YParam( + [ + "publisher_OCEAN", + "consumer_OCEAN", + "stakerSpeculator_OCEAN", + "speculator_OCEAN", + # "buySellRobot_OCEAN", + # "maliciousPublisher_OCEAN" + ], + [ + "publisher", + "consumer", + "staker", + "speculator", + # "buySellRobot", + # "maliciousPublisher" + ], + "Agents OCEAN wallet", + LINEAR, + MULT1, + DOLLAR, + ), + YParam( + [ + "DT_publisher", + "DT_consumer", + "DT_stakerSpeculator", + "DT_speculator", + "DT_buySellRobot" + # "DT_maliciousPublisher" + ], + [ + "publisher", + "consumer", + "staker", + "speculator", + "buySellRobot", + # "maliciousPublisher" + ], + "Agents Datatokens", + LINEAR, + MULT1, + DOLLAR, + ), + YParam( + ["DT_pool", "DT_1ss_contract"], + ["DT in pool", "DT in one-sided staking contract"], + "DT allocation", + LINEAR, + MULT1, + COUNT, + ), + YParam( + [ + "BPT_publisher", + "BPT_consumer", + "BPT_stakerSpeculator", + "BPT_speculator", + # "BPT_maliciousPublisher", + "BPT_1ss_contract", + ], + [ + "publisher", + "consumer", + "staker", + "speculator", + # "maliciousPublisher", + "one-sided staking contract", + ], + "BPT allocation", + LINEAR, + MULT1, + COUNT, + ), + YParam( + ["DT_price"], + [ + "DT_price", + ], + "dt price", + LINEAR, + MULT1, + DOLLAR, + ), + YParam(["n_pools"], ["# pools"], "n_pools", LINEAR, MULT1, COUNT), + YParam(["pool_OCEAN"], ["pool_OCEAN"], "pool_OCEAN", LINEAR, MULT1, DOLLAR), + ] + + return (x_label, x, y_params) diff --git a/netlists/oceanv4/SimState.py b/netlists/oceanv4/SimState.py new file mode 100644 index 0000000000000000000000000000000000000000..3d0ab2b00030fc796c260fb1ca2c2eaf34bef7ee --- /dev/null +++ b/netlists/oceanv4/SimState.py @@ -0,0 +1,104 @@ +from typing import Set, List + +from enforce_typing import enforce_types + +from agents.PublisherAgent import ( + PublisherAgentV4, + PublisherStrategyV4, +) +from agents.SpeculatorAgent import SpeculatorAgentV4, StakerspeculatorAgentV4 + +from engine import SimStateBase, AgentBase +from .KPIs import KPIs +from .SimStrategy import SimStrategy + + +@enforce_types +class SimState(SimStateBase.SimStateBase): + def __init__(self, ss=None): + # initialize self.tick, ss, agents, kpis + super().__init__(ss) + + # now, fill in actual values for ss, agents, kpis + if self.ss is None: + self.ss = SimStrategy() + ss = self.ss # for convenience as we go forward + + # wire up the circuit + new_agents: Set[AgentBase.AgentBaseAbstract] = set() # type:ignore + + pub_ss = PublisherStrategyV4( + DT_cap=self.ss.publisher_DT_cap, + vested_amount=self.ss.publisher_vested_amount, + # pool_weight_DT=self.ss.publisher_pool_weight_DT, + # pool_weight_OCEAN=self.ss.publisher_pool_weight_OCEAN, + s_between_create=self.ss.publisher_s_between_create, + s_between_unstake=self.ss.publisher_s_between_unstake, + s_between_sellDT=self.ss.publisher_s_between_sellDT, + is_malicious=False, + ) + new_agents.add( + PublisherAgentV4( + name="publisher", + USD=0.0, + OCEAN=self.ss.publisher_init_OCEAN, + pub_ss=pub_ss, + ) + ) + + # mal_pub_ss = PublisherStrategyV4( + # DT_cap=self.ss.mal_DT_cap, + # vested_amount=self.ss.mal_vested_amount, + # s_between_create=self.ss.mal_s_between_create, + # s_between_unstake=self.ss.mal_s_between_unstake, + # s_between_sellDT=self.ss.mal_s_between_sellDT, + # is_malicious=True, + # s_wait_to_rug=self.ss.mal_s_wait_to_rug, + # s_rug_time=self.ss.mal_s_rug_time, + # ) + # new_agents.add( + # PublisherAgentV4( + # name="maliciousPublisher", + # USD=0.0, + # OCEAN=self.ss.mal_init_OCEAN, + # pub_ss=mal_pub_ss, + # ) + # ) + + # new_agents.add( + # DataconsumerAgentV4( + # name="consumer", + # USD=0.0, + # OCEAN=self.ss.consumer_init_OCEAN, + # s_between_buys=self.ss.consumer_s_between_buys, + # profit_margin_on_consume=self.ss.consumer_profit_margin_on_consume, + # ) + # ) + + new_agents.add( + StakerspeculatorAgentV4( + name="stakerSpeculator", + USD=0.0, + OCEAN=self.ss.staker_init_OCEAN, + s_between_speculates=self.ss.staker_s_between_speculates, + ) + ) + + new_agents.add( + SpeculatorAgentV4( + name="speculator", + USD=0.0, + OCEAN=self.ss.speculator_init_OCEAN, + s_between_speculates=self.ss.speculator_s_between_speculates, + ) + ) + + for agent in new_agents: + self.agents[agent.name] = agent + + # kpis is defined in this netlist module + self.kpis = KPIs(self.ss.time_step) + + # pools that were rug-pulled by a malicious publisher. Some agents + # watch for 'state.rugged_pools' and act accordingly. + self.rugged_pools: List[str] = [] # type:ignore diff --git a/netlists/oceanv4/SimStrategy.py b/netlists/oceanv4/SimStrategy.py new file mode 100644 index 0000000000000000000000000000000000000000..affca85c0cbf1ce7dd72c0763ec182498cdace49 --- /dev/null +++ b/netlists/oceanv4/SimStrategy.py @@ -0,0 +1,53 @@ +from enforce_typing import enforce_types + +from engine import SimStrategyBase +from util.constants import S_PER_HOUR, S_PER_DAY + + +@enforce_types +class SimStrategy( + SimStrategyBase.SimStrategyBase +): # pylint: disable=too-many-instance-attributes + def __init__(self): + super().__init__() + + # ==baseline + self.setTimeStep(S_PER_HOUR) + self.setMaxTime(60, "days") + self.setLogInterval(12 * S_PER_HOUR) + + # data publisher + self.publisher_init_OCEAN = 5000.0 + self.publisher_DT_cap = 1000.0 + self.publisher_vested_amount = 100.0 + self.publisher_s_between_create = 1 * S_PER_DAY + self.publisher_s_between_unstake = 500 * S_PER_DAY + self.publisher_s_between_sellDT = 100 * S_PER_DAY + + # data consumer + self.consumer_init_OCEAN = 5000.0 + self.consumer_s_between_buys = 3 * S_PER_DAY + self.consumer_profit_margin_on_consume = 0.2 + + # staker-speculator + self.staker_init_OCEAN = 5000.0 + # self.staker_s_between_speculates = 8 * S_PER_HOUR + self.staker_s_between_speculates = 1 * S_PER_DAY + + # speculator + self.speculator_init_OCEAN = 5000.0 + self.speculator_s_between_speculates = 3 * S_PER_DAY + + # malicious publisher + # self.buySellRobot_init_OCEAN = 5000.0 + # self.buysell_s_between_speculates = 6 * S_PER_HOUR + + # malicious publisher + # self.mal_init_OCEAN = 5000.0 + # self.mal_DT_cap = 1000.0 + # self.mal_vested_amount = 100.0 + # self.mal_s_between_create = 2 * S_PER_DAY + # self.mal_s_between_unstake = 1 * S_PER_HOUR + # self.mal_s_between_sellDT = 1 * S_PER_HOUR + # self.mal_s_wait_to_rug = 5 * S_PER_DAY + # self.mal_s_rug_time = 2 * S_PER_DAY diff --git a/netlists/oceanv4/__init__.py b/netlists/oceanv4/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/oceanv4/about.md b/netlists/oceanv4/about.md new file mode 100644 index 0000000000000000000000000000000000000000..a5593bf90751954f69bc0e81eea363e1fbc5c55c --- /dev/null +++ b/netlists/oceanv4/about.md @@ -0,0 +1,7 @@ +## About oceanv4 Netlist + +Starts with Ocean V3, then makes staking safer via one-sided AMM bots. WIP. + +<img src="images/model-new1.png" width="100%"> + +[GSlides for the above image](https://docs.google.com/presentation/d/14BB50dkGXTcPjlbrZilQ3WYnFLDetgfMS1BKGuMX8Q0/edit#slide=id.gac81e1e848_0_8) diff --git a/netlists/oceanv4/images/model-new1.png b/netlists/oceanv4/images/model-new1.png new file mode 100644 index 0000000000000000000000000000000000000000..276baeabe9801488d629097810b326c68c2e8b36 Binary files /dev/null and b/netlists/oceanv4/images/model-new1.png differ diff --git a/netlists/oceanv4/netlist.py b/netlists/oceanv4/netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..fbcf719ef98f43a688ef86d2aa9f8b9e6244217e --- /dev/null +++ b/netlists/oceanv4/netlist.py @@ -0,0 +1,8 @@ +# pylint: disable=unused-import +""" +Netlist to simulate oceanV4 +""" + +from .SimStrategy import SimStrategy +from .SimState import SimState +from .KPIs import KPIs, netlist_createLogData, netlist_plotInstructions diff --git a/netlists/scheduler/__init__.py b/netlists/scheduler/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/scheduler/about.md b/netlists/scheduler/about.md new file mode 100644 index 0000000000000000000000000000000000000000..3f5da1ba30fcfdb41f975ca57585508057652dd5 --- /dev/null +++ b/netlists/scheduler/about.md @@ -0,0 +1,8 @@ +# scheduler Netlist + +Has these agents: + +- VestingFunderAgent - creates a VestingWalletAgent, specifies beneficiary, fills it with funds +- VestingWalletAgent -- a smart contract robot that vests over time +- VestingBeneficiaryAgent - designated to receive the funds. Pings for release. + diff --git a/netlists/scheduler/netlist.py b/netlists/scheduler/netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..07740270719b28258a960c4ed83b86850c3067a2 --- /dev/null +++ b/netlists/scheduler/netlist.py @@ -0,0 +1,131 @@ +from typing import List + +import brownie +from enforce_typing import enforce_types + +from agents.VestingBeneficiaryAgent import VestingBeneficiaryAgent +from agents.VestingFunderAgent import VestingFunderAgent +from engine import KPIsBase, SimStateBase, SimStrategyBase +from util.constants import S_PER_MONTH, S_PER_YEAR +from util.globaltokens import OCEAN_address +from util.plotutil import YParam, arrayToFloatList, LINEAR, MULT1, DOLLAR + +chain = brownie.network.chain + + +@enforce_types +class SimStrategy(SimStrategyBase.SimStrategyBase): + def __init__(self): + super().__init__() + + # ==baseline + self.setTimeStep(S_PER_MONTH) + self.setMaxTime(10, "years") + self.setLogInterval(3 * S_PER_MONTH) + + # ==attributes specific to this netlist + self.OCEAN_funded: float = 5.0 # type:ignore + self.start_timestamp: int = chain[-1].timestamp + S_PER_YEAR # type:ignore + self.duration_seconds: int = 5 * S_PER_YEAR # type:ignore + + +@enforce_types +class SimState(SimStateBase.SimStateBase): + def __init__(self, ss=None): + assert ss is None + super().__init__(ss) + + # ss is defined in this netlist module + self.ss = SimStrategy() + + # wire up the circuit + agents = [] + agents.append( + VestingFunderAgent( + name="funder1", + USD=0.0, + OCEAN=self.ss.OCEAN_funded, + vesting_wallet_agent_name="vw1", + beneficiary_agent_name="beneficiary1", + start_timestamp=self.ss.start_timestamp, + duration_seconds=self.ss.duration_seconds, + ) + ) + agents.append( + VestingBeneficiaryAgent( + name="beneficiary1", USD=0.0, OCEAN=0.0, vesting_wallet_agent_name="vw1" + ) + ) + self.agents = {agent.name: agent for agent in agents} + + # kpis is defined in this netlist module + self.kpis = KPIs(self.ss.time_step) + + +@enforce_types +class KPIs(KPIsBase.KPIsBase): + pass + + +@enforce_types +def netlist_createLogData(state): + """SimEngine constructor uses this.""" + + s = [] # for console logging + dataheader = [] # for csv logging: list of string + datarow = [] # for csv logging: list of float + + # SimEngine already logs: Tick, Second, Min, Hour, Day, Month, Year + # So we log other things... + + timestamp = chain[-1].timestamp + s += [f"; timestamp={timestamp}"] + dataheader += ["timestamp"] + datarow += [timestamp] + + if "vw1" in state.agents: + vw = state.getAgent("vw1").vesting_wallet + OCEAN_vested = vw.vestedAmount(OCEAN_address(), timestamp) / 1e18 + OCEAN_released = vw.released(OCEAN_address()) / 1e18 + else: + OCEAN_vested = OCEAN_released = 0 + s += [f"; OCEAN_vested={OCEAN_vested:.6f}, OCEAN_released={OCEAN_released:.6f}"] + dataheader += ["OCEAN_vested", "OCEAN_released"] + datarow += [OCEAN_vested, OCEAN_released] + + beneficiary_OCEAN = state.getAgent("beneficiary1").OCEAN() + s += [f"; beneficiary_OCEAN={beneficiary_OCEAN:.6f}"] + dataheader += ["beneficiary_OCEAN"] + datarow += [beneficiary_OCEAN] + + # done + return s, dataheader, datarow + + +@enforce_types +def netlist_plotInstructions(header: List[str], values): + """ + Describe how to plot the information. + tsp.do_plot() uses this. + + :param: header: List[str] holding 'Tick', 'Second', ... + :param: values: 2d array of float [tick_i, valuetype_i] + :return: x_label: str -- e.g. "Day", "Month", "Year" + :return: x: List[float] -- x-axis info on how to plot + :return: y_params: List[YParam] -- y-axis info on how to plot + """ + x_label = "Year" + x = arrayToFloatList(values[:, header.index(x_label)]) + + y_params = [ + YParam( + ["OCEAN_vested", "OCEAN_released", "beneficiary_OCEAN"], + ["OCEAN vested", "OCEAN released", "OCEAN to beneficiary"], + "Vesting over time", + LINEAR, + MULT1, + DOLLAR, + ) + ] + + return (x_label, x, y_params) diff --git a/netlists/scheduler/test/__init__.py b/netlists/scheduler/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/scheduler/test/test_netlist.py b/netlists/scheduler/test/test_netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..f8222bcc1db4cc2436e42ab58ddf8a576074ff9d --- /dev/null +++ b/netlists/scheduler/test/test_netlist.py @@ -0,0 +1,52 @@ +import inspect + +from enforce_typing import enforce_types + +from .. import netlist + + +@enforce_types +def test_scope(): + netlist.SimStrategy() + netlist.SimState() + + assert inspect.isclass(netlist.SimStrategy) + assert inspect.isclass(netlist.SimState) + assert inspect.isclass(netlist.KPIs) + assert callable(netlist.netlist_createLogData) + assert callable(netlist.netlist_plotInstructions) + + +@enforce_types +def test_SimStrategy(): + # import from `netlist` module, not a `SimState` module. Netlist has it all:) + ss = netlist.SimStrategy() + assert 0.0 <= ss.OCEAN_funded <= 1e9 + assert ss.start_timestamp > 0 + assert ss.duration_seconds > 0 + + +@enforce_types +def test_SimState(): + # import from `netlist` module + state = netlist.SimState() + + assert len(state.agents) == 2 + assert state.getAgent("funder1").OCEAN() == state.ss.OCEAN_funded + assert state.getAgent("beneficiary1").OCEAN() == 0.0 + + state.takeStep() + assert len(state.agents) == 3 + assert state.getAgent("vw1") is not None + + state.takeStep() + + +def test_KPIs(): + # import from `netlist` module + kpis = netlist.KPIs(time_step=12) + assert kpis.tick() == 0 + kpis.takeStep(state=None) + kpis.takeStep(state=None) + assert kpis.tick() == 2 + assert kpis.elapsedTime() == 12 * 2 diff --git a/netlists/simplegrant/__init__.py b/netlists/simplegrant/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/simplegrant/about.md b/netlists/simplegrant/about.md new file mode 100644 index 0000000000000000000000000000000000000000..ecd28cb2ecc08801d998ff2982608d08b3bf281d --- /dev/null +++ b/netlists/simplegrant/about.md @@ -0,0 +1,78 @@ +# simplegrant Netlist + +## Overview + +The [`simplegrant` netlist](netlist.py) at `netlists/simplegrant/netlist.py` has two agents (objects): + +- `granter`, a [GrantGivingAgent](../../agents/GrantGivingAgent.py) +- `taker`, a [GrantTakingAgent](../../agents/GrantTakingAgent.py) + +As one might expect, `granter` gives grants to `taker` over time according to a simple schedule. This continues until runs out of money. These two agents are instantiated in the netlist's `SimStrategy` class. + +[Here's the netlist code, in Python](netlist.py). It's just one file that defines `SimStrategy` class, `SimState` class, `KPIs` class, `netlist_createLogData()` function, and `netlist_plotInstructions()` function. + +The following sections elaborate on each of these, sequentially top-to-bottom in the `netlist.py` file. They're worth understanding, because when you create your own netlist, you'll be making your own versions of these. + +## Imports + +Imports are at the top of the netlist. + +<img src="images/imports.png" width="100%"> + +Lines 1β2 import from third-party libraries: `enforce_types` for dynamic type-checking, and `List` and `Set` to specify types for type-checking. + +Lines 4β6 import local modules: definitions for grant-giving and grant-taking agents from the agents directory; base classes for `KPIs`, `SimState` and `SimStrategy` (more on this later); and some constants. + +## SimStrategy Class + +The netlist defines the `SimStrategy` class by inheriting from a base class, then injecting code as needed. All magic numbers go here, versus scattering them throughout the code. + +<img src="images/simstrategy.png" width="100%"> + +Lines 14β15 define values that every `SimStrategy` needs: a time interval between steps (set to one hour); and a stopping condition (set to 10 days). + +Lines 18β20 define values specific to this netlist: how much OCEAN the granter starts with (1.0 OCEAN); the time interval between grants (3 days); and the number of grant actions (4 actions, therefore 1.0/4 = 0.25 OCEAN per grant). + +## SimState Class + +The netlist defines the `SimState` class by inheriting from a base class, then injecting code as needed. + +<img src="images/simstate.png" width="100%"> + +Line 28 instantiates an object of class SimStrategy. We'd just defined that class earlier in the netlist. + +Lines 32β37 instantiates the granter object. It's a `GrantGivingAgent` which is defined in `agents`. Like all agent instances, it's given a name ("granter1"), initial USD funds (0.0) and initial OCEAN funds (specified via `SimStrategy`). As a `GrantGivingAgent`, it needs a few more parameters: the name of the agent receiving funds ("taker1"), the time interval between grants (via `SimStrategy`), and number of actions (via `SimStrategy`). Note how magic numbers are kept out of here; they stay in SimStrategy. + +Lines 38β39 instantiates the `taker` object. It's a `GrantTakingAgent`. It's given a name ("taker1"); see how this is the same name that the `granter` has specified where funds go. This is how the netlist gets "wired up", similar in philosophy to [SPICE](https://medium.com/tokenspice/tokenspice-design-verification-flows-5b795c594f9c). Finally, the `taker`'s initial funds are specified, as 0.0 USD and 0.0 OCEAN. + +## KPIs Class + +The netlist defines the `KPIs` by inheriting from a base class, then injecting code as needed. The base class already tracks many metrics out-of-the-box including each agent's OCEAN balance at each time step. This netlist doesn't need more, so its `KPIs` class is simply a pass-through. + +<img src="images/kpis.png" width="100%"> + +## netlist_createLogData + +The netlist defines the `netlist_createLogData()` function, which is called by the core simulator engine `SimEngine` in each `takeStep()` iteration of a simulation run. + +<img src="images/netlist_createlogdata.png" width="100%"> + +Lines 58β60 initializes these 3 variables: `s`, `dataheader`, and `datarow`. The rest of the routine fills them in, iteratively. + +- Line 65 grabs the "granter1" agent from the `SimState` object `state`. The lines below will use that agent. This function can to grab any data from SimState. Since SimState holds all the agents, this function can grab any any agent. The lines that follow query information from the "granter" agent `g`. +- `s` is a list of strings to be logged to the console's standard output (stdout), where the final string is a concatenation of all items in the list. Line 66 updates `s` with another item, for the `granter`'s OCEAN balance and USD balance. Those values are retrieved by querying the `g` object. +- The other two variables are towards constructing a csv file, where the first row has all the header variable names and each remaining row is another datapoint corresponding to a time step. +- This function constructs `dataheader` as the list of header names. Line 67 adds the "granter_OCEAN" and "granter_USD" variables to that list. +- This function constructs `datarow` as a list of variable values, i.e. a datapoint. Each of these has 1:1 mapping to header names added to `dataheader`. This function queries the the `g` object to fill in the values. + +While this netlist (`simplegrant`) only records a single group of values (OCEAN and USD balance), other netlists like `wsloop` record several groups of values. + +## netlist_plotInstructions + +The netlist defines the `SimState` by inheriting from a base class, then injecting code as needed. + +<img src="images/netlist_plotinstructions.png" width="100%"> + +This concludes the description of the `simplegrant` netlist. If you're working on learning the structure of TokenSPICE netlists, a great next step is the description of `simplepool` netlist, [here](../simplepool/about.md). + + diff --git a/netlists/simplegrant/images/imports.png b/netlists/simplegrant/images/imports.png new file mode 100644 index 0000000000000000000000000000000000000000..d950c2865c33b31d3a05200e4ce9af2f136ef3b1 Binary files /dev/null and b/netlists/simplegrant/images/imports.png differ diff --git a/netlists/simplegrant/images/kpis.png b/netlists/simplegrant/images/kpis.png new file mode 100644 index 0000000000000000000000000000000000000000..60151788d91749ecc0cb7c1991bb0103ac5fc743 Binary files /dev/null and b/netlists/simplegrant/images/kpis.png differ diff --git a/netlists/simplegrant/images/netlist_createlogdata.png b/netlists/simplegrant/images/netlist_createlogdata.png new file mode 100644 index 0000000000000000000000000000000000000000..2bd9d625124621d3326c594bc650ffee8e573738 Binary files /dev/null and b/netlists/simplegrant/images/netlist_createlogdata.png differ diff --git a/netlists/simplegrant/images/netlist_plotinstructions.png b/netlists/simplegrant/images/netlist_plotinstructions.png new file mode 100644 index 0000000000000000000000000000000000000000..67bc7a6a5cb83de280ac1125db93a138df496825 Binary files /dev/null and b/netlists/simplegrant/images/netlist_plotinstructions.png differ diff --git a/netlists/simplegrant/images/simstate.png b/netlists/simplegrant/images/simstate.png new file mode 100644 index 0000000000000000000000000000000000000000..76247d8619e7071d0c62a73ff016101a5019c201 Binary files /dev/null and b/netlists/simplegrant/images/simstate.png differ diff --git a/netlists/simplegrant/images/simstrategy.png b/netlists/simplegrant/images/simstrategy.png new file mode 100644 index 0000000000000000000000000000000000000000..aafe863d7fa0b380460520fee329ae80e4ec22a4 Binary files /dev/null and b/netlists/simplegrant/images/simstrategy.png differ diff --git a/netlists/simplegrant/netlist.py b/netlists/simplegrant/netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..e407db398aca6ed6cf0002cf1eacff3eff840a12 --- /dev/null +++ b/netlists/simplegrant/netlist.py @@ -0,0 +1,100 @@ +from typing import List + +from enforce_typing import enforce_types + +from agents import GrantGivingAgent, GrantTakingAgent +from engine import KPIsBase, SimStateBase, SimStrategyBase +from util.constants import S_PER_HOUR, S_PER_DAY +from util.plotutil import YParam, arrayToFloatList, LINEAR, MULT1, DOLLAR + + +@enforce_types +class SimStrategy(SimStrategyBase.SimStrategyBase): + def __init__(self): + super().__init__() + + # ==baseline + self.setTimeStep(S_PER_HOUR) + self.setMaxTime(10, "days") + + # ==attributes specific to this netlist + self.granter_init_OCEAN: float = 1.0 # type:ignore + self.granter_s_between_grants: int = S_PER_DAY * 3 # type:ignore + self.granter_n_actions: int = 4 # type:ignore + + +@enforce_types +class SimState(SimStateBase.SimStateBase): + def __init__(self, ss=None): + assert ss is None + super().__init__(ss) + + # ss is defined in this netlist module + self.ss = SimStrategy() + + # wire up the circuit + granter = GrantGivingAgent.GrantGivingAgent( + name="granter1", + USD=0.0, + OCEAN=self.ss.granter_init_OCEAN, + receiving_agent_name="taker1", + s_between_grants=self.ss.granter_s_between_grants, + n_actions=self.ss.granter_n_actions, + ) + taker = GrantTakingAgent.GrantTakingAgent(name="taker1", USD=0.0, OCEAN=0.0) + for agent in [granter, taker]: + self.agents[agent.name] = agent + + # kpis is defined in this netlist module + self.kpis = KPIs(self.ss.time_step) + + def OCEANprice(self) -> float: + return 1.0 # arbitrary. Need GrantTakingAgent + + +@enforce_types +class KPIs(KPIsBase.KPIsBase): + pass + + +@enforce_types +def netlist_createLogData(state): + """SimEngine constructor uses this.""" + + s = [] # for console logging + dataheader = [] # for csv logging: list of string + datarow = [] # for csv logging: list of float + + # SimEngine already logs: Tick, Second, Min, Hour, Day, Month, Year + # So we log other things... + + g = state.getAgent("granter1") + s += [f"; granter OCEAN={g.OCEAN()}, USD={g.USD()}"] + dataheader += ["granter_OCEAN", "granter_USD"] + datarow += [g.OCEAN(), g.USD()] + + # done + return s, dataheader, datarow + + +@enforce_types +def netlist_plotInstructions(header: List[str], values): + """ + Describe how to plot the information. + tsp.do_plot() uses this. + + :param: header: List[str] holding 'Tick', 'Second', ... + :param: values: 2d array of float [tick_i, valuetype_i] + :return: x_label: str -- e.g. "Day", "Month", "Year" + :return: x: List[float] -- x-axis info on how to plot + :return: y_params: List[YParam] -- y-axis info on how to plot + """ + x_label = "Day" + x = arrayToFloatList(values[:, header.index(x_label)]) + + y_params = [ + YParam(["granter_OCEAN"], ["OCEAN"], "granter_OCEAN", LINEAR, MULT1, DOLLAR), + YParam(["granter_USD"], ["USD"], "granter_USD", LINEAR, MULT1, DOLLAR), + ] + + return (x_label, x, y_params) diff --git a/netlists/simplegrant/test/__init__.py b/netlists/simplegrant/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/simplegrant/test/test_netlist.py b/netlists/simplegrant/test/test_netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..fcee5c211318c85cbae259a4c8116e2a3db09fd9 --- /dev/null +++ b/netlists/simplegrant/test/test_netlist.py @@ -0,0 +1,49 @@ +import inspect + +from enforce_typing import enforce_types + +from .. import netlist + + +@enforce_types +def test_scope(): + netlist.SimStrategy() + netlist.SimState() + + assert inspect.isclass(netlist.SimStrategy) + assert inspect.isclass(netlist.SimState) + assert inspect.isclass(netlist.KPIs) + assert callable(netlist.netlist_createLogData) + assert callable(netlist.netlist_plotInstructions) + + +@enforce_types +def test_SimStrategy(): + # import from `netlist` module, not a `SimState` module. Netlist has it all:) + ss = netlist.SimStrategy() + assert 0.0 <= ss.granter_init_OCEAN <= 1e6 + assert ss.granter_s_between_grants > 0 + assert ss.granter_n_actions > 0 + + +@enforce_types +def test_SimState(): + # import from `netlist` module + state = netlist.SimState() + + assert len(state.agents) == 2 + assert state.getAgent("granter1").OCEAN() == state.ss.granter_init_OCEAN + assert state.getAgent("taker1").OCEAN() == 0.0 + + state.takeStep() + state.takeStep() + + +def test_KPIs(): + # import from `netlist` module + kpis = netlist.KPIs(time_step=12) + assert kpis.tick() == 0 + kpis.takeStep(state=None) + kpis.takeStep(state=None) + assert kpis.tick() == 2 + assert kpis.elapsedTime() == 12 * 2 diff --git a/netlists/simplepool/__init__.py b/netlists/simplepool/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/simplepool/about.md b/netlists/simplepool/about.md new file mode 100644 index 0000000000000000000000000000000000000000..b5f591af631021c7aae6b606cbeb0000177e70ec --- /dev/null +++ b/netlists/simplepool/about.md @@ -0,0 +1,19 @@ +# simplepool Netlist + +## Overview + +`simplepool` layers in EVM. [Its netlist](netlist.py) is at netlists/simplepool/netlist.py + +The netlist is a single file, which defines everything needed: `SimStrategy`, `SimState`, etc. + +The netlist's [`SimState`](https://github.com/tokenspice/tokenspice/blob/0826d78e0d8d6c4f3a03bf1916067fdfc77224fe/netlists/simplepool/netlist.py#L35) creates a [`PublisherAgent`](https://github.com/tokenspice/tokenspice/blob/main/agents/PublisherAgent.py) instance, which during the simulation run creates [`PoolAgent`](https://github.com/tokenspice/tokenspice/blob/main/agents/PoolAgent.py) objects. + +Each `PoolAgent` holds a full-fidelity EVM Balancer as follows: + +- At the top level, each `PoolAgent` Python object (an agent) holds a pool.BPool Python object (a driver to the lower level). +- One level lower, each `pool.BPool` Python object (a driver) points to a [BPool.sol](https://github.com/balancer-labs/balancer-core/blob/master/contracts/BPool.sol) contract deployment in Ganache EVM (actual contract). + +You can view `pool.BPool` as a middleware driver to the contract deployed to EVM. Like all drivers, is in the `web3engine/ directory`. + + + diff --git a/netlists/simplepool/netlist.py b/netlists/simplepool/netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..d1b73ce75ac207ecc051ebd48b96d552d5ea5064 --- /dev/null +++ b/netlists/simplepool/netlist.py @@ -0,0 +1,108 @@ +from typing import List + +from enforce_typing import enforce_types + +from agents import PublisherAgent +from engine import KPIsBase, SimStateBase, SimStrategyBase +from util.constants import S_PER_HOUR +from util.plotutil import YParam, arrayToFloatList, LINEAR, MULT1, COUNT, DOLLAR +from util.strutil import prettyBigNum + + +@enforce_types +class SimStrategy(SimStrategyBase.SimStrategyBase): + def __init__(self): + super().__init__() + + # ==baseline + self.setTimeStep(S_PER_HOUR) + self.setMaxTime(20, "days") + + # ==attributes specific to this netlist + + # publisher + self.pub_init_OCEAN = 10000.0 + + # pool + self.OCEAN_init = 1000.0 + self.OCEAN_stake = 200.0 + self.DT_init = 100.0 + self.DT_stake = 20.0 + + self.pool_weight_DT = 3.0 + self.pool_weight_OCEAN = 7.0 + assert (self.pool_weight_DT + self.pool_weight_OCEAN) == 10.0 + + +@enforce_types +class SimState(SimStateBase.SimStateBase): + def __init__(self, ss=None): + assert ss is None + super().__init__(ss) + + # ss is defined in this netlist module + self.ss = SimStrategy() + + # wire up the circuit + pub_agent = PublisherAgent.PublisherAgent( + name="pub1", USD=0.0, OCEAN=self.ss.pub_init_OCEAN + ) + self.agents[pub_agent.name] = pub_agent + + # kpis is defined in this netlist module + self.kpis = KPIs(self.ss.time_step) + + +@enforce_types +class KPIs(KPIsBase.KPIsBase): + pass + + +@enforce_types +def netlist_createLogData(state): + """SimEngine constructor uses this""" + s = [] # for console logging + dataheader = [] # for csv logging: list of string + datarow = [] # for csv logging: list of float + + # SimEngine already logs: Tick, Second, Min, Hour, Day, Month, Year + # So we log other things... + + publisher = state.getAgent("pub1") + s += [f"; publisher OCEAN={prettyBigNum(publisher.OCEAN(), False)}"] + dataheader += ["publisher_OCEAN"] + datarow += [publisher.OCEAN()] + + pool_agents = state.agents.filterToPool() + n_pools = len(pool_agents) + s += [f"; # pools={n_pools}"] + dataheader += ["n_pools"] + datarow += [n_pools] + + # done + return s, dataheader, datarow + + +@enforce_types +def netlist_plotInstructions(header: List[str], values): + """ + Describe how to plot the information. + tsp.do_plot() calls this + + :param: header: List[str] holding 'Tick', 'Second', ... + :param: values: 2d array of float [tick_i, valuetype_i] + :return: x_label: str -- e.g. "Day", "Month", "Year" + :return: x: List[float] -- x-axis info on how to plot + :return: y_params: List[YParam] -- y-axis info on how to plot + """ + x_label = "Day" + x = arrayToFloatList(values[:, header.index(x_label)]) + + y_params = [ + YParam( + ["publisher_OCEAN"], ["OCEAN"], "publisher_OCEAN", LINEAR, MULT1, DOLLAR + ), + YParam(["n_pools"], ["# pools"], "n_pools", LINEAR, MULT1, COUNT), + ] + + return (x_label, x, y_params) diff --git a/netlists/simplepool/test/__init__.py b/netlists/simplepool/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/simplepool/test/test_netlist.py b/netlists/simplepool/test/test_netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..395fbc5a19e7128fa7b6bf46954bcf67f9ded58d --- /dev/null +++ b/netlists/simplepool/test/test_netlist.py @@ -0,0 +1,65 @@ +import inspect + +from enforce_typing import enforce_types + +from .. import netlist + + +def test_scope(): + netlist.SimStrategy() + netlist.SimState() + + assert inspect.isclass(netlist.SimStrategy) + assert inspect.isclass(netlist.SimState) + assert inspect.isclass(netlist.KPIs) + assert callable(netlist.netlist_createLogData) + assert callable(netlist.netlist_plotInstructions) + + +@enforce_types +def test_SimStrategy(): + # import from `netlist` module, not a `SimState` module. Netlist has it all:) + ss = netlist.SimStrategy() + + assert ss.pub_init_OCEAN > 0.0 + assert ss.OCEAN_init > 0.0 + assert ss.OCEAN_stake > 0.0 + + assert ss.pub_init_OCEAN >= (ss.OCEAN_init + ss.OCEAN_stake) + + assert ss.DT_init > 0.0 + assert ss.DT_stake > 0.0 + assert ss.DT_init >= ss.DT_stake + + assert (ss.pool_weight_DT + ss.pool_weight_OCEAN) == 10.0 + + +@enforce_types +def test_SimState(): + # import from `netlist` module + state = netlist.SimState() + + assert len(state.agents) == 1 + + assert state.getAgent("pub1").OCEAN() == state.ss.pub_init_OCEAN + + for _ in range(1000): + state.takeStep() + if len(state.agents) > 1: + break + + assert len(state.agents) > 1 + pool_agents = state.agents.filterToPool() + assert len(pool_agents) > 0 + + +@enforce_types +def test_KPIs(): + # import from `netlist` module + kpis = netlist.KPIs(time_step=12) + + assert kpis.tick() == 0 + kpis.takeStep(state=None) + kpis.takeStep(state=None) + assert kpis.tick() == 2 + assert kpis.elapsedTime() == 12 * 2 diff --git a/netlists/wsloop/KPIs.py b/netlists/wsloop/KPIs.py new file mode 100644 index 0000000000000000000000000000000000000000..70ee50cf35981fb3139cb7f24a27cca31fd57044 --- /dev/null +++ b/netlists/wsloop/KPIs.py @@ -0,0 +1,468 @@ +import math +from typing import List + +from enforce_typing import enforce_types + +from engine import KPIsBase +from util.constants import S_PER_YEAR, S_PER_MONTH, INF +from util.strutil import prettyBigNum + +from util.plotutil import ( + YParam, + arrayToFloatList, + LOG, + BOTH, + MULT1, + MULT100, + DIV1M, + COUNT, + DOLLAR, + PERCENT, +) + + +@enforce_types +class KPIs( + KPIsBase.KPIsBase +): # pylint: disable=too-many-public-methods, too-many-instance-attributes + def __init__(self, ss): + super().__init__(ss.time_step) + self.ss = ss + + # for these, append a new value with each tick. All List[float] + self._granttakers_revenue_per_tick__per_tick = [] + self._consume_sales_per_marketplace_per_s__per_tick = [] + self._n_marketplaces__per_tick = [] # this one's List[int] + self._total_OCEAN_minted__per_tick = [] + self._total_OCEAN_burned__per_tick = [] + self._total_OCEAN_minted_USD__per_tick = [] + self._total_OCEAN_burned_USD__per_tick = [] + + def takeStep(self, state): + super().takeStep(state) # parent e.g. increments self._tick + + self._granttakers_revenue_per_tick__per_tick.append( + state.grantTakersSpentAtTick() + ) + + am = state.getAgent("marketplaces1") + self._consume_sales_per_marketplace_per_s__per_tick.append( + am.consumeSalesPerMarketplacePerSecond() + ) + self._n_marketplaces__per_tick.append(am.numMarketplaces()) + + O_minted = state.totalOCEANminted() + O_burned = state.totalOCEANburned() + self._total_OCEAN_minted__per_tick.append(O_minted) + self._total_OCEAN_burned__per_tick.append(O_burned) + + O_price = state.OCEANprice() + O_minted_USD = O_minted * O_price + O_burned_USD = state.totalOCEANburnedUSD() + self._total_OCEAN_minted_USD__per_tick.append(O_minted_USD) + self._total_OCEAN_burned_USD__per_tick.append(O_burned_USD) + + def tick(self) -> int: + """# ticks since start of run""" + assert len(self._consume_sales_per_marketplace_per_s__per_tick) == self._tick + return self._tick + + # ======================================================================= + # growth rate + def mktsRNDToSalesRatio(self) -> float: + """Return the ratio ($ into ocean R&D) / ($ from ocean sales ) + We average over the last month. This is important + because the grant takers often only get income monthly. + """ + monthly_RND = self.grantTakersMonthlyRevenueNow() + monthly_sales = self.monthlyNetworkRevenueNow() + if monthly_RND == 0.0: + ratio = 0.0 + else: + ratio = monthly_RND / monthly_sales + return ratio + + # ======================================================================= + # revenue numbers: grant takers + def grantTakersMonthlyRevenueNow(self) -> float: + ticks_1mo = self._ticksOneMonth() + rev_per_tick = self._granttakers_revenue_per_tick__per_tick + return float(sum(rev_per_tick[-ticks_1mo:])) + + # ======================================================================= + # sales numbers: 1 marketplace + def onemktMonthlyConsumeSalesNow(self) -> float: + t2 = self.elapsedTime() + t1 = t2 - S_PER_MONTH + return self._onemktConsumeSalesOverInterval(t1, t2) + + def onemktAnnualConsumeSalesNow(self) -> float: + t2 = self.elapsedTime() + t1 = t2 - S_PER_YEAR + return self._onemktConsumeSalesOverInterval(t1, t2) + + def onemktAnnualConsumeSalesOneYearAgo(self) -> float: + t2 = self.elapsedTime() - S_PER_YEAR + t1 = t2 - S_PER_YEAR + return self._onemktConsumeSalesOverInterval(t1, t2) + + def _onemktConsumeSalesOverInterval(self, t1: int, t2: int) -> float: + return self._salesOverInterval(t1, t2, self.onemktConsumeSalesPerSecond) + + def onemktConsumeSalesPerSecond(self, tick) -> float: + """Returns onemkt's sales per second at a given tick""" + return self._consume_sales_per_marketplace_per_s__per_tick[tick] + + # ======================================================================= + # sales numbers: n marketplaces + def allmktsMonthlyConsumeSalesNow(self) -> float: + t2 = self.elapsedTime() + t1 = t2 - S_PER_MONTH + return self._allmktsConsumeSalesOverInterval(t1, t2) + + def allmktsAnnualConsumeSalesNow(self) -> float: + t2 = self.elapsedTime() + t1 = t2 - S_PER_YEAR + return self._allmktsConsumeSalesOverInterval(t1, t2) + + def allmktsAnnualConsumeSalesOneYearAgo(self) -> float: + t2 = self.elapsedTime() - S_PER_YEAR + t1 = t2 - S_PER_YEAR + return self._allmktsConsumeSalesOverInterval(t1, t2) + + def allmktsConsumeSalesPerSecond(self, tick) -> float: + """Returns allmkt's sales per second at a given tick""" + return ( + self._consume_sales_per_marketplace_per_s__per_tick[tick] + * self._n_marketplaces__per_tick[tick] + ) + + def _allmktsConsumeSalesOverInterval(self, t1: int, t2: int) -> float: + return self._salesOverInterval(t1, t2, self.allmktsConsumeSalesPerSecond) + + # ======================================================================= + # revenue numbers: ocean community + def monthlyNetworkRevenueNow(self) -> float: + t2 = self.elapsedTime() + t1 = t2 - S_PER_MONTH + return self._networkRevenueOverInterval(t1, t2) + + def annualNetworkRevenueNow(self) -> float: + t2 = self.elapsedTime() + t1 = t2 - S_PER_YEAR + return self._networkRevenueOverInterval(t1, t2) + + def oceanMonthlyRevenueOneMonthAgo(self) -> float: + t2 = self.elapsedTime() - S_PER_MONTH + t1 = t2 - S_PER_MONTH + return self._networkRevenueOverInterval(t1, t2) + + def oceanAnnualRevenueOneYearAgo(self) -> float: + t2 = self.elapsedTime() - S_PER_YEAR + t1 = t2 - S_PER_YEAR + return self._networkRevenueOverInterval(t1, t2) + + def _networkRevenueOverInterval(self, t1: int, t2: int) -> float: + return self._salesOverInterval(t1, t2, self.networkRevenuePerSecond) + + def networkRevenuePerSecond(self, tick) -> float: + """Returns Network Revenue per second at a given tick""" + n = self._n_marketplaces__per_tick[tick] + consume1 = self._consume_sales_per_marketplace_per_s__per_tick[tick] + consumeN = n * consume1 + network_revenue = self.ss.networkRevenue(consumeN) + return network_revenue + + # ======================================================================= + def _salesOverInterval(self, t1: int, t2: int, revenuePerSecondFunc) -> float: + """ + Helper function for _{onemkt, allmkts, ocean}salesOverInterval(). + + In time from t1 to t2 (both in # seconds since start), + how much $ was earned, using the revenuePerSecondFunc. + """ + assert t2 > t1 + tick1: int = max(0, math.floor(t1 / self._time_step)) + tick2: int = min(self.tick(), math.floor(t2 / self._time_step) + 1) + rev = 0.0 + for tick_i in range(tick1, tick2): + rev_this_s = revenuePerSecondFunc(tick_i) + + start_s = max(t1, tick_i * self._time_step) + end_s = min(t2, (tick_i + 1) * self._time_step - 1) + n_s = end_s - start_s + 1 + rev_this_tick = rev_this_s * n_s + + rev += rev_this_tick + return rev + + # ======================================================================= + # revenue growth numbers: ocean community + def monthlyNetworkRevenueGrowth(self) -> float: + rev1 = self.oceanMonthlyRevenueOneMonthAgo() + rev2 = self.monthlyNetworkRevenueNow() + if rev1 == 0.0: + return INF + g = rev2 / rev1 - 1.0 + return g + + def annualNetworkRevenueGrowth(self) -> float: + rev1 = self.oceanAnnualRevenueOneYearAgo() + rev2 = self.annualNetworkRevenueNow() + if rev1 == 0.0: + return INF + g = rev2 / rev1 - 1.0 + return g + + # ======================================================================= + # OCEAN minted & burned per month, as a count (#) and as USD ($) + def OCEANmintedPrevMonth(self) -> float: + return self._OCEANchangePrevMonth(self._total_OCEAN_minted__per_tick) + + def OCEANburnedPrevMonth(self) -> float: + return self._OCEANchangePrevMonth(self._total_OCEAN_burned__per_tick) + + def OCEANmintedInUSDPrevMonth(self) -> float: + return self._OCEANchangePrevMonth(self._total_OCEAN_minted_USD__per_tick) + + def OCEANburnedInUSDPrevMonth(self) -> float: + return self._OCEANchangePrevMonth(self._total_OCEAN_burned_USD__per_tick) + + def _OCEANchangePrevMonth(self, O_per_tick) -> float: + ticks_total = len(O_per_tick) + + if ticks_total == 0: + return 0.0 + + ticks_1mo = self._ticksOneMonth() + if ticks_total <= ticks_1mo: + return O_per_tick[-1] + + return O_per_tick[-1] - O_per_tick[-(ticks_1mo + 1)] + + def _ticksOneMonth(self) -> int: + """Number of ticks in one month""" + ticks: int = math.ceil(S_PER_MONTH / float(self._time_step)) + return ticks + + +@enforce_types +def netlist_createLogData( + state, +): # pylint: disable=too-many-statements, too-many-locals + """pass this to SimEngine.__init__() as argument `netlist_createLogData`""" + F = False + ss = state.ss + kpis = state.kpis + + s = [] # for console logging + dataheader = [] # for csv logging: list of string + datarow = [] # for csv logging: list of float + + # SimEngine already logs: Tick, Second, Min, Hour, Day, Month, Year + # So we log other things... + + am = state.getAgent("marketplaces1") + # s += ["; # mkts=%s" % prettyBigNum(am._n_marketplaces,F)] + dataheader += ["Num_mkts"] + datarow += [am._n_marketplaces] + + onemkt_cons_sales_mo = kpis.onemktMonthlyConsumeSalesNow() + onemkt_cons_sales_yr = kpis.onemktAnnualConsumeSalesNow() + # s += ["; 1mkt_cons_sales/mo=$%s,/yr=$%s" % + # (prettyBigNum(onemkt_cons_sales_mo,F), prettyBigNum(onemkt_cons_sales_yr,F))] + dataheader += ["onemkt_cons_sales/mo", "onemkt_cons_sales/yr"] + datarow += [onemkt_cons_sales_mo, onemkt_cons_sales_yr] + + allmkts_cons_sales_mo = kpis.allmktsMonthlyConsumeSalesNow() + allmkts_cons_sales_yr = kpis.allmktsAnnualConsumeSalesNow() + # s += ["; allmkts_cons_sales/mo=$%s,/yr=$%s" % + # (prettyBigNum(allmkts_cons_sales_mo,F), prettyBigNum(allmkts_cons_sales_yr,F))] + dataheader += ["allmkts_cons_sales/mo", "allmkts_cons_sales/yr"] + datarow += [allmkts_cons_sales_mo, allmkts_cons_sales_yr] + + allmkts_tot_sales_day = ss.totalSales(allmkts_cons_sales_mo / 30.5) + allmkts_tot_sales_mo = ss.totalSales(allmkts_cons_sales_mo) + allmkts_tot_sales_yr = ss.totalSales(allmkts_cons_sales_yr) + # s += ["; allmkts_tot_sales/mo=$%s,/yr=$%s" % + # (prettyBigNum(allmkts_tot_sales_mo,F), prettyBigNum(allmkts_tot_sales_yr,F))] + dataheader += [ + "allmkts_tot_sales/day", + "allmkts_tot_sales/mo", + "allmkts_tot_sales/yr", + ] + datarow += [allmkts_tot_sales_day, allmkts_tot_sales_mo, allmkts_tot_sales_yr] + + tot_staked = ss.totalStaked(allmkts_tot_sales_day) + s += [f"; tot_staked=${prettyBigNum(tot_staked, F)}"] + dataheader += ["tot_staked"] + datarow += [tot_staked] + + network_rev_mo = kpis.monthlyNetworkRevenueNow() + network_rev_yr = kpis.annualNetworkRevenueNow() + # s += [f"; network_rev/mo=${prettyBigNum(network_rev_mo,F)}m" + # [",/yr=${prettyBigNum(network_rev_yr,F)}"] + s += [f"; network_rev/mo=${prettyBigNum(network_rev_mo, F)}m"] + dataheader += ["network_rev/mo", "network_rev/yr"] + datarow += [network_rev_mo, network_rev_yr] + + dataheader += ["network_rev_growth/mo", "network_rev_growth/yr"] + datarow += [kpis.monthlyNetworkRevenueGrowth(), kpis.annualNetworkRevenueGrowth()] + + overall_valuation = state.overallValuation() + dataheader += [ + "overall_valuation", + "fundamentals_valuation", + "speculation_valuation", + ] + s += [f"; valn=${prettyBigNum(overall_valuation, F)}"] + datarow += [ + overall_valuation, + state.fundamentalsValuation(), + state.speculationValuation(), + ] + + tot_O_supply = state.OCEANsupply() + s += [f"; #OCEAN={prettyBigNum(tot_O_supply)}"] + dataheader += ["tot_OCEAN_supply", "tot_OCEAN_minted", "tot_OCEAN_burned"] + datarow += [tot_O_supply, state.totalOCEANminted(), state.totalOCEANburned()] + + dataheader += ["OCEAN_minted/mo", "OCEAN_burned/mo"] + datarow += [kpis.OCEANmintedPrevMonth(), kpis.OCEANburnedPrevMonth()] + + dataheader += ["OCEAN_minted_USD/mo", "OCEAN_burned_USD/mo"] + datarow += [kpis.OCEANmintedInUSDPrevMonth(), kpis.OCEANburnedInUSDPrevMonth()] + + O_price = state.OCEANprice() + if O_price <= 10.0: + s += [f"; $OCEAN=${O_price:.3f}"] + else: + s += [f"; $OCEAN=${prettyBigNum(O_price, F)}"] + dataheader += ["OCEAN_price"] + datarow += [O_price] + + gt_rev = kpis.grantTakersMonthlyRevenueNow() + # s += [f"; r&d/mo=${prettyBigNum(gt_rev,F)}"] + dataheader += ["RND/mo"] + datarow += [gt_rev] + + ratio = kpis.mktsRNDToSalesRatio() + growth = ss.annualMktsGrowthRate(ratio) + # s += ["; r&d/sales ratio={ratio:.2f}, growth(ratio)={growth:.3f}"] + dataheader += ["rnd_to_cons_sales_ratio", "mkts_annual_growth_rate"] + datarow += [ratio, growth] + + dao = state.getAgent("ocean_dao") # RouterAgent + dao_USD = dao.monthlyUSDreceived(state) + dao_OCEAN = dao.monthlyOCEANreceived(state) + dao_OCEAN_in_USD = dao_OCEAN * O_price + dao_total_in_USD = dao_USD + dao_OCEAN_in_USD + # s += [f"; dao:[${prettyBigNum(dao_USD,F)}/mo" + # ",{prettyBigNum(dao_OCEAN,F)} OCEAN/mo" + # " (${prettyBigNum(dao_OCEAN_in_USD,F)})" + # ",total=${prettyBigNum(dao_total_in_USD,F)}/mo"] + dataheader += [ + "dao_USD/mo", + "dao_OCEAN/mo", + "dao_OCEAN_in_USD/mo", + "dao_total_in_USD/mo", + ] + datarow += [dao_USD, dao_OCEAN, dao_OCEAN_in_USD, dao_total_in_USD] + + # done + return s, dataheader, datarow + + +@enforce_types +def netlist_plotInstructions(header: List[str], values): + """ + Describe how to plot the information. + tsp.do_plot() calls this + + :param: header: List[str] holding 'Tick', 'Second', ... + :param: values: 2d array of float [tick_i, valuetype_i] + :return: x_label: str -- e.g. "Day", "Month", "Year" + :return: x: List[float] -- x-axis info on how to plot + :return: y_params: List[YParam] -- y-axis info on how to plot + """ + x_label = "Year" + x = arrayToFloatList(values[:, header.index(x_label)]) + + y_params = [ + YParam(["OCEAN_price"], [""], "OCEAN Price", LOG, MULT1, DOLLAR), + # YParam(["network_rev_growth/yr"], [""], + # "Annual Network Revenue Growth", BOTH, MULT100, PERCENT), + YParam( + ["overall_valuation", "fundamentals_valuation", "speculation_valuation"], + ["Overall", "Fundamentals (P/S=30)", "Speculation"], + "Valuation", + LOG, + DIV1M, + DOLLAR, + ), + YParam( + ["dao_USD/mo", "dao_OCEAN_in_USD/mo", "dao_total_in_USD/mo"], + [ + "Income as USD (ie network revenue)", + "Income as OCEAN (ie from 51%; priced in USD)", + "Total Income", + ], + "Monthly OceanDAO Income", + LOG, + DIV1M, + DOLLAR, + ), + YParam( + ["network_rev/yr", "allmkts_cons_sales/yr", "allmkts_tot_sales/yr"], + [ + "Network Revenue", + "All marketplaces consume sales", + "All marketplaces total sales", + ], + "Annual Revenue or Sales", + LOG, + DIV1M, + DOLLAR, + ), + YParam( + ["tot_staked"], + ["USD worth of OCEAN"], + "Total OCEAN Staked (in USD)", + LOG, + DIV1M, + DOLLAR, + ), + YParam( + ["tot_OCEAN_supply", "tot_OCEAN_minted", "tot_OCEAN_burned"], + ["Total supply", "Tot # Minted", "Tot # Burned"], + "OCEAN Token Count", + BOTH, + DIV1M, + COUNT, + ), + YParam( + ["OCEAN_minted/mo", "OCEAN_burned/mo"], + ["# Minted/mo", "# Burned/mo"], + "Monthly # OCEAN Minted & Burned", + BOTH, + DIV1M, + COUNT, + ), + YParam( + ["rnd_to_cons_sales_ratio", "mkts_annual_growth_rate"], + ["R&D/consume_sales ratio", "Marketplaces annual growth rate"], + "R&D/Sales Ratio and Marketplaces Growth Rate", + BOTH, + MULT100, + PERCENT, + ), + YParam(["RND/mo"], [""], "Monthly R&D Spend", BOTH, DIV1M, DOLLAR), + # YParam(["OCEAN_burned_USD/mo", "OCEAN_minted_USD/mo"], + # ["$ of OCEAN Burned/mo", "$ of OCEAN Minted/mo"], + # "Monthly OCEAN (in USD) Minted & Burned", LOG, DIV1M, DOLLAR), + # YParam(["OCEAN_burned_USD/mo", "network_rev/mo", "allmkts_total_sales/mo"], + # ["$ OCEAN Burned monthly", "Ocean monthly revenue", "Marketplaces monthly sales"], + # "Monthly OCEAN Burned and Revenues", LOG, DIV1M, DOLLAR), + ] + + return (x_label, x, y_params) diff --git a/netlists/wsloop/SimState.py b/netlists/wsloop/SimState.py new file mode 100644 index 0000000000000000000000000000000000000000..a318d39bab528f7a5ea9da634a5a7c404bcd6d9f --- /dev/null +++ b/netlists/wsloop/SimState.py @@ -0,0 +1,222 @@ +from typing import Set + +from enforce_typing import enforce_types + +from agents import MinterAgents +from agents.GrantGivingAgent import GrantGivingAgent +from agents.GrantTakingAgent import GrantTakingAgent +from agents.MarketplacesAgent import MarketplacesAgent +from agents.OCEANBurnerAgent import OCEANBurnerAgent +from agents.RouterAgent import RouterAgent +from engine import AgentBase, SimStateBase +from util import valuation +from util.constants import S_PER_MONTH, S_PER_DAY +from .KPIs import KPIs +from .SimStrategy import SimStrategy + + +@enforce_types +class SimState( + SimStateBase.SimStateBase +): # pylint: disable=too-many-instance-attributes + def __init__(self, ss=None): + # initialize self.tick, ss, agents, kpis + super().__init__(ss) + + # now, fill in actual values for ss, agents, kpis + if self.ss is None: + self.ss = SimStrategy() + ss = self.ss # for convenience as we go forward + + # used to manage names + self._next_free_marketplace_number = 0 + + # used to add agents + self._marketplace_tick_previous_add = 0 + + # as ecosystem improves, these parameters may change + self._total_OCEAN_minted: float = 0.0 # type:ignore + self._total_OCEAN_burned: float = 0.0 # type:ignore + self._total_OCEAN_burned_USD: float = 0.0 # type:ignore + self._speculation_valuation = ss._init_speculation_valuation # type:ignore + + # Instantiate and connnect agent instances. "Wire up the circuit" + new_agents: Set[AgentBase.AgentBaseAbstract] = set() # type:ignore + + # Note: could replace MarketplacesAgent with DataecosystemAgent, for a + # higher-fidelity simulation using EVM agents + new_agents.add( + MarketplacesAgent( + name="marketplaces1", + USD=0.0, + OCEAN=0.0, + toll_agent_name="opc_address", + n_marketplaces=float(ss.init_n_marketplaces), + sales_per_marketplace_per_s=20e3 / S_PER_MONTH, # magic number + time_step=self.ss.time_step, + ) + ) + + new_agents.add( + RouterAgent( + name="opc_address", + USD=0.0, + OCEAN=0.0, + receiving_agents={ + "ocean_dao": self.ss.percentToOceanDao, + "opc_burner": self.ss.percentToBurn, + }, + ) + ) + + new_agents.add(OCEANBurnerAgent(name="opc_burner", USD=0.0, OCEAN=0.0)) + + # func = MinterAgents.ExpFunc(H=4.0) + func = MinterAgents.RampedExpFunc( + H=4.0, T0=0.5, T1=1.0, T2=1.4, T3=3.0, M1=0.10, M2=0.25, M3=0.50 + ) + new_agents.add( + MinterAgents.OCEANFuncMinterAgent( + name="ocean_51", + receiving_agent_name="ocean_dao", + total_OCEAN_to_mint=ss.UNMINTED_OCEAN_SUPPLY, + s_between_mints=S_PER_DAY, + func=func, + ) + ) + + new_agents.add( + GrantGivingAgent( + name="opf_treasury_for_ocean_dao", + USD=0.0, + OCEAN=ss.OPF_TREASURY_OCEAN_FOR_OCEAN_DAO, + receiving_agent_name="ocean_dao", + s_between_grants=S_PER_MONTH, + n_actions=12 * 3, + ) + ) + + new_agents.add( + GrantGivingAgent( + name="opf_treasury_for_opf_mgmt", + USD=ss.OPF_TREASURY_USD, + OCEAN=ss.OPF_TREASURY_OCEAN_FOR_OPF_MGMT, + receiving_agent_name="opf_mgmt", + s_between_grants=S_PER_MONTH, + n_actions=12 * 3, + ) + ) + + new_agents.add( + GrantGivingAgent( + name="bdb_treasury", + USD=ss.BDB_TREASURY_USD, + OCEAN=ss.BDB_TREASURY_OCEAN, + receiving_agent_name="bdb_mgmt", + s_between_grants=S_PER_MONTH, + n_actions=17, + ) + ) + + new_agents.add( + RouterAgent( + name="ocean_dao", + receiving_agents={"opc_workers": funcOne}, + USD=0.0, + OCEAN=0.0, + ) + ) + + new_agents.add( + RouterAgent( + name="opf_mgmt", + receiving_agents={"opc_workers": funcOne}, + USD=0.0, + OCEAN=0.0, + ) + ) + + new_agents.add( + RouterAgent( + name="bdb_mgmt", + receiving_agents={"bdb_workers": funcOne}, + USD=0.0, + OCEAN=0.0, + ) + ) + + new_agents.add(GrantTakingAgent(name="opc_workers", USD=0.0, OCEAN=0.0)) + + new_agents.add(GrantTakingAgent(name="bdb_workers", USD=0.0, OCEAN=0.0)) + + for agent in new_agents: + self.agents[agent.name] = agent + + # track certain metrics over time, so that we don't have to load + self.kpis = KPIs(self.ss) + + def takeStep(self) -> None: + """This happens once per tick""" + # update agents + # update kpis + super().takeStep() + + # update global state values + self._updateSpeculationValuation() + + # ============================================================== + def grantTakersSpentAtTick(self) -> float: + return sum( + agent.spentAtTick() + for agent in self.agents.values() + if isinstance(agent, GrantTakingAgent) + ) + + # ============================================================== + def OCEANprice(self) -> float: + """Estimated price of $OCEAN token, in USD""" + price = valuation.OCEANprice(self.overallValuation(), self.OCEANsupply()) + assert price > 0.0 + return price + + # ============================================================== + def _updateSpeculationValuation(self): + self._speculation_valuation *= ( + 1.0 + + self.ss._percent_increase_speculation_valuation_per_s * self.ss.time_step + ) + + def overallValuation(self) -> float: # in USD + # fundamental valuation acts as a lower bound. + # sum() is too optimistic, so use max() + v = max(self.fundamentalsValuation(), self.speculationValuation()) + assert v > 0.0 + return v + + def fundamentalsValuation(self) -> float: # in USD + sales = self.kpis.annualNetworkRevenueNow() + return self.ss.fundamentalsValuation(sales) + + def speculationValuation(self) -> float: # in USD + return self._speculation_valuation + + # ============================================================== + def OCEANsupply(self) -> float: + """Current OCEAN token supply""" + return self.initialOCEAN() + self.totalOCEANminted() - self.totalOCEANburned() + + def initialOCEAN(self) -> float: + return self.ss.INIT_OCEAN_SUPPLY + + def totalOCEANminted(self) -> float: + return self._total_OCEAN_minted + + def totalOCEANburned(self) -> float: + return self._total_OCEAN_burned + + def totalOCEANburnedUSD(self) -> float: + return self._total_OCEAN_burned_USD + + +def funcOne(): + return 1.0 diff --git a/netlists/wsloop/SimStrategy.py b/netlists/wsloop/SimStrategy.py new file mode 100644 index 0000000000000000000000000000000000000000..f7e57f9d1f264d8a6bbcae9ccdb9442e8a621928 --- /dev/null +++ b/netlists/wsloop/SimStrategy.py @@ -0,0 +1,127 @@ +import math +from enforce_typing import enforce_types + +from engine import SimStrategyBase +from util.constants import S_PER_DAY, S_PER_YEAR + + +@enforce_types +class SimStrategy( + SimStrategyBase.SimStrategyBase +): # pylint: disable=too-many-instance-attributes + def __init__(self): + # ===initialize self.time_step, max_ticks==== + super().__init__() + + # ===set base-class values we want for this netlist==== + self.setTimeStep(S_PER_DAY) + self.setMaxTime(2, "years") # typical runs: 10 years, 20 years, 150 years + self.setLogInterval(10 * S_PER_DAY) + + # ===new attributes specific to this netlist=== + + # network revenue + self._percent_consume_sales_for_network = 0.03 # 0.1% no DF, 3% DF + self._percent_swap_sales_for_network = 0.001 # 0.1% + + # $ volume in swap : $ volume in consume + self._swap_to_consume_sales_ratio = 100.0 + + # initial # mkts + self.init_n_marketplaces = 1 + + # % network revenue to burn, vs to DAO + self._percent_burn: float = 0.05 # type:ignore + + # valuation + self._p_s_ratio = 30.0 + + self._init_speculation_valuation = 150e6 # in USD + self._percent_increase_speculation_valuation_per_s = 0.10 / S_PER_YEAR + + # for computing annualGrowthRate() of # marketplaces, revenue/mktplace + # -total marketplaces' growth = (1+annualGrowthRate)^2 - 1 + # -so, if we want upper bound of total marketplaces' growth of 50%, + # we need max_growth_rate = 22.5%. + # -and, if we want lower bound of total growth of -25%, + # we need growth_rate_if_0_sales = -11.8% + self.growth_rate_if_0_sales = -0.118 + self.max_growth_rate = 0.415 + self.tau = 0.6 + + # (taken from constants.py) + self.TOTAL_OCEAN_SUPPLY = 1.41e9 + self.INIT_OCEAN_SUPPLY = 0.49 * self.TOTAL_OCEAN_SUPPLY + self.UNMINTED_OCEAN_SUPPLY = self.TOTAL_OCEAN_SUPPLY - self.INIT_OCEAN_SUPPLY + + self.OPF_TREASURY_USD = 2e6 # (not the true number) + self.OPF_TREASURY_OCEAN = 200e6 # (not the true number) + self.OPF_TREASURY_OCEAN_FOR_OCEAN_DAO = 100e6 # (not the true number) + self.OPF_TREASURY_OCEAN_FOR_OPF_MGMT = ( + self.OPF_TREASURY_OCEAN - self.OPF_TREASURY_OCEAN_FOR_OCEAN_DAO + ) + + self.BDB_TREASURY_USD = 2e6 # (not the true number) + self.BDB_TREASURY_OCEAN = 20e6 # (not the true number) + + self.pool_weight_DT = 3.0 + self.pool_weight_OCEAN = 7.0 + assert (self.pool_weight_DT + self.pool_weight_OCEAN) == 10.0 + + def swapSales(self, consume_sales: float) -> float: + return self._swap_to_consume_sales_ratio * consume_sales + + def totalSales(self, consume_sales: float) -> float: + """Given marketplace consume sales, return swap + consume sales""" + swap_sales = self.swapSales(consume_sales) + return consume_sales + swap_sales + + def networkRevenue(self, consume_sales: float) -> float: + """Given marketplace consume sales, return network revenue""" + swap_sales = self.swapSales(consume_sales) + return ( + self._percent_consume_sales_for_network * consume_sales + + self._percent_swap_sales_for_network * swap_sales + ) + + def totalStaked(self, daily_consume_sales: float) -> float: + total_daily_sales = self.totalSales(daily_consume_sales) + total_staked = total_daily_sales # simple heuristic for now + return total_staked + + def percentToBurn(self) -> float: + return self._percent_burn + + def percentToOceanDao(self) -> float: + return 1.0 - self._percent_burn + + def fundamentalsValuation(self, annual_revenue: float) -> float: + """ + Valuation by price-to-sales (P/S) ratio. + Annual revenue = sales summed up over the last year (eg per quarter); + it's *not* based on run rate (sales based on this quarter * 4, + or projected sales over the next year). + The market decides P/S based on revenue growth. Higher growth -> higher P/S. + Most blockchain co's have P/S of 30x-50x, as do startups like Zoom. + """ + return annual_revenue * self._p_s_ratio + + def annualMktsGrowthRate(self, ratio_RND_to_sales: float) -> float: + """ + Growth rate for marketplaces. Starts low, and increases as + the ($ into R&D)/($ from sales) goes up, but w/ diminishing returns. + + Modeled as an exponential decay function. + -Input x: ratio_RND_to_sales + -Output y: growth rate + -Func params: + -self.tau: at x=tau, y has increased by 50% of its possible increase + - at x=2*tau, y ... 75% ... + -self.growth_rate_if_0_sales + -self.max_growth_rate + """ + mult = self.max_growth_rate - self.growth_rate_if_0_sales + growth_rate: float = self.growth_rate_if_0_sales + mult * ( + 1.0 - math.pow(0.5, ratio_RND_to_sales / self.tau) + ) + return growth_rate diff --git a/netlists/wsloop/__init__.py b/netlists/wsloop/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/wsloop/about.md b/netlists/wsloop/about.md new file mode 100644 index 0000000000000000000000000000000000000000..016fbd6612b7866ded23ac08bd58b2f80696c348 --- /dev/null +++ b/netlists/wsloop/about.md @@ -0,0 +1,57 @@ +## About wsloop Netlist + +### Overview + +Wsloop = Web3 Sustainability Loop. It's a system-level token design to catalyze near-term growth and drive long-term sustainability, drawing on elements of revenue, burning, and work done in the ecosystem. + +It first appeared as a blog post in 2020. [Here's the original article](https://blog.oceanprotocol.com/the-web3-sustainability-loop-b2a4097a36e). + +### Schematic / block diagram + +Here's the block diagram as presented for broader public consumption. While it says Ocean, the system-level design is really quite general (Web3 Sustainability Loop). + +<img src="images/block-diagram-simpler-for-public.png" width="70%"> + +That diagram glossed over some details. Here is a more accurate block diagram. + +<img src="images/block-diagram-actual.png" width="70%"> + +### KPIs modeled + +The plots show many key performance indicators (KPIs) and other variables changing over time. Here's how they affect each other. + +<img src="images/variables-being-modeled.png" width="100%"> + +We have slightly fancier models for growth rate and for token supply schedule. The details are [here](images/model-growth-rate.png) and [here](images/model-supply-schedule.png), respectively. + +### Model scope and limitations + +- This netlist does not attempt to model Ocean Market dynamics or the Balancer AMM at any level of fidelity, or staking in Ocean Market or elsewhere. See other netlists for this. + +### Resources + +Working Slides: +- [wsloop Key Variables Being Modeled](https://docs.google.com/presentation/d/1jsCg7iMcVM9Hk5wPeZyyN8WGXEzX2sDi98Yf0dH2F4Q/edit#slide=id.gfa42987b7d_0_73) + +Other: +- "The Web3 Sustainability Loop" blog post [[ref](https://blog.oceanprotocol.com/the-web3-sustainability-loop-b2a4097a36e)] is a good first external reference +- Then, "Ocean Token Model" blog post [[ref](https://blog.oceanprotocol.com/ocean-token-model-3e4e7af210f9)] adds a bit more fidelity. +- The Ocean Whitepaper [[ref](https://oceanprotocol.com/tech-whitepaper.pdf)] has more fidelity yet. +- Then, see [Ocean System TE SW Verification](https://docs.google.com/presentation/d/1DmC6wfyl7ZMjuB-h3Zbfy--xFuYSt3tGACpgfJH9ZFk) section of "20201209 Ocean TE Study Group" slides, which goes into detail about TokenSPICE and results. +- Finally, take a look at the netlist code here! + +### Example plots + +Running this netlist will give a plot for most of the variables modeled above, including market cap and token price. + +Here are some example output plots. + +<img src="images/token-count.png"> + +<img src="images/monthly-rnd-spend.png"> + +<img src="images/monthly-OCEAN-minted-burned.png"> + +<img src="images/dao-income.png"> + + diff --git a/netlists/wsloop/images/block-diagram-actual.png b/netlists/wsloop/images/block-diagram-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..872e92325d77cb02cc39dbf66d9ae9fce2565f50 Binary files /dev/null and b/netlists/wsloop/images/block-diagram-actual.png differ diff --git a/netlists/wsloop/images/block-diagram-simpler-for-public.png b/netlists/wsloop/images/block-diagram-simpler-for-public.png new file mode 100644 index 0000000000000000000000000000000000000000..a049d08f015a03528db5a7a068c833d10d1d13d4 Binary files /dev/null and b/netlists/wsloop/images/block-diagram-simpler-for-public.png differ diff --git a/netlists/wsloop/images/dao-income.png b/netlists/wsloop/images/dao-income.png new file mode 100644 index 0000000000000000000000000000000000000000..71ad5d4668be62f918558564fa78b73073b37561 Binary files /dev/null and b/netlists/wsloop/images/dao-income.png differ diff --git a/netlists/wsloop/images/model-growth-rate.png b/netlists/wsloop/images/model-growth-rate.png new file mode 100644 index 0000000000000000000000000000000000000000..80a57c4586c2ffa82ed3ad2c00f574576f8b93d8 Binary files /dev/null and b/netlists/wsloop/images/model-growth-rate.png differ diff --git a/netlists/wsloop/images/model-supply-schedule.png b/netlists/wsloop/images/model-supply-schedule.png new file mode 100644 index 0000000000000000000000000000000000000000..a669dc529b4348e86c69dc7c147a428d08792136 Binary files /dev/null and b/netlists/wsloop/images/model-supply-schedule.png differ diff --git a/netlists/wsloop/images/monthly-OCEAN-minted-burned.png b/netlists/wsloop/images/monthly-OCEAN-minted-burned.png new file mode 100644 index 0000000000000000000000000000000000000000..053ea2babe7f6a919deafbac24752461f0ae6f36 Binary files /dev/null and b/netlists/wsloop/images/monthly-OCEAN-minted-burned.png differ diff --git a/netlists/wsloop/images/monthly-rnd-spend.png b/netlists/wsloop/images/monthly-rnd-spend.png new file mode 100644 index 0000000000000000000000000000000000000000..5a0c0d024a85af434b03d0aedcaf2bd275392ea2 Binary files /dev/null and b/netlists/wsloop/images/monthly-rnd-spend.png differ diff --git a/netlists/wsloop/images/token-count.png b/netlists/wsloop/images/token-count.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ea1c9a281b8a7a1901dfa7263d4d8cd5666756 Binary files /dev/null and b/netlists/wsloop/images/token-count.png differ diff --git a/netlists/wsloop/images/variables-being-modeled.png b/netlists/wsloop/images/variables-being-modeled.png new file mode 100644 index 0000000000000000000000000000000000000000..198002de346c81b58bb6f82812d8df2d886e41f5 Binary files /dev/null and b/netlists/wsloop/images/variables-being-modeled.png differ diff --git a/netlists/wsloop/netlist.py b/netlists/wsloop/netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..14666942fb5ec869eec99907c7165a7fca6382ba --- /dev/null +++ b/netlists/wsloop/netlist.py @@ -0,0 +1,13 @@ +# pylint: disable=unused-import +""" +Netlist to simulate Web3 Sustainability Loop, with no EVM +""" + +# These puts all key interfaces in one module +# +# Users just refer to netlist.SimStrategy, netlist.SimState, etc., versus +# having to import directly from supporting modules. + +from .SimStrategy import SimStrategy +from .SimState import SimState +from .KPIs import KPIs, netlist_createLogData, netlist_plotInstructions diff --git a/netlists/wsloop/test/__init__.py b/netlists/wsloop/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/netlists/wsloop/test/test_KPIs.py b/netlists/wsloop/test/test_KPIs.py new file mode 100644 index 0000000000000000000000000000000000000000..6f6fc459057442966f49addbe1588ad6cc1129d4 --- /dev/null +++ b/netlists/wsloop/test/test_KPIs.py @@ -0,0 +1,281 @@ +import pytest + +from enforce_typing import enforce_types + +from util.constants import S_PER_DAY +from ..KPIs import KPIs + + +@enforce_types +class DummySS: + def __init__(self, time_step: int): + self.time_step: int = time_step + + self._percent_consume_sales_for_network = 0.10 + + def networkRevenue(self, consume_sales: float) -> float: + # NOTE: this dummy SS is simplistic, ignoring swap revenue. That's ok + # here since it's tested elsewhere + return consume_sales * self._percent_consume_sales_for_network + + +@enforce_types +class BaseDummyMarketplacesAgent: + def numMarketplaces(self) -> float: + return 0.0 + + def consumeSalesPerMarketplacePerSecond( + self, + ) -> float: + return 0.0 + + +@enforce_types +class BaseDummySimState: + def __init__(self): + self._marketplaces1_agent = None + + def takeStep(self) -> None: + pass + + def getAgent(self, name: str): # pylint: disable=unused-argument + return self._marketplaces1_agent + + def grantTakersSpentAtTick(self) -> float: + return 0.0 + + def OCEANprice(self) -> float: + return 0.0 + + def totalOCEANminted(self) -> float: + return 0.0 + + def totalOCEANburned(self) -> float: + return 0.0 + + def totalOCEANburnedUSD(self) -> float: + return 0.0 + + +@enforce_types +def testKPIs__Ratio_happypath(): + _testKPIs_Ratio(monthly_RND=10.0, monthly_sales=20.0, target=0.5) + + +@enforce_types +def testKPIs__Ratio_zero_RND(): + _testKPIs_Ratio(monthly_RND=0.0, monthly_sales=20.0, target=0.0) + + +@enforce_types +def testKPIs__Ratio_do_not_rail_to_less_than_1(): + _testKPIs_Ratio(monthly_RND=1000.0, monthly_sales=20.0, target=50.0) + + +@enforce_types +def _testKPIs_Ratio(monthly_RND, monthly_sales, target): + ss = DummySS(time_step=1) + kpis = KPIs(ss) + kpis.grantTakersMonthlyRevenueNow = lambda: monthly_RND + kpis.monthlyNetworkRevenueNow = lambda: monthly_sales + assert kpis.mktsRNDToSalesRatio() == target + + +@enforce_types +def testKPIs_GrantTakersRevenue(): + class DummySimState(BaseDummySimState): + def __init__(self): + super().__init__() + self.ss = DummySS(time_step=S_PER_DAY * 10) # 1 tick = 10/30 = 0.33 mos + self._marketplaces1_agent = BaseDummyMarketplacesAgent() + + def grantTakersSpentAtTick(self): + return 1e3 + + state = DummySimState() + kpis = KPIs(state.ss) + + # tick = 0, months = 0 + assert kpis.grantTakersMonthlyRevenueNow() == 0.0 + + # tick = 1, months = 0.33 + kpis.takeStep(state) + assert kpis._granttakers_revenue_per_tick__per_tick == [1e3] + assert ( + pytest.approx(kpis.grantTakersMonthlyRevenueNow()) == 1e3 + ) # NOT 1e3*S_PER_DAY*10 + + # tick = 2, months = 0.66 + kpis.takeStep(state) + assert kpis._granttakers_revenue_per_tick__per_tick == [1e3, 1e3] + assert ( + pytest.approx(kpis.grantTakersMonthlyRevenueNow()) == 2e3 + ) # NOT 2e3*S_PER_DAY*10 + + # tick = 3, months = 1.00 + kpis.takeStep(state) + assert kpis._granttakers_revenue_per_tick__per_tick == [1e3, 1e3, 1e3] + assert ( + pytest.approx(kpis.grantTakersMonthlyRevenueNow()) == 3e3 + ) # NOT 3e3*S_PER_DAY*10 + + # tick = 4, months = 1.33 + kpis.takeStep(state) + assert kpis._granttakers_revenue_per_tick__per_tick == [1e3, 1e3, 1e3, 1e3] + assert ( + pytest.approx(kpis.grantTakersMonthlyRevenueNow()) == 3e3 + ) # NOT 4e3. NOT 3e3*S_PER_DAY*10 + + # many more steps + for _ in range(20): + kpis.takeStep(state) + assert pytest.approx(kpis.grantTakersMonthlyRevenueNow()) == 3e3 + + +@enforce_types +def testKPIs__mktsConsumeSalesAndValuation(): + class DummyMarketplacesAgent(BaseDummyMarketplacesAgent): + def numMarketplaces(self) -> float: + return 5.0 + + def consumeSalesPerMarketplacePerSecond(self) -> float: + return 10.0 + + class DummySimState(BaseDummySimState): + def __init__(self): + super().__init__() + self._marketplaces1_agent = DummyMarketplacesAgent() + self.ss = DummySS(time_step=3) + + state = DummySimState() + kpis = KPIs(state.ss) + + # base case - no time passed + assert kpis.onemktMonthlyConsumeSalesNow() == 0.0 + assert kpis.onemktAnnualConsumeSalesNow() == 0.0 + assert kpis.onemktAnnualConsumeSalesOneYearAgo() == 0.0 + + assert kpis.allmktsMonthlyConsumeSalesNow() == 0.0 + assert kpis.allmktsAnnualConsumeSalesNow() == 0.0 + assert kpis.allmktsAnnualConsumeSalesOneYearAgo() == 0.0 + + assert kpis.monthlyNetworkRevenueNow() == 0.0 + assert kpis.annualNetworkRevenueNow() == 0.0 + assert kpis.oceanAnnualRevenueOneYearAgo() == 0.0 + + # let time pass + for _ in range(8): + kpis.takeStep(state) + + # key numbers: + # sales_per_marketplace_per_s = 10.0 + # n_marketplaces = 5 + # marketplace_percent_toll_to_network = 0.10 + # time_step = 3 (seconds per tick) + # num time steps = num elapsed ticks = 8 + # elapsed time = 8 * 3 = 24 + # + # therefore, for the elapsed time period + # rev one mkt = 10 * 24 = 240 + # rev all mkts = 10 * 24 * 5 = 1200 + # rev ocean = 10 * 24 * 5 * 0.10 = 120 + + assert kpis.onemktConsumeSalesPerSecond(0) == (10.0) + assert kpis._onemktConsumeSalesOverInterval(0, 24 - 1) == (240.0) + assert kpis._onemktConsumeSalesOverInterval(0, 23 - 1) == (230.0) + assert kpis._onemktConsumeSalesOverInterval(0, 23 - 2) == (220.0) + assert kpis._onemktConsumeSalesOverInterval(1, 24) == (230.0) + assert kpis._onemktConsumeSalesOverInterval(2, 24) == (220.0) + + assert kpis.allmktsConsumeSalesPerSecond(0) == (50.0) + assert kpis._allmktsConsumeSalesOverInterval(0, 24 - 1) == (5 * 240.0) + assert kpis._allmktsConsumeSalesOverInterval(0, 23 - 1) == (5 * 230.0) + assert kpis._allmktsConsumeSalesOverInterval(1, 24) == (5 * 230.0) + + assert kpis.networkRevenuePerSecond(0) == (5.0) + assert kpis._networkRevenueOverInterval(0, 24 - 1) == (5 * 24.0) + assert kpis._networkRevenueOverInterval(0, 23 - 1) == (5 * 23.0) + assert kpis._networkRevenueOverInterval(1, 24) == (5 * 23.0) + + assert kpis.onemktMonthlyConsumeSalesNow() == (240.0) + assert kpis.allmktsMonthlyConsumeSalesNow() == (1200.0) + assert kpis.monthlyNetworkRevenueNow() == (120.0) + + +@enforce_types +def testKPIs__mintAndBurn(): + class DummyMarketplacesAgent(BaseDummyMarketplacesAgent): + def numMarketplaces(self) -> float: + return 0.0 + + def consumeSalesPerMarketplacePerSecond(self) -> float: + return 0.0 + + class DummySimState(BaseDummySimState): + def __init__(self): + super().__init__() + self.ss = DummySS(time_step=S_PER_DAY * 10) + self._marketplaces1_agent = DummyMarketplacesAgent() + + self._total_OCEAN_minted = 0.0 + self._total_OCEAN_burned = 0.0 + self._total_OCEAN_burned_USD = 0.0 + + def takeStep(self): + self._total_OCEAN_minted += 2.0 + self._total_OCEAN_burned += 3.0 + self._total_OCEAN_burned_USD += 3.0 * self.OCEANprice() + + def OCEANprice(self): + return 4.0 + + def totalOCEANminted(self) -> float: + return self._total_OCEAN_minted + + def totalOCEANburned(self) -> float: + return self._total_OCEAN_burned + + def totalOCEANburnedUSD(self) -> float: + return self._total_OCEAN_burned_USD + + state = DummySimState() + kpis = KPIs(state.ss) + assert kpis._ticksOneMonth() == (3) + + # tick = 0, months = 0 + assert kpis.OCEANmintedPrevMonth() == 0.0 + assert kpis.OCEANburnedPrevMonth() == 0.0 + + state.takeStep() + kpis.takeStep(state) # now, tick = 1, months = 0.33 + assert kpis._total_OCEAN_minted__per_tick == [2.0] + assert kpis.OCEANmintedPrevMonth() == 2.0 + assert kpis.OCEANburnedPrevMonth() == 3.0 + + state.takeStep() + kpis.takeStep(state) # now, tick = 2, months = 0.66 + assert kpis._total_OCEAN_minted__per_tick == [2.0, 4.0] + assert kpis.OCEANmintedPrevMonth() == 4.0 + assert kpis.OCEANburnedPrevMonth() == 6.0 + + state.takeStep() + kpis.takeStep(state) # now, tick = 3, months = 1.0 + assert kpis._total_OCEAN_minted__per_tick == [2.0, 4.0, 6.0] + assert kpis.OCEANmintedPrevMonth() == 6.0 + assert kpis.OCEANburnedPrevMonth() == 9.0 + + state.takeStep() + kpis.takeStep(state) # now, tick = 4, months = 1.33 + assert kpis._total_OCEAN_minted__per_tick == [2.0, 4.0, 6.0, 8.0] + assert kpis.OCEANmintedPrevMonth() == 6.0 # note: NOT 8.0 + assert kpis.OCEANburnedPrevMonth() == 9.0 # note: NOT 12.0 + + state.takeStep() + kpis.takeStep(state) # now, tick = 5 months = 1.66 + assert kpis._total_OCEAN_minted__per_tick == [2.0, 4.0, 6.0, 8.0, 10.0] + assert kpis.OCEANmintedPrevMonth() == 6.0 # note: NOT 8.0 + assert kpis.OCEANburnedPrevMonth() == 9.0 # note: NOT 12.0 + + # also test $ versions where OCEAN price is 4.0 + assert kpis.OCEANmintedInUSDPrevMonth() == (6.0 * 4.0) + assert kpis.OCEANburnedInUSDPrevMonth() == (9.0 * 4.0) diff --git a/netlists/wsloop/test/test_SimState.py b/netlists/wsloop/test/test_SimState.py new file mode 100644 index 0000000000000000000000000000000000000000..8cb396083cfec4b35401f7cb4b5ca25bb2edea29 --- /dev/null +++ b/netlists/wsloop/test/test_SimState.py @@ -0,0 +1,54 @@ +from enforce_typing import enforce_types + +from agents import MinterAgents +from ..SimState import SimState + + +@enforce_types +def testSimState_MoneyFlow1(): + state = SimState() + assert hasattr(state.ss, "_percent_burn") + state.ss._percent_burn = 0.20 + assert state.ss.percentToBurn() == 0.20 + + # opc_address -> (opc_burner, ocean_dao) + state.getAgent("opc_address").receiveUSD(100.0) + state.getAgent("opc_address").takeStep(state) + assert state.getAgent("opc_burner").USD() == (0.20 * 100.0) + assert state.getAgent("ocean_dao").USD() == (0.80 * 100.0) + + # ocean_dao -> opc_workers + state.getAgent("ocean_dao").takeStep(state) + assert state.getAgent("opc_workers").USD() == (0.80 * 100.0) + + # ocean_dao spends + state.getAgent("opc_workers").takeStep(state) + assert state.getAgent("opc_workers").USD() == 0.0 + + +@enforce_types +def testSimState_MoneyFlow2(): + state = SimState() + state.getAgent("ocean_51")._func = MinterAgents.ExpFunc(H=4.0) + + # send from money 51% minter -> ocean_dao + o51_OCEAN_t0 = state.getAgent("ocean_51").OCEAN() + dao_OCEAN_t0 = state.getAgent("ocean_dao").OCEAN() + + assert o51_OCEAN_t0 == 0.0 + assert dao_OCEAN_t0 == 0.0 + assert state._total_OCEAN_minted == 0.0 + + # ocean_51 should disburse at tick=1 + state.getAgent("ocean_51").takeStep(state) + state.tick += 1 + state.getAgent("ocean_51").takeStep(state) + state.tick += 1 + + o51_OCEAN_t1 = state.getAgent("ocean_51").OCEAN() + dao_OCEAN_t1 = state.getAgent("ocean_dao").OCEAN() + + assert o51_OCEAN_t1 == 0.0 + assert dao_OCEAN_t1 > 0.0 + assert state._total_OCEAN_minted > 0.0 + assert state._total_OCEAN_minted == dao_OCEAN_t1 diff --git a/netlists/wsloop/test/test_SimStrategy.py b/netlists/wsloop/test/test_SimStrategy.py new file mode 100644 index 0000000000000000000000000000000000000000..c94d97556ffafb58e4899a9b20f6179e9570e06d --- /dev/null +++ b/netlists/wsloop/test/test_SimStrategy.py @@ -0,0 +1,78 @@ +from enforce_typing import enforce_types +from pytest import approx + +from ..SimStrategy import SimStrategy + + +@enforce_types +def testTotalOceanSupply(): + ss = SimStrategy() + assert 1e6 < ss.TOTAL_OCEAN_SUPPLY < 2e9 + assert isinstance(ss.TOTAL_OCEAN_SUPPLY, float) + + +@enforce_types +def testFundamentalsValuation(): + ss = SimStrategy() + assert ss._p_s_ratio == 30.0 + assert ss.fundamentalsValuation(1e3) == (1e3 * 30.0) + + +@enforce_types +def testBurn(): + ss = SimStrategy() + assert hasattr(ss, "_percent_burn") + ss._percent_burn = 0.19 + + assert ss.percentToBurn() == 0.19 + assert ss.percentToOceanDao() == 0.81 + + +@enforce_types +def testNetworkRevenue(): + ss = SimStrategy() + + assert hasattr(ss, "_percent_consume_sales_for_network") + assert hasattr(ss, "_percent_swap_sales_for_network") + assert hasattr(ss, "_swap_to_consume_sales_ratio") + + ss._percent_consume_sales_for_network = 0.01 + ss._percent_swap_sales_for_network = 0.02 + ss._swap_to_consume_sales_ratio = 10.0 + + consume_sales = 2000.0 + target_swap_sales = 10.0 * consume_sales + assert ss.swapSales(consume_sales) == approx(target_swap_sales) + assert ss.totalSales(consume_sales) == approx(consume_sales + target_swap_sales) + + assert ss.networkRevenue(consume_sales) == approx( + 0.01 * consume_sales + 0.02 * target_swap_sales + ) + + +@enforce_types +def testTotalStaked(): + ss = SimStrategy() + daily_consume_sales = 1000.0 + target_total_staked = ss.totalSales(daily_consume_sales) + assert ss.totalStaked(daily_consume_sales) == approx(target_total_staked) + + +@enforce_types +def testAnnualMktsGrowthRate(): + ss = SimStrategy() + + assert hasattr(ss, "growth_rate_if_0_sales") + assert hasattr(ss, "max_growth_rate") + assert hasattr(ss, "tau") + + ss.growth_rate_if_0_sales = -0.25 + ss.max_growth_rate = 0.5 + ss.tau = 0.075 + + assert ss.annualMktsGrowthRate(0.0) == -0.25 + assert ss.annualMktsGrowthRate(0.0 + 1 * ss.tau) == (-0.25 + 0.75 / 2.0) + assert ss.annualMktsGrowthRate(0.0 + 2 * ss.tau) == ( + -0.25 + 0.75 / 2.0 + 0.75 / 4.0 + ) + assert ss.annualMktsGrowthRate(1e6) == 0.5 diff --git a/netlists/wsloop/test/test_netlist.py b/netlists/wsloop/test/test_netlist.py new file mode 100644 index 0000000000000000000000000000000000000000..0ba39ceaf0b886c92df622cdf6528f0d3591d08f --- /dev/null +++ b/netlists/wsloop/test/test_netlist.py @@ -0,0 +1,14 @@ +"""simply test for scope""" +import inspect +from .. import netlist + + +def test1(): + netlist.SimStrategy() + netlist.SimState() + + assert inspect.isclass(netlist.SimStrategy) + assert inspect.isclass(netlist.SimState) + assert inspect.isclass(netlist.KPIs) + assert callable(netlist.netlist_createLogData) + assert callable(netlist.netlist_plotInstructions) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..ac4bc9f32dc2cf254d6ada6a9f8c5a9fa8fc185f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +# pytest.ini +[pytest] +filterwarnings = + ignore::DeprecationWarning + +[coverage:run] +omit=*/site-packages/*,*/tests/*, */.eggs/* diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b0bec8ef84fe897ad0e50bc391d545d8e0a78486 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +black +platformdirs +regex +typing-extensions +coverage +enforce-typing +eth-brownie +matplotlib +mypy +pylint +pytest>=6.2.5 +click +tomli +referencing +attrs diff --git a/sol057/__init__.py b/sol057/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/__init__.py b/sol057/contracts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/oceanv3/DTFactory.sol b/sol057/contracts/oceanv3/DTFactory.sol new file mode 100644 index 0000000000000000000000000000000000000000..71328b58df0a286ad58ac26d89ce84d0d9f56ed4 --- /dev/null +++ b/sol057/contracts/oceanv3/DTFactory.sol @@ -0,0 +1,126 @@ +pragma solidity >=0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import './utils/Deployer.sol'; +import './interfaces/IERC20Template.sol'; + +/** + * @title DTFactory contract + * @author Ocean Protocol Team + * + * @dev Implementation of Ocean DataTokens Factory + * + * DTFactory deploys DataToken proxy contracts. + * New DataToken proxy contracts are links to the template contract's bytecode. + * Proxy contract functionality is based on Ocean Protocol custom implementation of ERC1167 standard. + */ +contract DTFactory is Deployer { + address private tokenTemplate; + address private communityFeeCollector; + uint256 private currentTokenCount = 1; + + event TokenCreated( + address indexed newTokenAddress, + address indexed templateAddress, + string indexed tokenName + ); + + event TokenRegistered( + address indexed tokenAddress, + string tokenName, + string tokenSymbol, + uint256 tokenCap, + address indexed registeredBy, + string indexed blob + ); + + /** + * @dev constructor + * Called on contract deployment. Could not be called with zero address parameters. + * @param _template refers to the address of a deployed DataToken contract. + * @param _collector refers to the community fee collector address + */ + constructor( + address _template, + address _collector + ) public { + require( + _template != address(0) && + _collector != address(0), + 'DTFactory: Invalid template token/community fee collector address' + ); + tokenTemplate = _template; + communityFeeCollector = _collector; + } + + /** + * @dev Deploys new DataToken proxy contract. + * Template contract address could not be a zero address. + * @param blob any string that hold data/metadata for the new token + * @param name token name + * @param symbol token symbol + * @param cap the maximum total supply + * @return address of a new proxy DataToken contract + */ + function createToken( + string memory blob, + string memory name, + string memory symbol, + uint256 cap + ) + public + returns (address token) + { + require( + cap != 0, + 'DTFactory: zero cap is not allowed' + ); + + token = deploy(tokenTemplate); + + require( + token != address(0), + 'DTFactory: Failed to perform minimal deploy of a new token' + ); + IERC20Template tokenInstance = IERC20Template(token); + require( + tokenInstance.initialize( + name, + symbol, + msg.sender, + cap, + blob, + communityFeeCollector + ), + 'DTFactory: Unable to initialize token instance' + ); + emit TokenCreated(token, tokenTemplate, name); + emit TokenRegistered( + token, + name, + symbol, + cap, + msg.sender, + blob + ); + currentTokenCount += 1; + } + + /** + * @dev get the current token count. + * @return the current token count + */ + function getCurrentTokenCount() external view returns (uint256) { + return currentTokenCount; + } + + /** + * @dev get the token template address + * @return the template address + */ + function getTokenTemplate() external view returns (address) { + return tokenTemplate; + } +} diff --git a/sol057/contracts/oceanv3/Migrations.sol b/sol057/contracts/oceanv3/Migrations.sol new file mode 100644 index 0000000000000000000000000000000000000000..bec223fa69c74110740d67edc46377bd6e1f9f67 --- /dev/null +++ b/sol057/contracts/oceanv3/Migrations.sol @@ -0,0 +1,19 @@ +pragma solidity >=0.4.21 <0.7.0; + +contract Migrations { + + address public owner; + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } +} diff --git a/sol057/contracts/oceanv3/__init__.py b/sol057/contracts/oceanv3/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/oceanv3/balancer/BConst.sol b/sol057/contracts/oceanv3/balancer/BConst.sol new file mode 100644 index 0000000000000000000000000000000000000000..34860665378def2143b4ece8816fda826f091b96 --- /dev/null +++ b/sol057/contracts/oceanv3/balancer/BConst.sol @@ -0,0 +1,39 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity ^0.5.7; + +contract BConst { + uint public constant BONE = 10**18; + + uint public constant MIN_BOUND_TOKENS = 2; + uint public constant MAX_BOUND_TOKENS = 8; + + uint public constant MIN_FEE = BONE / 10**6; + uint public constant MAX_FEE = BONE / 10; + uint public constant EXIT_FEE = 0; + + uint public constant MIN_WEIGHT = BONE; + uint public constant MAX_WEIGHT = BONE * 50; + uint public constant MAX_TOTAL_WEIGHT = BONE * 50; + uint public constant MIN_BALANCE = BONE / 10**12; + + uint public constant INIT_POOL_SUPPLY = BONE * 100; + + uint public constant MIN_BPOW_BASE = 1 wei; + uint public constant MAX_BPOW_BASE = (2 * BONE) - 1 wei; + uint public constant BPOW_PRECISION = BONE / 10**10; + + uint public constant MAX_IN_RATIO = BONE / 2; + uint public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; +} diff --git a/sol057/contracts/oceanv3/balancer/BFactory.sol b/sol057/contracts/oceanv3/balancer/BFactory.sol new file mode 100644 index 0000000000000000000000000000000000000000..0700d9a1c35aa878937e0fa933853c28ed6149bd --- /dev/null +++ b/sol057/contracts/oceanv3/balancer/BFactory.sol @@ -0,0 +1,72 @@ +pragma solidity ^0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import './BPool.sol'; +import './BConst.sol'; +import '../utils/Deployer.sol'; + +/* +* @title BFactory contract +* @author Ocean Protocol (with code from Balancer Labs) +* +* @dev Ocean implementation of Balancer BPool Factory +* BFactory deploys BPool proxy contracts. +* New BPool proxy contracts are links to the template contract's bytecode. +* Proxy contract functionality is based on Ocean Protocol custom +* implementation of ERC1167 standard. +*/ +contract BFactory is BConst, Deployer { + + address public bpoolTemplate; + + event BPoolCreated( + address indexed newBPoolAddress, + address indexed bpoolTemplateAddress + ); + + event BPoolRegistered( + address bpoolAddress, + address indexed registeredBy + ); + + /* @dev Called on contract deployment. Cannot be called with zero address. + @param _bpoolTemplate -- address of a deployed BPool contract. */ + constructor(address _bpoolTemplate) + public + { + require( + _bpoolTemplate != address(0), + 'BFactory: invalid bpool template zero address' + ); + bpoolTemplate = _bpoolTemplate; + } + + /* @dev Deploys new BPool proxy contract. + Template contract address could not be a zero address. + @return address of a new proxy BPool contract */ + function newBPool() + external + returns (address bpool) + { + bpool = deploy(bpoolTemplate); + require( + bpool != address(0), + 'BFactory: invalid bpool zero address' + ); + BPool bpoolInstance = BPool(bpool); + require( + bpoolInstance.initialize( + msg.sender, + address(this), + MIN_FEE, + false, + false + ), + 'ERR_INITIALIZE_BPOOL' + ); + emit BPoolCreated(bpool, bpoolTemplate); + emit BPoolRegistered(bpool, msg.sender); + } +} diff --git a/sol057/contracts/oceanv3/balancer/BMath.sol b/sol057/contracts/oceanv3/balancer/BMath.sol new file mode 100644 index 0000000000000000000000000000000000000000..711d88c2da5b30f884d487453f4ad252ef184c60 --- /dev/null +++ b/sol057/contracts/oceanv3/balancer/BMath.sol @@ -0,0 +1,283 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity ^0.5.7; + +import './BNum.sol'; + +contract BMath is BConst, BNum { + /********************************************************************************************** + // calcSpotPrice // + // sP = spotPrice // + // bI = tokenBalanceIn ( bI / wI ) 1 // + // bO = tokenBalanceOut sP = ----------- * ---------- // + // wI = tokenWeightIn ( bO / wO ) ( 1 - sF ) // + // wO = tokenWeightOut // + // sF = swapFee // + **********************************************************************************************/ + function calcSpotPrice( + uint tokenBalanceIn, + uint tokenWeightIn, + uint tokenBalanceOut, + uint tokenWeightOut, + uint swapFee + ) + public pure + returns (uint spotPrice) + { + uint numer = bdiv(tokenBalanceIn, tokenWeightIn); + uint denom = bdiv(tokenBalanceOut, tokenWeightOut); + uint ratio = bdiv(numer, denom); + uint scale = bdiv(BONE, bsub(BONE, swapFee)); + return (spotPrice = bmul(ratio, scale)); + } + + /********************************************************************************************** + // calcOutGivenIn // + // aO = tokenAmountOut // + // bO = tokenBalanceOut // + // bI = tokenBalanceIn / / bI \ (wI / wO) \ // + // aI = tokenAmountIn aO = bO * | 1 - | -------------------------- | ^ | // + // wI = tokenWeightIn \ \ ( bI + ( aI * ( 1 - sF )) / / // + // wO = tokenWeightOut // + // sF = swapFee // + **********************************************************************************************/ + function calcOutGivenIn( + uint tokenBalanceIn, + uint tokenWeightIn, + uint tokenBalanceOut, + uint tokenWeightOut, + uint tokenAmountIn, + uint swapFee + ) + public pure + returns (uint tokenAmountOut) + { + uint weightRatio = bdiv(tokenWeightIn, tokenWeightOut); + uint adjustedIn = bsub(BONE, swapFee); + adjustedIn = bmul(tokenAmountIn, adjustedIn); + uint y = bdiv(tokenBalanceIn, badd(tokenBalanceIn, adjustedIn)); + uint foo = bpow(y, weightRatio); + uint bar = bsub(BONE, foo); + tokenAmountOut = bmul(tokenBalanceOut, bar); + return tokenAmountOut; + } + + /********************************************************************************************** + // calcInGivenOut // + // aI = tokenAmountIn // + // bO = tokenBalanceOut / / bO \ (wO / wI) \ // + // bI = tokenBalanceIn bI * | | ------------ | ^ - 1 | // + // aO = tokenAmountOut aI = \ \ ( bO - aO ) / / // + // wI = tokenWeightIn -------------------------------------------- // + // wO = tokenWeightOut ( 1 - sF ) // + // sF = swapFee // + **********************************************************************************************/ + function calcInGivenOut( + uint tokenBalanceIn, + uint tokenWeightIn, + uint tokenBalanceOut, + uint tokenWeightOut, + uint tokenAmountOut, + uint swapFee + ) + public pure + returns (uint tokenAmountIn) + { + uint weightRatio = bdiv(tokenWeightOut, tokenWeightIn); + uint diff = bsub(tokenBalanceOut, tokenAmountOut); + uint y = bdiv(tokenBalanceOut, diff); + uint foo = bpow(y, weightRatio); + foo = bsub(foo, BONE); + tokenAmountIn = bsub(BONE, swapFee); + tokenAmountIn = bdiv(bmul(tokenBalanceIn, foo), tokenAmountIn); + return tokenAmountIn; + } + + /********************************************************************************************** + // calcPoolOutGivenSingleIn // + // pAo = poolAmountOut / \ // + // tAi = tokenAmountIn /// / // wI \ \\ \ wI \ // + // wI = tokenWeightIn //| tAi *| 1 - || 1 - -- | * sF || + tBi \ -- \ // + // tW = totalWeight pAo=|| \ \ \\ tW / // | ^ tW | * pS - pS // + // tBi = tokenBalanceIn \\ ------------------------------------- / / // + // pS = poolSupply \\ tBi / / // + // sF = swapFee \ / // + **********************************************************************************************/ + function calcPoolOutGivenSingleIn( + uint tokenBalanceIn, + uint tokenWeightIn, + uint poolSupply, + uint totalWeight, + uint tokenAmountIn, + uint swapFee + ) + public pure + returns (uint poolAmountOut) + { + // Charge the trading fee for the proportion of tokenAi + /// which is implicitly traded to the other pool tokens. + // That proportion is (1- weightTokenIn) + // tokenAiAfterFee = tAi * (1 - (1-weightTi) * poolFee); + uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); + uint zaz = bmul(bsub(BONE, normalizedWeight), swapFee); + uint tokenAmountInAfterFee = bmul(tokenAmountIn, bsub(BONE, zaz)); + + uint newTokenBalanceIn = badd(tokenBalanceIn, tokenAmountInAfterFee); + uint tokenInRatio = bdiv(newTokenBalanceIn, tokenBalanceIn); + + // uint newPoolSupply = (ratioTi ^ weightTi) * poolSupply; + uint poolRatio = bpow(tokenInRatio, normalizedWeight); + uint newPoolSupply = bmul(poolRatio, poolSupply); + poolAmountOut = bsub(newPoolSupply, poolSupply); + return poolAmountOut; + } + + /********************************************************************************************** + // calcSingleInGivenPoolOut // + // tAi = tokenAmountIn //(pS + pAo)\ / 1 \\ // + // pS = poolSupply || --------- | ^ | --------- || * bI - bI // + // pAo = poolAmountOut \\ pS / \(wI / tW)// // + // bI = balanceIn tAi = -------------------------------------------- // + // wI = weightIn / wI \ // + // tW = totalWeight | 1 - ---- | * sF // + // sF = swapFee \ tW / // + **********************************************************************************************/ + function calcSingleInGivenPoolOut( + uint tokenBalanceIn, + uint tokenWeightIn, + uint poolSupply, + uint totalWeight, + uint poolAmountOut, + uint swapFee + ) + public pure + returns (uint tokenAmountIn) + { + uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); + uint newPoolSupply = badd(poolSupply, poolAmountOut); + uint poolRatio = bdiv(newPoolSupply, poolSupply); + + //uint newBalTi = poolRatio^(1/weightTi) * balTi; + uint boo = bdiv(BONE, normalizedWeight); + uint tokenInRatio = bpow(poolRatio, boo); + uint newTokenBalanceIn = bmul(tokenInRatio, tokenBalanceIn); + uint tokenAmountInAfterFee = bsub(newTokenBalanceIn, tokenBalanceIn); + // Do reverse order of fees charged in joinswap_ExternAmountIn, this way + // ``` pAo == joinswap_ExternAmountIn(Ti, joinswap_PoolAmountOut(pAo, Ti)) ``` + //uint tAi = tAiAfterFee / (1 - (1-weightTi) * swapFee) ; + uint zar = bmul(bsub(BONE, normalizedWeight), swapFee); + tokenAmountIn = bdiv(tokenAmountInAfterFee, bsub(BONE, zar)); + return tokenAmountIn; + } + + /********************************************************************************************** + // calcSingleOutGivenPoolIn // + // tAo = tokenAmountOut / / \\ // + // bO = tokenBalanceOut / // pS - (pAi * (1 - eF)) \ / 1 \ \\ // + // pAi = poolAmountIn | bO - || ----------------------- | ^ | --------- | * b0 || // + // ps = poolSupply \ \\ pS / \(wO / tW)/ // // + // wI = tokenWeightIn tAo = \ \ // // + // tW = totalWeight / / wO \ \ // + // sF = swapFee * | 1 - | 1 - ---- | * sF | // + // eF = exitFee \ \ tW / / // + **********************************************************************************************/ + function calcSingleOutGivenPoolIn( + uint tokenBalanceOut, + uint tokenWeightOut, + uint poolSupply, + uint totalWeight, + uint poolAmountIn, + uint swapFee + ) + public pure + returns (uint tokenAmountOut) + { + uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); + // charge exit fee on the pool token side + // pAiAfterExitFee = pAi*(1-exitFee) + uint poolAmountInAfterExitFee = bmul( + poolAmountIn, + bsub(BONE, EXIT_FEE) + ); + uint newPoolSupply = bsub(poolSupply, poolAmountInAfterExitFee); + uint poolRatio = bdiv(newPoolSupply, poolSupply); + + // newBalTo = poolRatio^(1/weightTo) * balTo; + uint tokenOutRatio = bpow(poolRatio, bdiv(BONE, normalizedWeight)); + uint newTokenBalanceOut = bmul(tokenOutRatio, tokenBalanceOut); + + uint tokenAmountOutBeforeSwapFee = bsub( + tokenBalanceOut, + newTokenBalanceOut + ); + + // charge swap fee on the output token side + //uint tAo = tAoBeforeSwapFee * (1 - (1-weightTo) * swapFee) + uint zaz = bmul(bsub(BONE, normalizedWeight), swapFee); + tokenAmountOut = bmul(tokenAmountOutBeforeSwapFee, bsub(BONE, zaz)); + return tokenAmountOut; + } + + /********************************************************************************************** + // calcPoolInGivenSingleOut // + // pAi = poolAmountIn // / tAo \\ / wO \ \ // + // bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ // + // tAo = tokenAmountOut pS - || \ 1 - ((1 - (tO / tW)) * sF)/ | ^ \ tW / * pS | // + // ps = poolSupply \\ -----------------------------------/ / // + // wO = tokenWeightOut pAi = \\ bO / / // + // tW = totalWeight ------------------------------------------------------------- // + // sF = swapFee ( 1 - eF ) // + // eF = exitFee // + **********************************************************************************************/ + function calcPoolInGivenSingleOut( + uint tokenBalanceOut, + uint tokenWeightOut, + uint poolSupply, + uint totalWeight, + uint tokenAmountOut, + uint swapFee + ) + public pure + returns (uint poolAmountIn) + { + + // charge swap fee on the output token side + uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); + //uint tAoBeforeSwapFee = tAo / (1 - (1-weightTo) * swapFee) ; + uint zoo = bsub(BONE, normalizedWeight); + uint zar = bmul(zoo, swapFee); + uint tokenAmountOutBeforeSwapFee = bdiv( + tokenAmountOut, + bsub(BONE, zar) + ); + + uint newTokenBalanceOut = bsub( + tokenBalanceOut, + tokenAmountOutBeforeSwapFee + ); + uint tokenOutRatio = bdiv(newTokenBalanceOut, tokenBalanceOut); + + //uint newPoolSupply = (ratioTo ^ weightTo) * poolSupply; + uint poolRatio = bpow(tokenOutRatio, normalizedWeight); + uint newPoolSupply = bmul(poolRatio, poolSupply); + uint poolAmountInAfterExitFee = bsub(poolSupply, newPoolSupply); + + // charge exit fee on the pool token side + // pAi = pAiAfterExitFee/(1-exitFee) + poolAmountIn = bdiv(poolAmountInAfterExitFee, bsub(BONE, EXIT_FEE)); + return poolAmountIn; + } + + +} diff --git a/sol057/contracts/oceanv3/balancer/BNum.sol b/sol057/contracts/oceanv3/balancer/BNum.sol new file mode 100644 index 0000000000000000000000000000000000000000..17c831803a82f8f3ecee02f96428f59f9e5b5d6c --- /dev/null +++ b/sol057/contracts/oceanv3/balancer/BNum.sol @@ -0,0 +1,164 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity ^0.5.7; + +import './BConst.sol'; + +contract BNum is BConst { + + function btoi(uint a) + internal pure + returns (uint) + { + return a / BONE; + } + + function bfloor(uint a) + internal pure + returns (uint) + { + return btoi(a) * BONE; + } + + function badd(uint a, uint b) + internal pure + returns (uint) + { + uint c = a + b; + require(c >= a, 'ERR_ADD_OVERFLOW'); + return c; + } + + function bsub(uint a, uint b) + internal pure + returns (uint) + { + (uint c, bool flag) = bsubSign(a, b); + require(!flag, 'ERR_SUB_UNDERFLOW'); + return c; + } + + function bsubSign(uint a, uint b) + internal pure + returns (uint, bool) + { + if (a >= b) { + return (a - b, false); + } else { + return (b - a, true); + } + } + + function bmul(uint a, uint b) + internal pure + returns (uint) + { + uint c0 = a * b; + require(a == 0 || c0 / a == b, 'ERR_MUL_OVERFLOW'); + uint c1 = c0 + (BONE / 2); + require(c1 >= c0, 'ERR_MUL_OVERFLOW'); + uint c2 = c1 / BONE; + return c2; + } + + function bdiv(uint a, uint b) + internal pure + returns (uint) + { + require(b != 0, 'ERR_DIV_ZERO'); + uint c0 = a * BONE; + require(a == 0 || c0 / a == BONE, 'ERR_DIV_INTERNAL'); // bmul overflow + uint c1 = c0 + (b / 2); + require(c1 >= c0, 'ERR_DIV_INTERNAL'); // badd require + uint c2 = c1 / b; + return c2; + } + + // DSMath.wpow + function bpowi(uint a, uint n) + internal pure + returns (uint) + { + uint b = a; + uint z = n % 2 != 0 ? b : BONE; + + for (n /= 2; n != 0; n /= 2) { + b = bmul(b, b); + + if (n % 2 != 0) { + z = bmul(z, b); + } + } + return z; + } + + // Compute b^(e.w) by splitting it into (b^e)*(b^0.w). + // Use `bpowi` for `b^e` and `bpowK` for k iterations + // of approximation of b^0.w + function bpow(uint base, uint exp) + internal pure + returns (uint) + { + require(base >= MIN_BPOW_BASE, 'ERR_BPOW_BASE_TOO_LOW'); + require(base <= MAX_BPOW_BASE, 'ERR_BPOW_BASE_TOO_HIGH'); + + uint whole = bfloor(exp); + uint remain = bsub(exp, whole); + + uint wholePow = bpowi(base, btoi(whole)); + + if (remain == 0) { + return wholePow; + } + + uint partialResult = bpowApprox(base, remain, BPOW_PRECISION); + return bmul(wholePow, partialResult); + } + + function bpowApprox(uint base, uint exp, uint precision) + internal pure + returns (uint) + { + // term 0: + uint a = exp; + (uint x, bool xneg) = bsubSign(base, BONE); + uint term = BONE; + uint sum = term; + bool negative = false; + + + // term(k) = numer / denom + // = (product(a - i - 1, i=1-->k) * x^k) / (k!) + // each iteration, multiply previous term by (a-(k-1)) * x / k + // continue until term is less than precision + for (uint i = 1; term >= precision; i++) { + uint bigK = i * BONE; + (uint c, bool cneg) = bsubSign(a, bsub(bigK, BONE)); + term = bmul(term, bmul(c, x)); + term = bdiv(term, bigK); + if (term == 0) break; + + if (xneg) negative = !negative; + if (cneg) negative = !negative; + if (negative) { + sum = bsub(sum, term); + } else { + sum = badd(sum, term); + } + } + + return sum; + } + +} diff --git a/sol057/contracts/oceanv3/balancer/BPool.sol b/sol057/contracts/oceanv3/balancer/BPool.sol new file mode 100644 index 0000000000000000000000000000000000000000..58102194122cc6b11625d887ba5668fb9bf9a16a --- /dev/null +++ b/sol057/contracts/oceanv3/balancer/BPool.sol @@ -0,0 +1,919 @@ +pragma solidity ^0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import './BToken.sol'; +import './BMath.sol'; + +/** +* @title BPool +* +* @dev Used by the (Ocean version) BFactory contract as a bytecode reference to +* deploy new BPools. +* +* This contract is is nearly identical to the BPool.sol contract at [1] +* The only difference is the "Proxy contract functionality" section +* given below. We'd inherit from BPool if we could, for simplicity. +* But we can't, because the proxy section needs to access private +* variables declared in BPool, and Solidity disallows this. Therefore +* the best we can do for now is clearly demarcate the proxy section. +* +* [1] https://github.com/balancer-labs/balancer-core/contracts/. +*/ +contract BPool is BToken, BMath { + + struct Record { + bool bound; // is token bound to pool + uint index; // private + uint denorm; // denormalized weight + uint balance; + } + + event LOG_SWAP( + address indexed caller, + address indexed tokenIn, + address indexed tokenOut, + uint256 tokenAmountIn, + uint256 tokenAmountOut + ); + + event LOG_JOIN( + address indexed caller, + address indexed tokenIn, + uint256 tokenAmountIn + ); + + event LOG_EXIT( + address indexed caller, + address indexed tokenOut, + uint256 tokenAmountOut + ); + + event LOG_CALL( + bytes4 indexed sig, + address indexed caller, + bytes data + ) anonymous; + + modifier _logs_() { + emit LOG_CALL(msg.sig, msg.sender, msg.data); + _; + } + + modifier _lock_() { + require( + !_mutex, + 'ERR_REENTRY' + ); + _mutex = true; + _; + _mutex = false; + } + + modifier _viewlock_() { + require(!_mutex, 'ERR_REENTRY'); + _; + } + + bool private _mutex; + + address private _factory; // BFactory address to push token exitFee to + address private _controller; // has CONTROL role + bool private _publicSwap; // true if PUBLIC can call SWAP functions + + // `setSwapFee` and `finalize` require CONTROL + // `finalize` sets `PUBLIC can SWAP`, `PUBLIC can JOIN` + uint private _swapFee; + bool private _finalized; + + address[] private _tokens; + mapping(address=>Record) private _records; + uint private _totalWeight; + + //----------------------------------------------------------------------- + //Proxy contract functionality: begin + bool private initialized = false; + modifier onlyNotInitialized() { + require( + !initialized, + 'ERR_ALREADY_INITIALIZED' + ); + _; + } + function isInitialized() external view returns(bool) { + return initialized; + } + + // Called prior to contract deployment + constructor() public { + _initialize(msg.sender, msg.sender, MIN_FEE, false, false); + } + + // Called prior to contract initialization (e.g creating new BPool instance) + // Calls private _initialize function. Only if contract is not initialized. + function initialize( + address controller, + address factory, + uint swapFee, + bool publicSwap, + bool finalized + ) + external + onlyNotInitialized + returns(bool) + { + require( + controller != address(0), + 'ERR_INVALID_CONTROLLER_ADDRESS' + ); + require( + factory != address(0), + 'ERR_INVALID_FACTORY_ADDRESS' + ); + require(swapFee >= MIN_FEE, 'ERR_MIN_FEE'); + require(swapFee <= MAX_FEE, 'ERR_MAX_FEE'); + return _initialize(controller, factory, swapFee, publicSwap, finalized); + } + + // Private function called on contract initialization. + function _initialize( + address controller, + address factory, + uint swapFee, + bool publicSwap, + bool finalized + ) + private + returns(bool) + { + _controller = controller; + _factory = factory; + _swapFee = swapFee; + _publicSwap = publicSwap; + _finalized = finalized; + + initialized = true; + return initialized; + } + + function setup( + address dataTokenAaddress, + uint256 dataTokenAmount, + uint256 dataTokenWeight, + address baseTokenAddress, + uint256 baseTokenAmount, + uint256 baseTokenWeight, + uint256 swapFee + ) + external + _logs_ + { + require( + dataTokenAaddress != address(0), + 'ERR_INVALID_DATATOKEN_ADDRESS' + ); + require( + baseTokenAddress != address(0), + 'ERR_INVALID_BASETOKEN_ADDRESS' + ); + // other inputs will be validated prior + // calling the below functions + // bind data token + bind( + dataTokenAaddress, + dataTokenAmount, + dataTokenWeight + ); + emit LOG_JOIN(msg.sender, dataTokenAaddress, dataTokenAmount); + // bind base token + bind( + baseTokenAddress, + baseTokenAmount, + baseTokenWeight + ); + emit LOG_JOIN(msg.sender, baseTokenAddress, baseTokenAmount); + setSwapFee(swapFee); + // finalize + finalize(); + } + + //Proxy contract functionality: end + //----------------------------------------------------------------------- + + function isPublicSwap() + external view + returns (bool) + { + return _publicSwap; + } + + function isFinalized() + external view + returns (bool) + { + return _finalized; + } + + function isBound(address t) + external view + returns (bool) + { + return _records[t].bound; + } + + function getNumTokens() + external view + returns (uint) + { + return _tokens.length; + } + + function getCurrentTokens() + external view _viewlock_ + returns (address[] memory tokens) + { + return _tokens; + } + + function getFinalTokens() + external view + _viewlock_ + returns (address[] memory tokens) + { + require(_finalized, 'ERR_NOT_FINALIZED'); + return _tokens; + } + + function getDenormalizedWeight(address token) + external view + _viewlock_ + returns (uint) + { + + require(_records[token].bound, 'ERR_NOT_BOUND'); + return _records[token].denorm; + } + + function getTotalDenormalizedWeight() + external view + _viewlock_ + returns (uint) + { + return _totalWeight; + } + + function getNormalizedWeight(address token) + external view + _viewlock_ + returns (uint) + { + + require(_records[token].bound, 'ERR_NOT_BOUND'); + uint denorm = _records[token].denorm; + return bdiv(denorm, _totalWeight); + } + + function getBalance(address token) + external view + _viewlock_ + returns (uint) + { + + require(_records[token].bound, 'ERR_NOT_BOUND'); + return _records[token].balance; + } + + function getSwapFee() + external view + _viewlock_ + returns (uint) + { + return _swapFee; + } + + function getController() + external view + _viewlock_ + returns (address) + { + return _controller; + } + + function setSwapFee(uint swapFee) + public + _logs_ + _lock_ + { + require(!_finalized, 'ERR_IS_FINALIZED'); + require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + require(swapFee >= MIN_FEE, 'ERR_MIN_FEE'); + require(swapFee <= MAX_FEE, 'ERR_MAX_FEE'); + _swapFee = swapFee; + } + + function setController(address manager) + external + _logs_ + _lock_ + { + require( + manager != address(0), + 'ERR_INVALID_MANAGER_ADDRESS' + ); + require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + _controller = manager; + } + + function setPublicSwap(bool public_) + public + _logs_ + _lock_ + { + require(!_finalized, 'ERR_IS_FINALIZED'); + require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + _publicSwap = public_; + } + + function finalize() + public + _logs_ + _lock_ + { + require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + require(!_finalized, 'ERR_IS_FINALIZED'); + require(_tokens.length >= MIN_BOUND_TOKENS, 'ERR_MIN_TOKENS'); + + _finalized = true; + _publicSwap = true; + + _mintPoolShare(INIT_POOL_SUPPLY); + _pushPoolShare(msg.sender, INIT_POOL_SUPPLY); + } + + + function bind(address token, uint balance, uint denorm) + public + _logs_ + // _lock_ Bind does not lock because it jumps to `rebind`, which does + { + require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + require(!_records[token].bound, 'ERR_IS_BOUND'); + require(!_finalized, 'ERR_IS_FINALIZED'); + + require(_tokens.length < MAX_BOUND_TOKENS, 'ERR_MAX_TOKENS'); + + _records[token] = Record({ + bound: true, + index: _tokens.length, + denorm: 0, + // balance and denorm will be validated + balance: 0 // and set by `rebind` + }); + _tokens.push(token); + rebind(token, balance, denorm); + } + + function rebind(address token, uint balance, uint denorm) + public + _logs_ + _lock_ + { + + require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + require(_records[token].bound, 'ERR_NOT_BOUND'); + require(!_finalized, 'ERR_IS_FINALIZED'); + + require(denorm >= MIN_WEIGHT, 'ERR_MIN_WEIGHT'); + require(denorm <= MAX_WEIGHT, 'ERR_MAX_WEIGHT'); + require(balance >= MIN_BALANCE, 'ERR_MIN_BALANCE'); + + // Adjust the denorm and totalWeight + uint oldWeight = _records[token].denorm; + if (denorm > oldWeight) { + _totalWeight = badd(_totalWeight, bsub(denorm, oldWeight)); + require(_totalWeight <= MAX_TOTAL_WEIGHT, 'ERR_MAX_TOTAL_WEIGHT'); + } else if (denorm < oldWeight) { + _totalWeight = bsub(_totalWeight, bsub(oldWeight, denorm)); + } + _records[token].denorm = denorm; + + // Adjust the balance record and actual token balance + uint oldBalance = _records[token].balance; + _records[token].balance = balance; + if (balance > oldBalance) { + _pullUnderlying(token, msg.sender, bsub(balance, oldBalance)); + } else if (balance < oldBalance) { + // In this case liquidity is being withdrawn, so charge EXIT_FEE + uint tokenBalanceWithdrawn = bsub(oldBalance, balance); + uint tokenExitFee = bmul(tokenBalanceWithdrawn, EXIT_FEE); + _pushUnderlying(token, msg.sender, bsub(tokenBalanceWithdrawn, tokenExitFee)); + _pushUnderlying(token, _factory, tokenExitFee); + } + } + + function unbind(address token) + external + _logs_ + _lock_ + { + + require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + require(_records[token].bound, 'ERR_NOT_BOUND'); + require(!_finalized, 'ERR_IS_FINALIZED'); + + uint tokenBalance = _records[token].balance; + uint tokenExitFee = bmul(tokenBalance, EXIT_FEE); + + _totalWeight = bsub(_totalWeight, _records[token].denorm); + + // Swap the token-to-unbind with the last token, + // then delete the last token + uint index = _records[token].index; + uint last = _tokens.length - 1; + _tokens[index] = _tokens[last]; + _records[_tokens[index]].index = index; + _tokens.pop(); + _records[token] = Record({ + bound: false, + index: 0, + denorm: 0, + balance: 0 + }); + + _pushUnderlying(token, msg.sender, bsub(tokenBalance, tokenExitFee)); + _pushUnderlying(token, _factory, tokenExitFee); + } + + // Absorb any tokens that have been sent to this contract into the pool + function gulp(address token) + external + _logs_ + _lock_ + { + require(_records[token].bound, 'ERR_NOT_BOUND'); + _records[token].balance = IERC20(token).balanceOf(address(this)); + } + + function getSpotPrice(address tokenIn, address tokenOut) + external view + _viewlock_ + returns (uint spotPrice) + { + require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + Record storage inRecord = _records[tokenIn]; + Record storage outRecord = _records[tokenOut]; + return calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + _swapFee + ); + } + + function getSpotPriceSansFee(address tokenIn, address tokenOut) + external view + _viewlock_ + returns (uint spotPrice) + { + require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + Record storage inRecord = _records[tokenIn]; + Record storage outRecord = _records[tokenOut]; + return calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + 0 + ); + } + + function joinPool(uint poolAmountOut, uint[] calldata maxAmountsIn) + external + _logs_ + _lock_ + { + require(_finalized, 'ERR_NOT_FINALIZED'); + + uint poolTotal = totalSupply(); + uint ratio = bdiv(poolAmountOut, poolTotal); + require(ratio != 0, 'ERR_MATH_APPROX'); + + for (uint i = 0; i < _tokens.length; i++) { + address t = _tokens[i]; + uint bal = _records[t].balance; + uint tokenAmountIn = bmul(ratio, bal); + require(tokenAmountIn != 0, 'ERR_MATH_APPROX'); + require(tokenAmountIn <= maxAmountsIn[i], 'ERR_LIMIT_IN'); + _records[t].balance = badd(_records[t].balance, tokenAmountIn); + emit LOG_JOIN(msg.sender, t, tokenAmountIn); + _pullUnderlying(t, msg.sender, tokenAmountIn); + } + _mintPoolShare(poolAmountOut); + _pushPoolShare(msg.sender, poolAmountOut); + } + + function exitPool(uint poolAmountIn, uint[] calldata minAmountsOut) + external + _logs_ + _lock_ + { + require(_finalized, 'ERR_NOT_FINALIZED'); + + uint poolTotal = totalSupply(); + uint exitFee = bmul(poolAmountIn, EXIT_FEE); + uint pAiAfterExitFee = bsub(poolAmountIn, exitFee); + uint ratio = bdiv(pAiAfterExitFee, poolTotal); + require(ratio != 0, 'ERR_MATH_APPROX'); + + _pullPoolShare(msg.sender, poolAmountIn); + _pushPoolShare(_factory, exitFee); + _burnPoolShare(pAiAfterExitFee); + + for (uint i = 0; i < _tokens.length; i++) { + address t = _tokens[i]; + uint bal = _records[t].balance; + uint tokenAmountOut = bmul(ratio, bal); + require(tokenAmountOut != 0, 'ERR_MATH_APPROX'); + require(tokenAmountOut >= minAmountsOut[i], 'ERR_LIMIT_OUT'); + _records[t].balance = bsub(_records[t].balance, tokenAmountOut); + emit LOG_EXIT(msg.sender, t, tokenAmountOut); + _pushUnderlying(t, msg.sender, tokenAmountOut); + } + + } + + + function swapExactAmountIn( + address tokenIn, + uint tokenAmountIn, + address tokenOut, + uint minAmountOut, + uint maxPrice + ) + external + _logs_ + _lock_ + returns (uint tokenAmountOut, uint spotPriceAfter) + { + + require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + require(_publicSwap, 'ERR_SWAP_NOT_PUBLIC'); + + Record storage inRecord = _records[address(tokenIn)]; + Record storage outRecord = _records[address(tokenOut)]; + + require( + tokenAmountIn <= bmul(inRecord.balance, MAX_IN_RATIO), + 'ERR_MAX_IN_RATIO' + ); + + uint spotPriceBefore = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + _swapFee + ); + require(spotPriceBefore <= maxPrice, 'ERR_BAD_LIMIT_PRICE'); + + tokenAmountOut = calcOutGivenIn( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + tokenAmountIn, + _swapFee + ); + require(tokenAmountOut >= minAmountOut, 'ERR_LIMIT_OUT'); + + inRecord.balance = badd(inRecord.balance, tokenAmountIn); + outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + + spotPriceAfter = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + _swapFee + ); + require(spotPriceAfter >= spotPriceBefore, 'ERR_MATH_APPROX'); + require(spotPriceAfter <= maxPrice, 'ERR_LIMIT_PRICE'); + require( + spotPriceBefore <= bdiv(tokenAmountIn, tokenAmountOut), + 'ERR_MATH_APPROX' + ); + + emit LOG_SWAP( + msg.sender, + tokenIn, + tokenOut, + tokenAmountIn, + tokenAmountOut + ); + + _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); + _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); + + return (tokenAmountOut, spotPriceAfter); + } + + function swapExactAmountOut( + address tokenIn, + uint maxAmountIn, + address tokenOut, + uint tokenAmountOut, + uint maxPrice + ) + external + _logs_ + _lock_ + returns (uint tokenAmountIn, uint spotPriceAfter) + { + require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + require(_publicSwap, 'ERR_SWAP_NOT_PUBLIC'); + + Record storage inRecord = _records[address(tokenIn)]; + Record storage outRecord = _records[address(tokenOut)]; + + require( + tokenAmountOut <= bmul(outRecord.balance, MAX_OUT_RATIO), + 'ERR_MAX_OUT_RATIO' + ); + + uint spotPriceBefore = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + _swapFee + ); + + require(spotPriceBefore <= maxPrice, 'ERR_BAD_LIMIT_PRICE'); + + tokenAmountIn = calcInGivenOut( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + tokenAmountOut, + _swapFee + ); + require(tokenAmountIn <= maxAmountIn, 'ERR_LIMIT_IN'); + + inRecord.balance = badd(inRecord.balance, tokenAmountIn); + outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + + spotPriceAfter = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + _swapFee + ); + require(spotPriceAfter >= spotPriceBefore, 'ERR_MATH_APPROX'); + require(spotPriceAfter <= maxPrice, 'ERR_LIMIT_PRICE'); + require( + spotPriceBefore <= bdiv(tokenAmountIn, tokenAmountOut), + 'ERR_MATH_APPROX' + ); + + emit LOG_SWAP( + msg.sender, + tokenIn, + tokenOut, + tokenAmountIn, + tokenAmountOut + ); + + _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); + _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); + + return (tokenAmountIn, spotPriceAfter); + } + + + function joinswapExternAmountIn( + address tokenIn, + uint tokenAmountIn, + uint minPoolAmountOut + ) + external + _logs_ + _lock_ + returns (uint poolAmountOut) + + { + require(_finalized, 'ERR_NOT_FINALIZED'); + require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + require( + tokenAmountIn <= bmul(_records[tokenIn].balance, MAX_IN_RATIO), + 'ERR_MAX_IN_RATIO' + ); + + Record storage inRecord = _records[tokenIn]; + + poolAmountOut = calcPoolOutGivenSingleIn( + inRecord.balance, + inRecord.denorm, + _totalSupply, + _totalWeight, + tokenAmountIn, + _swapFee + ); + + require(poolAmountOut >= minPoolAmountOut, 'ERR_LIMIT_OUT'); + + inRecord.balance = badd(inRecord.balance, tokenAmountIn); + + emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); + + _mintPoolShare(poolAmountOut); + _pushPoolShare(msg.sender, poolAmountOut); + _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); + + return poolAmountOut; + } + + function joinswapPoolAmountOut( + address tokenIn, + uint poolAmountOut, + uint maxAmountIn + ) + external + _logs_ + _lock_ + returns (uint tokenAmountIn) + { + require(_finalized, 'ERR_NOT_FINALIZED'); + require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + + Record storage inRecord = _records[tokenIn]; + + tokenAmountIn = calcSingleInGivenPoolOut( + inRecord.balance, + inRecord.denorm, + _totalSupply, + _totalWeight, + poolAmountOut, + _swapFee + ); + + require(tokenAmountIn != 0, 'ERR_MATH_APPROX'); + require(tokenAmountIn <= maxAmountIn, 'ERR_LIMIT_IN'); + + require( + tokenAmountIn <= bmul(_records[tokenIn].balance, MAX_IN_RATIO), + 'ERR_MAX_IN_RATIO' + ); + + inRecord.balance = badd(inRecord.balance, tokenAmountIn); + + emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); + + _mintPoolShare(poolAmountOut); + _pushPoolShare(msg.sender, poolAmountOut); + _pullUnderlying(tokenIn, msg.sender, tokenAmountIn); + + return tokenAmountIn; + } + + function exitswapPoolAmountIn( + address tokenOut, + uint poolAmountIn, + uint minAmountOut + ) + external + _logs_ + _lock_ + returns (uint tokenAmountOut) + { + require(_finalized, 'ERR_NOT_FINALIZED'); + require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + + Record storage outRecord = _records[tokenOut]; + + tokenAmountOut = calcSingleOutGivenPoolIn( + outRecord.balance, + outRecord.denorm, + _totalSupply, + _totalWeight, + poolAmountIn, + _swapFee + ); + + require(tokenAmountOut >= minAmountOut, 'ERR_LIMIT_OUT'); + + require( + tokenAmountOut <= bmul(_records[tokenOut].balance, MAX_OUT_RATIO), + 'ERR_MAX_OUT_RATIO' + ); + + outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + + uint exitFee = bmul(poolAmountIn, EXIT_FEE); + + emit LOG_EXIT(msg.sender, tokenOut, tokenAmountOut); + + _pullPoolShare(msg.sender, poolAmountIn); + _burnPoolShare(bsub(poolAmountIn, exitFee)); + _pushPoolShare(_factory, exitFee); + _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); + + return tokenAmountOut; + } + + function exitswapExternAmountOut( + address tokenOut, + uint tokenAmountOut, + uint maxPoolAmountIn + ) + external + _logs_ + _lock_ + returns (uint poolAmountIn) + { + require(_finalized, 'ERR_NOT_FINALIZED'); + require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + require( + tokenAmountOut <= bmul(_records[tokenOut].balance, MAX_OUT_RATIO), + 'ERR_MAX_OUT_RATIO' + ); + + Record storage outRecord = _records[tokenOut]; + + poolAmountIn = calcPoolInGivenSingleOut( + outRecord.balance, + outRecord.denorm, + _totalSupply, + _totalWeight, + tokenAmountOut, + _swapFee + ); + + require(poolAmountIn != 0, 'ERR_MATH_APPROX'); + require(poolAmountIn <= maxPoolAmountIn, 'ERR_LIMIT_IN'); + + outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + + uint exitFee = bmul(poolAmountIn, EXIT_FEE); + + emit LOG_EXIT(msg.sender, tokenOut, tokenAmountOut); + + _pullPoolShare(msg.sender, poolAmountIn); + _burnPoolShare(bsub(poolAmountIn, exitFee)); + _pushPoolShare(_factory, exitFee); + _pushUnderlying(tokenOut, msg.sender, tokenAmountOut); + + return poolAmountIn; + } + + + // == + // 'Underlying' token-manipulation functions make external calls but are NOT locked + // You must `_lock_` or otherwise ensure reentry-safety + + function _pullUnderlying(address erc20, address from, uint amount) + internal + { + bool xfer = IERC20(erc20).transferFrom(from, address(this), amount); + require(xfer, 'ERR_ERC20_FALSE'); + } + + function _pushUnderlying(address erc20, address to, uint amount) + internal + { + bool xfer = IERC20(erc20).transfer(to, amount); + require(xfer, 'ERR_ERC20_FALSE'); + } + + function _pullPoolShare(address from, uint amount) + internal + { + _pull(from, amount); + } + + function _pushPoolShare(address to, uint amount) + internal + { + _push(to, amount); + } + + function _mintPoolShare(uint amount) + internal + { + _mint(amount); + } + + function _burnPoolShare(uint amount) + internal + { + _burn(amount); + } + +} diff --git a/sol057/contracts/oceanv3/balancer/BToken.sol b/sol057/contracts/oceanv3/balancer/BToken.sol new file mode 100644 index 0000000000000000000000000000000000000000..021204458a0840b0e19e82d7cb3d0e886c58e37f --- /dev/null +++ b/sol057/contracts/oceanv3/balancer/BToken.sol @@ -0,0 +1,154 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity ^0.5.7; + +import './BNum.sol'; +import 'OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/token/ERC20/IERC20.sol'; + +// Highly opinionated token implementation + +// interface IERC20 { +// event Approval(address indexed src, address indexed dst, uint amt); +// event Transfer(address indexed src, address indexed dst, uint amt); + +// function totalSupply() external view returns (uint); +// function balanceOf(address whom) external view returns (uint); +// function allowance(address src, address dst) external view returns (uint); + +// function approve(address dst, uint amt) external returns (bool); +// function transfer(address dst, uint amt) external returns (bool); +// function transferFrom( +// address src, address dst, uint amt +// ) external returns (bool); +// } + +contract BTokenBase is BNum { + + mapping(address => uint) internal _balance; + mapping(address => mapping(address=>uint)) internal _allowance; + uint internal _totalSupply; + + event Approval(address indexed src, address indexed dst, uint amt); + event Transfer(address indexed src, address indexed dst, uint amt); + + function _mint(uint amt) internal { + _balance[address(this)] = badd(_balance[address(this)], amt); + _totalSupply = badd(_totalSupply, amt); + emit Transfer(address(0), address(this), amt); + } + + function _burn(uint amt) internal { + require( + _balance[address(this)] >= amt, + 'ERR_INSUFFICIENT_BAL' + ); + _balance[address(this)] = bsub(_balance[address(this)], amt); + _totalSupply = bsub(_totalSupply, amt); + emit Transfer(address(this), address(0), amt); + } + + function _move(address src, address dst, uint amt) internal { + require(_balance[src] >= amt, 'ERR_INSUFFICIENT_BAL'); + _balance[src] = bsub(_balance[src], amt); + _balance[dst] = badd(_balance[dst], amt); + emit Transfer(src, dst, amt); + } + + function _push(address to, uint amt) internal { + _move(address(this), to, amt); + } + + function _pull(address from, uint amt) internal { + _move(from, address(this), amt); + } +} + +contract BToken is BTokenBase, IERC20 { + + string private _name = 'Balancer Pool Token'; + string private _symbol = 'BPT'; + uint8 private _decimals = 18; + + function name() public view returns (string memory) { + return _name; + } + + function symbol() public view returns (string memory) { + return _symbol; + } + + function decimals() public view returns(uint8) { + return _decimals; + } + + function allowance(address src, address dst) external view returns (uint) { + return _allowance[src][dst]; + } + + function balanceOf(address whom) external view returns (uint) { + return _balance[whom]; + } + + function totalSupply() public view returns (uint) { + return _totalSupply; + } + + function approve(address dst, uint amt) external returns (bool) { + _allowance[msg.sender][dst] = amt; + emit Approval(msg.sender, dst, amt); + return true; + } + + function increaseApproval(address dst, uint amt) external returns (bool) { + _allowance[msg.sender][dst] = badd(_allowance[msg.sender][dst], amt); + emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); + return true; + } + + function decreaseApproval(address dst, uint amt) external returns (bool) { + uint oldValue = _allowance[msg.sender][dst]; + if (amt > oldValue) { + _allowance[msg.sender][dst] = 0; + } else { + _allowance[msg.sender][dst] = bsub(oldValue, amt); + } + emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); + return true; + } + + function transfer(address dst, uint amt) external returns (bool) { + _move(msg.sender, dst, amt); + return true; + } + + function transferFrom( + address src, + address dst, + uint amt + ) + external + returns (bool) + { + require( + msg.sender == src || amt <= _allowance[src][msg.sender], + 'ERR_BTOKEN_BAD_CALLER' + ); + _move(src, dst, amt); + if (msg.sender != src && _allowance[src][msg.sender] != uint256(-1)) { + _allowance[src][msg.sender] = bsub(_allowance[src][msg.sender], amt); + emit Approval(msg.sender, dst, _allowance[src][msg.sender]); + } + return true; + } +} diff --git a/sol057/contracts/oceanv3/communityFee/OPFCommunityFeeCollector.sol b/sol057/contracts/oceanv3/communityFee/OPFCommunityFeeCollector.sol new file mode 100644 index 0000000000000000000000000000000000000000..7252d6d72df9aa43b16bac14addba29c25b7ffa4 --- /dev/null +++ b/sol057/contracts/oceanv3/communityFee/OPFCommunityFeeCollector.sol @@ -0,0 +1,99 @@ +pragma solidity ^0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 +import '../interfaces/IERC20Template.sol'; +import 'OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/ownership/Ownable.sol'; + + +/** + * @title OPFCommunityFeeCollector + * @dev Ocean Protocol Foundation Community Fee Collector contract + * allows consumers to pay very small fee as part of the exchange of + * data tokens with ocean token in order to support the community of + * ocean protocol and provide a sustainble development. + */ +contract OPFCommunityFeeCollector is Ownable { + address payable private collector; + /** + * @dev constructor + * Called prior contract deployment. set the controller address and + * the contract owner address + * @param newCollector the fee collector address. + * @param OPFOwnerAddress the contract owner address + */ + constructor( + address payable newCollector, + address OPFOwnerAddress + ) + public + Ownable() + { + require( + newCollector != address(0)&& + OPFOwnerAddress != address(0), + 'OPFCommunityFeeCollector: collector address or owner is invalid address' + ); + collector = newCollector; + transferOwnership(OPFOwnerAddress); + } + /** + * @dev fallback function + * this is a default fallback function in which receives + * the collected ether. + */ + function() external payable {} + + /** + * @dev withdrawETH + * transfers all the accumlated ether the collector address + */ + function withdrawETH() + external + payable + { + collector.transfer(address(this).balance); + } + + /** + * @dev withdrawToken + * transfers all the accumlated tokens the collector address + * @param tokenAddress the token contract address + */ + function withdrawToken( + address tokenAddress + ) + external + { + require( + tokenAddress != address(0), + 'OPFCommunityFeeCollector: invalid token contract address' + ); + + require ( + IERC20Template(tokenAddress).transfer( + collector, + IERC20Template(tokenAddress).balanceOf(address(this)) + ), + 'OPFCommunityFeeCollector: failed to withdraw tokens' + ); + } + + /** + * @dev changeCollector + * change the current collector address. Only owner can do that. + * @param newCollector the new collector address + */ + function changeCollector( + address payable newCollector + ) + external + onlyOwner + { + require( + newCollector != address(0), + 'OPFCommunityFeeCollector: invalid collector address' + ); + collector = newCollector; + } +} diff --git a/sol057/contracts/oceanv3/dispenser/Dispenser.sol b/sol057/contracts/oceanv3/dispenser/Dispenser.sol new file mode 100644 index 0000000000000000000000000000000000000000..65cb353375984315f8199d5d7bdd1406f43a62e7 --- /dev/null +++ b/sol057/contracts/oceanv3/dispenser/Dispenser.sol @@ -0,0 +1,243 @@ +pragma solidity 0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import '../interfaces/IERC20Template.sol'; + + + +contract Dispenser { + struct DataToken { + bool active; // if the dispenser is active for this datatoken + address owner; // owner of this dispenser + bool minterApproved; // if the dispenser is a minter for this datatoken + uint256 maxTokens; // max tokens to dispense + uint256 maxBalance; // max balance of requester. + //If the balance is higher, the dispense is rejected + } + mapping(address => DataToken) datatokens; + address[] public datatokensList; + constructor() public {} + + event Activated( // emited when a dispenser is activated + address indexed datatokenAddress + ); + + event Deactivated( // emited when a dispenser is deactivated + address indexed datatokenAddress + ); + + event AcceptedMinter( + // emited when a dispenser becomes minter of the datatoken + address indexed datatokenAddress + ); + + event RemovedMinter( + // emited when a dispenser if removed as minter of the datatoken + address indexed datatokenAddress + ); + + event TokensDispensed( + // emited when tokens are dispended + address indexed datatokenAddress, + address indexed userAddress, + uint256 amount + ); + + event OwnerWithdrawed( + address indexed datatoken, + address indexed owner, + uint256 amount + ); + + /** + * @dev status + * Get information about a datatoken dispenser + * @param datatoken refers to datatoken address. + * @return active - if the dispenser is active for this datatoken + * @return owner - owner of this dispenser + * @return minterApproved - if the dispenser is a minter for this datatoken + * @return isTrueMinter - check the datatoken contract if this contract is really a minter + * @return maxTokens - max tokens to dispense + * @return maxBalance - max balance of requester. If the balance is higher, the dispense is rejected + * @return balance - internal balance of the contract (if any) + */ + function status(address datatoken) + external view + returns(bool active,address owner,bool minterApproved, + bool isTrueMinter,uint256 maxTokens,uint256 maxBalance, uint256 balance){ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + active = datatokens[datatoken].active; + owner = datatokens[datatoken].owner; + minterApproved = datatokens[datatoken].minterApproved; + maxTokens = datatokens[datatoken].maxTokens; + maxBalance = datatokens[datatoken].maxBalance; + IERC20Template tokenInstance = IERC20Template(datatoken); + balance = tokenInstance.balanceOf(address(this)); + isTrueMinter = tokenInstance.isMinter(address(this)); + } + + /** + * @dev activate + * Activate a new dispenser + * @param datatoken refers to datatoken address. + * @param maxTokens - max tokens to dispense + * @param maxBalance - max balance of requester. + */ + function activate(address datatoken,uint256 maxTokens, uint256 maxBalance) + external { + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == address(0) || datatokens[datatoken].owner == msg.sender, + 'DataToken already activated' + ); + IERC20Template tokenInstance = IERC20Template(datatoken); + require( + tokenInstance.isMinter(msg.sender), + 'Sender does not have the minter role' + ); + datatokens[datatoken].active = true; + datatokens[datatoken].owner = msg.sender; + datatokens[datatoken].maxTokens = maxTokens; + datatokens[datatoken].maxBalance = maxBalance; + datatokens[datatoken].minterApproved = false; + datatokensList.push(datatoken); + emit Activated(datatoken); + } + + /** + * @dev deactivate + * Deactivate an existing dispenser + * @param datatoken refers to datatoken address. + */ + function deactivate(address datatoken) external{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == msg.sender, + 'DataToken already activated' + ); + datatokens[datatoken].active = false; + emit Deactivated(datatoken); + } + + /** + * @dev acceptMinter + * Accepts Minter role (existing datatoken minter has to call datatoken.proposeMinter(dispenserAddress) first) + * @param datatoken refers to datatoken address. + */ + function acceptMinter(address datatoken) external{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + IERC20Template tokenInstance = IERC20Template(datatoken); + tokenInstance.approveMinter(); + require( + tokenInstance.isMinter(address(this)), + 'ERR: Cannot accept minter role' + ); + datatokens[datatoken].minterApproved = true; + emit AcceptedMinter(datatoken); + } + /** + * @dev removeMinter + * Removes Minter role and proposes the owner as a new minter (the owner has to call approveMinter after this) + * @param datatoken refers to datatoken address. + */ + function removeMinter(address datatoken) external{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == msg.sender, + 'DataToken already activated' + ); + IERC20Template tokenInstance = IERC20Template(datatoken); + require( + tokenInstance.isMinter(address(this)), + 'ERR: Cannot accept minter role' + ); + tokenInstance.proposeMinter(datatokens[datatoken].owner); + datatokens[datatoken].minterApproved = false; + emit RemovedMinter(datatoken); + } + + /** + * @dev dispense + * Dispense datatokens to caller. The dispenser must be active, hold enough DT (or be able to mint more) and respect maxTokens/maxBalance requirements + * @param datatoken refers to datatoken address. + * @param datatoken amount of datatokens required. + */ + function dispense(address datatoken, uint256 amount) external payable{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].active == true, + 'Dispenser not active' + ); + require( + amount > 0, + 'Invalid zero amount' + ); + require( + datatokens[datatoken].maxTokens >= amount, + 'Amount too high' + ); + IERC20Template tokenInstance = IERC20Template(datatoken); + uint256 callerBalance = tokenInstance.balanceOf(msg.sender); + require( + callerBalance<datatokens[datatoken].maxBalance, + 'Caller balance too high' + ); + uint256 ourBalance = tokenInstance.balanceOf(address(this)); + if(ourBalance<amount && tokenInstance.isMinter(address(this))){ + //we need to mint the difference if we can + tokenInstance.mint(address(this),amount - ourBalance); + ourBalance = tokenInstance.balanceOf(address(this)); + } + require( + ourBalance>=amount, + 'Not enough reserves' + ); + tokenInstance.transfer(msg.sender,amount); + emit TokensDispensed(datatoken, msg.sender, amount); + } + + /** + * @dev ownerWithdraw + * Allow owner to withdraw all datatokens in this dispenser balance + * @param datatoken refers to datatoken address. + */ + function ownerWithdraw(address datatoken) external{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == msg.sender, + 'Invalid owner' + ); + IERC20Template tokenInstance = IERC20Template(datatoken); + uint256 ourBalance = tokenInstance.balanceOf(address(this)); + if(ourBalance>0){ + tokenInstance.transfer(msg.sender,ourBalance); + emit OwnerWithdrawed(datatoken, msg.sender, ourBalance); + } + } + function() external payable { + //thank you for your donation + } +} \ No newline at end of file diff --git a/sol057/contracts/oceanv3/fixedRate/FixedRateExchange.sol b/sol057/contracts/oceanv3/fixedRate/FixedRateExchange.sol new file mode 100644 index 0000000000000000000000000000000000000000..527dbf762031aa53957947a878e663cc1fcdc9bc --- /dev/null +++ b/sol057/contracts/oceanv3/fixedRate/FixedRateExchange.sol @@ -0,0 +1,405 @@ +pragma solidity ^0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) + +import '../interfaces/IERC20Template.sol'; +import 'OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/math/SafeMath.sol'; + +/** + * @title FixedRateExchange + * @dev FixedRateExchange is a fixed rate exchange Contract + * Marketplaces uses this contract to allow consumers + * exchanging datatokens with ocean token using a fixed + * exchange rate. + */ +contract FixedRateExchange { + using SafeMath for uint256; + uint256 private constant BASE = 10 ** 18; + struct Exchange { + bool active; + address exchangeOwner; + address dataToken; + address baseToken; + uint256 fixedRate; + } + + // maps an exchangeId to an exchange + mapping(bytes32 => Exchange) private exchanges; + bytes32[] private exchangeIds; + + modifier onlyActiveExchange( + bytes32 exchangeId + ) + { + require( + exchanges[exchangeId].fixedRate != 0 && + exchanges[exchangeId].active == true, + 'FixedRateExchange: Exchange does not exist!' + ); + _; + } + + modifier onlyExchangeOwner( + bytes32 exchangeId + ) + { + require( + exchanges[exchangeId].exchangeOwner == msg.sender, + 'FixedRateExchange: invalid exchange owner' + ); + _; + } + + event ExchangeCreated( + bytes32 indexed exchangeId, + address indexed baseToken, + address indexed dataToken, + address exchangeOwner, + uint256 fixedRate + ); + + event ExchangeRateChanged( + bytes32 indexed exchangeId, + address indexed exchangeOwner, + uint256 newRate + ); + + event ExchangeActivated( + bytes32 indexed exchangeId, + address indexed exchangeOwner + ); + + event ExchangeDeactivated( + bytes32 indexed exchangeId, + address indexed exchangeOwner + ); + + event Swapped( + bytes32 indexed exchangeId, + address indexed by, + uint256 baseTokenSwappedAmount, + uint256 dataTokenSwappedAmount + ); + + /** + * @dev create + * creates new exchange pairs between base token + * (ocean token) and data tokens. + * @param baseToken refers to a ocean token contract address + * @param dataToken refers to a data token contract address + * @param fixedRate refers to the exact fixed exchange rate in wei + */ + function create( + address baseToken, + address dataToken, + uint256 fixedRate + ) + external + { + require( + baseToken != address(0), + 'FixedRateExchange: Invalid basetoken, zero address' + ); + require( + dataToken != address(0), + 'FixedRateExchange: Invalid datatoken, zero address' + ); + require( + baseToken != dataToken, + 'FixedRateExchange: Invalid datatoken, equals basetoken' + ); + require( + fixedRate != 0, + 'FixedRateExchange: Invalid exchange rate value' + ); + bytes32 exchangeId = generateExchangeId( + baseToken, + dataToken, + msg.sender + ); + require( + exchanges[exchangeId].fixedRate == 0, + 'FixedRateExchange: Exchange already exists!' + ); + exchanges[exchangeId] = Exchange({ + active: true, + exchangeOwner: msg.sender, + dataToken: dataToken, + baseToken: baseToken, + fixedRate: fixedRate + }); + exchangeIds.push(exchangeId); + + emit ExchangeCreated( + exchangeId, + baseToken, + dataToken, + msg.sender, + fixedRate + ); + + emit ExchangeActivated( + exchangeId, + msg.sender + ); + } + + /** + * @dev generateExchangeId + * creates unique exchange identifier for two token pairs. + * @param baseToken refers to a ocean token contract address + * @param dataToken refers to a data token contract address + * @param exchangeOwner exchange owner address + */ + function generateExchangeId( + address baseToken, + address dataToken, + address exchangeOwner + ) + public + pure + returns (bytes32) + { + return keccak256( + abi.encode( + baseToken, + dataToken, + exchangeOwner + ) + ); + } + + /** + * @dev CalcInGivenOut + * Calculates how many basetokens are needed to get specifyed amount of datatokens + * @param exchangeId a unique exchange idnetifier + * @param dataTokenAmount the amount of data tokens to be exchanged + */ + function CalcInGivenOut( + bytes32 exchangeId, + uint256 dataTokenAmount + ) + public + view + onlyActiveExchange( + exchangeId + ) + returns (uint256 baseTokenAmount) + { + baseTokenAmount = dataTokenAmount.mul( + exchanges[exchangeId].fixedRate).div(BASE); + } + + /** + * @dev swap + * atomic swap between two registered fixed rate exchange. + * @param exchangeId a unique exchange idnetifier + * @param dataTokenAmount the amount of data tokens to be exchanged + */ + function swap( + bytes32 exchangeId, + uint256 dataTokenAmount + ) + external + onlyActiveExchange( + exchangeId + ) + { + require( + dataTokenAmount != 0, + 'FixedRateExchange: zero data token amount' + ); + uint256 baseTokenAmount = CalcInGivenOut(exchangeId,dataTokenAmount); + require( + IERC20Template(exchanges[exchangeId].baseToken).transferFrom( + msg.sender, + exchanges[exchangeId].exchangeOwner, + baseTokenAmount + ), + 'FixedRateExchange: transferFrom failed in the baseToken contract' + ); + require( + IERC20Template(exchanges[exchangeId].dataToken).transferFrom( + exchanges[exchangeId].exchangeOwner, + msg.sender, + dataTokenAmount + ), + 'FixedRateExchange: transferFrom failed in the dataToken contract' + ); + + emit Swapped( + exchangeId, + msg.sender, + baseTokenAmount, + dataTokenAmount + ); + } + + /** + * @dev getNumberOfExchanges + * gets the total number of registered exchanges + * @return total number of registered exchange IDs + */ + function getNumberOfExchanges() + external + view + returns (uint256) + { + return exchangeIds.length; + } + + /** + * @dev setRate + * changes the fixed rate for an exchange with a new rate + * @param exchangeId a unique exchange idnetifier + * @param newRate new fixed rate value + */ + function setRate( + bytes32 exchangeId, + uint256 newRate + ) + external + onlyExchangeOwner(exchangeId) + { + require( + newRate != 0, + 'FixedRateExchange: Ratio must be >0' + ); + + exchanges[exchangeId].fixedRate = newRate; + emit ExchangeRateChanged( + exchangeId, + msg.sender, + newRate + ); + } + + /** + * @dev toggleExchangeState + * toggles the active state of an existing exchange + * @param exchangeId a unique exchange idnetifier + */ + function toggleExchangeState( + bytes32 exchangeId + ) + external + onlyExchangeOwner(exchangeId) + { + if(exchanges[exchangeId].active){ + exchanges[exchangeId].active = false; + emit ExchangeDeactivated( + exchangeId, + msg.sender + ); + } else { + exchanges[exchangeId].active = true; + emit ExchangeActivated( + exchangeId, + msg.sender + ); + } + } + + /** + * @dev getRate + * gets the current fixed rate for an exchange + * @param exchangeId a unique exchange idnetifier + * @return fixed rate value + */ + function getRate( + bytes32 exchangeId + ) + external + view + returns(uint256) + { + return exchanges[exchangeId].fixedRate; + } + + /** + * @dev getSupply + * gets the current supply of datatokens in an fixed + * rate exchagne + * @param exchangeId the exchange ID + * @return supply + */ + function getSupply(bytes32 exchangeId) + public + view + returns (uint256 supply) + { + if(exchanges[exchangeId].active == false) + supply = 0; + else { + uint256 balance = IERC20Template(exchanges[exchangeId].dataToken) + .balanceOf(exchanges[exchangeId].exchangeOwner); + uint256 allowance = IERC20Template(exchanges[exchangeId].dataToken) + .allowance(exchanges[exchangeId].exchangeOwner, address(this)); + if(balance < allowance) + supply = balance; + else + supply = allowance; + } + } + + /** + * @dev getExchange + * gets all the exchange details + * @param exchangeId a unique exchange idnetifier + * @return all the exchange details including the exchange Owner + * the dataToken contract address, the base token address, the + * fixed rate, whether the exchange is active and the supply or the + * the current data token liquidity. + */ + function getExchange( + bytes32 exchangeId + ) + external + view + returns ( + address exchangeOwner, + address dataToken, + address baseToken, + uint256 fixedRate, + bool active, + uint256 supply + ) + { + Exchange memory exchange = exchanges[exchangeId]; + exchangeOwner = exchange.exchangeOwner; + dataToken = exchange.dataToken; + baseToken = exchange.baseToken; + fixedRate = exchange.fixedRate; + active = exchange.active; + supply = getSupply(exchangeId); + } + + /** + * @dev getExchanges + * gets all the exchanges list + * @return a list of all registered exchange Ids + */ + function getExchanges() + external + view + returns (bytes32[] memory) + { + return exchangeIds; + } + + /** + * @dev isActive + * checks whether exchange is active + * @param exchangeId a unique exchange idnetifier + * @return true if exchange is true, otherwise returns false + */ + function isActive( + bytes32 exchangeId + ) + external + view + returns (bool) + { + return exchanges[exchangeId].active; + } +} diff --git a/sol057/contracts/oceanv3/interfaces/IERC20Template.sol b/sol057/contracts/oceanv3/interfaces/IERC20Template.sol new file mode 100644 index 0000000000000000000000000000000000000000..e588b697d16c0c733878299f5d7467306fe613c2 --- /dev/null +++ b/sol057/contracts/oceanv3/interfaces/IERC20Template.sol @@ -0,0 +1,35 @@ +pragma solidity >=0.5.0; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) + +interface IERC20Template { + function initialize( + string calldata name, + string calldata symbol, + address minter, + uint256 cap, + string calldata blob, + address collector + ) external returns (bool); + + function mint(address account, uint256 value) external; + function minter() external view returns(address); + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function cap() external view returns (uint256); + function isMinter(address account) external view returns (bool); + function isInitialized() external view returns (bool); + function allowance(address owner, address spender) + external + view + returns (uint256); + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function proposeMinter(address newMinter) external; + function approveMinter() external; +} diff --git a/sol057/contracts/oceanv3/metadata/Metadata.sol b/sol057/contracts/oceanv3/metadata/Metadata.sol new file mode 100644 index 0000000000000000000000000000000000000000..d6636e8b19cc7acf7a079b96a38c860712cc3837 --- /dev/null +++ b/sol057/contracts/oceanv3/metadata/Metadata.sol @@ -0,0 +1,87 @@ +pragma solidity 0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import '../interfaces/IERC20Template.sol'; + + +/** +* @title Metadata +* +* @dev Metadata stands for Decentralized Document. It allows publishers +* to publish their dataset metadata in decentralized way. +* It follows the Ocean DID Document standard: +* https://github.com/oceanprotocol/OEPs/blob/master/7/v0.2/README.md +*/ +contract Metadata { + + event MetadataCreated( + address indexed dataToken, + address indexed createdBy, + bytes flags, + bytes data + ); + event MetadataUpdated( + address indexed dataToken, + address indexed updatedBy, + bytes flags, + bytes data + ); + + modifier onlyDataTokenMinter(address dataToken) + { + IERC20Template token = IERC20Template(dataToken); + require( + token.minter() == msg.sender, + 'Metadata: Invalid DataToken Minter' + ); + _; + } + + /** + * @dev create + * creates/publishes new metadata/DDO document on-chain. + * @param dataToken refers to data token address + * @param flags special flags associated with metadata + * @param data referes to the actual metadata + */ + function create( + address dataToken, + bytes calldata flags, + bytes calldata data + ) + external + onlyDataTokenMinter(dataToken) + { + emit MetadataCreated( + dataToken, + msg.sender, + flags, + data + ); + } + + /** + * @dev update + * allows only datatoken minter(s) to update the DDO/metadata content + * @param dataToken refers to data token address + * @param flags special flags associated with metadata + * @param data referes to the actual metadata + */ + function update( + address dataToken, + bytes calldata flags, + bytes calldata data + ) + external + onlyDataTokenMinter(dataToken) + { + emit MetadataUpdated( + dataToken, + msg.sender, + flags, + data + ); + } +} \ No newline at end of file diff --git a/sol057/contracts/oceanv3/oceanv3util.py b/sol057/contracts/oceanv3/oceanv3util.py new file mode 100644 index 0000000000000000000000000000000000000000..02cf4a50f4d6438a2be138b689fce32b8d3cf47c --- /dev/null +++ b/sol057/contracts/oceanv3/oceanv3util.py @@ -0,0 +1,97 @@ +import brownie +from enforce_typing import enforce_types + +from util.base18 import toBase18 +from util.constants import BROWNIE_PROJECT057, GOD_ACCOUNT +from util.tx import txdict + +GOD_ADDRESS = GOD_ACCOUNT.address + +# =============================================================== +# datatokens: template, factory, creation +@enforce_types +def templateDatatoken(): + return BROWNIE_PROJECT057.DataTokenTemplate.deploy( + "TT", + "TemplateToken", + GOD_ADDRESS, + toBase18(1e3), + "blob", + GOD_ADDRESS, + txdict(GOD_ACCOUNT), + ) + + +_DTFACTORY = None + + +@enforce_types +def DTFactory(): + global _DTFACTORY # pylint: disable=global-statement + try: + dt = templateDatatoken() + factory = _DTFACTORY # may trigger failure + if factory is not None: + x = factory.address # "" #pylint: disable=unused-variable + except brownie.exceptions.ContractNotFound: + factory = None + if factory is None: + factory = _DTFACTORY = BROWNIE_PROJECT057.DTFactory.deploy( + dt.address, GOD_ACCOUNT, txdict(GOD_ACCOUNT) + ) + return factory + + +@enforce_types +def dtAddressFromCreateTokenTx(tx): + return tx.events["TokenCreated"]["newTokenAddress"] + + +@enforce_types +def newDatatoken(blob: str, name: str, symbol: str, cap: int, account): + f = DTFactory() + tx = f.createToken(blob, name, symbol, cap, txdict(account)) + dt_address = dtAddressFromCreateTokenTx(tx) + dt = BROWNIE_PROJECT057.DataTokenTemplate.at(dt_address) + return dt + + +# =============================================================== +# pools: template, factory, creation +@enforce_types +def templatePool(): + return BROWNIE_PROJECT057.BPool.deploy(txdict(GOD_ACCOUNT)) + + +_BFACTORY = None + + +@enforce_types +def BFactory(): + global _BFACTORY # pylint: disable=global-statement + try: + pool = templatePool() + factory = _BFACTORY # may trigger failure + if factory is not None: + x = factory.address # "" #pylint: disable=unused-variable + except brownie.exceptions.ContractNotFound: + factory = None + if factory is None: + factory = _BFACTORY = BROWNIE_PROJECT057.BFactory.deploy( + pool.address, txdict(GOD_ACCOUNT) + ) + return factory + + +@enforce_types +def poolAddressFromNewBPoolTx(tx): + return tx.events["BPoolCreated"]["newBPoolAddress"] + + +@enforce_types +def newBPool(account): + bfactory = BFactory() + tx = bfactory.newBPool(txdict(account)) + pool_address = poolAddressFromNewBPoolTx(tx) + pool = BROWNIE_PROJECT057.BPool.at(pool_address) + return pool diff --git a/sol057/contracts/oceanv3/templates/DataTokenTemplate.sol b/sol057/contracts/oceanv3/templates/DataTokenTemplate.sol new file mode 100644 index 0000000000000000000000000000000000000000..ee574d80a9b267f7998143fbbc57cb87eba7fab8 --- /dev/null +++ b/sol057/contracts/oceanv3/templates/DataTokenTemplate.sol @@ -0,0 +1,423 @@ +pragma solidity 0.5.7; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import '../interfaces/IERC20Template.sol'; +import 'OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/token/ERC20/ERC20.sol'; + + +/** +* @title DataTokenTemplate +* +* @dev DataTokenTemplate is an ERC20 compliant token template +* Used by the factory contract as a bytecode reference to +* deploy new DataTokens. +*/ +contract DataTokenTemplate is IERC20Template, ERC20 { + using SafeMath for uint256; + + string private _name; + string private _symbol; + string private _blob; + uint256 private _cap; + uint8 private constant _decimals = 18; + address private _communityFeeCollector; + bool private initialized = false; + address private _minter; + address private _proposedMinter; + uint256 public constant BASE = 10**18; + uint256 public constant BASE_COMMUNITY_FEE_PERCENTAGE = BASE / 1000; + uint256 public constant BASE_MARKET_FEE_PERCENTAGE = BASE / 1000; + + event OrderStarted( + address indexed consumer, + address indexed payer, + uint256 amount, + uint256 serviceId, + uint256 timestamp, + address indexed mrktFeeCollector, + uint256 marketFee + ); + + event OrderFinished( + bytes32 orderTxId, + address indexed consumer, + uint256 amount, + uint256 serviceId, + address indexed provider, + uint256 timestamp + ); + + event MinterProposed( + address currentMinter, + address newMinter + ); + + event MinterApproved( + address currentMinter, + address newMinter + ); + + modifier onlyNotInitialized() { + require( + !initialized, + 'DataTokenTemplate: token instance already initialized' + ); + _; + } + + modifier onlyMinter() { + require( + msg.sender == _minter, + 'DataTokenTemplate: invalid minter' + ); + _; + } + + /** + * @dev constructor + * Called prior contract deployment + * @param name refers to a template DataToken name + * @param symbol refers to a template DataToken symbol + * @param minterAddress refers to an address that has minter role + * @param cap the total ERC20 cap + * @param blob data string refering to the resolver for the metadata + * @param feeCollector it is the community fee collector address + */ + constructor( + string memory name, + string memory symbol, + address minterAddress, + uint256 cap, + string memory blob, + address feeCollector + ) + public + { + _initialize( + name, + symbol, + minterAddress, + cap, + blob, + feeCollector + ); + } + + /** + * @dev initialize + * Called prior contract initialization (e.g creating new DataToken instance) + * Calls private _initialize function. Only if contract is not initialized. + * @param name refers to a new DataToken name + * @param symbol refers to a nea DataToken symbol + * @param minterAddress refers to an address that has minter rights + * @param cap the total ERC20 cap + * @param blob data string refering to the resolver for the metadata + * @param feeCollector it is the community fee collector address + */ + function initialize( + string calldata name, + string calldata symbol, + address minterAddress, + uint256 cap, + string calldata blob, + address feeCollector + ) + external + onlyNotInitialized + returns(bool) + { + return _initialize( + name, + symbol, + minterAddress, + cap, + blob, + feeCollector + ); + } + + /** + * @dev _initialize + * Private function called on contract initialization. + * @param name refers to a new DataToken name + * @param symbol refers to a nea DataToken symbol + * @param minterAddress refers to an address that has minter rights + * @param cap the total ERC20 cap + * @param blob data string refering to the resolver for the metadata + * @param feeCollector it is the community fee collector address + */ + function _initialize( + string memory name, + string memory symbol, + address minterAddress, + uint256 cap, + string memory blob, + address feeCollector + ) + private + returns(bool) + { + require( + minterAddress != address(0), + 'DataTokenTemplate: Invalid minter, zero address' + ); + + require( + _minter == address(0), + 'DataTokenTemplate: Invalid minter, zero address' + ); + + require( + feeCollector != address(0), + 'DataTokenTemplate: Invalid community fee collector, zero address' + ); + + require( + cap != 0, + 'DataTokenTemplate: Invalid cap value' + ); + _cap = cap; + _name = name; + _blob = blob; + _symbol = symbol; + _minter = minterAddress; + _communityFeeCollector = feeCollector; + initialized = true; + return initialized; + } + + /** + * @dev mint + * Only the minter address can call it. + * msg.value should be higher than zero and gt or eq minting fee + * @param account refers to an address that token is going to be minted to. + * @param value refers to amount of tokens that is going to be minted. + */ + function mint( + address account, + uint256 value + ) + external + onlyMinter + { + require( + totalSupply().add(value) <= _cap, + 'DataTokenTemplate: cap exceeded' + ); + _mint(account, value); + } + + /** + * @dev startOrder + * called by payer or consumer prior ordering a service consume on a marketplace. + * @param consumer is the consumer address (payer could be different address) + * @param amount refers to amount of tokens that is going to be transfered. + * @param serviceId service index in the metadata + * @param mrktFeeCollector marketplace fee collector + */ + function startOrder( + address consumer, + uint256 amount, + uint256 serviceId, + address mrktFeeCollector + ) + external + { + uint256 marketFee = 0; + uint256 communityFee = calculateFee( + amount, + BASE_COMMUNITY_FEE_PERCENTAGE + ); + transfer(_communityFeeCollector, communityFee); + if(mrktFeeCollector != address(0)){ + marketFee = calculateFee( + amount, + BASE_MARKET_FEE_PERCENTAGE + ); + transfer(mrktFeeCollector, marketFee); + } + uint256 totalFee = communityFee.add(marketFee); + transfer(_minter, amount.sub(totalFee)); + emit OrderStarted( + consumer, + msg.sender, + amount, + serviceId, + /* solium-disable-next-line */ + block.timestamp, + mrktFeeCollector, + marketFee + ); + } + + /** + * @dev finishOrder + * called by provider prior completing service delivery only + * if there is a partial or full refund. + * @param orderTxId refers to the transaction Id of startOrder acts + * as a payment reference. + * @param consumer refers to an address that has consumed that service. + * @param amount refers to amount of tokens that is going to be transfered. + * @param serviceId service index in the metadata. + */ + function finishOrder( + bytes32 orderTxId, + address consumer, + uint256 amount, + uint256 serviceId + ) + external + { + if ( amount != 0 ) + require( + transfer(consumer, amount), + 'DataTokenTemplate: failed to finish order' + ); + + emit OrderFinished( + orderTxId, + consumer, + amount, + serviceId, + msg.sender, + /* solium-disable-next-line */ + block.timestamp + ); + } + + /** + * @dev proposeMinter + * It proposes a new token minter address. + * Only the current minter can call it. + * @param newMinter refers to a new token minter address. + */ + function proposeMinter(address newMinter) + external + onlyMinter + { + _proposedMinter = newMinter; + emit MinterProposed( + msg.sender, + _proposedMinter + ); + } + + /** + * @dev approveMinter + * It approves a new token minter address. + * Only the current minter can call it. + */ + function approveMinter() + external + { + require( + msg.sender == _proposedMinter, + 'DataTokenTemplate: invalid proposed minter address' + ); + emit MinterApproved( + _minter, + _proposedMinter + ); + _minter = _proposedMinter; + _proposedMinter = address(0); + } + + /** + * @dev name + * It returns the token name. + * @return DataToken name. + */ + function name() external view returns(string memory) { + return _name; + } + + /** + * @dev symbol + * It returns the token symbol. + * @return DataToken symbol. + */ + function symbol() external view returns(string memory) { + return _symbol; + } + + /** + * @dev blob + * It returns the blob (e.g https://123.com). + * @return DataToken blob. + */ + function blob() external view returns(string memory) { + return _blob; + } + + /** + * @dev decimals + * It returns the token decimals. + * how many supported decimal points + * @return DataToken decimals. + */ + function decimals() external view returns(uint8) { + return _decimals; + } + + /** + * @dev cap + * it returns the capital. + * @return DataToken cap. + */ + function cap() external view returns (uint256) { + return _cap; + } + + /** + * @dev isMinter + * It takes the address and checks whether it has a minter role. + * @param account refers to the address. + * @return true if account has a minter role. + */ + function isMinter(address account) external view returns(bool) { + return (_minter == account); + } + + /** + * @dev minter + * @return minter's address. + */ + function minter() + external + view + returns(address) + { + return _minter; + } + + /** + * @dev isInitialized + * It checks whether the contract is initialized. + * @return true if the contract is initialized. + */ + function isInitialized() external view returns(bool) { + return initialized; + } + + /** + * @dev calculateFee + * giving a fee percentage, and amount it calculates the actual fee + * @param amount the amount of token + * @param feePercentage the fee percentage + * @return the token fee. + */ + function calculateFee( + uint256 amount, + uint256 feePercentage + ) + public + pure + returns(uint256) + { + if(amount == 0) return 0; + if(feePercentage == 0) return 0; + return amount.mul(feePercentage).div(BASE); + } +} diff --git a/sol057/contracts/oceanv3/test/__init__.py b/sol057/contracts/oceanv3/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/oceanv3/test/conftest.py b/sol057/contracts/oceanv3/test/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..a29a4d2945f05d7b69179c8a37748ee13caf2941 --- /dev/null +++ b/sol057/contracts/oceanv3/test/conftest.py @@ -0,0 +1,29 @@ +import brownie +import pytest + +from sol057.contracts.oceanv3.oceanv3util import newDatatoken +from util.base18 import toBase18 +from util.tx import txdict + +account0 = brownie.network.accounts[0] + + +@pytest.fixture +def T1(): + return _dts()[0] + + +@pytest.fixture +def T2(): + return _dts()[1] + + +def _dts(): + """Create datatokens, and mint into account0""" + cap = toBase18(1e3) + dts = [] + for i in range(2): + dt = newDatatoken("blob", f"datatoken{i+1}", f"DT{i+1}", cap, account0) + dt.mint(account0, toBase18(1e3), txdict(account0)) + dts.append(dt) + return dts diff --git a/sol057/contracts/oceanv3/test/test_bfactory.py b/sol057/contracts/oceanv3/test/test_bfactory.py new file mode 100644 index 0000000000000000000000000000000000000000..03246e7c998da6b49fdecc9e679e6be25ae9b07f --- /dev/null +++ b/sol057/contracts/oceanv3/test/test_bfactory.py @@ -0,0 +1,38 @@ +import brownie + +import sol057.contracts.oceanv3.oceanv3util +from util.constants import BROWNIE_PROJECT057 +from util.tx import txdict + +account0 = brownie.network.accounts[0] + + +def test_direct(): + templatepool = BROWNIE_PROJECT057.BPool.deploy(txdict(account0)) + + bfactory = BROWNIE_PROJECT057.BFactory.deploy( + templatepool.address, txdict(account0) + ) + + tx = bfactory.newBPool(txdict(account0)) + pool_address = tx.events["BPoolCreated"]["newBPoolAddress"] + pool = BROWNIE_PROJECT057.BPool.at(pool_address) + assert pool.address == pool_address + assert pool.isFinalized() in [False, True] + + +def test_via_BFactory_util(): + bfactory = sol057.contracts.oceanv3.oceanv3util.BFactory() + tx = bfactory.newBPool(txdict(account0)) + + pool_address = sol057.contracts.oceanv3.oceanv3util.poolAddressFromNewBPoolTx(tx) + assert pool_address == tx.events["BPoolCreated"]["newBPoolAddress"] + + pool = BROWNIE_PROJECT057.BPool.at(pool_address) + assert pool.address == pool_address + assert pool.isFinalized() in [False, True] + + +def test_via_newBPool_util(): + pool = sol057.contracts.oceanv3.oceanv3util.newBPool(account0) + assert pool.isFinalized() in [False, True] diff --git a/sol057/contracts/oceanv3/test/test_bpool.py b/sol057/contracts/oceanv3/test/test_bpool.py new file mode 100644 index 0000000000000000000000000000000000000000..aecb95db81e07555ba71fbe11613aa738546eac7 --- /dev/null +++ b/sol057/contracts/oceanv3/test/test_bpool.py @@ -0,0 +1,497 @@ +import brownie +import pytest + +import sol057.contracts.oceanv3.oceanv3util +from util import globaltokens +from util.base18 import toBase18, fromBase18 +from util.tx import txdict + +accounts = brownie.network.accounts +account0, account1 = accounts[0], accounts[1] +address0, address1 = account0.address, account1.address +OCEAN_address = globaltokens.OCEAN_address() +HUGEINT = 2**255 + + +def test_notokens_basic(): + pool = _deployBPool() + + assert not pool.isPublicSwap() + assert not pool.isFinalized() + assert not pool.isBound(OCEAN_address) + assert pool.getNumTokens() == 0 + assert pool.getCurrentTokens() == [] + with pytest.raises(Exception): + pool.getFinalTokens() # pool's not finalized + assert pool.getSwapFee() == toBase18(1e-6) + assert pool.getController() == address0 + assert str(pool) + + with pytest.raises(Exception): + pool.finalize() # can't finalize if no tokens + + +def test_setSwapFee_works(): + pool = _deployBPool() + pool.setSwapFee(toBase18(0.011), txdict(account0)) + assert fromBase18(pool.getSwapFee()) == 0.011 + + +def test_setSwapFee_fails(): + pool = _deployBPool() + with pytest.raises(Exception): + # fail, because account1 isn't controller + pool.setSwapFee(toBase18(0.011), txdict(account1)) + pool.setController(address1, txdict(account0)) + pool.setSwapFee(toBase18(0.011), txdict(account1)) # pass now + + +def test_setController(): + pool = _deployBPool() + pool.setController(address1, txdict(account0)) + assert pool.getController() == address1 + + pool.setController(address0, txdict(account1)) + assert pool.getController() == address0 + + +def test_setPublicSwap(): + pool = _deployBPool() + pool.setPublicSwap(True, txdict(account0)) + assert pool.isPublicSwap() + pool.setPublicSwap(False, txdict(account0)) + assert not pool.isPublicSwap() + + +def test_2tokens_basic(T1, T2): + pool = _deployBPool() + assert T1.address != T2.address + assert T1.address != pool.address + + assert fromBase18(T1.balanceOf(address0)) >= 90.0 + assert fromBase18(T2.balanceOf(address0)) >= 10.0 + + with pytest.raises(Exception): # can't bind until we approve + pool.bind(T1.address, toBase18(90.0), toBase18(9.0)) + + # Bind two tokens to the pool + T1.approve(pool.address, toBase18(90.0), txdict(account0)) + T2.approve(pool.address, toBase18(10.0), txdict(account0)) + + assert fromBase18(T1.allowance(address0, pool.address)) == 90.0 + assert fromBase18(T2.allowance(address0, pool.address)) == 10.0 + + assert not pool.isBound(T1.address) and not pool.isBound(T1.address) + pool.bind(T1.address, toBase18(90.0), toBase18(9.0), txdict(account0)) + pool.bind(T2.address, toBase18(10.0), toBase18(1.0), txdict(account0)) + assert pool.isBound(T1.address) and pool.isBound(T2.address) + + assert pool.getNumTokens() == 2 + assert pool.getCurrentTokens() == [T1.address, T2.address] + + assert pool.getDenormalizedWeight(T1.address) == toBase18(9.0) + assert pool.getDenormalizedWeight(T2.address) == toBase18(1.0) + assert pool.getTotalDenormalizedWeight() == toBase18(9.0 + 1.0) + + assert pool.getNormalizedWeight(T1.address) == toBase18(0.9) + assert pool.getNormalizedWeight(T2.address) == toBase18(0.1) + + assert pool.getBalance(T1.address) == toBase18(90.0) + assert pool.getBalance(T2.address) == toBase18(10.0) + + assert str(pool) + + +def test_unbind(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 1.0, 1.0, 1.0, 1.0) + + pool.unbind(T1.address, txdict(account0)) + + assert pool.getNumTokens() == 1 + assert pool.getCurrentTokens() == [T2.address] + assert fromBase18(pool.getBalance(T2.address)) == 1.0 + + +def test_finalize(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 90.0, 10.0, 9.0, 1.0) + + assert not pool.isPublicSwap() + assert not pool.isFinalized() + assert pool.totalSupply() == 0 + assert pool.balanceOf(address0) == 0 + assert pool.allowance(address0, pool.address) == 0 + + pool.finalize(txdict(account0)) + + assert pool.isPublicSwap() + assert pool.isFinalized() + assert pool.totalSupply() == toBase18(100.0) + assert pool.balanceOf(address0) == toBase18(100.0) + assert pool.allowance(address0, pool.address) == 0 + + assert pool.getFinalTokens() == [T1.address, T2.address] + assert pool.getCurrentTokens() == [T1.address, T2.address] + + +def test_public_pool(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 90.0, 10.0, 9.0, 1.0) + BPT = pool + + # alice give Bob some tokens + T1.transfer(address1, toBase18(100.0), txdict(account0)) + T2.transfer(address1, toBase18(100.0), txdict(account0)) + + # verify holdings + assert fromBase18(T1.balanceOf(address0)) == (1000.0 - 90.0 - 100.0) + assert fromBase18(T2.balanceOf(address0)) == (1000.0 - 10.0 - 100.0) + assert fromBase18(BPT.balanceOf(address0)) == 0 + + assert fromBase18(T1.balanceOf(address1)) == 100.0 + assert fromBase18(T2.balanceOf(address1)) == 100.0 + assert fromBase18(BPT.balanceOf(address1)) == 0 + + assert fromBase18(T1.balanceOf(pool.address)) == 90.0 + assert fromBase18(T2.balanceOf(pool.address)) == 10.0 + assert fromBase18(BPT.balanceOf(pool.address)) == 0 + + # finalize + pool.finalize(txdict(account0)) + + # verify holdings + assert fromBase18(T1.balanceOf(address0)) == (1000.0 - 90.0 - 100.0) + assert fromBase18(T2.balanceOf(address0)) == (1000.0 - 10.0 - 100.0) + assert fromBase18(BPT.balanceOf(address0)) == 100.0 # new! + + assert fromBase18(T1.balanceOf(pool.address)) == 90.0 + assert fromBase18(T2.balanceOf(pool.address)) == 10.0 + assert fromBase18(BPT.balanceOf(pool.address)) == 0 + + # bob join pool. Wants 10 BPT + T1.approve(pool.address, toBase18(100.0), txdict(account1)) + T2.approve(pool.address, toBase18(100.0), txdict(account1)) + poolAmountOut = toBase18(10.0) # 10 BPT + maxAmountsIn = [toBase18(100.0), toBase18(100.0)] + pool.joinPool(poolAmountOut, maxAmountsIn, txdict(account1)) + + # verify holdings + assert fromBase18(T1.balanceOf(address0)) == (1000.0 - 90.0 - 100.0) + assert fromBase18(T2.balanceOf(address0)) == (1000.0 - 10.0 - 100.0) + assert fromBase18(BPT.balanceOf(address0)) == 100.0 + + assert fromBase18(T1.balanceOf(address1)) == (100.0 - 9.0) + assert fromBase18(T2.balanceOf(address1)) == (100.0 - 1.0) + assert fromBase18(BPT.balanceOf(address1)) == 10.0 + + assert fromBase18(T1.balanceOf(pool.address)) == (90.0 + 9.0) + assert fromBase18(T2.balanceOf(pool.address)) == (10.0 + 1.0) + assert fromBase18(BPT.balanceOf(pool.address)) == 0 + + # bob sells 2 BPT + # -this is where BLabs fee kicks in. But the fee is currently set to 0. + poolAmountIn = toBase18(2.0) + minAmountsOut = [toBase18(0.0), toBase18(0.0)] + pool.exitPool(poolAmountIn, minAmountsOut, txdict(account1)) + assert fromBase18(T1.balanceOf(address1)) == 92.8 + assert fromBase18(T2.balanceOf(address1)) == 99.2 + assert fromBase18(BPT.balanceOf(address1)) == 8.0 + + # bob buys 5 more BPT + poolAmountOut = toBase18(5.0) + maxAmountsIn = [toBase18(90.0), toBase18(90.0)] + pool.joinPool(poolAmountOut, maxAmountsIn, txdict(account1)) + assert fromBase18(BPT.balanceOf(address1)) == 13.0 + + # bob fully exits + poolAmountIn = toBase18(13.0) + minAmountsOut = [toBase18(0.0), toBase18(0.0)] + pool.exitPool(poolAmountIn, minAmountsOut, txdict(account1)) + assert fromBase18(BPT.balanceOf(address1)) == 0.0 + + +def test_rebind_more_tokens(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 90.0, 10.0, 9.0, 1.0) + + # insufficient allowance + with pytest.raises(Exception): + pool.rebind(T1.address, toBase18(120.0), toBase18(9.0), txdict(account0)) + + # sufficient allowance + T1.approve(pool.address, toBase18(30.0), txdict(account0)) + pool.rebind(T1.address, toBase18(120.0), toBase18(9.0), txdict(account0)) + + +def test_gulp(T1): + pool = _deployBPool() + + # bind T1 to the pool, with a balance of 2.0 + T1.approve(pool.address, toBase18(50.0), txdict(account0)) + pool.bind(T1.address, toBase18(2.0), toBase18(50.0), txdict(account0)) + + # T1 is now pool's (a) ERC20 balance (b) _records[token].balance + assert T1.balanceOf(pool.address) == toBase18(2.0) # ERC20 balance + assert pool.getBalance(T1.address) == toBase18(2.0) # records[] + + # but then some joker accidentally sends 5.0 tokens to the pool's address + # rather than binding / rebinding. So it's in ERC20 bal but not records[] + T1.transfer(pool.address, toBase18(5.0), txdict(account0)) + assert T1.balanceOf(pool.address) == toBase18(2.0 + 5.0) # ERC20 bal + assert pool.getBalance(T1.address) == toBase18(2.0) # records[] + + # so, 'gulp' gets the pool to absorb the tokens into its balances. + # i.e. to update _records[token].balance to be in sync with ERC20 balance + pool.gulp(T1.address, txdict(account0)) + assert T1.balanceOf(pool.address) == toBase18(2.0 + 5.0) # ERC20 + assert pool.getBalance(T1.address) == toBase18(2.0 + 5.0) # records[] + + +def test_spot_price(T1, T2): + (p, p_sans) = _spotPrices(T1, T2, 1.0, 1.0, 1.0, 1.0) + assert p_sans == 1.0 + assert round(p, 8) == 1.000001 + + (p, p_sans) = _spotPrices(T1, T2, 90.0, 10.0, 9.0, 1.0) + assert p_sans == 1.0 + assert round(p, 8) == 1.000001 + + (p, p_sans) = _spotPrices(T1, T2, 1.0, 2.0, 1.0, 1.0) + assert p_sans == 0.5 + assert round(p, 8) == 0.5000005 + + (p, p_sans) = _spotPrices(T1, T2, 2.0, 1.0, 1.0, 1.0) + assert p_sans == 2.0 + assert round(p, 8) == 2.000002 + + (p, p_sans) = _spotPrices(T1, T2, 9.0, 10.0, 9.0, 1.0) + assert p_sans == 0.1 + assert round(p, 8) == 0.1000001 + + +def _spotPrices( + T1, T2, bal1: float, bal2: float, w1: float, w2: float +): # pylint: disable=too-many-arguments + pool = _createPoolWith2Tokens(T1, T2, bal1, bal2, w1, w2) + a1, a2 = T1.address, T2.address + return ( + fromBase18(pool.getSpotPrice(a1, a2)), + fromBase18(pool.getSpotPriceSansFee(a1, a2)), + ) + + +def test_joinSwapExternAmountIn(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 90.0, 10.0, 9.0, 1.0) + T1.approve(pool.address, toBase18(100.0), txdict(account0)) + + # pool's not public + with pytest.raises(Exception): + tokenIn_address = T1.address + maxAmountIn = toBase18(100.0) + tokenOut_address = T2.address + tokenAmountOut = toBase18(10.0) + maxPrice = HUGEINT + pool.swapExactAmountOut( + tokenIn_address, + maxAmountIn, + tokenOut_address, + tokenAmountOut, + maxPrice, + txdict(account0), + ) + + # pool's public + pool.setPublicSwap(True, txdict(account0)) + tokenIn_address = T1.address + maxAmountIn = toBase18(100.0) + tokenOut_address = T2.address + tokenAmountOut = toBase18(1.0) + maxPrice = HUGEINT + pool.swapExactAmountOut( + tokenIn_address, + maxAmountIn, + tokenOut_address, + tokenAmountOut, + maxPrice, + txdict(account0), + ) + assert 908.94 <= fromBase18(T1.balanceOf(address0)) <= 908.95 + assert fromBase18(T2.balanceOf(address0)) == (1000.0 - 9.0) + + +def test_joinswapPoolAmountOut(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 90.0, 10.0, 9.0, 1.0) + BPT = pool + pool.finalize(txdict(account0)) + T1.approve(pool.address, toBase18(90.0), txdict(account0)) + assert fromBase18(T1.balanceOf(address0)) == 910.0 + tokenIn_address = T1.address + poolAmountOut = toBase18(10.0) # BPT wanted + maxAmountIn = toBase18(90.0) # max T1 to spend + pool.joinswapPoolAmountOut( + tokenIn_address, poolAmountOut, maxAmountIn, txdict(account0) + ) + assert fromBase18(T1.balanceOf(address0)) >= (910.0 - 90.0) + assert fromBase18(BPT.balanceOf(address0)) == (100.0 + 10.0) + + +def test_exitswapPoolAmountIn(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 90.0, 10.0, 9.0, 1.0) + BPT = pool + pool.finalize(txdict(account0)) + assert fromBase18(T1.balanceOf(address0)) == 910.0 + tokenOut_address = T1.address + poolAmountIn = toBase18(10.0) # BPT spent + minAmountOut = toBase18(1.0) # min T1 wanted + pool.exitswapPoolAmountIn( + tokenOut_address, poolAmountIn, minAmountOut, txdict(account0) + ) + assert fromBase18(T1.balanceOf(address0)) >= (910.0 + 1.0) + assert fromBase18(BPT.balanceOf(address0)) == (100.0 - 10.0) + + +def test_exitswapExternAmountOut(T1, T2): + pool = _createPoolWith2Tokens(T1, T2, 90.0, 10.0, 9.0, 1.0) + BPT = pool + pool.finalize(txdict(account0)) + assert fromBase18(T1.balanceOf(address0)) == 910.0 + tokenOut_address = T1.address + tokenAmountOut = toBase18(2.0) # T1 wanted + maxPoolAmountIn = toBase18(10.0) # max BPT spent + pool.exitswapExternAmountOut( + tokenOut_address, + tokenAmountOut, # T1 wanted + maxPoolAmountIn, # max BPT spent + txdict(account0), + ) + assert fromBase18(T1.balanceOf(address0)) == (910.0 + 2.0) + assert fromBase18(BPT.balanceOf(address0)) >= (100.0 - 10.0) + + +def test_calcSpotPrice(): + pool = _deployBPool() + tokenBalanceIn = toBase18(10.0) + tokenWeightIn = toBase18(1.0) + tokenBalanceOut = toBase18(11.0) + tokenWeightOut = toBase18(1.0) + swapFee = 0 + x = pool.calcSpotPrice( + tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, swapFee + ) + assert round(fromBase18(x), 3) == 0.909 + + +def test_calcOutGivenIn(): + pool = _deployBPool() + tokenBalanceIn = toBase18(10.0) + tokenWeightIn = toBase18(1.0) + tokenBalanceOut = toBase18(10.1) + tokenWeightOut = toBase18(1.0) + tokenAmountIn = toBase18(1.0) + swapFee = 0 + x = pool.calcOutGivenIn( + tokenBalanceIn, + tokenWeightIn, + tokenBalanceOut, + tokenWeightOut, + tokenAmountIn, + swapFee, + ) + assert round(fromBase18(x), 3) == 0.918 + + +def test_calcInGivenOut(): + pool = _deployBPool() + tokenBalanceIn = toBase18(10.0) + tokenWeightIn = toBase18(1.0) + tokenBalanceOut = toBase18(10.1) + tokenWeightOut = toBase18(1.0) + tokenAmountOut = toBase18(1.0) + swapFee = 0 + x = pool.calcInGivenOut( + tokenBalanceIn, + tokenWeightIn, + tokenBalanceOut, + tokenWeightOut, + tokenAmountOut, + swapFee, + ) + assert round(fromBase18(x), 3) == 1.099 + + +def test_calcPoolOutGivenSingleIn(): + pool = _deployBPool() + tokenBalanceIn = toBase18(10.0) + tokenWeightIn = toBase18(1.0) + poolSupply = toBase18(120.0) + totalWeight = toBase18(2.0) + tokenAmountIn = toBase18(0.1) + swapFee = 0 + x = pool.calcPoolOutGivenSingleIn( + tokenBalanceIn, tokenWeightIn, poolSupply, totalWeight, tokenAmountIn, swapFee + ) + assert round(fromBase18(x), 3) == 0.599 + + +def test_calcSingleInGivenPoolOut(): + pool = _deployBPool() + tokenBalanceIn = toBase18(10.0) + tokenWeightIn = toBase18(1.0) + poolSupply = toBase18(120.0) + totalWeight = toBase18(2.0) + poolAmountOut = toBase18(10.0) + swapFee = 0 + x = pool.calcSingleInGivenPoolOut( + tokenBalanceIn, tokenWeightIn, poolSupply, totalWeight, poolAmountOut, swapFee + ) + assert round(fromBase18(x), 3) == 1.736 + + +def test_calcSingleOutGivenPoolIn(): + pool = _deployBPool() + tokenBalanceOut = toBase18(10.0) + tokenWeightOut = toBase18(1.0) + poolSupply = toBase18(120.0) + totalWeight = toBase18(2.0) + poolAmountIn = toBase18(10.0) + swapFee = 0 + x = pool.calcSingleOutGivenPoolIn( + tokenBalanceOut, tokenWeightOut, poolSupply, totalWeight, poolAmountIn, swapFee + ) + assert round(fromBase18(x), 3) == 1.597 + + +def test_calcPoolInGivenSingleOut(): + pool = _deployBPool() + tokenBalanceOut = toBase18(1000.0) + tokenWeightOut = toBase18(5.0) + poolSupply = toBase18(100.0) + totalWeight = toBase18(10.0) + tokenAmountOut = toBase18(0.1) + swapFee = 0 + x = pool.calcPoolInGivenSingleOut( + tokenBalanceOut, + tokenWeightOut, + poolSupply, + totalWeight, + tokenAmountOut, + swapFee, + ) + assert round(fromBase18(x), 3) == 0.005 + + +def _createPoolWith2Tokens( + T1, T2, bal1: float, bal2: float, w1: float, w2: float +): # pylint: disable=too-many-arguments + pool = _deployBPool() + + T1.approve(pool.address, toBase18(bal1), txdict(account0)) + T2.approve(pool.address, toBase18(bal2), txdict(account0)) + + assert T1.balanceOf(address0) >= toBase18(bal1) + assert T2.balanceOf(address0) >= toBase18(bal2) + pool.bind(T1.address, toBase18(bal1), toBase18(w1), txdict(account0)) + pool.bind(T2.address, toBase18(bal2), toBase18(w2), txdict(account0)) + + return pool + + +def _deployBPool(): + return sol057.contracts.oceanv3.oceanv3util.newBPool(account0) diff --git a/sol057/contracts/oceanv3/test/test_datatoken.py b/sol057/contracts/oceanv3/test/test_datatoken.py new file mode 100644 index 0000000000000000000000000000000000000000..7d5b962a5eab978ed61921c23520ebdf16f412e5 --- /dev/null +++ b/sol057/contracts/oceanv3/test/test_datatoken.py @@ -0,0 +1,47 @@ +import brownie + +import sol057.contracts.oceanv3.oceanv3util +from util.base18 import toBase18 +from util.constants import BROWNIE_PROJECT057 +from util.tx import txdict + +accounts = brownie.network.accounts +account0, account1 = accounts[0], accounts[1] +address0, address1 = account0.address, account1.address + + +def test_direct(): + dtfactory = sol057.contracts.oceanv3.oceanv3util.DTFactory() + + tx = dtfactory.createToken( + "foo_blob", "datatoken1", "DT1", toBase18(100.0), txdict(account0) + ) + dt_address = tx.events["TokenCreated"]["newTokenAddress"] + dt = BROWNIE_PROJECT057.DataTokenTemplate.at(dt_address) + dt.mint(address0, toBase18(100.0), txdict(account0)) + + # functionality inherited from btoken + assert dt.address == dt_address + assert dt.name() == "datatoken1" + assert dt.symbol() == "DT1" + assert dt.decimals() == 18 + assert dt.balanceOf(address0) == toBase18(100.0) + dt.transfer(address1, toBase18(50.0), txdict(account0)) + assert dt.allowance(address0, address1) == 0 + dt.approve(address1, toBase18(1.0), txdict(account0)) + assert dt.allowance(address0, address1) == toBase18(1.0) + + # functionality for just datatoken + assert dt.blob() == "foo_blob" + + +def test_via_util(): + dt = sol057.contracts.oceanv3.oceanv3util.newDatatoken( + "foo_blob", "datatoken1", "DT1", toBase18(100.0), account0 + ) + dt.mint(address0, toBase18(100.0), txdict(account0)) + assert dt.name() == "datatoken1" + assert dt.symbol() == "DT1" + assert dt.decimals() == 18 + assert dt.balanceOf(address0) == toBase18(100.0) + assert dt.blob() == "foo_blob" diff --git a/sol057/contracts/oceanv3/test/test_dtfactory.py b/sol057/contracts/oceanv3/test/test_dtfactory.py new file mode 100644 index 0000000000000000000000000000000000000000..b7b1e964a6523cc7751bc849136c1108f6449c45 --- /dev/null +++ b/sol057/contracts/oceanv3/test/test_dtfactory.py @@ -0,0 +1,59 @@ +import brownie + +import sol057.contracts.oceanv3.oceanv3util +from util.base18 import toBase18 +from util.constants import BROWNIE_PROJECT057 +from util.tx import txdict + +account0 = brownie.network.accounts[0] +address0 = account0.address + + +def test_direct(): + tt = BROWNIE_PROJECT057.DataTokenTemplate.deploy( + "TT", + "TemplateToken", + address0, + toBase18(1e3), + "blob", + address0, + txdict(account0), + ) + + dtfactory = BROWNIE_PROJECT057.DTFactory.deploy( + tt.address, account0.address, txdict(account0) + ) + + tx = dtfactory.createToken( + "foo_blob", "DT", "DT", toBase18(100.0), txdict(account0) + ) + dt_address = tx.events["TokenCreated"]["newTokenAddress"] + + dt = BROWNIE_PROJECT057.DataTokenTemplate.at(dt_address) + assert dt.address == dt_address + assert dt.blob() == "foo_blob" + + +def test_via_DTFactory_util(): + dtfactory = sol057.contracts.oceanv3.oceanv3util.DTFactory() + tx = dtfactory.createToken( + "foo_blob", "datatoken1", "DT1", toBase18(100.0), txdict(account0) + ) + + dt_address = sol057.contracts.oceanv3.oceanv3util.dtAddressFromCreateTokenTx(tx) + assert dt_address == tx.events["TokenCreated"]["newTokenAddress"] + + dt = BROWNIE_PROJECT057.DataTokenTemplate.at(dt_address) + assert dt.address == dt_address + assert dt.blob() == "foo_blob" + assert dt.name() == "datatoken1" + assert dt.symbol() == "DT1" + + +def test_via_newDatatoken_util(): + dt = sol057.contracts.oceanv3.oceanv3util.newDatatoken( + "foo_blob", "datatoken1", "DT1", toBase18(100.0), account0 + ) + assert dt.blob() == "foo_blob" + assert dt.name() == "datatoken1" + assert dt.symbol() == "DT1" diff --git a/sol057/contracts/oceanv3/test/test_oceanv3util.py b/sol057/contracts/oceanv3/test/test_oceanv3util.py new file mode 100644 index 0000000000000000000000000000000000000000..e0f5396b5036cea13a6ba0aba2c8c6b9d405a3b0 --- /dev/null +++ b/sol057/contracts/oceanv3/test/test_oceanv3util.py @@ -0,0 +1,3 @@ +# oceanv2util.py is tested piecewise by other test files in this directory, +# such as test_bfactory.py. Therefore we don't need to repeat ourselves and +# have tests in this file. We keep this file, and these comments, as a reminder. diff --git a/sol057/contracts/oceanv3/utils/Deployer.sol b/sol057/contracts/oceanv3/utils/Deployer.sol new file mode 100644 index 0000000000000000000000000000000000000000..6f04436d255a6ba2bf1753cf6770ceaf763892f0 --- /dev/null +++ b/sol057/contracts/oceanv3/utils/Deployer.sol @@ -0,0 +1,46 @@ +pragma solidity >=0.5.7 <0.6.0; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +/** + * @title Deployer Contract + * @author Ocean Protocol Team + * + * @dev Contract Deployer + * This contract allowes factory contract + * to deploy new contract instances using + * the same library pattern in solidity. + * the logic it self is deployed only once, but + * executed in the context of the new storage + * contract (new contract instance) + */ +contract Deployer { + event InstanceDeployed(address instance); + + /** + * @dev deploy + * deploy new contract instance + * @param _logic the logic contract address + * @return address of the new instance + */ + function deploy( + address _logic + ) + internal + returns (address instance) + { + bytes20 targetBytes = bytes20(_logic); + // Follows OpenZeppelin Implementation https://github.com/OpenZeppelin/openzeppelin-sdk/blob/71c9ad77e0326db079e6a643eca8568ab316d4a9/packages/lib/contracts/upgradeability/ProxyFactory.sol + // Adapted from https://github.com/optionality/clone-factory/blob/32782f82dfc5a00d103a7e61a17a5dedbd1e8e9d/contracts/CloneFactory.sol + /* solium-disable-next-line security/no-inline-assembly */ + assembly { + let clone := mload(0x40) + mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(clone, 0x14), targetBytes) + mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + instance := create(0, clone, 0x37) + } + emit InstanceDeployed(address(instance)); + } +} diff --git a/sol057/contracts/scheduler057/VestingWallet057.sol b/sol057/contracts/scheduler057/VestingWallet057.sol new file mode 100644 index 0000000000000000000000000000000000000000..ce31ff525680927674ff8ba5f3e4c9cb93c2154c --- /dev/null +++ b/sol057/contracts/scheduler057/VestingWallet057.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.5.7; + +import "OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/token/ERC20/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/utils/Address.sol"; +import "OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/math/SafeMath.sol"; + +/** + * @title VestingWallet057 + * @dev This contract handles the vesting of Eth and ERC20 tokens for a given beneficiary. Custody of multiple tokens + * can be given to this contract, which will release the token to the beneficiary following a given vesting schedule. + * The vesting schedule is customizable through the {vestedAmount} function. + * + * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. + * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) + * be immediately releasable. + */ +contract VestingWallet057 { + event EtherReleased(uint256 amount); + event ERC20Released(address token, uint256 amount); + + uint256 private _released; + mapping(address => uint256) private _erc20Released; + //address private immutable _beneficiary; //>=0.6.5. Makes them read-only + address private _beneficiary; + //uint64 private immutable _start; //>=0.6.5 + uint64 private _start; + //uint64 private immutable _duration; //>=0.6.5 + uint64 private _duration; + + /** + * @dev Set the beneficiary, start timestamp and vesting duration of the vesting wallet. + */ + constructor( + address beneficiaryAddress, + uint64 startTimestamp, + uint64 durationSeconds + ) public { + require(beneficiaryAddress != address(0), "VestingWallet057: beneficiary is zero address"); + _beneficiary = beneficiaryAddress; + _start = startTimestamp; + _duration = durationSeconds; + } + + /** + * @dev The contract should be able to receive Eth. + */ + function () external payable {} + + /** + * @dev Getter for the beneficiary address. + */ + function beneficiary() public view returns (address) { + return _beneficiary; + } + + /** + * @dev Getter for the start timestamp. + */ + function start() public view returns (uint256) { + return _start; + } + + /** + * @dev Getter for the vesting duration. + */ + function duration() public view returns (uint256) { + return _duration; + } + + /** + * @dev Amount of eth already released + */ + function released() public view returns (uint256) { + return _released; + } + + /** + * @dev Amount of token already released + */ + function released(address token) public view returns (uint256) { + return _erc20Released[token]; + } + + /** + * @dev Release the native token (ether) that have already vested. + * + * Emits a {TokensReleased} event. + */ + function release() public { + uint256 releasable = vestedAmount(uint64(block.timestamp)) - released(); + _released += releasable; + emit EtherReleased(releasable); + address payable b = address(uint160(beneficiary())); + b.send(releasable); + } + + /** + * @dev Release the tokens that have already vested. + * + * Emits a {TokensReleased} event. + */ + function release(address token) public { + uint256 releasable = vestedAmount(token, uint64(block.timestamp)) - released(token); + _erc20Released[token] += releasable; + emit ERC20Released(token, releasable); + SafeERC20.safeTransfer(IERC20(token), beneficiary(), releasable); + } + + /** + * @dev Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. + */ + function vestedAmount(uint64 timestamp) public view returns (uint256) { + return _vestingSchedule(address(this).balance + released(), timestamp); + } + + /** + * @dev Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve. + */ + function vestedAmount(address token, uint64 timestamp) public view returns (uint256) { + return _vestingSchedule(IERC20(token).balanceOf(address(this)) + released(token), timestamp); + } + + /** + * @dev implementation of the vesting formula. This returns the amout vested, as a function of time, for + * an asset given its total historical allocation. + */ + function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view returns (uint256) { + if (timestamp < start()) { + return 0; + } else if (timestamp > start() + duration()) { + return totalAllocation; + } else { + return (totalAllocation * (timestamp - start())) / duration(); + } + } +} diff --git a/sol057/contracts/scheduler057/__init__.py b/sol057/contracts/scheduler057/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/scheduler057/test/__init__.py b/sol057/contracts/scheduler057/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/scheduler057/test/test_VestingWallet057.py b/sol057/contracts/scheduler057/test/test_VestingWallet057.py new file mode 100644 index 0000000000000000000000000000000000000000..0e37af71f48a9cc8917d0b6165c447f5f008e539 --- /dev/null +++ b/sol057/contracts/scheduler057/test/test_VestingWallet057.py @@ -0,0 +1,181 @@ +import brownie +from pytest import approx + +from util.base18 import toBase18 as toWei +from util.base18 import fromBase18 as fromWei +from util.constants import BROWNIE_PROJECT057, GOD_ACCOUNT +from util.tx import txdict, transferETH + +accounts = brownie.network.accounts +chain = brownie.network.chain + + +def test_init(): + vesting_wallet = _vesting_wallet() + assert vesting_wallet.beneficiary() == accounts[1].address + assert vesting_wallet.start() > chain[-1].timestamp + assert vesting_wallet.duration() == 30 + assert vesting_wallet.released() == 0 + + +def test_noFunding(): + vesting_wallet = _vesting_wallet() + chain.mine(blocks=3, timedelta=10) + assert vesting_wallet.released() == 0 + vesting_wallet.release(txdict(accounts[0])) + assert vesting_wallet.released() == 0 # wallet never got funds _to_ release! + + +def test_ethFunding(): + # ensure each account has exactly 30 ETH + for i in range(3): + bal_before = fromWei(accounts[i].balance()) + if bal_before > 30.0: + transferETH(accounts[i], GOD_ACCOUNT, toWei(bal_before - 30.0)) + elif bal_before < 30.0: + transferETH(GOD_ACCOUNT, accounts[i], toWei(30.0 - bal_before)) + transferETH(GOD_ACCOUNT, accounts[i], toWei(1e-4)) # safety margin + + assert fromWei(accounts[0].balance()) == approx(30.0, abs=1e-3) + assert fromWei(accounts[1].balance()) == approx(30.0, abs=1e-3) + + # set up vesting wallet (account). It vests all ETH/tokens that it receives. + # where beneficiary is accounts[1] + start_timestamp = chain[-1].timestamp + 5 # magic number + duration_seconds = 30 # magic number + wallet = BROWNIE_PROJECT057.VestingWallet057.deploy( + accounts[1].address, start_timestamp, duration_seconds, txdict(GOD_ACCOUNT) + ) + + # send ETH to the wallet. It has a function: + # receive() external payable virtual {} + # which allows it to receive ETH. It's called for plain ETH transfers, + # ie every call with empty calldata. + # https://medium.com/coinmonks/solidity-v0-6-0-is-here-things-you-should-know-7d4ab5bca5f1 + transferETH(accounts[0], wallet.address, "30 ether") + assert fromWei(accounts[0].balance()) == approx(0.0, abs=1e-3) + assert fromWei(accounts[1].balance()) == approx(30.0, abs=1e-3) # unchanged so far + assert wallet.vestedAmount(chain[1].timestamp) == 0 + assert wallet.released() == 0 + + for i in range(1000): + chain.mine(blocks=30, timedelta=1000) + vested_amt = fromWei(wallet.vestedAmount(chain[-1].timestamp)) + if vested_amt >= 29.9: + break + assert vested_amt == approx(30.0, abs=1e-3) + assert wallet.released() == 0 + assert fromWei(accounts[1].balance()) == approx(30.0, abs=1e-3) # not released yet! + + # release the ETH. Anyone can call it + wallet.release(txdict(accounts[2])) + for i in range(1000): + chain.mine(blocks=30, timedelta=1000) + released_amt = fromWei(wallet.released()) + if released_amt > 29.9: + break + assert released_amt == approx(30.0, abs=1e-3) # now it's released! + assert fromWei(accounts[1].balance()) == approx( + 30.0 + 30.0, abs=1e-3 + ) # beneficiary is richer + + # put some new ETH into wallet. It's immediately vested, but not released + transferETH(accounts[2], wallet.address, "10 ether") + for i in range(1000): + chain.mine(blocks=30, timedelta=1000) + vested_amt = fromWei(wallet.vestedAmount(chain[-1].timestamp)) + if vested_amt >= 29.9: + break + assert vested_amt == approx(40.0, abs=1e-3) + assert fromWei(wallet.released()) == approx( + 30.0 + 0.0, abs=1e-3 + ) # not released yet! + + # release the new ETH + wallet.release(txdict(accounts[3])) + for i in range(1000): + chain.mine(blocks=30, timedelta=1000) + released_amt = fromWei(wallet.released()) + if released_amt > 29.9: + break + assert released_amt == approx(30.0, abs=1e-3) # now it's released! + assert fromWei(accounts[1].balance()) == approx(60.0, abs=1e-3) + + +def test_tokenFunding(): + # accounts 0, 1, 2 should each start with 100 TOK + token = BROWNIE_PROJECT057.Simpletoken.deploy( + "TOK", "Test Token", 18, toWei(300.0), txdict(accounts[0]) + ) + token.transfer(accounts[1], toWei(100.0), txdict(accounts[0])) + token.transfer(accounts[2], toWei(100.0), txdict(accounts[0])) + + assert token.balanceOf(accounts[0]) / 1e18 == approx(100.0) + assert token.balanceOf(accounts[1]) / 1e18 == approx(100.0) + assert token.balanceOf(accounts[2]) / 1e18 == approx(100.0) + + # account0 should be able to freely transfer TOK + token.transfer(accounts[1], toWei(10.0), txdict(accounts[0])) + assert token.balanceOf(accounts[0]) / 1e18 == approx(90.0) + assert token.balanceOf(accounts[1]) / 1e18 == approx(110.0) + + # set up vesting wallet (account). It vests all ETH/tokens that it receives. + beneficiary_address = accounts[1].address + start_timestamp = chain[-1].timestamp + 5 # magic number + duration_seconds = 30 # magic number + wallet = BROWNIE_PROJECT057.VestingWallet057.deploy( + beneficiary_address, start_timestamp, duration_seconds, txdict(accounts[0]) + ) + + # send TOK to the wallet + token.transfer(wallet.address, toWei(90.0), txdict(accounts[0])) + assert token.balanceOf(accounts[0]) == 0 + assert token.balanceOf(accounts[1]) / 1e18 == approx(110.0) + assert wallet.vestedAmount(token.address, chain[-1].timestamp) == 0 + assert wallet.released(token.address) == 0 + + # make enough time pass for everything to vest + chain.mine(blocks=3, timedelta=100) + assert wallet.vestedAmount(token.address, chain[-1].timestamp) / 1e18 == approx( + 90.0 + ) + assert wallet.released(token.address) == 0 + assert token.balanceOf(accounts[1]) / 1e18 == approx(110.0) # not released yet! + + # release the TOK. Anyone can call it + wallet.release(token.address, txdict(accounts[2])) + assert wallet.released(token.address) / 1e18 == approx(90.0) # now it's released! + assert token.balanceOf(accounts[1]) / 1e18 == approx(200.0) # beneficiary is richer + + # put some new TOK into wallet. It's immediately vested, but not released + token.transfer(wallet.address, toWei(10.0), txdict(accounts[2])) + assert wallet.vestedAmount(token.address, chain[-1].timestamp) / 1e18 == approx( + 100.0 + ) + assert wallet.released(token.address) / 1e18 == approx(90.0) # not released yet! + + # release the new TOK + wallet.release(token.address, txdict(accounts[3])) + assert wallet.released(token.address) / 1e18 == approx( + 100.0 + ) # now new TOK is released! + assert token.balanceOf(accounts[1]) / 1e18 == approx( + 210.0 + ) # beneficiary got +10 ETH + + +def _vesting_wallet(): + # note: eth timestamps are in unix time (seconds since jan 1, 1970) + beneficiary_address = accounts[1].address + + start_timestamp = chain[-1].timestamp + 5 # magic number + + duration_seconds = 30 # magic number + + w = BROWNIE_PROJECT057.VestingWallet057.deploy( + beneficiary_address, + start_timestamp, + duration_seconds, + txdict(accounts[0]), + ) + return w diff --git a/sol057/contracts/simpletoken/Simpletoken.sol b/sol057/contracts/simpletoken/Simpletoken.sol new file mode 100644 index 0000000000000000000000000000000000000000..2b0df3d5a3d5b81eb9df848dba73fa48221f373d --- /dev/null +++ b/sol057/contracts/simpletoken/Simpletoken.sol @@ -0,0 +1,87 @@ +pragma solidity ^0.5.7; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) + +import "OpenZeppelin/openzeppelin-contracts@2.1.1/contracts/math/SafeMath.sol"; + +contract Simpletoken { + + using SafeMath for uint256; + + string public symbol; + string public name; + uint256 public decimals; + uint256 public totalSupply; + + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowed; + + + event Transfer(address from, address to, uint256 value); + event Approval(address owner, address spender, uint256 value); + + constructor( + string memory _symbol, + string memory _name, + uint256 _decimals, + uint256 _totalSupply + ) + public + { + symbol = _symbol; + name = _name; + decimals = _decimals; + totalSupply = _totalSupply; + balances[msg.sender] = _totalSupply; + emit Transfer(address(0), msg.sender, _totalSupply); + } + + //fallback () external payable { // >= 0.6.0 + function () external payable { //0.5.x + revert(); + } + + function balanceOf(address _owner) public view returns (uint256) { + return balances[_owner]; + } + + function allowance( + address _owner, + address _spender + ) + public + view + returns (uint256) + { + return allowed[_owner][_spender]; + } + + function approve(address _spender, uint256 _value) public returns (bool) { + allowed[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + function transfer(address _to, uint256 _value) public returns (bool) { + balances[msg.sender] = balances[msg.sender].sub(_value); + balances[_to] = balances[_to].add(_value); + emit Transfer(msg.sender, _to, _value); + return true; + } + + function transferFrom( + address _from, + address _to, + uint256 _value + ) + public + returns (bool) + { + require(allowed[_from][msg.sender] >=_value, "Insufficient allowance"); + balances[_from] = balances[_from].sub(_value); + allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); + balances[_to] = balances[_to].add(_value); + emit Transfer(_from, _to, _value); + return true; + } + +} diff --git a/sol057/contracts/simpletoken/__init__.py b/sol057/contracts/simpletoken/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/simpletoken/test/__init__.py b/sol057/contracts/simpletoken/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol057/contracts/simpletoken/test/test_Simpletoken.py b/sol057/contracts/simpletoken/test/test_Simpletoken.py new file mode 100644 index 0000000000000000000000000000000000000000..0b46433f2609ac90f21a0cce8ca5cc1ebd8a4ec4 --- /dev/null +++ b/sol057/contracts/simpletoken/test/test_Simpletoken.py @@ -0,0 +1,41 @@ +import brownie + +from util.constants import BROWNIE_PROJECT057 +from util.tx import txdict + +accounts = brownie.network.accounts + + +def test_transfer(): + token = _deployToken() + assert token.totalSupply() == 1e21 + token.transfer(accounts[1], 1e20, txdict(accounts[0])) + assert token.balanceOf(accounts[1]) == 1e20 + assert token.balanceOf(accounts[0]) == 9e20 + + +def test_approve(): + token = _deployToken() + token.approve(accounts[1], 1e19, txdict(accounts[0])) + assert token.allowance(accounts[0], accounts[1]) == 1e19 + assert token.allowance(accounts[0], accounts[2]) == 0 + + token.approve(accounts[1], 6e18, txdict(accounts[0])) + assert token.allowance(accounts[0], accounts[1]) == 6e18 + + +def test_transferFrom(): + token = _deployToken() + token.approve(accounts[1], 6e18, txdict(accounts[0])) + token.transferFrom(accounts[0], accounts[2], 5e18, txdict(accounts[1])) + + assert token.balanceOf(accounts[2]) == 5e18 + assert token.balanceOf(accounts[1]) == 0 + assert token.balanceOf(accounts[0]) == 9.95e20 + assert token.allowance(accounts[0], accounts[1]) == 1e18 + + +def _deployToken(): + return BROWNIE_PROJECT057.Simpletoken.deploy( + "TST", "Test Token", 18, 1e21, txdict(accounts[0]) + ) diff --git a/sol080/__init__.py b/sol080/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol080/contracts/__init__.py b/sol080/contracts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol080/contracts/oceanv4/ERC721Factory.sol b/sol080/contracts/oceanv4/ERC721Factory.sol new file mode 100644 index 0000000000000000000000000000000000000000..1f06439e022a8b8cde36adff714a18c701f02107 --- /dev/null +++ b/sol080/contracts/oceanv4/ERC721Factory.sol @@ -0,0 +1,818 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "./utils/Deployer.sol"; +import "./interfaces/IERC721Template.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/access/Ownable.sol"; +import "./interfaces/IERC20Template.sol"; +import "./interfaces/IERC721Template.sol"; +import "./interfaces/IERC20.sol"; +import "./utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/security/ReentrancyGuard.sol"; +/** + * @title DTFactory contract + * @author Ocean Protocol Team + * + * @dev Implementation of Ocean datatokens Factory + * + * DTFactory deploys datatoken proxy contracts. + * New datatoken proxy contracts are links to the template contract's bytecode. + * Proxy contract functionality is based on Ocean Protocol custom implementation of ERC1167 standard. + */ +contract ERC721Factory is Deployer, Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + using SafeMath for uint256; + address private communityFeeCollector; + uint256 private currentNFTCount; + address private erc20Factory; + uint256 private nftTemplateCount; + + struct Template { + address templateAddress; + bool isActive; + } + + mapping(uint256 => Template) public nftTemplateList; + + mapping(uint256 => Template) public templateList; + + mapping(address => address) public erc721List; + + mapping(address => bool) public erc20List; + + event NFTCreated( + address indexed newTokenAddress, + address indexed templateAddress, + string tokenName, + address admin, + string symbol, + string tokenURI + ); + + uint256 private currentTokenCount = 0; + uint256 public templateCount; + address public router; + + event Template721Added(address indexed _templateAddress, uint256 indexed nftTemplateCount); + event Template20Added(address indexed _templateAddress, uint256 indexed nftTemplateCount); + //stored here only for ABI reasons + event TokenCreated( + address indexed newTokenAddress, + address indexed templateAddress, + string name, + string symbol, + uint256 cap, + address creator + ); + + event NewPool( + address poolAddress, + address ssContract, + address baseTokenAddress + ); + + + event NewFixedRate(bytes32 exchangeId, address indexed owner, address exchangeContract, address indexed baseToken); + event NewDispenser(address dispenserContract); + + event DispenserCreated( // emited when a dispenser is created + address indexed datatokenAddress, + address indexed owner, + uint256 maxTokens, + uint256 maxBalance, + address allowedSwapper + ); + + + /** + * @dev constructor + * Called on contract deployment. Could not be called with zero address parameters. + * @param _template refers to the address of a deployed datatoken contract. + * @param _collector refers to the community fee collector address + * @param _router router contract address + */ + constructor( + address _template721, + address _template, + address _collector, + address _router + ) { + require( + _template != address(0) && + _collector != address(0) && + _template721 != address(0), + "ERC721DTFactory: Invalid template token/community fee collector address" + ); + require(_router != address(0), "ERC721DTFactory: Invalid router address"); + add721TokenTemplate(_template721); + addTokenTemplate(_template); + router = _router; + communityFeeCollector = _collector; + } + + + /** + * @dev deployERC721Contract + * + * @param name NFT name + * @param symbol NFT Symbol + * @param _templateIndex template index we want to use + * @param additionalERC20Deployer if != address(0), we will add it with ERC20Deployer role + * @param additionalMetaDataUpdater if != address(0), we will add it with updateMetadata role + */ + + function deployERC721Contract( + string memory name, + string memory symbol, + uint256 _templateIndex, + address additionalERC20Deployer, + address additionalMetaDataUpdater, + string memory tokenURI + ) public returns (address token) { + require( + _templateIndex <= nftTemplateCount && _templateIndex != 0, + "ERC721DTFactory: Template index doesnt exist" + ); + Template memory tokenTemplate = nftTemplateList[_templateIndex]; + + require( + tokenTemplate.isActive, + "ERC721DTFactory: ERC721Token Template disabled" + ); + + token = deploy(tokenTemplate.templateAddress); + + require( + token != address(0), + "ERC721DTFactory: Failed to perform minimal deploy of a new token" + ); + + erc721List[token] = token; + + IERC721Template tokenInstance = IERC721Template(token); + require( + tokenInstance.initialize( + msg.sender, + name, + symbol, + address(this), + additionalERC20Deployer, + additionalMetaDataUpdater, + tokenURI + ), + "ERC721DTFactory: Unable to initialize token instance" + ); + + emit NFTCreated(token, tokenTemplate.templateAddress, name, msg.sender, symbol, tokenURI); + currentNFTCount += 1; + } + + /** + * @dev get the current token count. + * @return the current token count + */ + function getCurrentNFTCount() external view returns (uint256) { + return currentNFTCount; + } + + /** + * @dev get the token template Object + * @param _index template Index + * @return the template struct + */ + function getNFTTemplate(uint256 _index) + external + view + returns (Template memory) + { + Template memory template = nftTemplateList[_index]; + return template; + } + + /** + * @dev add a new NFT Template. + Only Factory Owner can call it + * @param _templateAddress new template address + * @return the actual template count + */ + + function add721TokenTemplate(address _templateAddress) + public + onlyOwner + returns (uint256) + { + require( + _templateAddress != address(0), + "ERC721DTFactory: ERC721 template address(0) NOT ALLOWED" + ); + require(isContract(_templateAddress), "ERC721Factory: NOT CONTRACT"); + nftTemplateCount += 1; + Template memory template = Template(_templateAddress, true); + nftTemplateList[nftTemplateCount] = template; + emit Template721Added(_templateAddress,nftTemplateCount); + return nftTemplateCount; + } + /** + * @dev reactivate a disabled NFT Template. + Only Factory Owner can call it + * @param _index index we want to reactivate + */ + + // function to activate a disabled token. + function reactivate721TokenTemplate(uint256 _index) external onlyOwner { + require( + _index <= nftTemplateCount && _index != 0, + "ERC721DTFactory: Template index doesnt exist" + ); + Template storage template = nftTemplateList[_index]; + template.isActive = true; + } + + /** + * @dev disable an NFT Template. + Only Factory Owner can call it + * @param _index index we want to disable + */ + function disable721TokenTemplate(uint256 _index) external onlyOwner { + require( + _index <= nftTemplateCount && _index != 0, + "ERC721DTFactory: Template index doesnt exist" + ); + Template storage template = nftTemplateList[_index]; + template.isActive = false; + } + + function getCurrentNFTTemplateCount() external view returns (uint256) { + return nftTemplateCount; + } + + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { + size := extcodesize(account) + } + return size > 0; + } + + + struct tokenStruct{ + string[] strings; + address[] addresses; + uint256[] uints; + bytes[] bytess; + address owner; + } + /** + * @dev Deploys new datatoken proxy contract. + * This function is not called directly from here. It's called from the NFT contract. + An NFT contract can deploy multiple ERC20 tokens. + * @param _templateIndex ERC20Template index + * @param strings refers to an array of strings + * [0] = name + * [1] = symbol + * @param addresses refers to an array of addresses + * [0] = minter account who can mint datatokens (can have multiple minters) + * [1] = feeManager initial feeManager for this DT + * [2] = publishing Market Address + * [3] = publishing Market Fee Token + * @param uints refers to an array of uints + * [0] = cap_ the total ERC20 cap + * [1] = publishing Market Fee Amount + * @param bytess refers to an array of bytes, not in use now, left for future templates + * @return token address of a new proxy datatoken contract + */ + function createToken( + uint256 _templateIndex, + string[] memory strings, + address[] memory addresses, + uint256[] memory uints, + bytes[] memory bytess + ) external returns (address token) { + require( + erc721List[msg.sender] == msg.sender, + "ERC721Factory: ONLY ERC721 INSTANCE FROM ERC721FACTORY" + ); + token = _createToken(_templateIndex, strings, addresses, uints, bytess, msg.sender); + + } + function _createToken( + uint256 _templateIndex, + string[] memory strings, + address[] memory addresses, + uint256[] memory uints, + bytes[] memory bytess, + address owner + ) internal returns (address token) { + require(uints[0] != 0, "ERC20Factory: zero cap is not allowed"); + require( + _templateIndex <= templateCount && _templateIndex != 0, + "ERC20Factory: Template index doesnt exist" + ); + Template memory tokenTemplate = templateList[_templateIndex]; + + require( + tokenTemplate.isActive, + "ERC20Factory: ERC721Token Template disabled" + ); + token = deploy(tokenTemplate.templateAddress); + erc20List[token] = true; + + require( + token != address(0), + "ERC721Factory: Failed to perform minimal deploy of a new token" + ); + emit TokenCreated(token, tokenTemplate.templateAddress, strings[0], strings[1], uints[0], owner); + currentTokenCount += 1; + tokenStruct memory tokenData = tokenStruct(strings,addresses,uints,bytess,owner); + // tokenData.strings = strings; + // tokenData.addresses = addresses; + // tokenData.uints = uints; + // tokenData.owner = owner; + // tokenData.bytess = bytess; + _createTokenStep2(token, tokenData); + } + + function _createTokenStep2(address token, tokenStruct memory tokenData) internal { + + IERC20Template tokenInstance = IERC20Template(token); + address[] memory factoryAddresses = new address[](3); + factoryAddresses[0] = tokenData.owner; + + factoryAddresses[1] = communityFeeCollector; + + factoryAddresses[2] = router; + + require( + tokenInstance.initialize( + tokenData.strings, + tokenData.addresses, + factoryAddresses, + tokenData.uints, + tokenData.bytess + ), + "ERC20Factory: Unable to initialize token instance" + ); + + } + + /** + * @dev get the current ERC20token deployed count. + * @return the current token count + */ + function getCurrentTokenCount() external view returns (uint256) { + return currentTokenCount; + } + + /** + * @dev get the current ERC20token template. + @param _index template Index + * @return the token Template Object + */ + + function getTokenTemplate(uint256 _index) + external + view + returns (Template memory) + { + Template memory template = templateList[_index]; + require( + _index <= templateCount && _index != 0, + "ERC20Factory: Template index doesnt exist" + ); + return template; + } + + /** + * @dev add a new ERC20Template. + Only Factory Owner can call it + * @param _templateAddress new template address + * @return the actual template count + */ + + + function addTokenTemplate(address _templateAddress) + public + onlyOwner + returns (uint256) + { + require( + _templateAddress != address(0), + "ERC20Factory: ERC721 template address(0) NOT ALLOWED" + ); + require(isContract(_templateAddress), "ERC20Factory: NOT CONTRACT"); + templateCount += 1; + Template memory template = Template(_templateAddress, true); + templateList[templateCount] = template; + emit Template20Added(_templateAddress, templateCount); + return templateCount; + } + + /** + * @dev disable an ERC20Template. + Only Factory Owner can call it + * @param _index index we want to disable + */ + + function disableTokenTemplate(uint256 _index) external onlyOwner { + Template storage template = templateList[_index]; + template.isActive = false; + } + + + /** + * @dev reactivate a disabled ERC20Template. + Only Factory Owner can call it + * @param _index index we want to reactivate + */ + + // function to activate a disabled token. + function reactivateTokenTemplate(uint256 _index) external onlyOwner { + require( + _index <= templateCount && _index != 0, + "ERC20DTFactory: Template index doesnt exist" + ); + Template storage template = templateList[_index]; + template.isActive = true; + } + + // if templateCount is public we could remove it, or set templateCount to private + function getCurrentTemplateCount() external view returns (uint256) { + return templateCount; + } + + struct tokenOrder { + address tokenAddress; + address consumer; + uint256 serviceIndex; + IERC20Template.providerFee _providerFee; + IERC20Template.consumeMarketFee _consumeMarketFee; + } + + /** + * @dev startMultipleTokenOrder + * Used as a proxy to order multiple services + * Users can have inifinite approvals for fees for factory instead of having one approval/ erc20 contract + * Requires previous approval of all : + * - consumeFeeTokens + * - publishMarketFeeTokens + * - erc20 datatokens + * - providerFees + * @param orders an array of struct tokenOrder + */ + function startMultipleTokenOrder( + tokenOrder[] memory orders + ) external nonReentrant { + // TODO: to avoid DOS attack, we set a limit to maximum order (50 ?) + require(orders.length <= 50, 'ERC721Factory: Too Many Orders'); + // TO DO. We can do better here , by groupping publishMarketFeeTokens and consumeFeeTokens and have a single + // transfer for each one, instead of doing it per dt.. + for (uint256 i = 0; i < orders.length; i++) { + (address publishMarketFeeAddress, address publishMarketFeeToken, uint256 publishMarketFeeAmount) + = IERC20Template(orders[i].tokenAddress).getPublishingMarketFee(); + + // check if we have publishFees, if so transfer them to us and approve dttemplate to take them + if (publishMarketFeeAmount > 0 && publishMarketFeeToken!=address(0) + && publishMarketFeeAddress!=address(0)) { + _pullUnderlying(publishMarketFeeToken,msg.sender, + address(this), + publishMarketFeeAmount); + IERC20(publishMarketFeeToken).safeIncreaseAllowance(orders[i].tokenAddress, publishMarketFeeAmount); + } + // check if we have consumeMarketFee, if so transfer them to us and approve dttemplate to take them + if (orders[i]._consumeMarketFee.consumeMarketFeeAmount > 0 + && orders[i]._consumeMarketFee.consumeMarketFeeAddress!=address(0) + && orders[i]._consumeMarketFee.consumeMarketFeeToken!=address(0)) { + _pullUnderlying(orders[i]._consumeMarketFee.consumeMarketFeeToken,msg.sender, + address(this), + orders[i]._consumeMarketFee.consumeMarketFeeAmount); + IERC20(orders[i]._consumeMarketFee.consumeMarketFeeToken) + .safeIncreaseAllowance(orders[i].tokenAddress, orders[i]._consumeMarketFee.consumeMarketFeeAmount); + } + // handle provider fees + if (orders[i]._providerFee.providerFeeAmount > 0 && orders[i]._providerFee.providerFeeToken!=address(0) + && orders[i]._providerFee.providerFeeAddress!=address(0)) { + _pullUnderlying(orders[i]._providerFee.providerFeeToken,msg.sender, + address(this), + orders[i]._providerFee.providerFeeAmount); + IERC20(orders[i]._providerFee.providerFeeToken) + .safeIncreaseAllowance(orders[i].tokenAddress, orders[i]._providerFee.providerFeeAmount); + } + // transfer erc20 datatoken from consumer to us + _pullUnderlying(orders[i].tokenAddress,msg.sender, + address(this), + 1e18); + IERC20Template(orders[i].tokenAddress).startOrder( + orders[i].consumer, + orders[i].serviceIndex, + orders[i]._providerFee, + orders[i]._consumeMarketFee + ); + } + } + + struct reuseTokenOrder { + address tokenAddress; + bytes32 orderTxId; + IERC20Template.providerFee _providerFee; + } + /** + * @dev reuseMultipleTokenOrder + * Used as a proxy to order multiple reuses + * Users can have inifinite approvals for fees for factory instead of having one approval/ erc20 contract + * Requires previous approval of all : + * - consumeFeeTokens + * - publishMarketFeeTokens + * - erc20 datatokens + * - providerFees + * @param orders an array of struct tokenOrder + */ + function reuseMultipleTokenOrder( + reuseTokenOrder[] memory orders + ) external nonReentrant { + // TODO: to avoid DOS attack, we set a limit to maximum order (50 ?) + require(orders.length <= 50, 'ERC721Factory: Too Many Orders'); + // TO DO. We can do better here , by groupping publishMarketFeeTokens and consumeFeeTokens and have a single + // transfer for each one, instead of doing it per dt.. + for (uint256 i = 0; i < orders.length; i++) { + // handle provider fees + if (orders[i]._providerFee.providerFeeAmount > 0 && orders[i]._providerFee.providerFeeToken!=address(0) + && orders[i]._providerFee.providerFeeAddress!=address(0)) { + _pullUnderlying(orders[i]._providerFee.providerFeeToken,msg.sender, + address(this), + orders[i]._providerFee.providerFeeAmount); + IERC20(orders[i]._providerFee.providerFeeToken) + .safeIncreaseAllowance(orders[i].tokenAddress, orders[i]._providerFee.providerFeeAmount); + } + + IERC20Template(orders[i].tokenAddress).reuseOrder( + orders[i].orderTxId, + orders[i]._providerFee + ); + } + } + + // helper functions to save number of transactions + struct NftCreateData{ + string name; + string symbol; + uint256 templateIndex; + string tokenURI; + } + struct ErcCreateData{ + uint256 templateIndex; + string[] strings; + address[] addresses; + uint256[] uints; + bytes[] bytess; + } + + + + + + /** + * @dev createNftWithErc20 + * Creates a new NFT, then a ERC20,all in one call + * @param _NftCreateData input data for nft creation + * @param _ErcCreateData input data for erc20 creation + + */ + function createNftWithErc20( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData + ) external nonReentrant returns (address erc721Address, address erc20Address){ + //we are adding ourselfs as a ERC20 Deployer, because we need it in order to deploy the pool + erc721Address = deployERC721Contract( + _NftCreateData.name, + _NftCreateData.symbol, + _NftCreateData.templateIndex, + address(this), + address(0), + _NftCreateData.tokenURI); + erc20Address = IERC721Template(erc721Address).createERC20( + _ErcCreateData.templateIndex, + _ErcCreateData.strings, + _ErcCreateData.addresses, + _ErcCreateData.uints, + _ErcCreateData.bytess + ); + // remove our selfs from the erc20DeployerRole + IERC721Template(erc721Address).removeFromCreateERC20List(address(this)); + } + + struct PoolData{ + uint256[] ssParams; + uint256[] swapFees; + address[] addresses; + } + + /** + * @dev createNftWithErc20WithPool + * Creates a new NFT, then a ERC20, then a Pool, all in one call + * Use this carefully, because if Pool creation fails, you are still going to pay a lot of gas + * @param _NftCreateData input data for NFT Creation + * @param _ErcCreateData input data for ERC20 Creation + * @param _PoolData input data for Pool Creation + */ + function createNftWithErc20WithPool( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData, + PoolData calldata _PoolData + ) external nonReentrant returns (address erc721Address, address erc20Address, address poolAddress){ + _pullUnderlying(_PoolData.addresses[1],msg.sender, + address(this), + _PoolData.ssParams[4]); + //we are adding ourselfs as a ERC20 Deployer, because we need it in order to deploy the pool + erc721Address = deployERC721Contract( + _NftCreateData.name, + _NftCreateData.symbol, + _NftCreateData.templateIndex, + address(this), + address(0), + _NftCreateData.tokenURI); + erc20Address = IERC721Template(erc721Address).createERC20( + _ErcCreateData.templateIndex, + _ErcCreateData.strings, + _ErcCreateData.addresses, + _ErcCreateData.uints, + _ErcCreateData.bytess + ); + // allow router to take the liquidity + IERC20(_PoolData.addresses[1]).safeIncreaseAllowance(router,_PoolData.ssParams[4]); + + poolAddress = IERC20Template(erc20Address).deployPool( + _PoolData.ssParams, + _PoolData.swapFees, + _PoolData.addresses + ); + // remove our selfs from the erc20DeployerRole + IERC721Template(erc721Address).removeFromCreateERC20List(address(this)); + + } + + struct FixedData{ + address fixedPriceAddress; + address[] addresses; + uint256[] uints; + } + /** + * @dev createNftWithErc20WithFixedRate + * Creates a new NFT, then a ERC20, then a FixedRateExchange, all in one call + * Use this carefully, because if Fixed Rate creation fails, you are still going to pay a lot of gas + * @param _NftCreateData input data for NFT Creation + * @param _ErcCreateData input data for ERC20 Creation + * @param _FixedData input data for FixedRate Creation + */ + function createNftWithErc20WithFixedRate( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData, + FixedData calldata _FixedData + ) external nonReentrant returns (address erc721Address, address erc20Address, bytes32 exchangeId){ + //we are adding ourselfs as a ERC20 Deployer, because we need it in order to deploy the fixedrate + erc721Address = deployERC721Contract( + _NftCreateData.name, + _NftCreateData.symbol, + _NftCreateData.templateIndex, + address(this), + address(0), + _NftCreateData.tokenURI); + erc20Address = IERC721Template(erc721Address).createERC20( + _ErcCreateData.templateIndex, + _ErcCreateData.strings, + _ErcCreateData.addresses, + _ErcCreateData.uints, + _ErcCreateData.bytess + ); + exchangeId = IERC20Template(erc20Address).createFixedRate( + _FixedData.fixedPriceAddress, + _FixedData.addresses, + _FixedData.uints + ); + // remove our selfs from the erc20DeployerRole + IERC721Template(erc721Address).removeFromCreateERC20List(address(this)); + } + + struct DispenserData{ + address dispenserAddress; + uint256 maxTokens; + uint256 maxBalance; + bool withMint; + address allowedSwapper; + } + /** + * @dev createNftWithErc20WithDispenser + * Creates a new NFT, then a ERC20, then a Dispenser, all in one call + * Use this carefully + * @param _NftCreateData input data for NFT Creation + * @param _ErcCreateData input data for ERC20 Creation + * @param _DispenserData input data for Dispenser Creation + */ + function createNftWithErc20WithDispenser( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData, + DispenserData calldata _DispenserData + ) external nonReentrant returns (address erc721Address, address erc20Address){ + //we are adding ourselfs as a ERC20 Deployer, because we need it in order to deploy the fixedrate + erc721Address = deployERC721Contract( + _NftCreateData.name, + _NftCreateData.symbol, + _NftCreateData.templateIndex, + address(this), + address(0), + _NftCreateData.tokenURI); + erc20Address = IERC721Template(erc721Address).createERC20( + _ErcCreateData.templateIndex, + _ErcCreateData.strings, + _ErcCreateData.addresses, + _ErcCreateData.uints, + _ErcCreateData.bytess + ); + IERC20Template(erc20Address).createDispenser( + _DispenserData.dispenserAddress, + _DispenserData.maxTokens, + _DispenserData.maxBalance, + _DispenserData.withMint, + _DispenserData.allowedSwapper + ); + // remove our selfs from the erc20DeployerRole + IERC721Template(erc721Address).removeFromCreateERC20List(address(this)); + } + + + + struct MetaData { + uint8 _metaDataState; + string _metaDataDecryptorUrl; + string _metaDataDecryptorAddress; + bytes flags; + bytes data; + bytes32 _metaDataHash; + IERC721Template.metaDataProof[] _metadataProofs; + } + + /** + * @dev createNftWithMetaData + * Creates a new NFT, then sets the metadata, all in one call + * Use this carefully + * @param _NftCreateData input data for NFT Creation + * @param _MetaData input metadata + */ + function createNftWithMetaData( + NftCreateData calldata _NftCreateData, + MetaData calldata _MetaData + ) external nonReentrant returns (address erc721Address){ + //we are adding ourselfs as a ERC20 Deployer, because we need it in order to deploy the fixedrate + erc721Address = deployERC721Contract( + _NftCreateData.name, + _NftCreateData.symbol, + _NftCreateData.templateIndex, + address(0), + address(this), + _NftCreateData.tokenURI); + // set metadata + IERC721Template(erc721Address).setMetaData(_MetaData._metaDataState, _MetaData._metaDataDecryptorUrl + , _MetaData._metaDataDecryptorAddress, _MetaData.flags, + _MetaData.data,_MetaData._metaDataHash, _MetaData._metadataProofs); + // remove our selfs from the erc20DeployerRole + IERC721Template(erc721Address).removeFromMetadataList(address(this)); + } + + + function _pullUnderlying( + address erc20, + address from, + address to, + uint256 amount + ) internal { + uint256 balanceBefore = IERC20(erc20).balanceOf(to); + IERC20(erc20).safeTransferFrom(from, to, amount); + require(IERC20(erc20).balanceOf(to) >= balanceBefore.add(amount), + "Transfer amount is too low"); + } + +} diff --git a/sol080/contracts/oceanv4/__init__.py b/sol080/contracts/oceanv4/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol080/contracts/oceanv4/communityFee/OPFCommunityFeeCollector.sol b/sol080/contracts/oceanv4/communityFee/OPFCommunityFeeCollector.sol new file mode 100644 index 0000000000000000000000000000000000000000..307c975730ba7f302f0642985933230025a78a3e --- /dev/null +++ b/sol080/contracts/oceanv4/communityFee/OPFCommunityFeeCollector.sol @@ -0,0 +1,98 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 +import '../interfaces/IERC20.sol'; +import 'OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/access/Ownable.sol'; +import '../utils/SafeERC20.sol'; + + +/** + * @title OPFCommunityFeeCollector + * @dev Ocean Protocol Foundation Community Fee Collector contract + * allows consumers to pay very small fee as part of the exchange of + * datatokens with ocean token in order to support the community of + * ocean protocol and provide a sustainble development. + */ +contract OPFCommunityFeeCollector is Ownable { + using SafeERC20 for IERC20; + address payable private collector; + /** + * @dev constructor + * Called prior contract deployment. set the controller address and + * the contract owner address + * @param newCollector the fee collector address. + * @param OPFOwnerAddress the contract owner address + */ + constructor( + address payable newCollector, + address OPFOwnerAddress + ) + public + Ownable() + { + require( + newCollector != address(0)&& + OPFOwnerAddress != address(0), + 'OPFCommunityFeeCollector: collector address or owner is invalid address' + ); + collector = newCollector; + transferOwnership(OPFOwnerAddress); + } + /** + * @dev fallback function + * this is a default fallback function in which receives + * the collected ether. + */ + fallback() external payable {} + + /** + * @dev withdrawETH + * transfers all the accumlated ether the collector address + */ + function withdrawETH() + external + payable + { + collector.transfer(address(this).balance); + } + + /** + * @dev withdrawToken + * transfers all the accumlated tokens the collector address + * @param tokenAddress the token contract address + */ + function withdrawToken( + address tokenAddress + ) + external + { + require( + tokenAddress != address(0), + 'OPFCommunityFeeCollector: invalid token contract address' + ); + + IERC20(tokenAddress).safeTransfer( + collector, + IERC20(tokenAddress).balanceOf(address(this)) + ); + } + + /** + * @dev changeCollector + * change the current collector address. Only owner can do that. + * @param newCollector the new collector address + */ + function changeCollector( + address payable newCollector + ) + external + onlyOwner + { + require( + newCollector != address(0), + 'OPFCommunityFeeCollector: invalid collector address' + ); + collector = newCollector; + } +} diff --git a/sol080/contracts/oceanv4/interfaces/IDispenser.sol b/sol080/contracts/oceanv4/interfaces/IDispenser.sol new file mode 100644 index 0000000000000000000000000000000000000000..ec7447f4331dbdbcf5d72d71eafc9377e2215f76 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IDispenser.sol @@ -0,0 +1,22 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IDispenser { + + function status(address datatoken) external view + returns(bool active,address owner,bool isMinter,uint256 maxTokens,uint256 maxBalance, uint256 balance); + + function create( + address datatoken,uint256 maxTokens, uint256 maxBalance, address owner, address allowedSwapper) external; + function activate(address datatoken,uint256 maxTokens, uint256 maxBalance) external; + + function deactivate(address datatoken) external; + + function dispense(address datatoken, uint256 amount, address destination) external; + + function ownerWithdraw(address datatoken) external; + function setAllowedSwapper(address datatoken, address newAllowedSwapper) external; + function getId() pure external returns (uint8); +} diff --git a/sol080/contracts/oceanv4/interfaces/IERC1271.sol b/sol080/contracts/oceanv4/interfaces/IERC1271.sol new file mode 100644 index 0000000000000000000000000000000000000000..20f800f57430196a69608b2ace2e127188e98093 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IERC1271.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity 0.8.10; + +/** + * @notice ERC-1271: Standard Signature Validation Method for Contracts + */ +interface IERC1271 { + +// bytes4 internal constant _ERC1271MAGICVALUE = 0x1626ba7e; +// bytes4 internal constant _ERC1271FAILVALUE = 0xffffffff; + + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param _hash hash of the data signed//Arbitrary length data signed on the behalf of address(this) + * @param _signature Signature byte array associated with _data + * + * @return magicValue either 0x1626ba7e on success or 0xffffffff failure + */ + function isValidSignature( + bytes32 _hash, //bytes memory _data, + bytes calldata _signature + ) + external + view + returns (bytes4 magicValue); +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/interfaces/IERC20.sol b/sol080/contracts/oceanv4/interfaces/IERC20.sol new file mode 100644 index 0000000000000000000000000000000000000000..64a93ff773b35ed605aef84f7821f2aad97f782d --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IERC20.sol @@ -0,0 +1,80 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + function decimals() external view returns (uint8); + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/sol080/contracts/oceanv4/interfaces/IERC20Template.sol b/sol080/contracts/oceanv4/interfaces/IERC20Template.sol new file mode 100644 index 0000000000000000000000000000000000000000..948fbf4a09a19515b6016ac054076e09c555ad78 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IERC20Template.sol @@ -0,0 +1,135 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IERC20Template { + struct RolesERC20 { + bool minter; + bool feeManager; + } + struct providerFee{ + address providerFeeAddress; + address providerFeeToken; // address of the token marketplace wants to add fee on top + uint256 providerFeeAmount; // amount to be transfered to marketFeeCollector + uint8 v; // v of provider signed message + bytes32 r; // r of provider signed message + bytes32 s; // s of provider signed message + uint256 validUntil; //validity expresses in unix timestamp + bytes providerData; //data encoded by provider + } + struct consumeMarketFee{ + address consumeMarketFeeAddress; + address consumeMarketFeeToken; // address of the token marketplace wants to add fee on top + uint256 consumeMarketFeeAmount; // amount to be transfered to marketFeeCollector + } + function initialize( + string[] calldata strings_, + address[] calldata addresses_, + address[] calldata factoryAddresses_, + uint256[] calldata uints_, + bytes[] calldata bytes_ + ) external returns (bool); + + function name() external pure returns (string memory); + + function symbol() external pure returns (string memory); + + function decimals() external pure returns (uint8); + + function totalSupply() external view returns (uint256); + + function cap() external view returns (uint256); + + function balanceOf(address owner) external view returns (uint256); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function transfer(address to, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + function mint(address account, uint256 value) external; + + function isMinter(address account) external view returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function PERMIT_TYPEHASH() external pure returns (bytes32); + + function nonces(address owner) external view returns (uint256); + + function permissions(address user) + external + view + returns (RolesERC20 memory); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function cleanFrom721() external; + + function deployPool( + uint256[] memory ssParams, + uint256[] memory swapFees, + address[] memory addresses + ) external returns (address); + + function createFixedRate( + address fixedPriceAddress, + address[] memory addresses, + uint[] memory uints + ) external returns (bytes32); + function createDispenser( + address _dispenser, + uint256 maxTokens, + uint256 maxBalance, + bool withMint, + address allowedSwapper) external; + + function getPublishingMarketFee() external view returns (address , address, uint256); + function setPublishingMarketFee( + address _publishMarketFeeAddress, address _publishMarketFeeToken, uint256 _publishMarketFeeAmount + ) external; + + function startOrder( + address consumer, + uint256 serviceId, + providerFee calldata _providerFee, + consumeMarketFee calldata _consumeMarketFee + ) external; + + function reuseOrder( + bytes32 orderTxId, + providerFee calldata _providerFee + ) external; + + function burn(uint256 amount) external; + function burnFrom(address account, uint256 amount) external; + function getERC721Address() external view returns (address); + function isERC20Deployer(address user) external returns(bool); + function getPools() external view returns(address[] memory); + struct fixedRate{ + address contractAddress; + bytes32 id; + } + function getFixedRates() external view returns(fixedRate[] memory); + function getDispensers() external view returns(address[] memory); + function getId() pure external returns (uint8); +} diff --git a/sol080/contracts/oceanv4/interfaces/IERC721Template.sol b/sol080/contracts/oceanv4/interfaces/IERC721Template.sol new file mode 100644 index 0000000000000000000000000000000000000000..229d22cc0786fa453a1de93e966911d59caf7c78 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IERC721Template.sol @@ -0,0 +1,238 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/introspection/IERC165.sol"; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721Template is IERC165 { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + event MetadataCreated( + address indexed createdBy, + uint8 state, + string decryptorUrl, + bytes flags, + bytes data, + string metaDataDecryptorAddress, + uint256 timestamp, + uint256 blockNumber + ); + event MetadataUpdated( + address indexed updatedBy, + uint8 state, + string decryptorUrl, + bytes flags, + bytes data, + string metaDataDecryptorAddress, + uint256 timestamp, + uint256 blockNumber + ); + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + function isERC20Deployer(address acount) external view returns (bool); + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, + * it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, + * it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) + external + view + returns (address operator); + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) + external + view + returns (bool); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, + * it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + // function safeTransferFrom( + // address from, + // address to, + // uint256 tokenId, + // bytes calldata data + // ) external; + function transferFrom(address from, address to) external; + + function initialize( + address admin, + string calldata name, + string calldata symbol, + address erc20Factory, + address additionalERC20Deployer, + address additionalMetaDataUpdater, + string calldata tokenURI + ) external returns (bool); + + function hasRole(bytes32 role, address account) + external + view + returns (bool); + + struct Roles { + bool manager; + bool deployERC20; + bool updateMetadata; + bool store; + } + + struct metaDataProof { + address validatorAddress; + uint8 v; // v of validator signed message + bytes32 r; // r of validator signed message + bytes32 s; // s of validator signed message + } + function getPermissions(address user) external returns (Roles memory); + + function setDataERC20(bytes32 _key, bytes calldata _value) external; + function setMetaData(uint8 _metaDataState, string calldata _metaDataDecryptorUrl + , string calldata _metaDataDecryptorAddress, bytes calldata flags, + bytes calldata data,bytes32 _metaDataHash, metaDataProof[] memory _metadataProofs) external; + function getMetaData() external view returns (string memory, string memory, uint8, bool); + + function createERC20( + uint256 _templateIndex, + string[] calldata strings, + address[] calldata addresses, + uint256[] calldata uints, + bytes[] calldata bytess + ) external returns (address); + + + function removeFromCreateERC20List(address _allowedAddress) external; + function addToCreateERC20List(address _allowedAddress) external; + function addToMetadataList(address _allowedAddress) external; + function removeFromMetadataList(address _allowedAddress) external; + function getId() pure external returns (uint8); +} diff --git a/sol080/contracts/oceanv4/interfaces/IERC725X.sol b/sol080/contracts/oceanv4/interfaces/IERC725X.sol new file mode 100644 index 0000000000000000000000000000000000000000..0e29340fa82590ef3c64262e262e092322a90331 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IERC725X.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity 0.8.10; + +/** + * @dev Contract module which provides the ability to call arbitrary functions at any other smart contract and itself, + * including using `delegatecall`, as well creating contracts using `create` and `create2`. + * This is the basis for a smart contract based account system, but could also be used as a proxy account system. + * + * ERC 165 interface id: 0x44c028fe + * + * `execute` should only be callable by the owner of the contract set via ERC173. + */ +interface IERC725X /* is ERC165, ERC173 */ { + + /** + * @dev Emitted when a contract is created. + */ + event ContractCreated(address indexed contractAddress); + + /** + * @dev Emitted when a contract executed. + */ + event Executed(uint256 indexed _operation, address indexed _to, uint256 indexed _value, bytes _data); + + + /** + * @dev Executes any other smart contract. + * SHOULD only be callable by the owner of the contract set via ERC173. + * + * Requirements: + * + * - `operationType`, the operation to execute. So far defined is: + * CALL = 0; + * DELEGATECALL = 1; + * CREATE2 = 2; + * CREATE = 3; + * + * - `data` the call data that will be used with the contract at `to` + * + * Emits a {ContractCreated} event, when a contract is created under `operationType` 2 and 3. + */ + // function execute(uint256 operationType, address to, uint256 value, bytes calldata data) internal payable; +} diff --git a/sol080/contracts/oceanv4/interfaces/IERC725Y.sol b/sol080/contracts/oceanv4/interfaces/IERC725Y.sol new file mode 100644 index 0000000000000000000000000000000000000000..a37e2ce6821197afa31e5c14e94bf89b72643f49 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IERC725Y.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity 0.8.10; + +/** + * @title ERC725 Y data store + * @dev Contract module which provides the ability to set arbitrary key value sets that can be changed over time. + * It is intended to standardise certain keys value pairs to allow automated retrievals and interactions + * from interfaces and other smart contracts. + * + * ERC 165 interface id: 0x2bd57b73 + * + * `setData` should only be callable by the owner of the contract set via ERC173. + */ +interface IERC725Y /* is ERC165, ERC173 */ { + + /** + * @dev Emitted when data at a key is changed. + */ + event DataChanged(bytes32 indexed key, bytes value); + + /** + * @dev Gets data at a given `key` + */ + function getData(bytes32 key) external view returns (bytes memory value); + + /** + * @dev Sets data at a given `key`. + * SHOULD only be callable by the owner of the contract set via ERC173. + * + * Emits a {DataChanged} event. + */ + // function setData(bytes32 key, bytes calldata value) internal; +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/interfaces/IFactory.sol b/sol080/contracts/oceanv4/interfaces/IFactory.sol new file mode 100644 index 0000000000000000000000000000000000000000..c913c02b29f2b12e84ec86a202cc63fcc109218c --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IFactory.sol @@ -0,0 +1,95 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IFactory { + function initialize( + string calldata _name, + string calldata _symbol, + address _minter, + uint256 _cap, + string calldata blob, + address collector + ) external returns (bool); + + + + function isInitialized() external view returns (bool); + + + function createToken( + uint256 _templateIndex, + string[] calldata strings, + address[] calldata addresses, + uint256[] calldata uints, + bytes[] calldata bytess + ) external returns (address token); + + + function addToERC721Registry(address ERC721address) external; + + function erc721List(address ERC721address) external returns (address); + + function erc20List(address erc20dt) external view returns(bool); + + + struct NftCreateData{ + string name; + string symbol; + uint256 templateIndex; + string tokenURI; + } + struct ErcCreateData{ + uint256 templateIndex; + string[] strings; + address[] addresses; + uint256[] uints; + bytes[] bytess; + } + + struct PoolData{ + uint256[] ssParams; + uint256[] swapFees; + address[] addresses; + } + + struct FixedData{ + address fixedPriceAddress; + address[] addresses; + uint256[] uints; + } + + struct DispenserData{ + address dispenserAddress; + uint256 maxTokens; + uint256 maxBalance; + bool withMint; + address allowedSwapper; + } + + function createNftWithErc20( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData + ) external returns (address , address); + + function createNftWithErc20WithPool( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData, + PoolData calldata _PoolData + ) external returns (address, address , address); + + + function createNftWithErc20WithFixedRate( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData, + FixedData calldata _FixedData + ) external returns (address, address , bytes32 ); + + + function createNftWithErc20WithDispenser( + NftCreateData calldata _NftCreateData, + ErcCreateData calldata _ErcCreateData, + DispenserData calldata _DispenserData + ) external returns (address, address); +} diff --git a/sol080/contracts/oceanv4/interfaces/IFactoryRouter.sol b/sol080/contracts/oceanv4/interfaces/IFactoryRouter.sol new file mode 100644 index 0000000000000000000000000000000000000000..d7c440ba7e8109f3b9d7a1aeb7c56df930318c4a --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IFactoryRouter.sol @@ -0,0 +1,73 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IFactoryRouter { + function deployPool( + address[2] calldata tokens, // [datatokenAddress, baseTokenAddress] + uint256[] calldata ssParams, + uint256[] calldata swapFees, + address[] calldata addresses + ) external returns (address); + + function deployFixedRate( + address fixedPriceAddress, + address[] calldata addresses, + uint256[] calldata uints + ) external returns (bytes32 exchangeId); + + function getOPCFee(address baseToken) external view returns (uint256); + function getOPCFees() external view returns (uint256,uint256); + function getOPCConsumeFee() external view returns (uint256); + function getOPCProviderFee() external view returns (uint256); + + function getMinVestingPeriod() external view returns (uint256); + function deployDispenser( + address _dispenser, + address datatoken, + uint256 maxTokens, + uint256 maxBalance, + address owner, + address allowedSwapper + ) external; + + function isOceanToken(address) external view returns(bool); + function getOceanTokens() external view returns(address[] memory); + function isSSContract(address) external view returns(bool); + function getSSContracts() external view returns(address[] memory); + function isFixedRateContract(address) external view returns(bool); + function getFixedRatesContracts() external view returns(address[] memory); + function isDispenserContract(address) external view returns(bool); + function getDispensersContracts() external view returns(address[] memory); + function isPoolTemplate(address) external view returns(bool); + function getPoolTemplates() external view returns(address[] memory); + + struct Stakes { + address poolAddress; + uint256 tokenAmountIn; + uint256 minPoolAmountOut; + } + function stakeBatch(Stakes[] calldata) external; + + enum operationType { + SwapExactIn, + SwapExactOut, + FixedRate, + Dispenser + } + + struct Operations { + bytes32 exchangeIds; // used for fixedRate or dispenser + address source; // pool, dispenser or fixed rate address + operationType operation; // type of operation: enum operationType + address tokenIn; // token in address, only for pools + uint256 amountsIn; // ExactAmount In for swapExactIn operation, maxAmount In for swapExactOut + address tokenOut; // token out address, only for pools + uint256 amountsOut; // minAmountOut for swapExactIn or exactAmountOut for swapExactOut + uint256 maxPrice; // maxPrice, only for pools + uint256 swapMarketFee; + address marketFeeAddress; + } + function buyDTBatch(Operations[] calldata) external; +} diff --git a/sol080/contracts/oceanv4/interfaces/IFixedRateExchange.sol b/sol080/contracts/oceanv4/interfaces/IFixedRateExchange.sol new file mode 100644 index 0000000000000000000000000000000000000000..974c25ceb78471788204c49b8dd25f8a742f0bd3 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IFixedRateExchange.sol @@ -0,0 +1,73 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IFixedRateExchange { + function createWithDecimals( + address datatoken, + address[] calldata addresses, // [baseToken,owner,marketFeeCollector] + uint256[] calldata uints // [baseTokenDecimals,datatokenDecimals, fixedRate, marketFee] + ) external returns (bytes32 exchangeId); + + function buyDT(bytes32 exchangeId, uint256 datatokenAmount, + uint256 maxBaseTokenAmount, address consumeMarketAddress, uint256 consumeMarketSwapFeeAmount) external; + function sellDT(bytes32 exchangeId, uint256 datatokenAmount, + uint256 minBaseTokenAmount, address consumeMarketAddress, uint256 consumeMarketSwapFeeAmount) external; + + function getAllowedSwapper(bytes32 exchangeId) external view returns (address allowedSwapper); + function getExchange(bytes32 exchangeId) + external + view + returns ( + address exchangeOwner, + address datatoken, + uint256 dtDecimals, + address baseToken, + uint256 btDecimals, + uint256 fixedRate, + bool active, + uint256 dtSupply, + uint256 btSupply, + uint256 dtBalance, + uint256 btBalance, + bool withMint + //address allowedSwapper + ); + + function getFeesInfo(bytes32 exchangeId) + external + view + returns ( + uint256 marketFee, + address marketFeeCollector, + uint256 opcFee, + uint256 marketFeeAvailable, + uint256 oceanFeeAvailable + ); + + function isActive(bytes32 exchangeId) external view returns (bool); + + function calcBaseInGivenOutDT(bytes32 exchangeId, uint256 datatokenAmount, uint256 consumeMarketSwapFeeAmount) + external + view + returns ( + uint256 baseTokenAmount, + uint256 oceanFeeAmount, + uint256 publishMarketFeeAmount, + uint256 consumeMarketFeeAmount + ); + function calcBaseOutGivenInDT(bytes32 exchangeId, uint256 datatokenAmount, uint256 consumeMarketSwapFeeAmount) + external + view + returns ( + uint256 baseTokenAmount, + uint256 oceanFeeAmount, + uint256 publishMarketFeeAmount, + uint256 consumeMarketFeeAmount + ); + function updateMarketFee(bytes32 exchangeId, uint256 _newMarketFee) external; + function updateMarketFeeCollector(bytes32 exchangeId, address _newMarketCollector) external; + function setAllowedSwapper(bytes32 exchangeId, address newAllowedSwapper) external; + function getId() pure external returns (uint8); +} diff --git a/sol080/contracts/oceanv4/interfaces/IMetadata.sol b/sol080/contracts/oceanv4/interfaces/IMetadata.sol new file mode 100644 index 0000000000000000000000000000000000000000..1c051e8abacbbd99fd179ede4769f5d2285437fa --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IMetadata.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +interface IMetadata { + function create( + address dataToken, + bytes calldata flags, + bytes calldata data + ) external; + + function update( + address dataToken, + bytes calldata flags, + bytes calldata data + ) external; +} diff --git a/sol080/contracts/oceanv4/interfaces/IPool.sol b/sol080/contracts/oceanv4/interfaces/IPool.sol new file mode 100644 index 0000000000000000000000000000000000000000..9cb4ef5aac753705fb3c8f269a8f9e325355f008 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IPool.sol @@ -0,0 +1,66 @@ +// 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, see <http://www.gnu.org/licenses/>. + +pragma solidity 0.8.10; +// Copyright Balancer, BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IPool { + function getDatatokenAddress() external view returns (address); + + function getBaseTokenAddress() external view returns (address); + + function getController() external view returns (address); + + function setup( + address datatokenAddress, + uint256 datatokenAmount, + uint256 datatokennWeight, + address baseTokenAddress, + uint256 baseTokenAmount, + uint256 baseTokenWeight + ) external; + + function swapExactAmountIn( + address[3] calldata tokenInOutMarket, //[tokenIn,tokenOut,marketFeeAddress] + uint256[4] calldata amountsInOutMaxFee //[tokenAmountIn,minAmountOut,maxPrice,_swapMarketFee] + ) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter); + + function swapExactAmountOut( + address[3] calldata tokenInOutMarket, // [tokenIn,tokenOut,marketFeeAddress] + uint256[4] calldata amountsInOutMaxFee // [maxAmountIn,tokenAmountOut,maxPrice,_swapMarketFee] + ) external returns (uint256 tokenAmountIn, uint256 spotPriceAfter); + + function getAmountInExactOut( + address tokenIn, + address tokenOut, + uint256 tokenAmountOut, + uint256 _consumeMarketSwapFee + ) external view returns (uint256, uint256, uint256, uint256, uint256); + + function getAmountOutExactIn( + address tokenIn, + address tokenOut, + uint256 tokenAmountIn, + uint256 _consumeMarketSwapFee + ) external view returns (uint256, uint256, uint256, uint256, uint256); + + function setSwapFee(uint256 swapFee) external; + function getId() pure external returns (uint8); + + function exitswapPoolAmountIn( + uint256 poolAmountIn, + uint256 minAmountOut + ) external returns (uint256 tokenAmountOut); + + function joinswapExternAmountIn( + uint256 tokenAmountIn, + uint256 minPoolAmountOut + ) external returns (uint256 poolAmountOut); +} diff --git a/sol080/contracts/oceanv4/interfaces/ISideStaking.sol b/sol080/contracts/oceanv4/interfaces/ISideStaking.sol new file mode 100644 index 0000000000000000000000000000000000000000..5187a194bbc383c95c63f3173cc7da6ea495f2b3 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/ISideStaking.sol @@ -0,0 +1,102 @@ +// 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, see <http://www.gnu.org/licenses/>. + +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface ISideStaking { + + + function newDatatokenCreated( + address datatokenAddress, + address baseTokenAddress, + address poolAddress, + address publisherAddress, + uint256[] calldata ssParams + ) external returns (bool); + + function getDatatokenCirculatingSupply(address datatokenAddress) + external + view + returns (uint256); + + function getPublisherAddress(address datatokenAddress) + external + view + returns (address); + + function getBaseTokenAddress(address datatokenAddress) + external + view + returns (address); + + function getPoolAddress(address datatokenAddress) + external + view + returns (address); + + function getbBaseTokenBalance(address datatokenAddress) + external + view + returns (uint256); + + function getDatatokenBalance(address datatokenAddress) + external + view + returns (uint256); + + function getvestingEndBlock(address datatokenAddress) + external + view + returns (uint256); + + function getvestingAmount(address datatokenAddress) + external + view + returns (uint256); + + function getvestingLastBlock(address datatokenAddress) + external + view + returns (uint256); + + function getvestingAmountSoFar(address datatokenAddress) + external + view + returns (uint256); + + + + function canStake( + address datatokenAddress, + uint256 amount + ) external view returns (bool); + + function Stake( + address datatokenAddress, + uint256 amount + ) external; + + function canUnStake( + address datatokenAddress, + uint256 amount + ) external view returns (bool); + + function UnStake( + address datatokenAddress, + uint256 amount, + uint256 poolAmountIn + ) external; + + function notifyFinalize(address datatokenAddress) external; + function getId() pure external returns (uint8); + + +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/interfaces/IV3ERC20.sol b/sol080/contracts/oceanv4/interfaces/IV3ERC20.sol new file mode 100644 index 0000000000000000000000000000000000000000..672cedbe87f0bf20d5afd6eba16e24329c8d8ac2 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IV3ERC20.sol @@ -0,0 +1,31 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IV3ERC20 { + + function mint(address account, uint256 value) external; + function minter() external view returns(address); + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function cap() external view returns (uint256); + function isMinter(address account) external view returns (bool); + function isInitialized() external view returns (bool); + function allowance(address owner, address spender) + external + view + returns (uint256); + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function proposeMinter(address newMinter) external; + function approveMinter() external; + + +} diff --git a/sol080/contracts/oceanv4/interfaces/IV3Factory.sol b/sol080/contracts/oceanv4/interfaces/IV3Factory.sol new file mode 100644 index 0000000000000000000000000000000000000000..d813e43f2be6bf416730e4277875009a05d508b7 --- /dev/null +++ b/sol080/contracts/oceanv4/interfaces/IV3Factory.sol @@ -0,0 +1,16 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +interface IV3Factory { + + function newBPool( + address controller, + address datatokenAddress, + address baseTokenAddress, + address publisherAddress, + uint256 burnInEndBlock, + uint256[] memory ssParams) external + returns (address bpool); +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/oceanv4util.py b/sol080/contracts/oceanv4/oceanv4util.py new file mode 100644 index 0000000000000000000000000000000000000000..ab0d7b3082123604085ea309fa9d343a26b36e10 --- /dev/null +++ b/sol080/contracts/oceanv4/oceanv4util.py @@ -0,0 +1,210 @@ +from typing import Any, List +from enforce_typing import enforce_types + +import brownie + +from util.base18 import toBase18 +from util.constants import ( + BROWNIE_PROJECT080, + GOD_ACCOUNT, + ZERO_ADDRESS, + OPF_ADDRESS, +) +from util.globaltokens import OCEANtoken +from util.tx import txdict + +_ERC721_TEMPLATE = None + + +@enforce_types +def ERC721Template(): + global _ERC721_TEMPLATE # pylint: disable=global-statement + try: + erc721_template = _ERC721_TEMPLATE # may trigger failure + if erc721_template is not None: + x = erc721_template.address # "" # pylint: disable=unused-variable + except brownie.exceptions.ContractNotFound: + erc721_template = None + if erc721_template is None: + _ERC721_TEMPLATE = BROWNIE_PROJECT080.ERC721Template.deploy(txdict(GOD_ACCOUNT)) + erc721_template = _ERC721_TEMPLATE + return erc721_template + + +_ERC20_TEMPLATE = None + + +@enforce_types +def ERC20Template(): + global _ERC20_TEMPLATE # pylint: disable=global-statement + try: + erc20_template = _ERC20_TEMPLATE # may trigger failure + if erc20_template is not None: + x = erc20_template.address # "" # pylint: disable=unused-variable + except brownie.exceptions.ContractNotFound: + erc20_template = None + if erc20_template is None: + _ERC20_TEMPLATE = BROWNIE_PROJECT080.ERC20Template.deploy(txdict(GOD_ACCOUNT)) + erc20_template = _ERC20_TEMPLATE + return erc20_template + + +_POOL_TEMPLATE = None + + +@enforce_types +def POOLTemplate(): + global _POOL_TEMPLATE # pylint: disable=global-statement + try: + pool_template = _POOL_TEMPLATE # may trigger failure + if pool_template is not None: + x = pool_template.address # "" # pylint: disable=unused-variable + except brownie.exceptions.ContractNotFound: + pool_template = None + if pool_template is None: + _POOL_TEMPLATE = BROWNIE_PROJECT080.BPool.deploy(txdict(GOD_ACCOUNT)) + pool_template = _POOL_TEMPLATE + + return pool_template + + +@enforce_types +def deployRouter(from_account): + OCEAN = OCEANtoken() + pool_template = POOLTemplate() + router = BROWNIE_PROJECT080.FactoryRouter.deploy( + from_account.address, + OCEAN.address, + pool_template, + OPF_ADDRESS, + [], + txdict(from_account), + ) + return router + + +@enforce_types +def deployERC721Factory(from_account, router): + erc721_template = ERC721Template() + erc20_template = ERC20Template() + factory = BROWNIE_PROJECT080.ERC721Factory.deploy( + erc721_template.address, + erc20_template.address, + OPF_ADDRESS, + router.address, + txdict(from_account), + ) + return factory + + +@enforce_types +def createDataNFT(name: str, symbol: str, from_account, router): + erc721_factory = deployERC721Factory(from_account, router) + erc721_template_index = 1 + token_URI = "https://mystorage.com/mytoken.png" + tx = erc721_factory.deployERC721Contract( + name, + symbol, + erc721_template_index, + router.address, + ZERO_ADDRESS, # additionalMetaDataUpdater set to 0x00 for now + token_URI, + txdict(from_account), + ) + data_NFT_address = tx.events["NFTCreated"]["newTokenAddress"] + data_NFT = BROWNIE_PROJECT080.ERC721Template.at(data_NFT_address) + return (data_NFT, erc721_factory) + + +@enforce_types +def createDatatokenFromDataNFT( + DT_name: str, DT_symbol: str, DT_cap: int, dataNFT, from_account +): + + erc20_template_index = 1 + strings = [ + DT_name, + DT_symbol, + ] + addresses = [ + from_account.address, # minter + from_account.address, # fee mgr + from_account.address, # pub mkt + ZERO_ADDRESS, # pub mkt fee token addr + ] + uints = [ + toBase18(DT_cap), + toBase18(0.0), # pub mkt fee amt + ] + _bytes: List[Any] = [] + + tx = dataNFT.createERC20( + erc20_template_index, strings, addresses, uints, _bytes, txdict(from_account) + ) + DT_address = tx.events["TokenCreated"]["newTokenAddress"] + DT = BROWNIE_PROJECT080.ERC20Template.at(DT_address) + + return DT + + +@enforce_types +def deploySideStaking(from_account, router): + return BROWNIE_PROJECT080.SideStaking.deploy(router.address, txdict(from_account)) + + +@enforce_types +def createBPoolFromDatatoken( + datatoken, + erc721_factory, + from_account, + OCEAN_init_liquidity=2000, + DT_OCEAN_rate=0.1, + DT_vest_amt=1000, + DT_vest_num_blocks=600, + LP_swap_fee=0.03, + mkt_swap_fee=0.01, +): # pylint: disable=too-many-arguments + + OCEAN = OCEANtoken() + pool_template = POOLTemplate() + + router_address = datatoken.router() + router = BROWNIE_PROJECT080.FactoryRouter.at(router_address) + router.updateMinVestingPeriod(500, txdict(from_account)) + + OCEAN.approve(router.address, toBase18(OCEAN_init_liquidity), txdict(from_account)) + + ssbot = deploySideStaking(from_account, router) + router.addSSContract(ssbot.address, txdict(from_account)) + router.addFactory(erc721_factory.address, txdict(from_account)) + + ss_params = [ + toBase18(DT_OCEAN_rate), + OCEAN.decimals(), + toBase18(DT_vest_amt), + DT_vest_num_blocks, # do _not_ convert to wei + toBase18(OCEAN_init_liquidity), + ] + swap_fees = [ + toBase18(LP_swap_fee), + toBase18(mkt_swap_fee), + ] + addresses = [ + ssbot.address, + OCEAN.address, + from_account.address, + from_account.address, + OPF_ADDRESS, + pool_template.address, + ] + + tx = datatoken.deployPool(ss_params, swap_fees, addresses, txdict(from_account)) + pool_address = poolAddressFromNewBPoolTx(tx) + pool = BROWNIE_PROJECT080.BPool.at(pool_address) + + return pool + + +@enforce_types +def poolAddressFromNewBPoolTx(tx): + return tx.events["NewPool"]["poolAddress"] diff --git a/sol080/contracts/oceanv4/pools/FactoryRouter.sol b/sol080/contracts/oceanv4/pools/FactoryRouter.sol new file mode 100644 index 0000000000000000000000000000000000000000..932b36c714729038aed05dfec9ebd670f3a4a0ce --- /dev/null +++ b/sol080/contracts/oceanv4/pools/FactoryRouter.sol @@ -0,0 +1,772 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "./balancer/BFactory.sol"; +import "../interfaces/IFactory.sol"; +import "../interfaces/IERC20.sol"; +import "../interfaces/IFixedRateExchange.sol"; +import "../interfaces/IPool.sol"; +import "../interfaces/IDispenser.sol"; +import "../utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; + +contract FactoryRouter is BFactory { + using SafeERC20 for IERC20; + using SafeMath for uint256; + address public routerOwner; + address public factory; + address public fixedRate; + uint256 public minVestingPeriodInBlocks = 2426000; + + uint256 public swapOceanFee = 0; + uint256 public swapNonOceanFee = 1e15; // 0.1% + uint256 public consumeFee = 1e16; // 1% + uint256 public providerFee = 0; // 0% + address[] public oceanTokens; + address[] public ssContracts; + address[] public fixedrates; + address[] public dispensers; + // mapping(address => bool) public oceanTokens; + // mapping(address => bool) public ssContracts; + // mapping(address => bool) public fixedPrice; + // mapping(address => bool) public dispenser; + + event NewPool(address indexed poolAddress, bool isOcean); + event VestingPeriodChanges(address indexed caller, uint256 minVestingPeriodInBlocks); + event RouterChanged(address indexed caller, address indexed newRouter); + event FactoryContractChanged( + address indexed caller, + address indexed contractAddress + ); + event TokenAdded(address indexed caller, address indexed token); + event TokenRemoved(address indexed caller, address indexed token); + event SSContractAdded( + address indexed caller, + address indexed contractAddress + ); + event SSContractRemoved( + address indexed caller, + address indexed contractAddress + ); + event FixedRateContractAdded( + address indexed caller, + address indexed contractAddress + ); + event FixedRateContractRemoved( + address indexed caller, + address indexed contractAddress + ); + event DispenserContractAdded( + address indexed caller, + address indexed contractAddress + ); + event DispenserContractRemoved( + address indexed caller, + address indexed contractAddress + ); + + event OPCFeeChanged(address indexed caller, uint256 newSwapOceanFee, + uint256 newSwapNonOceanFee, uint256 newConsumeFee, uint256 newProviderFee); + + modifier onlyRouterOwner() { + require(routerOwner == msg.sender, "OceanRouter: NOT OWNER"); + _; + } + + constructor( + address _routerOwner, + address _oceanToken, + address _bpoolTemplate, + address _opcCollector, + address[] memory _preCreatedPools + ) public BFactory(_bpoolTemplate, _opcCollector, _preCreatedPools) { + require( + _routerOwner != address(0), + "FactoryRouter: Invalid router owner" + ); + require( + _opcCollector != address(0), + "FactoryRouter: Invalid opcCollector" + ); + require( + _oceanToken != address(0), + "FactoryRouter: Invalid Ocean Token address" + ); + routerOwner = _routerOwner; + opcCollector = _opcCollector; + _addOceanToken(_oceanToken); + } + + function changeRouterOwner(address _routerOwner) external onlyRouterOwner { + require(_routerOwner != address(0), "Invalid new router owner"); + routerOwner = _routerOwner; + emit RouterChanged(msg.sender, _routerOwner); + } + + /** + * @dev addOceanToken + * Adds a token to the list of tokens with reduced fees + * @param oceanTokenAddress address Token to be added + */ + function addOceanToken(address oceanTokenAddress) external onlyRouterOwner { + _addOceanToken(oceanTokenAddress); + } + + function _addOceanToken(address oceanTokenAddress) internal { + if(!isOceanToken(oceanTokenAddress)){ + oceanTokens.push(oceanTokenAddress); + emit TokenAdded(msg.sender, oceanTokenAddress); + } + } + + /** + * @dev removeOceanToken + * Removes a token if exists from the list of tokens with reduced fees + * @param oceanTokenAddress address Token to be removed + */ + function removeOceanToken(address oceanTokenAddress) + external + onlyRouterOwner + { + require( + oceanTokenAddress != address(0), + "FactoryRouter: Invalid Ocean Token address" + ); + uint256 i; + for (i = 0; i < oceanTokens.length; i++) { + if(oceanTokens[i] == oceanTokenAddress) break; + } + if(i < oceanTokens.length){ + // it's in the array + for (uint c = i; c < oceanTokens.length - 1; c++) { + oceanTokens[c] = oceanTokens[c + 1]; + } + oceanTokens.pop(); + emit TokenRemoved(msg.sender, oceanTokenAddress); + } + } + /** + * @dev isOceanToken + * Returns true if token exists in the list of tokens with reduced fees + * @param oceanTokenAddress address Token to be checked + */ + function isOceanToken(address oceanTokenAddress) public view returns(bool) { + for (uint256 i = 0; i < oceanTokens.length; i++) { + if(oceanTokens[i] == oceanTokenAddress) return true; + } + return false; + } + /** + * @dev getOceanTokens + * Returns the list of tokens with reduced fees + */ + function getOceanTokens() public view returns(address[] memory) { + return(oceanTokens); + } + + + /** + * @dev addSSContract + * Adds a token to the list of ssContracts + * @param _ssContract address Contract to be added + */ + + function addSSContract(address _ssContract) external onlyRouterOwner { + require( + _ssContract != address(0), + "FactoryRouter: Invalid _ssContract address" + ); + if(!isSSContract(_ssContract)){ + ssContracts.push(_ssContract); + emit SSContractAdded(msg.sender, _ssContract); + } + } + /** + * @dev removeSSContract + * Removes a token if exists from the list of ssContracts + * @param _ssContract address Contract to be removed + */ + + function removeSSContract(address _ssContract) external onlyRouterOwner { + require( + _ssContract != address(0), + "FactoryRouter: Invalid _ssContract address" + ); + uint256 i; + for (i = 0; i < ssContracts.length; i++) { + if(ssContracts[i] == _ssContract) break; + } + if(i < ssContracts.length){ + // it's in the array + for (uint c = i; c < ssContracts.length - 1; c++) { + ssContracts[c] = ssContracts[c + 1]; + } + ssContracts.pop(); + emit SSContractRemoved(msg.sender, _ssContract); + } + } + + /** + * @dev isSSContract + * Returns true if token exists in the list of ssContracts + * @param _ssContract address Contract to be checked + */ + function isSSContract(address _ssContract) public view returns(bool) { + for (uint256 i = 0; i < ssContracts.length; i++) { + if(ssContracts[i] == _ssContract) return true; + } + return false; + } + /** + * @dev getSSContracts + * Returns the list of ssContracts + */ + function getSSContracts() public view returns(address[] memory) { + return(ssContracts); + } + + function addFactory(address _factory) external onlyRouterOwner { + require( + _factory != address(0), + "FactoryRouter: Invalid _factory address" + ); + require(factory == address(0), "FACTORY ALREADY SET"); + factory = _factory; + emit FactoryContractChanged(msg.sender, _factory); + } + + + /** + * @dev addFixedRateContract + * Adds an address to the list of fixed rate contracts + * @param _fixedRate address Contract to be added + */ + function addFixedRateContract(address _fixedRate) external onlyRouterOwner { + require( + _fixedRate != address(0), + "FactoryRouter: Invalid _fixedRate address" + ); + if(!isFixedRateContract(_fixedRate)){ + fixedrates.push(_fixedRate); + emit FixedRateContractAdded(msg.sender, _fixedRate); + } + } + /** + * @dev removeFixedRateContract + * Removes an address from the list of fixed rate contracts + * @param _fixedRate address Contract to be removed + */ + function removeFixedRateContract(address _fixedRate) + external + onlyRouterOwner + { + require( + _fixedRate != address(0), + "FactoryRouter: Invalid _fixedRate address" + ); + uint256 i; + for (i = 0; i < fixedrates.length; i++) { + if(fixedrates[i] == _fixedRate) break; + } + if(i < fixedrates.length){ + // it's in the array + for (uint c = i; c < fixedrates.length - 1; c++) { + fixedrates[c] = fixedrates[c + 1]; + } + fixedrates.pop(); + emit FixedRateContractRemoved(msg.sender, _fixedRate); + } + } + /** + * @dev isFixedRateContract + * Removes true if address exists in the list of fixed rate contracts + * @param _fixedRate address Contract to be checked + */ + function isFixedRateContract(address _fixedRate) public view returns(bool) { + for (uint256 i = 0; i < fixedrates.length; i++) { + if(fixedrates[i] == _fixedRate) return true; + } + return false; + } + /** + * @dev getFixedRatesContracts + * Returns the list of fixed rate contracts + */ + function getFixedRatesContracts() public view returns(address[] memory) { + return(fixedrates); + } + + /** + * @dev addDispenserContract + * Adds an address to the list of dispensers + * @param _dispenser address Contract to be added + */ + function addDispenserContract(address _dispenser) external onlyRouterOwner { + require( + _dispenser != address(0), + "FactoryRouter: Invalid _dispenser address" + ); + if(!isDispenserContract(_dispenser)){ + dispensers.push(_dispenser); + emit DispenserContractAdded(msg.sender, _dispenser); + } + } + + /** + * @dev removeDispenserContract + * Removes an address from the list of dispensers + * @param _dispenser address Contract to be removed + */ + function removeDispenserContract(address _dispenser) + external + onlyRouterOwner + { + require( + _dispenser != address(0), + "FactoryRouter: Invalid _dispenser address" + ); + uint256 i; + for (i = 0; i < dispensers.length; i++) { + if(dispensers[i] == _dispenser) break; + } + if(i < dispensers.length){ + // it's in the array + for (uint c = i; c < dispensers.length - 1; c++) { + dispensers[c] = dispensers[c + 1]; + } + dispensers.pop(); + emit DispenserContractRemoved(msg.sender, _dispenser); + } + } + /** + * @dev isDispenserContract + * Returns true if address exists in the list of dispensers + * @param _dispenser address Contract to be checked + */ + function isDispenserContract(address _dispenser) public view returns(bool) { + for (uint256 i = 0; i < dispensers.length; i++) { + if(dispensers[i] == _dispenser) return true; + } + return false; + } + /** + * @dev getDispensersContracts + * Returns the list of fixed rate contracts + */ + function getDispensersContracts() public view returns(address[] memory) { + return(dispensers); + } + + /** + * @dev getOPCFee + * Gets OP Community Fees for a particular token + * @param baseToken address token to be checked + */ + function getOPCFee(address baseToken) public view returns (uint256) { + if (isOceanToken(baseToken)) { + return swapOceanFee; + } else return swapNonOceanFee; + } + + /** + * @dev getOPCFees + * Gets OP Community Fees for approved tokens and non approved tokens + */ + function getOPCFees() public view returns (uint256,uint256) { + return (swapOceanFee, swapNonOceanFee); + } + + /** + * @dev getConsumeFee + * Gets OP Community Fee cuts for consume fees + */ + function getOPCConsumeFee() public view returns (uint256) { + return consumeFee; + } + + /** + * @dev getOPCProviderFee + * Gets OP Community Fee cuts for provider fees + */ + function getOPCProviderFee() public view returns (uint256) { + return providerFee; + } + + + /** + * @dev updateOPCFee + * Updates OP Community Fees + * @param _newSwapOceanFee Amount charged for swapping with ocean approved tokens + * @param _newSwapNonOceanFee Amount charged for swapping with non ocean approved tokens + * @param _newConsumeFee Amount charged from consumeFees + * @param _newProviderFee Amount charged for providerFees + */ + function updateOPCFee(uint256 _newSwapOceanFee, uint256 _newSwapNonOceanFee, + uint256 _newConsumeFee, uint256 _newProviderFee) external onlyRouterOwner { + + swapOceanFee = _newSwapOceanFee; + swapNonOceanFee = _newSwapNonOceanFee; + consumeFee = _newConsumeFee; + providerFee = _newProviderFee; + emit OPCFeeChanged(msg.sender, _newSwapOceanFee, _newSwapNonOceanFee, _newConsumeFee, _newProviderFee); + } + + /* + * @dev getMinVestingPeriod + * Returns current minVestingPeriodInBlocks + @return minVestingPeriodInBlocks + */ + function getMinVestingPeriod() public view returns (uint256) { + return minVestingPeriodInBlocks; + } + /* + * @dev updateMinVestingPeriod + * Set new minVestingPeriodInBlocks + * @param _newPeriod + */ + function updateMinVestingPeriod(uint256 _newPeriod) external onlyRouterOwner { + minVestingPeriodInBlocks = _newPeriod; + emit VestingPeriodChanges(msg.sender, _newPeriod); + } + /** + * @dev Deploys a new `OceanPool` on Ocean Friendly Fork modified for 1SS. + This function cannot be called directly, but ONLY through the ERC20DT contract from a ERC20DEployer role + + ssContract address + tokens [datatokenAddress, baseTokenAddress] + publisherAddress user which will be assigned the vested amount. + * @param tokens precreated parameter + * @param ssParams params for the ssContract. + * [0] = rate (wei) + * [1] = baseToken decimals + * [2] = vesting amount (wei) + * [3] = vested blocks + * [4] = initial liquidity in baseToken for pool creation + * @param swapFees swapFees (swapFee, swapMarketFee), swapOceanFee will be set automatically later + * [0] = swapFee for LP Providers + * [1] = swapFee for marketplace runner + + . + * @param addresses refers to an array of addresses passed by user + * [0] = side staking contract address + * [1] = baseToken address for pool creation(OCEAN or other) + * [2] = baseTokenSender user which will provide the baseToken amount for initial liquidity + * [3] = publisherAddress user which will be assigned the vested amount + * [4] = marketFeeCollector marketFeeCollector address + [5] = poolTemplateAddress + + @return pool address + */ + function deployPool( + address[2] calldata tokens, + // [datatokenAddress, baseTokenAddress] + uint256[] calldata ssParams, + uint256[] calldata swapFees, + address[] calldata addresses + ) + external + returns ( + //[controller,baseTokenAddress,baseTokenSender,publisherAddress, marketFeeCollector,poolTemplateAddress] + + address + ) + { + require( + IFactory(factory).erc20List(msg.sender), + "FACTORY ROUTER: NOT ORIGINAL ERC20 TEMPLATE" + ); + require(isSSContract(addresses[0]), + "FACTORY ROUTER: invalid ssContract" + ); + require(ssParams[1] > 0, "Wrong decimals"); + + // we pull baseToken for creating initial pool and send it to the controller (ssContract) + _pullUnderlying(tokens[1],addresses[2], addresses[0], ssParams[4]); + + address pool = newBPool(tokens, ssParams, swapFees, addresses); + require(pool != address(0), "FAILED TO DEPLOY POOL"); + if (isOceanToken(tokens[1])) emit NewPool(pool, true); + else emit NewPool(pool, false); + return pool; + } + + function getLength(IERC20[] memory array) internal pure returns (uint256) { + return array.length; + } + + /** + * @dev deployFixedRate + * Creates a new FixedRateExchange setup. + * As for deployPool, this function cannot be called directly, + * but ONLY through the ERC20DT contract from a ERC20DEployer role + * @param fixedPriceAddress fixedPriceAddress + * @param addresses array of addresses [baseToken,owner,marketFeeCollector] + * @param uints array of uints [baseTokenDecimals,datatokenDecimals, fixedRate, marketFee, withMint] + @return exchangeId + */ + + function deployFixedRate( + address fixedPriceAddress, + address[] calldata addresses, + uint256[] calldata uints + ) external returns (bytes32 exchangeId) { + require( + IFactory(factory).erc20List(msg.sender), + "FACTORY ROUTER: NOT ORIGINAL ERC20 TEMPLATE" + ); + + require(isFixedRateContract(fixedPriceAddress), + "FACTORY ROUTER: Invalid FixedPriceContract" + ); + + exchangeId = IFixedRateExchange(fixedPriceAddress).createWithDecimals( + msg.sender, + addresses, + uints + ); + } + + /** + * @dev deployDispenser + * Activates a new Dispenser + * As for deployPool, this function cannot be called directly, + * but ONLY through the ERC20DT contract from a ERC20DEployer role + * @param _dispenser dispenser contract address + * @param datatoken refers to datatoken address. + * @param maxTokens - max tokens to dispense + * @param maxBalance - max balance of requester. + * @param owner - owner + * @param allowedSwapper - if !=0, only this address can request DTs + */ + + function deployDispenser( + address _dispenser, + address datatoken, + uint256 maxTokens, + uint256 maxBalance, + address owner, + address allowedSwapper + ) external { + require( + IFactory(factory).erc20List(msg.sender), + "FACTORY ROUTER: NOT ORIGINAL ERC20 TEMPLATE" + ); + + require(isDispenserContract(_dispenser), + "FACTORY ROUTER: Invalid DispenserContract" + ); + IDispenser(_dispenser).create( + datatoken, + maxTokens, + maxBalance, + owner, + allowedSwapper + ); + } + + /** + * @dev addPoolTemplate + * Adds an address to the list of pools templates + * @param poolTemplate address Contract to be added + */ + function addPoolTemplate(address poolTemplate) external onlyRouterOwner { + _addPoolTemplate(poolTemplate); + } + /** + * @dev removePoolTemplate + * Removes an address from the list of pool templates + * @param poolTemplate address Contract to be removed + */ + function removePoolTemplate(address poolTemplate) external onlyRouterOwner { + _removePoolTemplate(poolTemplate); + } + + // If you need to buy multiple DT (let's say for a compute job which has multiple datasets), + // you have to send one transaction for each DT that you want to buy. + + // Perks: + + // one single call to buy multiple DT for multiple assets (better UX, better gas optimization) + + enum operationType { + SwapExactIn, + SwapExactOut, + FixedRate, + Dispenser + } + + struct Operations { + bytes32 exchangeIds; // used for fixedRate or dispenser + address source; // pool, dispenser or fixed rate address + operationType operation; // type of operation: enum operationType + address tokenIn; // token in address, only for pools + uint256 amountsIn; // ExactAmount In for swapExactIn operation, maxAmount In for swapExactOut + address tokenOut; // token out address, only for pools + uint256 amountsOut; // minAmountOut for swapExactIn or exactAmountOut for swapExactOut + uint256 maxPrice; // maxPrice, only for pools + uint256 swapMarketFee; + address marketFeeAddress; + } + + // require tokenIn approvals for router from user. (except for dispenser operations) + function buyDTBatch(Operations[] calldata _operations) external { + // TODO: to avoid DOS attack, we set a limit to maximum orders (50?) + require(_operations.length <= 50, "FactoryRouter: Too Many Operations"); + for (uint256 i = 0; i < _operations.length; i++) { + // address[] memory tokenInOutMarket = new address[](3); + address[3] memory tokenInOutMarket = [ + _operations[i].tokenIn, + _operations[i].tokenOut, + _operations[i].marketFeeAddress + ]; + uint256[4] memory amountsInOutMaxFee = [ + _operations[i].amountsIn, + _operations[i].amountsOut, + _operations[i].maxPrice, + _operations[i].swapMarketFee + ]; + + // tokenInOutMarket[0] = + if (_operations[i].operation == operationType.SwapExactIn) { + // Get amountIn from user to router + _pullUnderlying(_operations[i].tokenIn,msg.sender, + address(this), + _operations[i].amountsIn); + // we approve pool to pull token from router + IERC20(_operations[i].tokenIn).safeIncreaseAllowance( + _operations[i].source, + _operations[i].amountsIn + ); + + // Perform swap + (uint256 amountReceived, ) = IPool(_operations[i].source) + .swapExactAmountIn(tokenInOutMarket, amountsInOutMaxFee); + // transfer token swapped to user + + IERC20(_operations[i].tokenOut).safeTransfer( + msg.sender, + amountReceived + ); + } else if (_operations[i].operation == operationType.SwapExactOut) { + // calculate how much amount In we need for exact Out + uint256 amountIn; + (amountIn, , , , ) = IPool(_operations[i].source) + .getAmountInExactOut( + _operations[i].tokenIn, + _operations[i].tokenOut, + _operations[i].amountsOut, + _operations[i].swapMarketFee + ); + // pull amount In from user + _pullUnderlying(_operations[i].tokenIn,msg.sender, + address(this), + amountIn); + // we approve pool to pull token from router + IERC20(_operations[i].tokenIn).safeIncreaseAllowance( + _operations[i].source, + amountIn + ); + // perform swap + (uint tokenAmountIn,) = IPool(_operations[i].source).swapExactAmountOut( + tokenInOutMarket, + amountsInOutMaxFee + ); + require(tokenAmountIn <= amountsInOutMaxFee[0], 'TOO MANY TOKENS IN'); + // send amount out back to user + IERC20(_operations[i].tokenOut).safeTransfer( + msg.sender, + _operations[i].amountsOut + ); + } else if (_operations[i].operation == operationType.FixedRate) { + // get datatoken address + (, address datatoken, , , , , , , , , , ) = IFixedRateExchange( + _operations[i].source + ).getExchange(_operations[i].exchangeIds); + // get tokenIn amount required for dt out + (uint256 baseTokenAmount, , , ) = IFixedRateExchange( + _operations[i].source + ).calcBaseInGivenOutDT( + _operations[i].exchangeIds, + _operations[i].amountsOut, + _operations[i].swapMarketFee + ); + + // pull tokenIn amount + _pullUnderlying(_operations[i].tokenIn,msg.sender, + address(this), + baseTokenAmount); + // we approve pool to pull token from router + IERC20(_operations[i].tokenIn).safeIncreaseAllowance( + _operations[i].source, + baseTokenAmount + ); + // perform swap + IFixedRateExchange(_operations[i].source).buyDT( + _operations[i].exchangeIds, + _operations[i].amountsOut, + _operations[i].amountsIn, + _operations[i].marketFeeAddress, + _operations[i].swapMarketFee + ); + // send dt out to user + IERC20(datatoken).safeTransfer( + msg.sender, + _operations[i].amountsOut + ); + } else { + IDispenser(_operations[i].source).dispense( + _operations[i].tokenOut, + _operations[i].amountsOut, + msg.sender + ); + } + } + } + + struct Stakes { + address poolAddress; + uint256 tokenAmountIn; + uint256 minPoolAmountOut; + } + // require pool[].baseToken (for each pool) approvals for router from user. + function stakeBatch(Stakes[] calldata _stakes) external { + // TODO: to avoid DOS attack, we set a limit to maximum orders (50?) + require(_stakes.length <= 50, "FactoryRouter: Too Many Operations"); + for (uint256 i = 0; i < _stakes.length; i++) { + address baseToken = IPool(_stakes[i].poolAddress).getBaseTokenAddress(); + _pullUnderlying(baseToken,msg.sender, + address(this), + _stakes[i].tokenAmountIn); + uint256 balanceBefore = IERC20(_stakes[i].poolAddress).balanceOf(address(this)); + // we approve pool to pull token from router + IERC20(baseToken).safeIncreaseAllowance( + _stakes[i].poolAddress, + _stakes[i].tokenAmountIn); + //now stake + uint poolAmountOut = IPool(_stakes[i].poolAddress).joinswapExternAmountIn( + _stakes[i].tokenAmountIn, _stakes[i].minPoolAmountOut + ); + require(poolAmountOut >= _stakes[i].minPoolAmountOut,'NOT ENOUGH LP'); + uint256 balanceAfter = IERC20(_stakes[i].poolAddress).balanceOf(address(this)); + //send LP shares to user + IERC20(_stakes[i].poolAddress).safeTransfer( + msg.sender, + balanceAfter.sub(balanceBefore) + ); + } + } + + function _pullUnderlying( + address erc20, + address from, + address to, + uint256 amount + ) internal { + uint256 balanceBefore = IERC20(erc20).balanceOf(to); + IERC20(erc20).safeTransferFrom(from, to, amount); + require(IERC20(erc20).balanceOf(to) >= balanceBefore.add(amount), + "Transfer amount is too low"); + } +} diff --git a/sol080/contracts/oceanv4/pools/balancer/BConst.sol b/sol080/contracts/oceanv4/pools/balancer/BConst.sol new file mode 100644 index 0000000000000000000000000000000000000000..3c2c287353cc91a191d0f515433a3687b32310f5 --- /dev/null +++ b/sol080/contracts/oceanv4/pools/balancer/BConst.sol @@ -0,0 +1,42 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity 0.8.10; +// Copyright Balancer, BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +contract BConst { + uint public constant BONE = 10**18; + + uint public constant MIN_BOUND_TOKENS = 2; + uint public constant MAX_BOUND_TOKENS = 2; + + uint public constant MIN_FEE = BONE / 10**4; + uint public constant MAX_FEE = BONE / 10; + uint public constant EXIT_FEE = 0; + + uint public constant MIN_WEIGHT = BONE; + uint public constant MAX_WEIGHT = BONE * 50; + uint public constant MAX_TOTAL_WEIGHT = BONE * 50; + uint public constant MIN_BALANCE = BONE / 10**12; + + uint public constant INIT_POOL_SUPPLY = BONE * 100; + + uint public constant MIN_BPOW_BASE = 1 wei; + uint public constant MAX_BPOW_BASE = (2 * BONE) - 1 wei; + uint public constant BPOW_PRECISION = BONE / 10**10; + + uint public constant MAX_IN_RATIO = BONE / 2; + uint public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; +} diff --git a/sol080/contracts/oceanv4/pools/balancer/BFactory.sol b/sol080/contracts/oceanv4/pools/balancer/BFactory.sol new file mode 100644 index 0000000000000000000000000000000000000000..5bec1d75f29d795064889a9dfb8b596edea88c29 --- /dev/null +++ b/sol080/contracts/oceanv4/pools/balancer/BFactory.sol @@ -0,0 +1,192 @@ +pragma solidity 0.8.10; +// Copyright Balancer, BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "./BPool.sol"; +import "./BConst.sol"; +import "../../utils/Deployer.sol"; +import "../../interfaces/ISideStaking.sol"; +import "../../interfaces/IERC20.sol"; + +/* + * @title BFactory contract + * @author Ocean Protocol (with code from Balancer Labs) + * + * @dev Ocean implementation of Balancer BPool Factory + * BFactory deploys BPool proxy contracts. + * New BPool proxy contracts are links to the template contract's bytecode. + * Proxy contract functionality is based on Ocean Protocol custom + * implementation of ERC1167 standard. + */ +contract BFactory is BConst, Deployer { + address public opcCollector; + + // mapping(address => bool) internal poolTemplates; + address[] public poolTemplates; + + event BPoolCreated( + address indexed newBPoolAddress, + address indexed registeredBy, + address indexed datatokenAddress, + address baseTokenAddress, + address bpoolTemplateAddress, + address ssAddress + ); + + event PoolTemplateAdded( + address indexed caller, + address indexed contractAddress + ); + event PoolTemplateRemoved( + address indexed caller, + address indexed contractAddress + ); + + /* @dev Called on contract deployment. Cannot be called with zero address. + @param _bpoolTemplate -- address of a deployed BPool contract. + @param _preCreatedPools list of pre-created pools. + It can be only used in case of migration from an old factory contract. + */ + constructor( + address _bpoolTemplate, + address _opcCollector, + address[] memory _preCreatedPools + ) public { + require( + _bpoolTemplate != address(0), + "BFactory: invalid bpool template zero address" + ); + require(_opcCollector != address(0), "BFactory: zero address"); + + opcCollector = _opcCollector; + _addPoolTemplate(_bpoolTemplate); + + if (_preCreatedPools.length > 0) { + for (uint256 i = 0; i < _preCreatedPools.length; i++) { + emit BPoolCreated( + _preCreatedPools[i], + msg.sender, + address(0), + address(0), + address(0), + address(0) + ); + } + } + } + + /** + * @dev Deploys new BPool proxy contract. + Template contract address could not be a zero address. + + * @param tokens [datatokenAddress, baseTokenAddress] + * publisherAddress user which will be assigned the vested amount. + * @param ssParams params for the ssContract. + * @param swapFees swapFees (swapFee, swapMarketFee), swapOceanFee will be set automatically later + marketFeeCollector marketFeeCollector address + @param addresses // array of addresses passed by the user + [controller,baseTokenAddress,baseTokenSender,publisherAddress, marketFeeCollector,poolTemplate address] + @return bpool address of a new proxy BPool contract + */ + + function newBPool( + address[2] memory tokens, + uint256[] memory ssParams, + uint256[] memory swapFees, + address[] memory addresses + ) internal returns (address bpool) { + require(isPoolTemplate(addresses[5]), "BFactory: Wrong Pool Template"); + address[2] memory feeCollectors = [addresses[4], opcCollector]; + + bpool = deploy(addresses[5]); + + require(bpool != address(0), "BFactory: invalid bpool zero address"); + BPool bpoolInstance = BPool(bpool); + + require( + bpoolInstance.initialize( + addresses[0], // ss is the pool controller + address(this), + swapFees, + false, + false, + tokens, + feeCollectors + ), + "ERR_INITIALIZE_BPOOL" + ); + + // emit BPoolCreated(bpool, msg.sender,datatokenAddress,baseTokenAddress,bpoolTemplate,controller); + + // requires approval first from baseTokenSender + require( + ISideStaking(addresses[0]).newDatatokenCreated( + tokens[0], + tokens[1], + bpool, + addresses[3], //publisherAddress + ssParams + ), + "ERR_INITIALIZE_SIDESTAKING" + ); + + return bpool; + } + + /** + * @dev _addPoolTemplate + * Adds an address to the list of pools templates + * @param poolTemplate address Contract to be added + */ + function _addPoolTemplate(address poolTemplate) internal { + require( + poolTemplate != address(0), + "FactoryRouter: Invalid poolTemplate address" + ); + if (!isPoolTemplate(poolTemplate)) { + poolTemplates.push(poolTemplate); + emit PoolTemplateAdded(msg.sender, poolTemplate); + } + } + + /** + * @dev _removeFixedRateContract + * Removes an address from the list of pool templates + * @param poolTemplate address Contract to be removed + */ + function _removePoolTemplate(address poolTemplate) internal { + uint256 i; + for (i = 0; i < poolTemplates.length; i++) { + if (poolTemplates[i] == poolTemplate) break; + } + if (i < poolTemplates.length) { + // it's in the array + for (uint256 c = i; c < poolTemplates.length - 1; c++) { + poolTemplates[c] = poolTemplates[c + 1]; + } + poolTemplates.pop(); + emit PoolTemplateRemoved(msg.sender, poolTemplate); + } + } + + /** + * @dev isPoolTemplate + * Removes true if address exists in the list of templates + * @param poolTemplate address Contract to be checked + */ + function isPoolTemplate(address poolTemplate) public view returns (bool) { + for (uint256 i = 0; i < poolTemplates.length; i++) { + if (poolTemplates[i] == poolTemplate) return true; + } + return false; + } + + /** + * @dev getPoolTemplates + * Returns the list of pool templates + */ + function getPoolTemplates() public view returns (address[] memory) { + return (poolTemplates); + } +} diff --git a/sol080/contracts/oceanv4/pools/balancer/BMath.sol b/sol080/contracts/oceanv4/pools/balancer/BMath.sol new file mode 100644 index 0000000000000000000000000000000000000000..c3673c00e63623570abe1f356b4d2b8dcadde67d --- /dev/null +++ b/sol080/contracts/oceanv4/pools/balancer/BMath.sol @@ -0,0 +1,446 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity 0.8.10; +// Copyright Balancer, BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import './BNum.sol'; + +import "../../interfaces/IFactoryRouter.sol"; + +contract BMath is BConst, BNum { + + // uint public _swapMarketFee; + uint public _swapPublishMarketFee; + uint internal _swapFee; + + address internal _factory; // BFactory address to push token exitFee to + + address internal _datatokenAddress; //datatoken address + address internal _baseTokenAddress; //base token address + mapping(address => uint) public communityFees; + + mapping(address => uint) public publishMarketFees; + // mapping(address => uint) public marketFees; + + + function getOPCFee() public view returns (uint) { + return IFactoryRouter(_factory).getOPCFee(_baseTokenAddress); + } + + struct swapfees{ + uint256 LPFee; + uint256 oceanFeeAmount; + uint256 publishMarketFeeAmount; + uint256 consumeMarketFee; + } + /********************************************************************************************** + // calcSpotPrice // + // sP = spotPrice // + // bI = tokenBalanceIn ( bI / wI ) 1 // + // bO = tokenBalanceOut sP = ----------- * ---------- // + // wI = tokenWeightIn ( bO / wO ) ( 1 - sF ) // + // wO = tokenWeightOut // + // sF = swapFee // + **********************************************************************************************/ + function calcSpotPrice( + uint tokenBalanceIn, + uint tokenWeightIn, + uint tokenBalanceOut, + uint tokenWeightOut, + uint _swapMarketFee + ) + internal view + returns (uint spotPrice) + + { + + + uint numer = bdiv(tokenBalanceIn, tokenWeightIn); + uint denom = bdiv(tokenBalanceOut, tokenWeightOut); + uint ratio = bdiv(numer, denom); + uint scale = bdiv(BONE, bsub(BONE, _swapFee+getOPCFee()+_swapPublishMarketFee+_swapMarketFee)); + + return (spotPrice = bmul(ratio, scale)); + } + + /********************************************************************************************** + // calcOutGivenIn // + // aO = tokenAmountOut // + // bO = tokenBalanceOut // + // bI = tokenBalanceIn / / bI \ (wI / wO) \ // + // aI = tokenAmountIn aO = bO * | 1 - | -------------------------- | ^ | // + // wI = tokenWeightIn \ \ ( bI + ( aI * ( 1 - sF )) / / // + // wO = tokenWeightOut // + // sF = swapFee // + **********************************************************************************************/ + // data = [ + // inRecord.balance, + // inRecord.denorm, + // outRecord.balance, + // outRecord.denorm + // ]; + function calcOutGivenIn( + uint[4] memory data, + uint tokenAmountIn, + //address tokenInAddress, + uint256 _consumeMarketSwapFee + + ) + public view + returns (uint tokenAmountOut, uint balanceInToAdd, swapfees memory _swapfees) + { + uint weightRatio = bdiv(data[1], data[3]); + + _swapfees.oceanFeeAmount = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, getOPCFee()))); + + + _swapfees.publishMarketFeeAmount = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, _swapPublishMarketFee))); + + + _swapfees.LPFee = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, _swapFee))); + _swapfees.consumeMarketFee = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, _consumeMarketSwapFee))); + uint totalFee =_swapFee+getOPCFee()+_swapPublishMarketFee+_consumeMarketSwapFee; + + uint adjustedIn = bsub(BONE, totalFee); + + adjustedIn = bmul(tokenAmountIn, adjustedIn); + + uint y = bdiv(data[0], badd(data[0], adjustedIn)); + uint foo = bpow(y, weightRatio); + uint bar = bsub(BONE, foo); + + + tokenAmountOut = bmul(data[2], bar); + + return (tokenAmountOut, bsub(tokenAmountIn,(_swapfees.oceanFeeAmount+_swapfees.publishMarketFeeAmount)), _swapfees); + + } + + /********************************************************************************************** + // calcOutGivenIn // + // aO = tokenAmountOut // + // bO = tokenBalanceOut // + // bI = tokenBalanceIn / / bI \ (wI / wO) \ // + // aI = tokenAmountIn aO = bO * | 1 - | -------------------------- | ^ | // + // wI = tokenWeightIn \ \ ( bI + ( aI * ( 1 - sF )) / / // + // wO = tokenWeightOut // + // sF = swapFee // + ********************************************************************************************** + function calcOutGivenIn( + uint tokenBalanceIn, + uint tokenWeightIn, + uint tokenBalanceOut, + uint tokenWeightOut, + uint tokenAmountIn, + uint _swapMarketFee + + ) + internal view + returns (uint , uint, uint, uint, uint) + { + uint weightRatio = bdiv(tokenWeightIn, tokenWeightOut); + + + uint totalFee = _swapFee+getOPCFee()+_swapMarketFee+_swapPublishMarketFee; + + uint adjustedIn = bsub(BONE, totalFee); + + adjustedIn = bmul(tokenAmountIn, adjustedIn); + + uint y = bdiv(tokenBalanceIn, badd(tokenBalanceIn, adjustedIn)); + + uint foo = bpow(y, weightRatio); + uint bar = bsub(BONE, foo); + + uint tokenAmountOut = bmul(tokenBalanceOut, bar); + + + return tokenAmountOut; + } + */ + + /********************************************************************************************** + // calcInGivenOut // + // aI = tokenAmountIn // + // bO = tokenBalanceOut / / bO \ (wO / wI) \ // + // bI = tokenBalanceIn bI * | | ------------ | ^ - 1 | // + // aO = tokenAmountOut aI = \ \ ( bO - aO ) / / // + // wI = tokenWeightIn -------------------------------------------- // + // wO = tokenWeightOut ( 1 - sF ) // + // sF = swapFee // + ********************************************************************************************** + function calcInGivenOut( + uint tokenBalanceIn, + uint tokenWeightIn, + uint tokenBalanceOut, + uint tokenWeightOut, + uint tokenAmountOut, + uint _consumeMarketSwapFee + ) + internal view + returns (uint, uint, uint, uint, uint) + { + uint weightRatio = bdiv(tokenWeightOut, tokenWeightIn); + uint diff = bsub(tokenBalanceOut, tokenAmountOut); + uint y = bdiv(tokenBalanceOut, diff); + uint foo = bpow(y, weightRatio); + foo = bsub(foo, BONE); + uint _opcFee = getOPCFee(); + uint totalFee =_swapFee+_opcFee +_consumeMarketSwapFee+_swapPublishMarketFee; + + uint tokenAmountIn = bsub(BONE, totalFee); + tokenAmountIn = bdiv(bmul(tokenBalanceIn, foo), tokenAmountIn); + + uint swapFee = bsub(BONE, _swapFee); + swapFee = bdiv(bmul(tokenBalanceIn, foo), swapFee); + + uint opcFee = bsub(BONE, _opcFee); + opcFee = bdiv(bmul(tokenBalanceIn, foo), opcFee); + + uint consumeMarketSwapFee = bsub(BONE, _consumeMarketSwapFee); + consumeMarketSwapFee = bdiv(bmul(tokenBalanceIn, foo), consumeMarketSwapFee); + + uint publishMarketSwapFee = bsub(BONE, _swapPublishMarketFee); + publishMarketSwapFee = bdiv(bmul(tokenBalanceIn, foo), publishMarketSwapFee); + + + return (tokenAmountIn, swapFee, opcFee , consumeMarketSwapFee, publishMarketSwapFee); + } + + /********************************************************************************************** + // calcInGivenOut // + // aI = tokenAmountIn // + // bO = tokenBalanceOut / / bO \ (wO / wI) \ // + // bI = tokenBalanceIn bI * | | ------------ | ^ - 1 | // + // aO = tokenAmountOut aI = \ \ ( bO - aO ) / / // + // wI = tokenWeightIn -------------------------------------------- // + // wO = tokenWeightOut ( 1 - sF ) // + // sF = swapFee // + **********************************************************************************************/ + function calcInGivenOut( + uint[4] memory data, + uint tokenAmountOut, + //address tokenInAddress, + // address marketFeeAddress, + uint _consumeMarketSwapFee + ) + public view + returns (uint tokenAmountIn, uint tokenAmountInBalance, swapfees memory _swapfees) + { + uint weightRatio = bdiv(data[3], data[1]); + uint diff = bsub(data[2], tokenAmountOut); + uint y = bdiv(data[2], diff); + uint foo = bpow(y, weightRatio); + foo = bsub(foo, BONE); + uint totalFee =_swapFee+getOPCFee()+_consumeMarketSwapFee+_swapPublishMarketFee; + + + tokenAmountIn = bdiv(bmul(data[0], foo), bsub(BONE, totalFee)); + _swapfees.oceanFeeAmount = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, getOPCFee()))); + + + _swapfees.publishMarketFeeAmount = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, _swapPublishMarketFee))); + + + _swapfees.LPFee = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, _swapFee))); + _swapfees.consumeMarketFee = bsub(tokenAmountIn, bmul(tokenAmountIn, bsub(BONE, _consumeMarketSwapFee))); + + + tokenAmountInBalance = bdiv(bmul(data[0], foo), bsub(BONE, _swapFee)); + + + return (tokenAmountIn, tokenAmountInBalance,_swapfees); + } + + /********************************************************************************************** + // calcPoolOutGivenSingleIn // + // pAo = poolAmountOut / \ // + // tAi = tokenAmountIn /// / // wI \ \\ \ wI \ // + // wI = tokenWeightIn //| tAi *| 1 - || 1 - -- | * sF || + tBi \ -- \ // + // tW = totalWeight pAo=|| \ \ \\ tW / // | ^ tW | * pS - pS // + // tBi = tokenBalanceIn \\ ------------------------------------- / / // + // pS = poolSupply \\ tBi / / // + // sF = swapFee \ / // + **********************************************************************************************/ + function calcPoolOutGivenSingleIn( + uint tokenBalanceIn, + uint tokenWeightIn, + uint poolSupply, + uint totalWeight, + uint tokenAmountIn + + ) + internal view + returns (uint poolAmountOut) + { + /* Charge the trading fee for the proportion of tokenAi + which is implicitly traded to the other pool tokens. + That proportion is (1- weightTokenIn) + tokenAiAfterFee = tAi * (1 - (1-weightTi) * poolFee); + */ + + uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); + uint zaz = bmul(bsub(BONE, normalizedWeight), _swapFee); + uint tokenAmountInAfterFee = bmul(tokenAmountIn, bsub(BONE, zaz)); + + uint newTokenBalanceIn = badd(tokenBalanceIn, tokenAmountInAfterFee); + uint tokenInRatio = bdiv(newTokenBalanceIn, tokenBalanceIn); + + // uint newPoolSupply = (ratioTi ^ weightTi) * poolSupply; + uint poolRatio = bpow(tokenInRatio, normalizedWeight); + uint newPoolSupply = bmul(poolRatio, poolSupply); + poolAmountOut = bsub(newPoolSupply, poolSupply); + return poolAmountOut; + } + + /********************************************************************************************** + // calcSingleInGivenPoolOut // + // tAi = tokenAmountIn //(pS + pAo)\ / 1 \\ // + // pS = poolSupply || --------- | ^ | --------- || * bI - bI // + // pAo = poolAmountOut \\ pS / \(wI / tW)// // + // bI = balanceIn tAi = -------------------------------------------- // + // wI = weightIn / wI \ // + // tW = totalWeight | 1 - ---- | * sF // + // sF = swapFee \ tW / // + **********************************************************************************************/ + function calcSingleInGivenPoolOut( + uint tokenBalanceIn, + uint tokenWeightIn, + uint poolSupply, + uint totalWeight, + uint poolAmountOut + //uint swapFee + ) + internal view + returns (uint tokenAmountIn) + { + uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); + uint newPoolSupply = badd(poolSupply, poolAmountOut); + uint poolRatio = bdiv(newPoolSupply, poolSupply); + + //uint newBalTi = poolRatio^(1/weightTi) * balTi; + uint boo = bdiv(BONE, normalizedWeight); + uint tokenInRatio = bpow(poolRatio, boo); + uint newTokenBalanceIn = bmul(tokenInRatio, tokenBalanceIn); + uint tokenAmountInAfterFee = bsub(newTokenBalanceIn, tokenBalanceIn); + // Do reverse order of fees charged in joinswap_ExternAmountIn, this way + // ``` pAo == joinswap_ExternAmountIn(Ti, joinswap_PoolAmountOut(pAo, Ti)) ``` + //uint tAi = tAiAfterFee / (1 - (1-weightTi) * swapFee) ; + uint zar = bmul(bsub(BONE, normalizedWeight), _swapFee); + tokenAmountIn = bdiv(tokenAmountInAfterFee, bsub(BONE, zar)); + return tokenAmountIn; + } + + /********************************************************************************************** + // calcSingleOutGivenPoolIn // + // tAo = tokenAmountOut / / \\ // + // bO = tokenBalanceOut / // pS - (pAi * (1 - eF)) \ / 1 \ \\ // + // pAi = poolAmountIn | bO - || ----------------------- | ^ | --------- | * b0 || // + // ps = poolSupply \ \\ pS / \(wO / tW)/ // // + // wI = tokenWeightIn tAo = \ \ // // + // tW = totalWeight / / wO \ \ // + // sF = swapFee * | 1 - | 1 - ---- | * sF | // + // eF = exitFee \ \ tW / / // + **********************************************************************************************/ + function calcSingleOutGivenPoolIn( + uint tokenBalanceOut, + uint tokenWeightOut, + uint poolSupply, + uint totalWeight, + uint poolAmountIn + ) + internal view + returns (uint tokenAmountOut) + { + uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); + // charge exit fee on the pool token side + // pAiAfterExitFee = pAi*(1-exitFee) + uint poolAmountInAfterExitFee = bmul( + poolAmountIn, + bsub(BONE, EXIT_FEE) + ); + uint newPoolSupply = bsub(poolSupply, poolAmountInAfterExitFee); + uint poolRatio = bdiv(newPoolSupply, poolSupply); + + // newBalTo = poolRatio^(1/weightTo) * balTo; + uint tokenOutRatio = bpow(poolRatio, bdiv(BONE, normalizedWeight)); + uint newTokenBalanceOut = bmul(tokenOutRatio, tokenBalanceOut); + + uint tokenAmountOutBeforeSwapFee = bsub( + tokenBalanceOut, + newTokenBalanceOut + ); + + // charge swap fee on the output token side + //uint tAo = tAoBeforeSwapFee * (1 - (1-weightTo) * swapFee) + uint zaz = bmul(bsub(BONE, normalizedWeight), _swapFee); + tokenAmountOut = bmul(tokenAmountOutBeforeSwapFee, bsub(BONE, zaz)); + return tokenAmountOut; + } + + /********************************************************************************************** + // calcPoolInGivenSingleOut // + // pAi = poolAmountIn // / tAo \\ / wO \ \ // + // bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ // + // tAo = tokenAmountOut pS - || \ 1 - ((1 - (tO / tW)) * sF)/ | ^ \ tW / * pS | // + // ps = poolSupply \\ -----------------------------------/ / // + // wO = tokenWeightOut pAi = \\ bO / / // + // tW = totalWeight ------------------------------------------------------------- // + // sF = swapFee ( 1 - eF ) // + // eF = exitFee // + **********************************************************************************************/ + function calcPoolInGivenSingleOut( + uint tokenBalanceOut, + uint tokenWeightOut, + uint poolSupply, + uint totalWeight, + uint tokenAmountOut + ) + internal view + returns (uint poolAmountIn) + { + + // charge swap fee on the output token side + uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); + //uint tAoBeforeSwapFee = tAo / (1 - (1-weightTo) * swapFee) ; + uint zoo = bsub(BONE, normalizedWeight); + uint zar = bmul(zoo, _swapFee); + uint tokenAmountOutBeforeSwapFee = bdiv( + tokenAmountOut, + bsub(BONE, zar) + ); + + uint newTokenBalanceOut = bsub( + tokenBalanceOut, + tokenAmountOutBeforeSwapFee + ); + uint tokenOutRatio = bdiv(newTokenBalanceOut, tokenBalanceOut); + + //uint newPoolSupply = (ratioTo ^ weightTo) * poolSupply; + uint poolRatio = bpow(tokenOutRatio, normalizedWeight); + uint newPoolSupply = bmul(poolRatio, poolSupply); + uint poolAmountInAfterExitFee = bsub(poolSupply, newPoolSupply); + + // charge exit fee on the pool token side + // pAi = pAiAfterExitFee/(1-exitFee) + poolAmountIn = bdiv(poolAmountInAfterExitFee, bsub(BONE, EXIT_FEE)); + return poolAmountIn; + } + + + + +} diff --git a/sol080/contracts/oceanv4/pools/balancer/BNum.sol b/sol080/contracts/oceanv4/pools/balancer/BNum.sol new file mode 100644 index 0000000000000000000000000000000000000000..a1de8ac5139dbfa40e94a79bf49871fc5de0d0c9 --- /dev/null +++ b/sol080/contracts/oceanv4/pools/balancer/BNum.sol @@ -0,0 +1,167 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity 0.8.10; +// Copyright Balancer, BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import './BConst.sol'; + +contract BNum is BConst { + + function btoi(uint a) + internal pure + returns (uint) + { + return a / BONE; + } + + function bfloor(uint a) + internal pure + returns (uint) + { + return btoi(a) * BONE; + } + + function badd(uint a, uint b) + internal pure + returns (uint) + { + uint c = a + b; + require(c >= a, 'ERR_ADD_OVERFLOW'); + return c; + } + + function bsub(uint a, uint b) + internal pure + returns (uint) + { + (uint c, bool flag) = bsubSign(a, b); + require(!flag, 'ERR_SUB_UNDERFLOW'); + return c; + } + + function bsubSign(uint a, uint b) + internal pure + returns (uint, bool) + { + if (a >= b) { + return (a - b, false); + } else { + return (b - a, true); + } + } + + function bmul(uint a, uint b) + internal pure + returns (uint) + { + uint c0 = a * b; + require(a == 0 || c0 / a == b, 'ERR_MUL_OVERFLOW'); + uint c1 = c0 + (BONE / 2); + require(c1 >= c0, 'ERR_MUL_OVERFLOW'); + uint c2 = c1 / BONE; + return c2; + } + + function bdiv(uint a, uint b) + internal pure + returns (uint) + { + require(b != 0, 'ERR_DIV_ZERO'); + uint c0 = a * BONE; + require(a == 0 || c0 / a == BONE, 'ERR_DIV_INTERNAL'); // bmul overflow + uint c1 = c0 + (b / 2); + require(c1 >= c0, 'ERR_DIV_INTERNAL'); // badd require + uint c2 = c1 / b; + return c2; + } + + // DSMath.wpow + function bpowi(uint a, uint n) + internal pure + returns (uint) + { + uint b = a; + uint z = n % 2 != 0 ? b : BONE; + + for (n /= 2; n != 0; n /= 2) { + b = bmul(b, b); + + if (n % 2 != 0) { + z = bmul(z, b); + } + } + return z; + } + + // Compute b^(e.w) by splitting it into (b^e)*(b^0.w). + // Use `bpowi` for `b^e` and `bpowK` for k iterations + // of approximation of b^0.w + function bpow(uint base, uint exp) + internal pure + returns (uint) + { + require(base >= MIN_BPOW_BASE, 'ERR_BPOW_BASE_TOO_LOW'); + require(base <= MAX_BPOW_BASE, 'ERR_BPOW_BASE_TOO_HIGH'); + + uint whole = bfloor(exp); + uint remain = bsub(exp, whole); + + uint wholePow = bpowi(base, btoi(whole)); + + if (remain == 0) { + return wholePow; + } + + uint partialResult = bpowApprox(base, remain, BPOW_PRECISION); + return bmul(wholePow, partialResult); + } + + function bpowApprox(uint base, uint exp, uint precision) + internal pure + returns (uint) + { + // term 0: + uint a = exp; + (uint x, bool xneg) = bsubSign(base, BONE); + uint term = BONE; + uint sum = term; + bool negative = false; + + + // term(k) = numer / denom + // = (product(a - i - 1, i=1-->k) * x^k) / (k!) + // each iteration, multiply previous term by (a-(k-1)) * x / k + // continue until term is less than precision + for (uint i = 1; term >= precision; i++) { + uint bigK = i * BONE; + (uint c, bool cneg) = bsubSign(a, bsub(bigK, BONE)); + term = bmul(term, bmul(c, x)); + term = bdiv(term, bigK); + if (term == 0) break; + + if (xneg) negative = !negative; + if (cneg) negative = !negative; + if (negative) { + sum = bsub(sum, term); + } else { + sum = badd(sum, term); + } + } + + return sum; + } + +} diff --git a/sol080/contracts/oceanv4/pools/balancer/BPool.sol b/sol080/contracts/oceanv4/pools/balancer/BPool.sol new file mode 100644 index 0000000000000000000000000000000000000000..255bb25dec5e5275a87ba358aa3b215fcaf75f27 --- /dev/null +++ b/sol080/contracts/oceanv4/pools/balancer/BPool.sol @@ -0,0 +1,1309 @@ +pragma solidity 0.8.10; +// Copyright Balancer, BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "./BToken.sol"; +import "./BMath.sol"; +import "../../interfaces/ISideStaking.sol"; +import "../../utils/SafeERC20.sol"; + +/** + * @title BPool + * + * @dev Used by the (Ocean version) BFactory contract as a bytecode reference to + * deploy new BPools. + * + * This contract is a friendly fork of Balancer [1] + * [1] https://github.com/balancer-labs/balancer-core/contracts/. + + * All fees are expressed in wei. Examples: + * (1e17 = 10 % , 1e16 = 1% , 1e15 = 0.1%, 1e14 = 0.01%) + */ +contract BPool is BMath, BToken { + using SafeERC20 for IERC20; + struct Record { + bool bound; // is token bound to pool + uint256 index; // private + uint256 denorm; // denormalized weight + uint256 balance; + } + + event LOG_SWAP( + address indexed caller, + address indexed tokenIn, + address indexed tokenOut, + uint256 tokenAmountIn, + uint256 tokenAmountOut, + uint256 timestamp + ); + + event LOG_JOIN( + address indexed caller, + address indexed tokenIn, + uint256 tokenAmountIn, + uint256 timestamp + ); + event LOG_SETUP( + address indexed caller, + address indexed baseToken, + uint256 baseTokenAmountIn, + uint256 baseTokenWeight, + address indexed datatoken, + uint256 datatokenAmountIn, + uint256 datatokenWeight + ); + + event LOG_EXIT( + address indexed caller, + address indexed tokenOut, + uint256 tokenAmountOut, + uint256 timestamp + ); + + event LOG_CALL( + bytes4 indexed sig, + address indexed caller, + uint256 timestamp, + bytes data + ); + + event LOG_BPT(uint256 bptAmount); + event LOG_BPT_SS(uint256 bptAmount); //emitted for SS contract + + event OPCFee( + address caller, + address OPCWallet, + address token, + uint256 amount + ); + event SwapFeeChanged(address caller, uint256 amount); + event PublishMarketFee( + address caller, + address marketAddress, + address token, + uint256 amount + ); + // emited for fees sent to consumeMarket + event ConsumeMarketFee(address to, address token, uint256 amount); + event SWAP_FEES(uint LPFeeAmount, uint oceanFeeAmount, uint marketFeeAmount, + uint consumeMarketFeeAmount, address tokenFeeAddress); + //emitted for every change done by publisherMarket + event PublishMarketFeeChanged(address caller, address newMarketCollector, uint256 swapFee); + + modifier _lock_() { + require(!_mutex, "ERR_REENTRY"); + _mutex = true; + _; + _mutex = false; + } + + modifier _viewlock_() { + require(!_mutex, "ERR_REENTRY"); + _; + } + + bool private _mutex; + + address private _controller; // has CONTROL role + bool private _publicSwap; // true if PUBLIC can call SWAP functions + + //address public _publishMarketCollector; + address public _publishMarketCollector; + address public _opcCollector; + // `setSwapFee` and `finalize` require CONTROL + // `finalize` sets `PUBLIC can SWAP`, `PUBLIC can JOIN` + bool private _finalized; + + address[] private _tokens; + mapping(address => Record) private _records; + uint256 private _totalWeight; + ISideStaking ssContract; + + //----------------------------------------------------------------------- + //Proxy contract functionality: begin + bool private initialized; + + /** + * @dev getId + * Return template id in case we need different ABIs. + * If you construct your own template, please make sure to change the hardcoded value + */ + function getId() pure public returns (uint8) { + return 1; + } + + function isInitialized() external view returns (bool) { + return initialized; + } + + // Called prior to contract initialization (e.g creating new BPool instance) + // Calls private _initialize function. Only if contract is not initialized. + function initialize( + address controller, + address factory, + uint256[] calldata swapFees, + bool publicSwap, + bool finalized, + address[2] calldata tokens, + address[2] calldata feeCollectors + ) external returns (bool) { + require(!initialized, "ERR_ALREADY_INITIALIZED"); + require(controller != address(0), "ERR_INVALID_CONTROLLER_ADDRESS"); + require(factory != address(0), "ERR_INVALID_FACTORY_ADDRESS"); + require(swapFees[0] >= MIN_FEE, "ERR_MIN_FEE"); + require(swapFees[0] <= MAX_FEE, "ERR_MAX_FEE"); + require(swapFees[1] == 0 || swapFees[1]>= MIN_FEE, "ERR_MIN_FEE"); + require(swapFees[1] <= MAX_FEE, "ERR_MAX_FEE"); + return + _initialize( + controller, + factory, + swapFees, + publicSwap, + finalized, + tokens, + feeCollectors + ); + } + + // Private function called on contract initialization. + function _initialize( + address controller, + address factory, + uint256[] memory swapFees, + bool publicSwap, + bool finalized, + address[2] memory tokens, + address[2] memory feeCollectors + ) private returns (bool) { + _controller = controller; + _factory = factory; + _swapFee = swapFees[0]; + emit SwapFeeChanged(msg.sender, _swapFee); + _swapPublishMarketFee = swapFees[1]; + _publicSwap = publicSwap; + _finalized = finalized; + _datatokenAddress = tokens[0]; + _baseTokenAddress = tokens[1]; + _publishMarketCollector = feeCollectors[0]; + emit PublishMarketFeeChanged(msg.sender, _publishMarketCollector, _swapPublishMarketFee); + _opcCollector = feeCollectors[1]; + initialized = true; + ssContract = ISideStaking(_controller); + return initialized; + } + + + /** + * @dev setup + * Initial setup of the pool + * Can be called only by the controller + * @param datatokenAddress datatokenAddress + * @param datatokenAmount how many datatokens in the initial reserve + * @param datatokenWeight datatoken weight (hardcoded in deployer at 50%) + * @param baseTokenAddress base token + * @param baseTokenAmount how many basetokens in the initial reserve + * @param baseTokenWeight base weight (hardcoded in deployer at 50%) + */ + function setup( + address datatokenAddress, + uint256 datatokenAmount, + uint256 datatokenWeight, + address baseTokenAddress, + uint256 baseTokenAmount, + uint256 baseTokenWeight + ) external _lock_ { + require(msg.sender == _controller, "ERR_INVALID_CONTROLLER"); + require( + datatokenAddress == _datatokenAddress, + "ERR_INVALID_DATATOKEN_ADDRESS" + ); + require( + baseTokenAddress == _baseTokenAddress, + "ERR_INVALID_baseToken_ADDRESS" + ); + // other inputs will be validated prior + // calling the below functions + // bind datatoken + bind(datatokenAddress, datatokenAmount, datatokenWeight); + emit LOG_JOIN( + msg.sender, + datatokenAddress, + datatokenAmount, + block.timestamp + ); + + // bind baseToken + bind(baseTokenAddress, baseTokenAmount, baseTokenWeight); + emit LOG_JOIN( + msg.sender, + baseTokenAddress, + baseTokenAmount, + block.timestamp + ); + // finalize + finalize(); + emit LOG_SETUP( + msg.sender, + baseTokenAddress, + baseTokenAmount, + baseTokenWeight, + datatokenAddress, + datatokenAmount, + datatokenWeight + ); + } + + //Proxy contract functionality: end + //----------------------------------------------------------------------- + /** + * @dev isPublicSwap + * Returns true if swapping is allowed + */ + function isPublicSwap() external view returns (bool) { + return _publicSwap; + } + /** + * @dev isFinalized + * Returns true if pool is finalized + */ + function isFinalized() external view returns (bool) { + return _finalized; + } + + /** + * @dev isBound + * Returns true if token is bound + * @param t token to be checked + */ + function isBound(address t) external view returns (bool) { + return _records[t].bound; + } + + function _checkBound(address token) internal view { + require(_records[token].bound, "ERR_NOT_BOUND"); + } + + /** + * @dev getNumTokens + * Returns number of tokens bounded to pool + */ + function getNumTokens() external view returns (uint256) { + return _tokens.length; + } + + /** + * @dev getCurrentTokens + * Returns tokens bounded to pool, before the pool is finalized + */ + function getCurrentTokens() + external + view + _viewlock_ + returns (address[] memory tokens) + { + return _tokens; + } + + /** + * @dev getFinalTokens + * Returns tokens bounded to pool, after the pool was finalized + */ + function getFinalTokens() + public + view + _viewlock_ + returns (address[] memory tokens) + { + require(_finalized, "ERR_NOT_FINALIZED"); + return _tokens; + } + + /** + * @dev collectOPC + * Collects and send all OPC Fees to _opcCollector. + * This funtion can be called by anyone, because fees are being sent to _opcCollector + */ + function collectOPC() external { + address[] memory tokens = getFinalTokens(); + for (uint256 i = 0; i < tokens.length; i++) { + uint256 amount = communityFees[tokens[i]]; + communityFees[tokens[i]] = 0; + IERC20(tokens[i]).safeTransfer(_opcCollector, amount); + emit OPCFee(msg.sender, _opcCollector, tokens[i], amount); + } + } + + /** + * @dev getCurrentOPCFees + * Get the current amount of fees which can be withdrawned by OPC + * @return address[] - array of tokens addresses + * uint256[] - array of amounts + */ + function getCurrentOPCFees() + public + view + returns (address[] memory, uint256[] memory) + { + address[] memory poolTokens = getFinalTokens(); + address[] memory tokens = new address[](poolTokens.length); + uint256[] memory amounts = new uint256[](poolTokens.length); + for (uint256 i = 0; i < poolTokens.length; i++) { + tokens[i] = poolTokens[i]; + amounts[i] = communityFees[poolTokens[i]]; + } + return (tokens, amounts); + } + + /** + * @dev getCurrentMarketFees + * Get the current amount of fees which can be withdrawned by _publishMarketCollector + * @return address[] - array of tokens addresses + * uint256[] - array of amounts + */ + function getCurrentMarketFees() + public + view + returns (address[] memory, uint256[] memory) + { + address[] memory poolTokens = getFinalTokens(); + address[] memory tokens = new address[](poolTokens.length); + uint256[] memory amounts = new uint256[](poolTokens.length); + for (uint256 i = 0; i < poolTokens.length; i++) { + tokens[i] = poolTokens[i]; + amounts[i] = publishMarketFees[poolTokens[i]]; + } + return (tokens, amounts); + } + + /** + * @dev collectMarketFee + * Collects and send all Market Fees to _publishMarketCollector. + * This function can be called by anyone, because fees are being sent to _publishMarketCollector + */ + function collectMarketFee() external { + address[] memory tokens = getFinalTokens(); + for (uint256 i = 0; i < tokens.length; i++) { + uint256 amount = publishMarketFees[tokens[i]]; + publishMarketFees[tokens[i]] = 0; + IERC20(tokens[i]).safeTransfer(_publishMarketCollector, amount); + emit PublishMarketFee( + msg.sender, + _publishMarketCollector, + tokens[i], + amount + ); + } + } + + /** + * @dev updatePublishMarketFee + * Set _newCollector as _publishMarketCollector + * @param _newCollector new _publishMarketCollector + * @param _newSwapFee new swapFee + */ + function updatePublishMarketFee(address _newCollector, uint256 _newSwapFee) external { + require(_publishMarketCollector == msg.sender, "ONLY MARKET COLLECTOR"); + require(_newCollector != address(0), "Invalid _newCollector address"); + require(_newSwapFee ==0 || _newSwapFee >= MIN_FEE, "ERR_MIN_FEE"); + require(_newSwapFee <= MAX_FEE, "ERR_MAX_FEE"); + _publishMarketCollector = _newCollector; + _swapPublishMarketFee = _newSwapFee; + emit PublishMarketFeeChanged(msg.sender, _publishMarketCollector, _swapPublishMarketFee); + } + + /** + * @dev getDenormalizedWeight + * Returns denormalized weight of a token + * @param token token to be checked + */ + function getDenormalizedWeight(address token) + external + view + _viewlock_ + returns (uint256) + { + _checkBound(token); + return _records[token].denorm; + } + + /** + * @dev getTotalDenormalizedWeight + * Returns total denormalized weught of the pool + */ + function getTotalDenormalizedWeight() + external + view + _viewlock_ + returns (uint256) + { + return _totalWeight; + } + + /** + * @dev getNormalizedWeight + * Returns normalized weight of a token + * @param token token to be checked + */ + + function getNormalizedWeight(address token) + external + view + _viewlock_ + returns (uint256) + { + _checkBound(token); + uint256 denorm = _records[token].denorm; + return bdiv(denorm, _totalWeight); + } + + + /** + * @dev getBalance + * Returns the current token reserve amount + * @param token token to be checked + */ + function getBalance(address token) + external + view + _viewlock_ + returns (uint256) + { + _checkBound(token); + return _records[token].balance; + } + + /** + * @dev getSwapFee + * Returns the current Liquidity Providers swap fee + */ + function getSwapFee() external view returns (uint256) { + return _swapFee; + } + + /** + * @dev getMarketFee + * Returns the current fee of publishingMarket + */ + function getMarketFee() external view returns (uint256) { + return _swapPublishMarketFee; + } + + /** + * @dev getController + * Returns the current controller address (ssBot) + */ + function getController() external view returns (address) { + return _controller; + } + + /** + * @dev getDatatokenAddress + * Returns the current datatoken address + */ + function getDatatokenAddress() external view returns (address) { + return _datatokenAddress; + } + + /** + * @dev getBaseTokenAddress + * Returns the current baseToken address + */ + function getBaseTokenAddress() external view returns (address) { + return _baseTokenAddress; + } + + + /** + * @dev setSwapFee + * Allows controller to change the swapFee + * @param swapFee new swap fee (max 1e17 = 10 % , 1e16 = 1% , 1e15 = 0.1%, 1e14 = 0.01%) + */ + function setSwapFee(uint256 swapFee) public { + require(msg.sender == _controller, "ERR_NOT_CONTROLLER"); + require(swapFee >= MIN_FEE, "ERR_MIN_FEE"); + require(swapFee <= MAX_FEE, "ERR_MAX_FEE"); + _swapFee = swapFee; + emit SwapFeeChanged(msg.sender, swapFee); + } + + /** + * @dev finalize + * Finalize pool. After this,new tokens cannot be bound + */ + function finalize() internal { + _finalized = true; + _publicSwap = true; + + _mintPoolShare(INIT_POOL_SUPPLY); + _pushPoolShare(msg.sender, INIT_POOL_SUPPLY); + } + + /** + * @dev bind + * Bind a new token to the pool. + * @param token token address + * @param balance initial reserve + * @param denorm denormalized weight + */ + function bind( + address token, + uint256 balance, + uint256 denorm + ) internal { + require(msg.sender == _controller, "ERR_NOT_CONTROLLER"); + require(!_records[token].bound, "ERR_IS_BOUND"); + require(!_finalized, "ERR_IS_FINALIZED"); + + require(_tokens.length < MAX_BOUND_TOKENS, "ERR_MAX_TOKENS"); + + _records[token] = Record({ + bound: true, + index: _tokens.length, + denorm: 0, // balance and denorm will be validated + balance: 0 // and set by `rebind` + }); + _tokens.push(token); + rebind(token, balance, denorm); + } + + /** + * @dev rebind + * Update pool reserves & weight after a token bind + * @param token token address + * @param balance initial reserve + * @param denorm denormalized weight + */ + function rebind( + address token, + uint256 balance, + uint256 denorm + ) internal { + require(denorm >= MIN_WEIGHT, "ERR_MIN_WEIGHT"); + require(denorm <= MAX_WEIGHT, "ERR_MAX_WEIGHT"); + require(balance >= MIN_BALANCE, "ERR_MIN_BALANCE"); + + // Adjust the denorm and totalWeight + uint256 oldWeight = _records[token].denorm; + if (denorm > oldWeight) { + _totalWeight = badd(_totalWeight, bsub(denorm, oldWeight)); + require(_totalWeight <= MAX_TOTAL_WEIGHT, "ERR_MAX_TOTAL_WEIGHT"); + } else if (denorm < oldWeight) { + _totalWeight = bsub(_totalWeight, bsub(oldWeight, denorm)); + } + _records[token].denorm = denorm; + + // Adjust the balance record and actual token balance + uint256 oldBalance = _records[token].balance; + _records[token].balance = balance; + if (balance > oldBalance) { + _pullUnderlying(token, msg.sender, bsub(balance, oldBalance)); + } else if (balance < oldBalance) { + // In this case liquidity is being withdrawn, we don't have EXIT_FEES + uint256 tokenBalanceWithdrawn = bsub(oldBalance, balance); + _pushUnderlying( + token, + msg.sender, + tokenBalanceWithdrawn + ); + } + } + + /** + * @dev getSpotPrice + * Return the spot price of swapping tokenIn to tokenOut + * @param tokenIn in token + * @param tokenOut out token + * @param _consumeMarketSwapFee consume market swap fee + */ + function getSpotPrice( + address tokenIn, + address tokenOut, + uint256 _consumeMarketSwapFee + ) external view _viewlock_ returns (uint256 spotPrice) { + _checkBound(tokenIn); + _checkBound(tokenOut); + Record storage inRecord = _records[tokenIn]; + Record storage outRecord = _records[tokenOut]; + return + calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + _consumeMarketSwapFee + ); + } + + // view function used for batch buy. useful for frontend + /** + * @dev getAmountInExactOut + * How many tokensIn do you need in order to get exact tokenAmountOut. + Returns: tokenAmountIn, LPFee, opcFee , publishMarketSwapFee, consumeMarketSwapFee + * @param tokenIn token to be swaped + * @param tokenOut token to get + * @param tokenAmountOut exact amount of tokenOut + * @param _consumeMarketSwapFee consume market swap fee + */ + + function getAmountInExactOut( + address tokenIn, + address tokenOut, + uint256 tokenAmountOut, + uint256 _consumeMarketSwapFee + ) + external + view + returns ( + // _viewlock_ + uint256 tokenAmountIn, uint lpFeeAmount, + uint oceanFeeAmount, + uint publishMarketSwapFeeAmount, + uint consumeMarketSwapFeeAmount + ) + { + _checkBound(tokenIn); + _checkBound(tokenOut); + uint256[4] memory data = [ + _records[tokenIn].balance, + _records[tokenIn].denorm, + _records[tokenOut].balance, + _records[tokenOut].denorm + ]; + uint tokenAmountInBalance; + swapfees memory _swapfees; + (tokenAmountIn, tokenAmountInBalance, _swapfees) = + calcInGivenOut( + data, + tokenAmountOut, + // tokenIn, + _consumeMarketSwapFee + ); + return(tokenAmountIn, _swapfees.LPFee, _swapfees.oceanFeeAmount, + _swapfees.publishMarketFeeAmount, _swapfees.consumeMarketFee); + + } + + // view function useful for frontend + /** + * @dev getAmountOutExactIn + * How many tokensOut you will get for a exact tokenAmountIn + Returns: tokenAmountOut, LPFee, opcFee , publishMarketSwapFee, consumeMarketSwapFee + * @param tokenIn token to be swaped + * @param tokenOut token to get + * @param tokenAmountOut exact amount of tokenOut + * @param _consumeMarketSwapFee consume market swap fee + */ + function getAmountOutExactIn( + address tokenIn, + address tokenOut, + uint256 tokenAmountIn, + uint256 _consumeMarketSwapFee + ) + external + view + returns ( + // _viewlock_ + uint256 tokenAmountOut, + uint lpFeeAmount, + uint oceanFeeAmount, + uint publishMarketSwapFeeAmount, + uint consumeMarketSwapFeeAmount + ) + { + _checkBound(tokenIn); + _checkBound(tokenOut); + uint256[4] memory data = [ + _records[tokenIn].balance, + _records[tokenIn].denorm, + _records[tokenOut].balance, + _records[tokenOut].denorm + ]; + uint balanceInToAdd; + swapfees memory _swapfees; + (tokenAmountOut, balanceInToAdd, _swapfees) = + calcOutGivenIn( + data, + tokenAmountIn, + // tokenIn, + _consumeMarketSwapFee + ); + return(tokenAmountOut, _swapfees.LPFee, + _swapfees.oceanFeeAmount, _swapfees.publishMarketFeeAmount, _swapfees.consumeMarketFee); + } + + + /** + * @dev joinPool + * Adds dual side liquidity to the pool (both datatoken and basetoken) + * @param poolAmountOut expected number of pool shares that you will get + * @param maxAmountsIn array with maxium amounts spent + */ + function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) + external + _lock_ + { + require(_finalized, "ERR_NOT_FINALIZED"); + + uint256 poolTotal = totalSupply(); + uint256 ratio = bdiv(poolAmountOut, poolTotal); + require(ratio != 0, "ERR_MATH_APPROX"); + for (uint256 i = 0; i < _tokens.length; i++) { + address t = _tokens[i]; + uint256 bal = _records[t].balance; + uint256 tokenAmountIn = bmul(ratio, bal); + require(tokenAmountIn != 0, "ERR_MATH_APPROX"); + require(tokenAmountIn <= maxAmountsIn[i], "ERR_LIMIT_IN"); + _records[t].balance = badd(_records[t].balance, tokenAmountIn); + emit LOG_JOIN(msg.sender, t, tokenAmountIn, block.timestamp); + _pullUnderlying(t, msg.sender, tokenAmountIn); + } + _mintPoolShare(poolAmountOut); + _pushPoolShare(msg.sender, poolAmountOut); + emit LOG_BPT(poolAmountOut); + } + + /** + * @dev exitPool + * Removes dual side liquidity from the pool (both datatoken and basetoken) + * @param poolAmountIn amount of pool shares spent + * @param minAmountsOut array with minimum amount of tokens expected + */ + function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) + external + _lock_ + { + require(_finalized, "ERR_NOT_FINALIZED"); + + uint256 poolTotal = totalSupply(); + //uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); + //uint256 pAiAfterExitFee = bsub(poolAmountIn, exitFee); + + uint256 ratio = bdiv(poolAmountIn, poolTotal); + require(ratio != 0, "ERR_MATH_APPROX"); + + _pullPoolShare(msg.sender, poolAmountIn); + //_pushPoolShare(_factory, exitFee); + _burnPoolShare(poolAmountIn); + + for (uint256 i = 0; i < _tokens.length; i++) { + address t = _tokens[i]; + uint256 bal = _records[t].balance; + uint256 tokenAmountOut = bmul(ratio, bal); + require(tokenAmountOut != 0, "ERR_MATH_APPROX"); + require(tokenAmountOut >= minAmountsOut[i], "ERR_LIMIT_OUT"); + _records[t].balance = bsub(_records[t].balance, tokenAmountOut); + emit LOG_EXIT(msg.sender, t, tokenAmountOut, block.timestamp); + _pushUnderlying(t, msg.sender, tokenAmountOut); + } + emit LOG_BPT(poolAmountIn); + } + + /** + * @dev swapExactAmountIn + * Swaps an exact amount of tokensIn to get a mimum amount of tokenOut + * @param tokenInOutMarket array of addreses: [tokenIn, tokenOut, consumeMarketFeeAddress] + * @param amountsInOutMaxFee array of ints: [tokenAmountIn, minAmountOut, maxPrice, consumeMarketSwapFee] + */ + function swapExactAmountIn( + address[3] calldata tokenInOutMarket, + uint256[4] calldata amountsInOutMaxFee + ) external _lock_ returns (uint256 tokenAmountOut, uint256 spotPriceAfter) { + require(_finalized, "ERR_NOT_FINALIZED"); + _checkBound(tokenInOutMarket[0]); + _checkBound(tokenInOutMarket[1]); + Record storage inRecord = _records[address(tokenInOutMarket[0])]; + Record storage outRecord = _records[address(tokenInOutMarket[1])]; + require(amountsInOutMaxFee[3] ==0 || amountsInOutMaxFee[3] >= MIN_FEE,'ConsumeSwapFee too low'); + require(amountsInOutMaxFee[3] <= MAX_FEE,'ConsumeSwapFee too high'); + require( + amountsInOutMaxFee[0] <= bmul(inRecord.balance, MAX_IN_RATIO), + "ERR_MAX_IN_RATIO" + ); + + uint256 spotPriceBefore = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + amountsInOutMaxFee[3] + ); + + require( + spotPriceBefore <= amountsInOutMaxFee[2], + "ERR_BAD_LIMIT_PRICE" + ); + uint256 balanceInToAdd; + uint256[4] memory data = [ + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm + ]; + swapfees memory _swapfees; + (tokenAmountOut, balanceInToAdd, _swapfees) = calcOutGivenIn( + data, + amountsInOutMaxFee[0], + // tokenInOutMarket[0], + amountsInOutMaxFee[3] + ); + // update balances + communityFees[tokenInOutMarket[0]] = badd(communityFees[tokenInOutMarket[0]],_swapfees.oceanFeeAmount); + publishMarketFees[tokenInOutMarket[0]] = + badd(publishMarketFees[tokenInOutMarket[0]],_swapfees.publishMarketFeeAmount); + emit SWAP_FEES(_swapfees.LPFee, _swapfees.oceanFeeAmount, + _swapfees.publishMarketFeeAmount,_swapfees.consumeMarketFee, tokenInOutMarket[0]); + require(tokenAmountOut >= amountsInOutMaxFee[1], "ERR_LIMIT_OUT"); + + inRecord.balance = badd(inRecord.balance, balanceInToAdd); + outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + + spotPriceAfter = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + amountsInOutMaxFee[3] + ); + + require(spotPriceAfter >= spotPriceBefore, "ERR_MATH_APPROX"); + require(spotPriceAfter <= amountsInOutMaxFee[2], "ERR_LIMIT_PRICE"); + + require( + spotPriceBefore <= bdiv(amountsInOutMaxFee[0], tokenAmountOut), + "ERR_MATH_APPROX" + ); + + emit LOG_SWAP( + msg.sender, + tokenInOutMarket[0], + tokenInOutMarket[1], + amountsInOutMaxFee[0], + tokenAmountOut, + block.timestamp + ); + + _pullUnderlying(tokenInOutMarket[0], msg.sender, amountsInOutMaxFee[0]); + uint256 consumeMarketFeeAmount = bsub( + amountsInOutMaxFee[0], + bmul(amountsInOutMaxFee[0], bsub(BONE, amountsInOutMaxFee[3])) + ); + if (amountsInOutMaxFee[3] > 0) { + IERC20(tokenInOutMarket[0]).safeTransfer( + tokenInOutMarket[2], + consumeMarketFeeAmount + ); + emit ConsumeMarketFee( + tokenInOutMarket[2], + tokenInOutMarket[0], + consumeMarketFeeAmount + ); + } + _pushUnderlying(tokenInOutMarket[1], msg.sender, tokenAmountOut); + + return (tokenAmountOut, spotPriceAfter); //returning spot price 0 because there is no public spotPrice + } + + + /** + * @dev swapExactAmountOut + * Swaps a maximum maxAmountIn of tokensIn to get an exact amount of tokenOut + * @param tokenInOutMarket array of addreses: [tokenIn, tokenOut, consumeMarketFeeAddress] + * @param amountsInOutMaxFee array of ints: [maxAmountIn,tokenAmountOut,maxPrice, consumeMarketSwapFee] + */ + function swapExactAmountOut( + address[3] calldata tokenInOutMarket, + uint256[4] calldata amountsInOutMaxFee + ) external _lock_ returns (uint256 tokenAmountIn, uint256 spotPriceAfter) { + require(_finalized, "ERR_NOT_FINALIZED"); + require(amountsInOutMaxFee[3] ==0 || amountsInOutMaxFee[3] >= MIN_FEE,'ConsumeSwapFee too low'); + require(amountsInOutMaxFee[3] <= MAX_FEE,'ConsumeSwapFee too high'); + _checkBound(tokenInOutMarket[0]); + _checkBound(tokenInOutMarket[1]); + Record storage inRecord = _records[address(tokenInOutMarket[0])]; + Record storage outRecord = _records[address(tokenInOutMarket[1])]; + + require( + amountsInOutMaxFee[1] <= bmul(outRecord.balance, MAX_OUT_RATIO), + "ERR_MAX_OUT_RATIO" + ); + + uint256 spotPriceBefore = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + amountsInOutMaxFee[3] + ); + + require( + spotPriceBefore <= amountsInOutMaxFee[2], + "ERR_BAD_LIMIT_PRICE" + ); + // this is the amount we are going to register in balances + // (only takes account of swapFee, not OPC and market fee, + //in order to not affect price during following swaps, fee wtihdrawl etc) + uint256 balanceToAdd; + uint256[4] memory data = [ + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm + ]; + swapfees memory _swapfees; + (tokenAmountIn, balanceToAdd, + _swapfees) = calcInGivenOut( + data, + amountsInOutMaxFee[1], + //tokenInOutMarket[0], + amountsInOutMaxFee[3] + ); + communityFees[tokenInOutMarket[0]] = badd(communityFees[tokenInOutMarket[0]],_swapfees.oceanFeeAmount); + publishMarketFees[tokenInOutMarket[0]] + = badd(publishMarketFees[tokenInOutMarket[0]],_swapfees.publishMarketFeeAmount); + emit SWAP_FEES(_swapfees.LPFee, _swapfees.oceanFeeAmount, + _swapfees.publishMarketFeeAmount,_swapfees.consumeMarketFee, tokenInOutMarket[0]); + require(tokenAmountIn <= amountsInOutMaxFee[0], "ERR_LIMIT_IN"); + + inRecord.balance = badd(inRecord.balance, balanceToAdd); + outRecord.balance = bsub(outRecord.balance, amountsInOutMaxFee[1]); + + spotPriceAfter = calcSpotPrice( + inRecord.balance, + inRecord.denorm, + outRecord.balance, + outRecord.denorm, + amountsInOutMaxFee[3] + ); + + require(spotPriceAfter >= spotPriceBefore, "ERR_MATH_APPROX"); + require(spotPriceAfter <= amountsInOutMaxFee[2], "ERR_LIMIT_PRICE"); + require( + spotPriceBefore <= bdiv(tokenAmountIn, amountsInOutMaxFee[1]), + "ERR_MATH_APPROX" + ); + + emit LOG_SWAP( + msg.sender, + tokenInOutMarket[0], + tokenInOutMarket[1], + tokenAmountIn, + amountsInOutMaxFee[1], + block.timestamp + ); + _pullUnderlying(tokenInOutMarket[0], msg.sender, tokenAmountIn); + uint256 consumeMarketFeeAmount = bsub( + tokenAmountIn, + bmul(tokenAmountIn, bsub(BONE, amountsInOutMaxFee[3])) + ); + if (amountsInOutMaxFee[3] > 0) { + IERC20(tokenInOutMarket[0]).safeTransfer( + tokenInOutMarket[2],// market address + consumeMarketFeeAmount + ); + emit ConsumeMarketFee( + tokenInOutMarket[2], // to (market address) + tokenInOutMarket[0], // token + consumeMarketFeeAmount + ); + } + _pushUnderlying(tokenInOutMarket[1], msg.sender, amountsInOutMaxFee[1]); + return (tokenAmountIn, spotPriceAfter); + } + + /** + * @dev joinswapExternAmountIn + * Single side add liquidity to the pool, + * expecting a minPoolAmountOut of shares for spending tokenAmountIn basetokens + * @param tokenAmountIn exact number of base tokens to spend + * @param minPoolAmountOut minimum of pool shares expectex + */ + function joinswapExternAmountIn( + uint256 tokenAmountIn, + uint256 minPoolAmountOut + ) external _lock_ returns (uint256 poolAmountOut) { + //tokenIn = _baseTokenAddress; + require(_finalized, "ERR_NOT_FINALIZED"); + _checkBound(_baseTokenAddress); + require( + tokenAmountIn <= bmul(_records[_baseTokenAddress].balance, MAX_IN_RATIO), + "ERR_MAX_IN_RATIO" + ); + //ask ssContract + Record storage inRecord = _records[_baseTokenAddress]; + + poolAmountOut = calcPoolOutGivenSingleIn( + inRecord.balance, + inRecord.denorm, + _totalSupply, + _totalWeight, + tokenAmountIn + ); + + require(poolAmountOut >= minPoolAmountOut, "ERR_LIMIT_OUT"); + + inRecord.balance = badd(inRecord.balance, tokenAmountIn); + + emit LOG_JOIN(msg.sender, _baseTokenAddress, tokenAmountIn, block.timestamp); + emit LOG_BPT(poolAmountOut); + _mintPoolShare(poolAmountOut); + _pushPoolShare(msg.sender, poolAmountOut); + + _pullUnderlying(_baseTokenAddress, msg.sender, tokenAmountIn); + + //ask the ssContract to stake as well + //calculate how much should the 1ss stake + Record storage ssInRecord = _records[_datatokenAddress]; + uint256 ssAmountIn = calcSingleInGivenPoolOut( + ssInRecord.balance, + ssInRecord.denorm, + _totalSupply, + _totalWeight, + poolAmountOut + ); + if (ssContract.canStake(_datatokenAddress, ssAmountIn)) { + //call 1ss to approve + ssContract.Stake(_datatokenAddress, ssAmountIn); + // follow the same path + ssInRecord.balance = badd(ssInRecord.balance, ssAmountIn); + emit LOG_JOIN( + _controller, + _datatokenAddress, + ssAmountIn, + block.timestamp + ); + emit LOG_BPT_SS(poolAmountOut); + _mintPoolShare(poolAmountOut); + _pushPoolShare(_controller, poolAmountOut); + _pullUnderlying(_datatokenAddress, _controller, ssAmountIn); + } + return poolAmountOut; + } + + + /** + * @dev exitswapPoolAmountIn + * Single side remove liquidity from the pool, + * expecting a minAmountOut of basetokens for spending poolAmountIn pool shares + * @param poolAmountIn exact number of pool shares to spend + * @param minAmountOut minimum amount of basetokens expected + */ + function exitswapPoolAmountIn( + uint256 poolAmountIn, + uint256 minAmountOut + ) external _lock_ returns (uint256 tokenAmountOut) { + //tokenOut = _baseTokenAddress; + require(_finalized, "ERR_NOT_FINALIZED"); + _checkBound(_baseTokenAddress); + + Record storage outRecord = _records[_baseTokenAddress]; + + tokenAmountOut = calcSingleOutGivenPoolIn( + outRecord.balance, + outRecord.denorm, + _totalSupply, + _totalWeight, + poolAmountIn + ); + + require(tokenAmountOut >= minAmountOut, "ERR_LIMIT_OUT"); + + require( + tokenAmountOut <= bmul(_records[_baseTokenAddress].balance, MAX_OUT_RATIO), + "ERR_MAX_OUT_RATIO" + ); + + outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + + //uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); + + emit LOG_EXIT(msg.sender, _baseTokenAddress, tokenAmountOut, block.timestamp); + emit LOG_BPT(poolAmountIn); + + _pullPoolShare(msg.sender, poolAmountIn); + + //_burnPoolShare(bsub(poolAmountIn, exitFee)); + _burnPoolShare(poolAmountIn); + //_pushPoolShare(_factory, exitFee); + _pushUnderlying(_baseTokenAddress, msg.sender, tokenAmountOut); + + //ask the ssContract to unstake as well + //calculate how much should the 1ss unstake + + if ( + ssContract.canUnStake(_datatokenAddress, poolAmountIn) + ) { + Record storage ssOutRecord = _records[_datatokenAddress]; + uint256 ssAmountOut = calcSingleOutGivenPoolIn( + ssOutRecord.balance, + ssOutRecord.denorm, + _totalSupply, + _totalWeight, + poolAmountIn + ); + + ssOutRecord.balance = bsub(ssOutRecord.balance, ssAmountOut); + //exitFee = bmul(poolAmountIn, EXIT_FEE); + emit LOG_EXIT( + _controller, + _datatokenAddress, + ssAmountOut, + block.timestamp + ); + _pullPoolShare(_controller, poolAmountIn); + //_burnPoolShare(bsub(poolAmountIn, exitFee)); + _burnPoolShare(poolAmountIn); + //_pushPoolShare(_factory, exitFee); + _pushUnderlying(_datatokenAddress, _controller, ssAmountOut); + //call unstake on 1ss to do cleanup on their side + ssContract.UnStake( + _datatokenAddress, + ssAmountOut, + poolAmountIn + ); + emit LOG_BPT_SS(poolAmountIn); + } + return tokenAmountOut; + } + + + + /** + * @dev calcSingleOutPoolIn + * Returns expected amount of tokenOut for removing exact poolAmountIn pool shares from the pool + * @param tokenOut tokenOut + * @param poolAmountIn amount of shares spent + */ + function calcSingleOutPoolIn(address tokenOut, uint256 poolAmountIn) + external + view + returns (uint256 tokenAmountOut) + { + Record memory outRecord = _records[tokenOut]; + + tokenAmountOut = calcSingleOutGivenPoolIn( + outRecord.balance, + outRecord.denorm, + _totalSupply, + _totalWeight, + poolAmountIn + ); + + return tokenAmountOut; + } + + /** + * @dev calcPoolInSingleOut + * Returns number of poolshares needed to withdraw exact tokenAmountOut tokens + * @param tokenOut tokenOut + * @param tokenAmountOut expected amount of tokensOut + */ + function calcPoolInSingleOut(address tokenOut, uint256 tokenAmountOut) + external + view + returns (uint256 poolAmountIn) + { + Record memory outRecord = _records[tokenOut]; + + poolAmountIn = calcPoolInGivenSingleOut( + outRecord.balance, + outRecord.denorm, + _totalSupply, + _totalWeight, + tokenAmountOut + ); + return poolAmountIn; + } + + /** + * @dev calcSingleInPoolOut + * Returns number of tokens to be staked to the pool in order to get an exact number of poolshares + * @param tokenIn tokenIn + * @param poolAmountOut expected amount of pool shares + */ + function calcSingleInPoolOut(address tokenIn, uint256 poolAmountOut) + external + view + returns (uint256 tokenAmountIn) + { + Record memory inRecord = _records[tokenIn]; + + tokenAmountIn = calcSingleInGivenPoolOut( + inRecord.balance, + inRecord.denorm, + _totalSupply, + _totalWeight, + poolAmountOut + ); + + return tokenAmountIn; + } + + /** + * @dev calcPoolOutSingleIn + * Returns number of poolshares obtain by staking exact tokenAmountIn tokens + * @param tokenIn tokenIn + * @param tokenAmountIn exact number of tokens staked + */ + function calcPoolOutSingleIn(address tokenIn, uint256 tokenAmountIn) + external + view + returns (uint256 poolAmountOut) + { + Record memory inRecord = _records[tokenIn]; + + poolAmountOut = calcPoolOutGivenSingleIn( + inRecord.balance, + inRecord.denorm, + _totalSupply, + _totalWeight, + tokenAmountIn + ); + + return poolAmountOut; + } + + + // Internal functions below + + // == + // 'Underlying' token-manipulation functions make external calls but are NOT locked + // You must `_lock_` or otherwise ensure reentry-safety + function _pullUnderlying( + address erc20, + address from, + uint256 amount + ) internal { + uint256 balanceBefore = IERC20(erc20).balanceOf(address(this)); + IERC20(erc20).safeTransferFrom(from, address(this), amount); + require(IERC20(erc20).balanceOf(address(this)) >= balanceBefore + amount, + "Transfer amount is too low"); + //require(xfer, "ERR_ERC20_FALSE"); + } + + function _pushUnderlying( + address erc20, + address to, + uint256 amount + ) internal { + IERC20(erc20).safeTransfer(to, amount); + //require(xfer, "ERR_ERC20_FALSE"); + } + + function _pullPoolShare(address from, uint256 amount) internal { + _pull(from, amount); + } + + function _pushPoolShare(address to, uint256 amount) internal { + _push(to, amount); + } + + function _mintPoolShare(uint256 amount) internal { + _mint(amount); + } + + function _burnPoolShare(uint256 amount) internal { + _burn(amount); + } +} diff --git a/sol080/contracts/oceanv4/pools/balancer/BToken.sol b/sol080/contracts/oceanv4/pools/balancer/BToken.sol new file mode 100644 index 0000000000000000000000000000000000000000..ab06b115ef91e657d856f33abd2e6053d948e7e5 --- /dev/null +++ b/sol080/contracts/oceanv4/pools/balancer/BToken.sol @@ -0,0 +1,157 @@ +// 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 3 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, see <http://www.gnu.org/licenses/>. + +pragma solidity 0.8.10; +// Copyright Balancer, BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import './BNum.sol'; +// import 'OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/IERC20.sol'; +import '../../interfaces/IERC20.sol'; +// Highly opinionated token implementation + +// interface IERC20 { +// event Approval(address indexed src, address indexed dst, uint amt); +// event Transfer(address indexed src, address indexed dst, uint amt); + +// function totalSupply() external view returns (uint); +// function balanceOf(address whom) external view returns (uint); +// function allowance(address src, address dst) external view returns (uint); + +// function approve(address dst, uint amt) external returns (bool); +// function transfer(address dst, uint amt) external returns (bool); +// function transferFrom( +// address src, address dst, uint amt +// ) external returns (bool); +// } + +contract BTokenBase is BNum { + + mapping(address => uint) internal _balance; + mapping(address => mapping(address=>uint)) internal _allowance; + uint internal _totalSupply; + + event Approval(address indexed src, address indexed dst, uint amt); + event Transfer(address indexed src, address indexed dst, uint amt); + + function _mint(uint amt) internal { + _balance[address(this)] = badd(_balance[address(this)], amt); + _totalSupply = badd(_totalSupply, amt); + emit Transfer(address(0), address(this), amt); + } + + function _burn(uint amt) internal { + require( + _balance[address(this)] >= amt, + 'ERR_INSUFFICIENT_BAL' + ); + _balance[address(this)] = bsub(_balance[address(this)], amt); + _totalSupply = bsub(_totalSupply, amt); + emit Transfer(address(this), address(0), amt); + } + + function _move(address src, address dst, uint amt) internal { + require(_balance[src] >= amt, 'ERR_INSUFFICIENT_BAL'); + _balance[src] = bsub(_balance[src], amt); + _balance[dst] = badd(_balance[dst], amt); + emit Transfer(src, dst, amt); + } + + function _push(address to, uint amt) internal { + _move(address(this), to, amt); + } + + function _pull(address from, uint amt) internal { + _move(from, address(this), amt); + } +} + +contract BToken is BTokenBase { + + string private _name = 'Balancer Pool Token'; + string private _symbol = 'BPT'; + uint8 private _decimals = 18; + + function name() external view returns (string memory) { + return _name; + } + + function symbol() external view returns (string memory) { + return _symbol; + } + + function decimals() external view returns(uint8) { + return _decimals; + } + + function allowance(address src, address dst) external view returns (uint256) { + return _allowance[src][dst]; + } + + function balanceOf(address whom) external view returns (uint) { + return _balance[whom]; + } + + function totalSupply() public view returns (uint) { + return _totalSupply; + } + + function approve(address dst, uint amt) external returns (bool) { + _allowance[msg.sender][dst] = amt; + emit Approval(msg.sender, dst, amt); + return true; + } + + function increaseApproval(address dst, uint amt) external returns (bool) { + _allowance[msg.sender][dst] = badd(_allowance[msg.sender][dst], amt); + emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); + return true; + } + + function decreaseApproval(address dst, uint amt) external returns (bool) { + uint oldValue = _allowance[msg.sender][dst]; + if (amt > oldValue) { + _allowance[msg.sender][dst] = 0; + } else { + _allowance[msg.sender][dst] = bsub(oldValue, amt); + } + emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); + return true; + } + + function transfer(address dst, uint amt) external returns (bool) { + _move(msg.sender, dst, amt); + return true; + } + + function transferFrom( + address src, + address dst, + uint amt + ) + external + returns (bool) + { + require( + msg.sender == src || amt <= _allowance[src][msg.sender], + 'ERR_BTOKEN_BAD_CALLER' + ); + _move(src, dst, amt); + if (msg.sender != src && _allowance[src][msg.sender] != uint256(int(-1)) ) { + _allowance[src][msg.sender] = bsub(_allowance[src][msg.sender], amt); + emit Approval(msg.sender, dst, _allowance[src][msg.sender]); + } + return true; + } +} diff --git a/sol080/contracts/oceanv4/pools/dispenser/Dispenser.sol b/sol080/contracts/oceanv4/pools/dispenser/Dispenser.sol new file mode 100644 index 0000000000000000000000000000000000000000..287eb304648096edb7367e2bd8c4390bb4d7abb4 --- /dev/null +++ b/sol080/contracts/oceanv4/pools/dispenser/Dispenser.sol @@ -0,0 +1,273 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "../../interfaces/IERC20.sol"; +import "../../interfaces/IERC20Template.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; + +import "../../utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/security/ReentrancyGuard.sol"; + +contract Dispenser is ReentrancyGuard{ + using SafeMath for uint256; + using SafeERC20 for IERC20; + address public router; + + struct DataToken { + bool active; // if the dispenser is active for this datatoken + address owner; // owner of this dispenser + uint256 maxTokens; // max tokens to dispense + uint256 maxBalance; // max balance of requester. + address allowedSwapper; + //If the balance is higher, the dispense is rejected + } + mapping(address => DataToken) datatokens; + address[] public datatokensList; + + + event DispenserCreated( // emited when a dispenser is created + address indexed datatokenAddress, + address indexed owner, + uint256 maxTokens, + uint256 maxBalance, + address allowedSwapper + ); + event DispenserActivated( // emited when a dispenser is activated + address indexed datatokenAddress + ); + + event DispenserDeactivated( // emited when a dispenser is deactivated + address indexed datatokenAddress + ); + event DispenserAllowedSwapperChanged( // emited when allowedSwapper is changed + address indexed datatoken, + address indexed newAllowedSwapper); + + event TokensDispensed( + // emited when tokens are dispended + address indexed datatokenAddress, + address indexed userAddress, + uint256 amount + ); + + event OwnerWithdrawed( + address indexed datatoken, + address indexed owner, + uint256 amount + ); + + modifier onlyRouter() { + require(msg.sender == router, "Dispenser: only router"); + _; + } + + constructor(address _router) { + require(_router != address(0), "Dispenser: Wrong Router address"); + router = _router; + } + + /** + * @dev getId + * Return template id in case we need different ABIs. + * If you construct your own template, please make sure to change the hardcoded value + */ + function getId() pure public returns (uint8) { + return 1; + } + /** + * @dev status + * Get information about a datatoken dispenser + * @param datatoken refers to datatoken address. + * @return active - if the dispenser is active for this datatoken + * @return owner - owner of this dispenser + * @return isMinter - check the datatoken contract if the dispenser has mint roles + * @return maxTokens - max tokens to dispense + * @return maxBalance - max balance of requester. If the balance is higher, the dispense is rejected + * @return balance - internal balance of the contract (if any) + * @return allowedSwapper - address allowed to request DT if != 0 + */ + function status(address datatoken) + external view + returns(bool active,address owner, + bool isMinter,uint256 maxTokens,uint256 maxBalance, uint256 balance, address allowedSwapper){ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + active = datatokens[datatoken].active; + owner = datatokens[datatoken].owner; + maxTokens = datatokens[datatoken].maxTokens; + maxBalance = datatokens[datatoken].maxBalance; + IERC20Template tokenInstance = IERC20Template(datatoken); + balance = tokenInstance.balanceOf(address(this)); + isMinter = tokenInstance.isMinter(address(this)); + allowedSwapper = datatokens[datatoken].allowedSwapper; + } + + /** + * @dev create + * Create a new dispenser + * @param datatoken refers to datatoken address. + * @param maxTokens - max tokens to dispense + * @param maxBalance - max balance of requester. + * @param owner - owner + * @param allowedSwapper - if !=0, only this address can request DTs + */ + function create(address datatoken,uint256 maxTokens, uint256 maxBalance, address owner, address allowedSwapper) + external onlyRouter{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == address(0) || datatokens[datatoken].owner == owner, + 'Datatoken already created' + ); + datatokens[datatoken].active = true; + datatokens[datatoken].owner = owner; + datatokens[datatoken].maxTokens = maxTokens; + datatokens[datatoken].maxBalance = maxBalance; + datatokens[datatoken].allowedSwapper = allowedSwapper; + datatokensList.push(datatoken); + emit DispenserCreated(datatoken, owner, maxTokens, maxBalance, allowedSwapper); + emit DispenserAllowedSwapperChanged(datatoken, allowedSwapper); + } + /** + * @dev activate + * Activate a new dispenser + * @param datatoken refers to datatoken address. + * @param maxTokens - max tokens to dispense + * @param maxBalance - max balance of requester. + */ + function activate(address datatoken,uint256 maxTokens, uint256 maxBalance) + external { + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == msg.sender, 'Invalid owner' + ); + datatokens[datatoken].active = true; + datatokens[datatoken].maxTokens = maxTokens; + datatokens[datatoken].maxBalance = maxBalance; + datatokensList.push(datatoken); + emit DispenserActivated(datatoken); + } + + /** + * @dev deactivate + * Deactivate an existing dispenser + * @param datatoken refers to datatoken address. + */ + function deactivate(address datatoken) external{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == msg.sender, + 'Datatoken already activated' + ); + datatokens[datatoken].active = false; + emit DispenserDeactivated(datatoken); + } + + /** + * @dev setAllowedSwapper + * Sets a new allowedSwapper + * @param datatoken refers to datatoken address. + * @param newAllowedSwapper refers to the new allowedSwapper + */ + function setAllowedSwapper(address datatoken, address newAllowedSwapper) external{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == msg.sender, + 'DataToken already activated' + ); + datatokens[datatoken].allowedSwapper= newAllowedSwapper; + emit DispenserAllowedSwapperChanged(datatoken, newAllowedSwapper); + } + + + + /** + * @dev dispense + * Dispense datatokens to caller. + * The dispenser must be active, hold enough DT (or be able to mint more) + * and respect maxTokens/maxBalance requirements + * @param datatoken refers to datatoken address. + * @param amount amount of datatokens required. + * @param destination refers to who will receive the tokens + */ + function dispense(address datatoken, uint256 amount, address destination) external nonReentrant payable{ + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].active, + 'Dispenser not active' + ); + require( + amount > 0, + 'Invalid zero amount' + ); + require( + datatokens[datatoken].maxTokens >= amount, + 'Amount too high' + ); + if(datatokens[datatoken].allowedSwapper != address(0)){ + require( + datatokens[datatoken].allowedSwapper == msg.sender, + "This address is not allowed to request DT" + ); + } + + IERC20Template tokenInstance = IERC20Template(datatoken); + uint256 callerBalance = tokenInstance.balanceOf(destination); + require( + callerBalance<datatokens[datatoken].maxBalance, + 'Caller balance too high' + ); + uint256 ourBalance = tokenInstance.balanceOf(address(this)); + if(ourBalance<amount && tokenInstance.isMinter(address(this))){ + //we need to mint the difference if we can + tokenInstance.mint(address(this),amount - ourBalance); + ourBalance = tokenInstance.balanceOf(address(this)); + } + require( + ourBalance>=amount, + 'Not enough reserves' + ); + IERC20(datatoken).safeTransfer(destination,amount); + emit TokensDispensed(datatoken, destination, amount); + } + + /** + * @dev ownerWithdraw + * Allow owner to withdraw all datatokens in this dispenser balance + * @param datatoken refers to datatoken address. + */ + function ownerWithdraw(address datatoken) external nonReentrant { + require( + datatoken != address(0), + 'Invalid token contract address' + ); + require( + datatokens[datatoken].owner == msg.sender, + 'Invalid owner' + ); + IERC20Template tokenInstance = IERC20Template(datatoken); + uint256 ourBalance = tokenInstance.balanceOf(address(this)); + if(ourBalance>0){ + IERC20(datatoken).safeTransfer(msg.sender,ourBalance); + emit OwnerWithdrawed(datatoken, msg.sender, ourBalance); + } + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/pools/fixedRate/FixedRateExchange.sol b/sol080/contracts/oceanv4/pools/fixedRate/FixedRateExchange.sol new file mode 100644 index 0000000000000000000000000000000000000000..0ed15f4f0b10d09b17ecb556396f1f38f41f5c3e --- /dev/null +++ b/sol080/contracts/oceanv4/pools/fixedRate/FixedRateExchange.sol @@ -0,0 +1,931 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 +import "../../interfaces/IERC20.sol"; +import "../../interfaces/IERC20Template.sol"; +import "../../interfaces/IFactoryRouter.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; +import "../../utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/security/ReentrancyGuard.sol"; + +/** + * @title FixedRateExchange + * @dev FixedRateExchange is a fixed rate exchange Contract + * Marketplaces uses this contract to allow consumers + * exchanging datatokens with ocean token using a fixed + * exchange rate. + */ + + + +contract FixedRateExchange is ReentrancyGuard { + using SafeMath for uint256; + using SafeERC20 for IERC20; + uint256 private constant BASE = 10**18; + uint public constant MIN_FEE = BASE / 10**4; + uint public constant MAX_FEE = BASE / 10; + uint public constant MIN_RATE = 10 ** 10; + + address public router; + address public opcCollector; + + struct Exchange { + bool active; + address exchangeOwner; + address datatoken; + address baseToken; + uint256 fixedRate; + uint256 dtDecimals; + uint256 btDecimals; + uint256 dtBalance; + uint256 btBalance; + uint256 marketFee; + address marketFeeCollector; + uint256 marketFeeAvailable; + uint256 oceanFeeAvailable; + bool withMint; + address allowedSwapper; + } + + // maps an exchangeId to an exchange + mapping(bytes32 => Exchange) private exchanges; + bytes32[] private exchangeIds; + + modifier onlyActiveExchange(bytes32 exchangeId) { + require( + //exchanges[exchangeId].fixedRate != 0 && + exchanges[exchangeId].active, + "FixedRateExchange: Exchange does not exist!" + ); + _; + } + + modifier onlyExchangeOwner(bytes32 exchangeId) { + require( + exchanges[exchangeId].exchangeOwner == msg.sender, + "FixedRateExchange: invalid exchange owner" + ); + _; + } + + modifier onlyRouter() { + require(msg.sender == router, "FixedRateExchange: only router"); + _; + } + + event ExchangeCreated( + bytes32 indexed exchangeId, + address indexed baseToken, + address indexed datatoken, + address exchangeOwner, + uint256 fixedRate + ); + + event ExchangeRateChanged( + bytes32 indexed exchangeId, + address indexed exchangeOwner, + uint256 newRate + ); + + //triggered when the withMint state is changed + event ExchangeMintStateChanged( + bytes32 indexed exchangeId, + address indexed exchangeOwner, + bool withMint + ); + + event ExchangeActivated( + bytes32 indexed exchangeId, + address indexed exchangeOwner + ); + + event ExchangeDeactivated( + bytes32 indexed exchangeId, + address indexed exchangeOwner + ); + + event ExchangeAllowedSwapperChanged( + bytes32 indexed exchangeId, + address indexed allowedSwapper + ); + + event Swapped( + bytes32 indexed exchangeId, + address indexed by, + uint256 baseTokenSwappedAmount, + uint256 datatokenSwappedAmount, + address tokenOutAddress, + uint256 marketFeeAmount, + uint256 oceanFeeAmount, + uint256 consumeMarketFeeAmount + ); + + event TokenCollected( + bytes32 indexed exchangeId, + address indexed to, + address indexed token, + uint256 amount + ); + + event OceanFeeCollected( + bytes32 indexed exchangeId, + address indexed feeToken, + uint256 feeAmount + ); + event MarketFeeCollected( + bytes32 indexed exchangeId, + address indexed feeToken, + uint256 feeAmount + ); + // emited for fees sent to consumeMarket + event ConsumeMarketFee( + bytes32 indexed exchangeId, + address to, + address token, + uint256 amount); + event SWAP_FEES( + bytes32 indexed exchangeId, + uint oceanFeeAmount, + uint marketFeeAmount, + uint consumeMarketFeeAmount, + address tokenFeeAddress); + event PublishMarketFeeChanged( + bytes32 indexed exchangeId, + address caller, + address newMarketCollector, + uint256 swapFee); + + constructor(address _router, address _opcCollector) { + require(_router != address(0), "FixedRateExchange: Wrong Router address"); + require(_opcCollector != address(0), "FixedRateExchange: Wrong OPC address"); + router = _router; + opcCollector = _opcCollector; + } + + /** + * @dev getId + * Return template id in case we need different ABIs. + * If you construct your own template, please make sure to change the hardcoded value + */ + function getId() pure public returns (uint8) { + return 1; + } + + function getOPCFee(address baseTokenAddress) public view returns (uint) { + return IFactoryRouter(router).getOPCFee(baseTokenAddress); + } + + + /** + * @dev create + * creates new exchange pairs between a baseToken + * (ocean token) and datatoken. + * datatoken refers to a datatoken contract address + * addresses - array of addresses with the following struct: + * [0] - baseToken + * [1] - owner + * [2] - marketFeeCollector + * [3] - allowedSwapper - if != address(0), only that is allowed to swap (used for ERC20Enterprise) + * uints - array of uints with the following struct: + * [0] - baseTokenDecimals + * [1] - datatokenDecimals + * [2] - fixedRate + * [3] - marketFee + * [4] - withMint + */ + function createWithDecimals( + address datatoken, + address[] memory addresses, + uint256[] memory uints + ) external onlyRouter returns (bytes32 exchangeId) { + + require( + addresses[0] != address(0), + "FixedRateExchange: Invalid baseToken, zero address" + ); + require( + datatoken != address(0), + "FixedRateExchange: Invalid datatoken, zero address" + ); + require( + addresses[0] != datatoken, + "FixedRateExchange: Invalid datatoken, equals baseToken" + ); + require( + uints[2] >= MIN_RATE, + "FixedRateExchange: Invalid exchange rate value" + ); + exchangeId = generateExchangeId(addresses[0], datatoken, addresses[1]); + require( + exchanges[exchangeId].fixedRate == 0, + "FixedRateExchange: Exchange already exists!" + ); + bool withMint=true; + if(uints[4] == 0) withMint = false; + exchanges[exchangeId] = Exchange({ + active: true, + exchangeOwner: addresses[1], + datatoken: datatoken, + baseToken: addresses[0], + fixedRate: uints[2], + dtDecimals: uints[1], + btDecimals: uints[0], + dtBalance: 0, + btBalance: 0, + marketFee: uints[3], + marketFeeCollector: addresses[2], + marketFeeAvailable: 0, + oceanFeeAvailable: 0, + withMint: withMint, + allowedSwapper: addresses[3] + }); + require(uints[3] ==0 || uints[3] >= MIN_FEE,'SwapFee too low'); + require(uints[3] <= MAX_FEE,'SwapFee too high'); + exchangeIds.push(exchangeId); + + emit ExchangeCreated( + exchangeId, + addresses[0], // + datatoken, + addresses[1], + uints[2] + ); + + emit ExchangeActivated(exchangeId, addresses[1]); + emit ExchangeAllowedSwapperChanged(exchangeId, addresses[3]); + emit PublishMarketFeeChanged(exchangeId,msg.sender, addresses[2], uints[3]); + } + + /** + * @dev generateExchangeId + * creates unique exchange identifier for two token pairs. + * @param baseToken refers to a base token contract address + * @param datatoken refers to a datatoken contract address + * @param exchangeOwner exchange owner address + */ + function generateExchangeId( + address baseToken, + address datatoken, + address exchangeOwner + ) public pure returns (bytes32) { + return keccak256(abi.encode(baseToken, datatoken, exchangeOwner)); + } + + struct Fees{ + uint256 baseTokenAmount; + uint256 oceanFeeAmount; + uint256 publishMarketFeeAmount; + uint256 consumeMarketFeeAmount; + } + + function getBaseTokenOutPrice(bytes32 exchangeId, uint256 datatokenAmount) + internal view returns (uint256 baseTokenAmount){ + baseTokenAmount = datatokenAmount + .mul(exchanges[exchangeId].fixedRate) + .mul(10**exchanges[exchangeId].btDecimals) + .div(10**exchanges[exchangeId].dtDecimals) + .div(BASE); + } + /** + * @dev calcBaseInGivenOutDT + * Calculates how many baseTokens are needed to get exact amount of datatokens + * @param exchangeId a unique exchange idnetifier + * @param datatokenAmount the amount of datatokens to be exchanged + * @param consumeMarketSwapFeeAmount fee amount for consume market + */ + function calcBaseInGivenOutDT(bytes32 exchangeId, uint256 datatokenAmount, uint256 consumeMarketSwapFeeAmount) + public + view + onlyActiveExchange(exchangeId) + returns ( + uint256 baseTokenAmount, + uint256 oceanFeeAmount, + uint256 publishMarketFeeAmount, + uint256 consumeMarketFeeAmount + ) + + + { + uint256 baseTokenAmountBeforeFee = getBaseTokenOutPrice(exchangeId, datatokenAmount); + Fees memory fee = Fees(0,0,0,0); + uint256 opcFee = getOPCFee(exchanges[exchangeId].baseToken); + if (opcFee != 0) { + fee.oceanFeeAmount = baseTokenAmountBeforeFee + .mul(opcFee) + .div(BASE); + } + else + fee.oceanFeeAmount = 0; + + if( exchanges[exchangeId].marketFee !=0){ + fee.publishMarketFeeAmount = baseTokenAmountBeforeFee + .mul(exchanges[exchangeId].marketFee) + .div(BASE); + } + else{ + fee.publishMarketFeeAmount = 0; + } + + if( consumeMarketSwapFeeAmount !=0){ + fee.consumeMarketFeeAmount = baseTokenAmountBeforeFee + .mul(consumeMarketSwapFeeAmount) + .div(BASE); + } + else{ + fee.consumeMarketFeeAmount = 0; + } + + + fee.baseTokenAmount = baseTokenAmountBeforeFee.add(fee.publishMarketFeeAmount) + .add(fee.oceanFeeAmount).add(fee.consumeMarketFeeAmount); + + return(fee.baseTokenAmount,fee.oceanFeeAmount,fee.publishMarketFeeAmount,fee.consumeMarketFeeAmount); + } + + + /** + * @dev calcBaseOutGivenInDT + * Calculates how many basteTokens you will get for selling exact amount of baseTokens + * @param exchangeId a unique exchange idnetifier + * @param datatokenAmount the amount of datatokens to be exchanged + * @param consumeMarketSwapFeeAmount fee amount for consume market + */ + function calcBaseOutGivenInDT(bytes32 exchangeId, uint256 datatokenAmount, uint256 consumeMarketSwapFeeAmount) + public + view + onlyActiveExchange(exchangeId) + returns ( + uint256 baseTokenAmount, + uint256 oceanFeeAmount, + uint256 publishMarketFeeAmount, + uint256 consumeMarketFeeAmount + ) + { + uint256 baseTokenAmountBeforeFee = getBaseTokenOutPrice(exchangeId, datatokenAmount); + + Fees memory fee = Fees(0,0,0,0); + uint256 opcFee = getOPCFee(exchanges[exchangeId].baseToken); + if (opcFee != 0) { + fee.oceanFeeAmount = baseTokenAmountBeforeFee + .mul(opcFee) + .div(BASE); + } + else fee.oceanFeeAmount=0; + + if(exchanges[exchangeId].marketFee !=0 ){ + fee.publishMarketFeeAmount = baseTokenAmountBeforeFee + .mul(exchanges[exchangeId].marketFee) + .div(BASE); + } + else{ + fee.publishMarketFeeAmount = 0; + } + + if( consumeMarketSwapFeeAmount !=0){ + fee.consumeMarketFeeAmount = baseTokenAmountBeforeFee + .mul(consumeMarketSwapFeeAmount) + .div(BASE); + } + else{ + fee.consumeMarketFeeAmount = 0; + } + + fee.baseTokenAmount = baseTokenAmountBeforeFee.sub(fee.publishMarketFeeAmount) + .sub(fee.oceanFeeAmount).sub(fee.consumeMarketFeeAmount); + return(fee.baseTokenAmount,fee.oceanFeeAmount,fee.publishMarketFeeAmount,fee.consumeMarketFeeAmount); + } + + + /** + * @dev swap + * atomic swap between two registered fixed rate exchange. + * @param exchangeId a unique exchange idnetifier + * @param datatokenAmount the amount of datatokens to be exchanged + * @param maxBaseTokenAmount maximum amount of base tokens to pay + * @param consumeMarketAddress consumeMarketAddress + * @param consumeMarketSwapFeeAmount fee amount for consume market + */ + function buyDT(bytes32 exchangeId, uint256 datatokenAmount, uint256 maxBaseTokenAmount, + address consumeMarketAddress, uint256 consumeMarketSwapFeeAmount) + external + onlyActiveExchange(exchangeId) + nonReentrant + { + require( + datatokenAmount != 0, + "FixedRateExchange: zero datatoken amount" + ); + require(consumeMarketSwapFeeAmount ==0 || consumeMarketSwapFeeAmount >= MIN_FEE,'ConsumeSwapFee too low'); + require(consumeMarketSwapFeeAmount <= MAX_FEE,'ConsumeSwapFee too high'); + if(exchanges[exchangeId].allowedSwapper != address(0)){ + require( + exchanges[exchangeId].allowedSwapper == msg.sender, + "FixedRateExchange: This address is not allowed to swap" + ); + } + if(consumeMarketAddress == address(0)) consumeMarketSwapFeeAmount=0; + Fees memory fee = Fees(0,0,0,0); + (fee.baseTokenAmount, + fee.oceanFeeAmount, + fee.publishMarketFeeAmount, + fee.consumeMarketFeeAmount + ) + = calcBaseInGivenOutDT(exchangeId, datatokenAmount, consumeMarketSwapFeeAmount); + require( + fee.baseTokenAmount <= maxBaseTokenAmount, + "FixedRateExchange: Too many base tokens" + ); + // we account fees , fees are always collected in baseToken + exchanges[exchangeId].oceanFeeAvailable = exchanges[exchangeId] + .oceanFeeAvailable + .add(fee.oceanFeeAmount); + exchanges[exchangeId].marketFeeAvailable = exchanges[exchangeId] + .marketFeeAvailable + .add(fee.publishMarketFeeAmount); + _pullUnderlying(exchanges[exchangeId].baseToken,msg.sender, + address(this), + fee.baseTokenAmount); + uint256 baseTokenAmountBeforeFee = fee.baseTokenAmount.sub(fee.oceanFeeAmount). + sub(fee.publishMarketFeeAmount).sub(fee.consumeMarketFeeAmount); + exchanges[exchangeId].btBalance = (exchanges[exchangeId].btBalance).add( + baseTokenAmountBeforeFee + ); + + if (datatokenAmount > exchanges[exchangeId].dtBalance) { + //first, let's try to mint + if(exchanges[exchangeId].withMint + && IERC20Template(exchanges[exchangeId].datatoken).isMinter(address(this))) + { + IERC20Template(exchanges[exchangeId].datatoken).mint(msg.sender,datatokenAmount); + } + else{ + _pullUnderlying(exchanges[exchangeId].datatoken,exchanges[exchangeId].exchangeOwner, + msg.sender, + datatokenAmount); + } + } else { + exchanges[exchangeId].dtBalance = (exchanges[exchangeId].dtBalance) + .sub(datatokenAmount); + IERC20(exchanges[exchangeId].datatoken).safeTransfer( + msg.sender, + datatokenAmount + ); + } + if(consumeMarketAddress!= address(0) && fee.consumeMarketFeeAmount>0){ + IERC20(exchanges[exchangeId].baseToken).safeTransfer(consumeMarketAddress, fee.consumeMarketFeeAmount); + emit ConsumeMarketFee( + exchangeId, + consumeMarketAddress, + exchanges[exchangeId].baseToken, + fee.consumeMarketFeeAmount); + } + emit Swapped( + exchangeId, + msg.sender, + fee.baseTokenAmount, + datatokenAmount, + exchanges[exchangeId].datatoken, + fee.publishMarketFeeAmount, + fee.oceanFeeAmount, + fee.consumeMarketFeeAmount + ); + } + + + /** + * @dev sellDT + * Sell datatokenAmount while expecting at least minBaseTokenAmount + * @param exchangeId a unique exchange idnetifier + * @param datatokenAmount the amount of datatokens to be exchanged + * @param minBaseTokenAmount minimum amount of base tokens to cash in + * @param consumeMarketAddress consumeMarketAddress + * @param consumeMarketSwapFeeAmount fee amount for consume market + */ + function sellDT(bytes32 exchangeId, uint256 datatokenAmount, + uint256 minBaseTokenAmount, address consumeMarketAddress, uint256 consumeMarketSwapFeeAmount) + external + onlyActiveExchange(exchangeId) + nonReentrant + { + require( + datatokenAmount != 0, + "FixedRateExchange: zero datatoken amount" + ); + require(consumeMarketSwapFeeAmount ==0 || consumeMarketSwapFeeAmount >= MIN_FEE,'ConsumeSwapFee too low'); + require(consumeMarketSwapFeeAmount <= MAX_FEE,'ConsumeSwapFee too high'); + if(exchanges[exchangeId].allowedSwapper != address(0)){ + require( + exchanges[exchangeId].allowedSwapper == msg.sender, + "FixedRateExchange: This address is not allowed to swap" + ); + } + Fees memory fee = Fees(0,0,0,0); + if(consumeMarketAddress == address(0)) consumeMarketSwapFeeAmount=0; + (fee.baseTokenAmount, + fee.oceanFeeAmount, + fee.publishMarketFeeAmount, + fee.consumeMarketFeeAmount + ) = calcBaseOutGivenInDT(exchangeId, datatokenAmount, consumeMarketSwapFeeAmount); + require( + fee.baseTokenAmount >= minBaseTokenAmount, + "FixedRateExchange: Too few base tokens" + ); + // we account fees , fees are always collected in baseToken + exchanges[exchangeId].oceanFeeAvailable = exchanges[exchangeId] + .oceanFeeAvailable + .add(fee.oceanFeeAmount); + exchanges[exchangeId].marketFeeAvailable = exchanges[exchangeId] + .marketFeeAvailable + .add(fee.publishMarketFeeAmount); + uint256 baseTokenAmountWithFees = fee.baseTokenAmount.add(fee.oceanFeeAmount) + .add(fee.publishMarketFeeAmount).add(fee.consumeMarketFeeAmount); + _pullUnderlying(exchanges[exchangeId].datatoken,msg.sender, + address(this), + datatokenAmount); + exchanges[exchangeId].dtBalance = (exchanges[exchangeId].dtBalance).add( + datatokenAmount + ); + if (baseTokenAmountWithFees > exchanges[exchangeId].btBalance) { + _pullUnderlying(exchanges[exchangeId].baseToken,exchanges[exchangeId].exchangeOwner, + address(this), + baseTokenAmountWithFees); + IERC20(exchanges[exchangeId].baseToken).safeTransfer( + msg.sender, + fee.baseTokenAmount); + } else { + exchanges[exchangeId].btBalance = (exchanges[exchangeId].btBalance) + .sub(baseTokenAmountWithFees); + IERC20(exchanges[exchangeId].baseToken).safeTransfer( + msg.sender, + fee.baseTokenAmount + ); + } + if(consumeMarketAddress!= address(0) && fee.consumeMarketFeeAmount>0){ + IERC20(exchanges[exchangeId].baseToken).safeTransfer(consumeMarketAddress, fee.consumeMarketFeeAmount); + emit ConsumeMarketFee( + exchangeId, + consumeMarketAddress, + exchanges[exchangeId].baseToken, + fee.consumeMarketFeeAmount); + } + emit Swapped( + exchangeId, + msg.sender, + fee.baseTokenAmount, + datatokenAmount, + exchanges[exchangeId].baseToken, + fee.publishMarketFeeAmount, + fee.oceanFeeAmount, + fee.consumeMarketFeeAmount + ); + } + + function collectBT(bytes32 exchangeId) + external + onlyExchangeOwner(exchangeId) + nonReentrant + { + uint256 amount = exchanges[exchangeId].btBalance; + exchanges[exchangeId].btBalance = 0; + IERC20(exchanges[exchangeId].baseToken).safeTransfer( + exchanges[exchangeId].exchangeOwner, + amount + ); + + emit TokenCollected( + exchangeId, + exchanges[exchangeId].exchangeOwner, + exchanges[exchangeId].baseToken, + amount + ); + } + + function collectDT(bytes32 exchangeId) + external + onlyExchangeOwner(exchangeId) + nonReentrant + { + uint256 amount = exchanges[exchangeId].dtBalance; + exchanges[exchangeId].dtBalance = 0; + IERC20(exchanges[exchangeId].datatoken).safeTransfer( + exchanges[exchangeId].exchangeOwner, + amount + ); + + emit TokenCollected( + exchangeId, + exchanges[exchangeId].exchangeOwner, + exchanges[exchangeId].datatoken, + amount + ); + } + + function collectMarketFee(bytes32 exchangeId) external nonReentrant { + // anyone call call this function, because funds are sent to the correct address + uint256 amount = exchanges[exchangeId].marketFeeAvailable; + exchanges[exchangeId].marketFeeAvailable = 0; + IERC20(exchanges[exchangeId].baseToken).safeTransfer( + exchanges[exchangeId].marketFeeCollector, + amount + ); + emit MarketFeeCollected( + exchangeId, + exchanges[exchangeId].baseToken, + amount + ); + } + + function collectOceanFee(bytes32 exchangeId) external nonReentrant { + // anyone call call this function, because funds are sent to the correct address + uint256 amount = exchanges[exchangeId].oceanFeeAvailable; + exchanges[exchangeId].oceanFeeAvailable = 0; + IERC20(exchanges[exchangeId].baseToken).safeTransfer( + opcCollector, + amount + ); + emit OceanFeeCollected( + exchangeId, + exchanges[exchangeId].baseToken, + amount + ); + } + + /** + * @dev updateMarketFeeCollector + * Set _newMarketCollector as _publishMarketCollector + * @param _newMarketCollector new _publishMarketCollector + */ + function updateMarketFeeCollector( + bytes32 exchangeId, + address _newMarketCollector + ) external { + require( + msg.sender == exchanges[exchangeId].marketFeeCollector, + "not marketFeeCollector" + ); + exchanges[exchangeId].marketFeeCollector = _newMarketCollector; + emit PublishMarketFeeChanged(exchangeId, msg.sender, _newMarketCollector, exchanges[exchangeId].marketFee); + } + + function updateMarketFee( + bytes32 exchangeId, + uint256 _newMarketFee + ) external { + require( + msg.sender == exchanges[exchangeId].marketFeeCollector, + "not marketFeeCollector" + ); + require(_newMarketFee ==0 || _newMarketFee >= MIN_FEE,'SwapFee too low'); + require(_newMarketFee <= MAX_FEE,'SwapFee too high'); + exchanges[exchangeId].marketFee = _newMarketFee; + emit PublishMarketFeeChanged(exchangeId, msg.sender, exchanges[exchangeId].marketFeeCollector, _newMarketFee); + } + + function getMarketFee(bytes32 exchangeId) view public returns(uint256){ + return(exchanges[exchangeId].marketFee); + } + + /** + * @dev getNumberOfExchanges + * gets the total number of registered exchanges + * @return total number of registered exchange IDs + */ + function getNumberOfExchanges() external view returns (uint256) { + return exchangeIds.length; + } + + /** + * @dev setRate + * changes the fixed rate for an exchange with a new rate + * @param exchangeId a unique exchange idnetifier + * @param newRate new fixed rate value + */ + function setRate(bytes32 exchangeId, uint256 newRate) + external + onlyExchangeOwner(exchangeId) + { + require(newRate != 0, "FixedRateExchange: Ratio must be >0"); + + exchanges[exchangeId].fixedRate = newRate; + emit ExchangeRateChanged(exchangeId, msg.sender, newRate); + } + + /** + * @dev toggleMintState + * toggle withMint state + * @param exchangeId a unique exchange idnetifier + * @param withMint new value + */ + function toggleMintState(bytes32 exchangeId, bool withMint) + external + onlyExchangeOwner(exchangeId) + { + exchanges[exchangeId].withMint = withMint; + emit ExchangeMintStateChanged(exchangeId, msg.sender, withMint); + } + + /** + * @dev toggleExchangeState + * toggles the active state of an existing exchange + * @param exchangeId a unique exchange identifier + */ + function toggleExchangeState(bytes32 exchangeId) + external + onlyExchangeOwner(exchangeId) + { + if (exchanges[exchangeId].active) { + exchanges[exchangeId].active = false; + emit ExchangeDeactivated(exchangeId, msg.sender); + } else { + exchanges[exchangeId].active = true; + emit ExchangeActivated(exchangeId, msg.sender); + } + } + + /** + * @dev setAllowedSwapper + * Sets a new allowedSwapper + * @param exchangeId a unique exchange identifier + * @param newAllowedSwapper refers to the new allowedSwapper + */ + function setAllowedSwapper(bytes32 exchangeId, address newAllowedSwapper) external + onlyExchangeOwner(exchangeId) + { + exchanges[exchangeId].allowedSwapper = newAllowedSwapper; + emit ExchangeAllowedSwapperChanged(exchangeId, newAllowedSwapper); + } + /** + * @dev getRate + * gets the current fixed rate for an exchange + * @param exchangeId a unique exchange idnetifier + * @return fixed rate value + */ + function getRate(bytes32 exchangeId) external view returns (uint256) { + return exchanges[exchangeId].fixedRate; + } + + /** + * @dev getSupply + * gets the current supply of datatokens in an fixed + * rate exchagne + * @param exchangeId the exchange ID + * @return supply + */ + function getDTSupply(bytes32 exchangeId) + public + view + returns (uint256 supply) + { + if (exchanges[exchangeId].active == false) supply = 0; + else if (exchanges[exchangeId].withMint + && IERC20Template(exchanges[exchangeId].datatoken).isMinter(address(this))){ + supply = IERC20Template(exchanges[exchangeId].datatoken).cap() + - IERC20Template(exchanges[exchangeId].datatoken).totalSupply(); + } + else { + uint256 balance = IERC20Template(exchanges[exchangeId].datatoken) + .balanceOf(exchanges[exchangeId].exchangeOwner); + uint256 allowance = IERC20Template(exchanges[exchangeId].datatoken) + .allowance(exchanges[exchangeId].exchangeOwner, address(this)); + if (balance < allowance) + supply = balance.add(exchanges[exchangeId].dtBalance); + else supply = allowance.add(exchanges[exchangeId].dtBalance); + } + } + + /** + * @dev getSupply + * gets the current supply of datatokens in an fixed + * rate exchagne + * @param exchangeId the exchange ID + * @return supply + */ + function getBTSupply(bytes32 exchangeId) + public + view + returns (uint256 supply) + { + if (exchanges[exchangeId].active == false) supply = 0; + else { + uint256 balance = IERC20Template(exchanges[exchangeId].baseToken) + .balanceOf(exchanges[exchangeId].exchangeOwner); + uint256 allowance = IERC20Template(exchanges[exchangeId].baseToken) + .allowance(exchanges[exchangeId].exchangeOwner, address(this)); + if (balance < allowance) + supply = balance.add(exchanges[exchangeId].btBalance); + else supply = allowance.add(exchanges[exchangeId].btBalance); + } + } + + // /** + // * @dev getExchange + // * gets all the exchange details + // * @param exchangeId a unique exchange idnetifier + // * @return all the exchange details including the exchange Owner + // * the datatoken contract address, the base token address, the + // * fixed rate, whether the exchange is active and the supply or the + // * the current datatoken liquidity. + // */ + function getExchange(bytes32 exchangeId) + external + view + returns ( + address exchangeOwner, + address datatoken, + uint256 dtDecimals, + address baseToken, + uint256 btDecimals, + uint256 fixedRate, + bool active, + uint256 dtSupply, + uint256 btSupply, + uint256 dtBalance, + uint256 btBalance, + bool withMint + // address allowedSwapper + ) + { + Exchange memory exchange = exchanges[exchangeId]; + exchangeOwner = exchange.exchangeOwner; + datatoken = exchange.datatoken; + dtDecimals = exchange.dtDecimals; + baseToken = exchange.baseToken; + btDecimals = exchange.btDecimals; + fixedRate = exchange.fixedRate; + active = exchange.active; + dtSupply = getDTSupply(exchangeId); + btSupply = getBTSupply(exchangeId); + dtBalance = exchange.dtBalance; + btBalance = exchange.btBalance; + withMint = exchange.withMint; + // allowedSwapper = exchange.allowedSwapper; + } + + // /** + // * @dev getAllowedSwapper + // * gets allowedSwapper + // * @param exchangeId a unique exchange idnetifier + // * @return address of allowedSwapper + // */ + function getAllowedSwapper(bytes32 exchangeId) + external + view + returns ( + address allowedSwapper + ) + { + Exchange memory exchange = exchanges[exchangeId]; + allowedSwapper = exchange.allowedSwapper; + } + + function getFeesInfo(bytes32 exchangeId) + external + view + returns ( + uint256 marketFee, + address marketFeeCollector, + uint256 opcFee, + uint256 marketFeeAvailable, + uint256 oceanFeeAvailable + ) + { + Exchange memory exchange = exchanges[exchangeId]; + marketFee = exchange.marketFee; + marketFeeCollector = exchange.marketFeeCollector; + opcFee = getOPCFee(exchanges[exchangeId].baseToken); + marketFeeAvailable = exchange.marketFeeAvailable; + oceanFeeAvailable = exchange.oceanFeeAvailable; + } + + /** + * @dev getExchanges + * gets all the exchanges list + * @return a list of all registered exchange Ids + */ + function getExchanges() external view returns (bytes32[] memory) { + return exchangeIds; + } + + /** + * @dev isActive + * checks whether exchange is active + * @param exchangeId a unique exchange idnetifier + * @return true if exchange is true, otherwise returns false + */ + function isActive(bytes32 exchangeId) external view returns (bool) { + return exchanges[exchangeId].active; + } + + function _pullUnderlying( + address erc20, + address from, + address to, + uint256 amount + ) internal { + uint256 balanceBefore = IERC20(erc20).balanceOf(to); + IERC20(erc20).safeTransferFrom(from, to, amount); + require(IERC20(erc20).balanceOf(to) >= balanceBefore.add(amount), + "Transfer amount is too low"); + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/pools/ssContracts/SideStaking.sol b/sol080/contracts/oceanv4/pools/ssContracts/SideStaking.sol new file mode 100644 index 0000000000000000000000000000000000000000..74fb7ee09e657df3b14bb2749fae237d867d8d18 --- /dev/null +++ b/sol080/contracts/oceanv4/pools/ssContracts/SideStaking.sol @@ -0,0 +1,559 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 +import "../../interfaces/IERC20.sol"; +import "../../interfaces/IERC20Template.sol"; +import "../../interfaces/IPool.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; +import "../../utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/security/ReentrancyGuard.sol"; + +/**@title SideStaking + * + * @dev SideStaking is a contract that monitors stakings in pools, + adding or removing dt when only baseToken liquidity is added or removed + * Called by the pool contract + * Every ss newDatatokenCreated function has a ssParams array, + which for this contract has the following structure: + * [0] = rate (wei) + * [1] = baseToken decimals + * [2] = vesting amount (wei) + * [3] = vested blocks + * [4] = initial liquidity in baseToken for pool creation + * + */ +contract SideStaking is ReentrancyGuard { + using SafeMath for uint256; + using SafeERC20 for IERC20; + address public router; + + // emitted when a new vesting is created + event VestingCreated( + address indexed datatokenAddress, + address indexed publisherAddress, + uint256 vestingEndBlock, + uint256 totalVestingAmount + ); + // emited each time when tokens are vested to the publisher + event Vesting( + address indexed datatokenAddress, + address indexed publisherAddress, + address indexed caller, + uint256 amountVested + ); + + struct Record { + bool bound; //datatoken bounded + address baseTokenAddress; + address poolAddress; + bool poolFinalized; // did we finalized the pool ? We have to do it after burn-in + uint256 datatokenBalance; //current dt balance + uint256 datatokenCap; //dt cap + uint256 baseTokenBalance; //current baseToken balance + uint256 lastPrice; //used for creating the pool + uint256 rate; // rate to exchange DT<->baseToken + address publisherAddress; + uint256 blockDeployed; //when this record was created + uint256 vestingEndBlock; //see below + uint256 vestingAmount; // total amount to be vested to publisher until vestingEndBlock + uint256 vestingLastBlock; //last block in which a vesting has been granted + uint256 vestingAmountSoFar; //how much was vested so far + } + + mapping(address => Record) private _datatokens; + uint256 private constant BASE = 10**18; + + modifier onlyRouter() { + require(msg.sender == router, "ONLY ROUTER"); + _; + } + + /** + * @dev constructor + * Called on contract deployment. + */ + constructor(address _router) public { + require(_router != address(0), "Invalid _router address"); + router = _router; + } + + /** + * @dev getId + * Return template id in case we need different ABIs. + * If you construct your own template, please make sure to change the hardcoded value + */ + function getId() public pure returns (uint8) { + return 1; + } + + /** + * @dev newDatatokenCreated + * Called when new Datatoken is deployed by the DatatokenFactory + * @param datatokenAddress - datatokenAddress + * @param baseTokenAddress - + * @param poolAddress - poolAddress + * @param publisherAddress - publisherAddress + * @param ssParams - ss Params, see below + */ + + function newDatatokenCreated( + address datatokenAddress, + address baseTokenAddress, + address poolAddress, + address publisherAddress, + uint256[] memory ssParams + ) external onlyRouter nonReentrant returns (bool) { + //check if we are the controller of the pool + require(poolAddress != address(0), "Invalid poolAddress"); + IPool bpool = IPool(poolAddress); + require( + bpool.getController() == address(this), + "We are not the pool controller" + ); + //check if the tokens are bound + require( + bpool.getDatatokenAddress() == datatokenAddress, + "Datatoken address missmatch" + ); + require( + bpool.getBaseTokenAddress() == baseTokenAddress, + "baseToken address missmatch" + ); + // check if we are the minter of DT + IERC20Template dt = IERC20Template(datatokenAddress); + require( + (dt.permissions(address(this))).minter, + "baseToken address mismatch" + ); + // get cap and mint it.. + dt.mint(address(this), dt.cap()); + + require(dt.balanceOf(address(this)) >= dt.totalSupply(), "Mint failed"); + require(dt.totalSupply().div(10) >= ssParams[2], "Max vesting 10%"); + //we are rich :)let's setup the records and we are good to go + _datatokens[datatokenAddress] = Record({ + bound: true, + baseTokenAddress: baseTokenAddress, + poolAddress: poolAddress, + poolFinalized: false, + datatokenBalance: dt.totalSupply(), + datatokenCap: dt.cap(), + baseTokenBalance: ssParams[4], + lastPrice: 0, + rate: ssParams[0], + publisherAddress: publisherAddress, + blockDeployed: block.number, + vestingEndBlock: block.number + ssParams[3], + vestingAmount: ssParams[2], + vestingLastBlock: block.number, + vestingAmountSoFar: 0 + }); + emit VestingCreated( + datatokenAddress, + publisherAddress, + _datatokens[datatokenAddress].vestingEndBlock, + _datatokens[datatokenAddress].vestingAmount + ); + + notifyFinalize(datatokenAddress, ssParams[1]); + + return (true); + } + + //public getters + /** + * Returns (total vesting amount + token released from the contract when adding liquidity) + * @param datatokenAddress - datatokenAddress + + */ + + function getDatatokenCirculatingSupply(address datatokenAddress) + external + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].datatokenCap - + _datatokens[datatokenAddress].datatokenBalance); + } + + /** + * Returns actual dts in circulation (vested token withdrawn from the contract + + token released from the contract when adding liquidity) + * @param datatokenAddress - datatokenAddress + + */ + + function getDatatokenCurrentCirculatingSupply(address datatokenAddress) + external + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].datatokenCap - + _datatokens[datatokenAddress].datatokenBalance - + _datatokens[datatokenAddress].vestingAmountSoFar); + } + + /** + * Returns publisher address + * @param datatokenAddress - datatokenAddress + + */ + + function getPublisherAddress(address datatokenAddress) + external + view + returns (address) + { + if (!_datatokens[datatokenAddress].bound) return (address(0)); + return (_datatokens[datatokenAddress].publisherAddress); + } + + /** + * Returns baseToken address + * @param datatokenAddress - datatokenAddress + + */ + + function getBaseTokenAddress(address datatokenAddress) + external + view + returns (address) + { + if (!_datatokens[datatokenAddress].bound) return (address(0)); + return (_datatokens[datatokenAddress].baseTokenAddress); + } + + /** + * Returns pool address + * @param datatokenAddress - datatokenAddress + + */ + + function getPoolAddress(address datatokenAddress) + external + view + returns (address) + { + if (!_datatokens[datatokenAddress].bound) return (address(0)); + return (_datatokens[datatokenAddress].poolAddress); + } + + /** + * Returns baseToken balance in the contract + * @param datatokenAddress - datatokenAddress + + */ + function getBaseTokenBalance(address datatokenAddress) + external + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].baseTokenBalance); + } + + /** + * Returns datatoken balance in the contract + * @param datatokenAddress - datatokenAddress + + */ + + function getDatatokenBalance(address datatokenAddress) + external + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].datatokenBalance); + } + + /** + * Returns last vesting block + * @param datatokenAddress - datatokenAddress + + */ + + function getvestingEndBlock(address datatokenAddress) + external + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].vestingEndBlock); + } + + /** + * Returns total vesting amount + * @param datatokenAddress - datatokenAddress + + */ + + function getvestingAmount(address datatokenAddress) + public + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].vestingAmount); + } + + /** + * Returns last block when some vesting tokens were collected + * @param datatokenAddress - datatokenAddress + + */ + + function getvestingLastBlock(address datatokenAddress) + external + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].vestingLastBlock); + } + + /** + * Returns amount of vested tokens that have been withdrawn from the contract so far + * @param datatokenAddress - datatokenAddress + + */ + + function getvestingAmountSoFar(address datatokenAddress) + public + view + returns (uint256) + { + if (!_datatokens[datatokenAddress].bound) return (0); + return (_datatokens[datatokenAddress].vestingAmountSoFar); + } + + //called by pool to confirm that we can stake a token (add pool liquidty). If true, pool will call Stake function + function canStake(address datatokenAddress, uint256 amount) + public + view + returns (bool) + { + require( + msg.sender == _datatokens[datatokenAddress].poolAddress, + "ERR: Only pool can call this" + ); + if (!_datatokens[datatokenAddress].bound) return (false); + + //check balances. Make sure that we have enough to vest + if ( + _datatokens[datatokenAddress].datatokenBalance >= + (amount + + (_datatokens[datatokenAddress].vestingAmount - + _datatokens[datatokenAddress].vestingAmountSoFar)) + ) return (true); + return (false); + } + + //called by pool so 1ss will stake a token (add pool liquidty). + // Function only needs to approve the amount to be spent by the pool, pool will do the rest + function Stake(address datatokenAddress, uint256 amount) + external + nonReentrant + { + if (!_datatokens[datatokenAddress].bound) return; + require( + msg.sender == _datatokens[datatokenAddress].poolAddress, + "ERR: Only pool can call this" + ); + bool ok = canStake(datatokenAddress, amount); + if (!ok) return; + IERC20 dt = IERC20(datatokenAddress); + dt.safeIncreaseAllowance( + _datatokens[datatokenAddress].poolAddress, + amount + ); + _datatokens[datatokenAddress].datatokenBalance -= amount; + } + + //called by pool to confirm that we can stake a token (add pool liquidty). If true, pool will call Unstake function + function canUnStake(address datatokenAddress, uint256 lptIn) + public + view + returns (bool) + { + //TO DO + if (!_datatokens[datatokenAddress].bound) return (false); + require( + msg.sender == _datatokens[datatokenAddress].poolAddress, + "ERR: Only pool can call this" + ); + + // we check LPT balance TODO: review this part + if (IERC20(msg.sender).balanceOf(address(this)) >= lptIn) { + return true; + } + return false; + } + + //called by pool so 1ss will unstake a token (remove pool liquidty). + // In our case the balancer pool will handle all, this is just a notifier so 1ss can handle internal kitchen + function UnStake( + address datatokenAddress, + uint256 dtAmountIn, + uint256 poolAmountOut + ) external nonReentrant { + if (!_datatokens[datatokenAddress].bound) return; + require( + msg.sender == _datatokens[datatokenAddress].poolAddress, + "ERR: Only pool can call this" + ); + bool ok = canUnStake(datatokenAddress, poolAmountOut); + if (!ok) return; + _datatokens[datatokenAddress].datatokenBalance += dtAmountIn; + } + + //called by the pool (or by us) when we should finalize the pool + function notifyFinalize(address datatokenAddress, uint256 decimals) + internal + { + if (!_datatokens[datatokenAddress].bound) return; + if (_datatokens[datatokenAddress].poolFinalized) return; + _datatokens[datatokenAddress].poolFinalized = true; + uint256 baseTokenWeight = 5 * BASE; //pool weight: 50-50 + uint256 datatokenWeight = 5 * BASE; //pool weight: 50-50 + uint256 baseTokenAmount = _datatokens[datatokenAddress] + .baseTokenBalance; + //given the price, compute datatokenAmount + + uint256 datatokenAmount = (10**(18 - decimals))*_datatokens[datatokenAddress].rate * + baseTokenAmount * + datatokenWeight / + baseTokenWeight / + BASE; + + //substract + _datatokens[datatokenAddress].baseTokenBalance -= baseTokenAmount; + _datatokens[datatokenAddress].datatokenBalance -= datatokenAmount; + //approve the tokens and amounts + IERC20 dt = IERC20(datatokenAddress); + dt.safeIncreaseAllowance( + _datatokens[datatokenAddress].poolAddress, + datatokenAmount + ); + IERC20 dtBase = IERC20(_datatokens[datatokenAddress].baseTokenAddress); + dtBase.safeIncreaseAllowance( + _datatokens[datatokenAddress].poolAddress, + baseTokenAmount + ); + + + // call the pool, bind the tokens, set the price, finalize pool + IPool pool = IPool(_datatokens[datatokenAddress].poolAddress); + pool.setup( + datatokenAddress, + datatokenAmount, + datatokenWeight, + _datatokens[datatokenAddress].baseTokenAddress, + baseTokenAmount, + baseTokenWeight + ); + // send 50% of the pool shares back to the publisher + IERC20 lPTokens = IERC20(_datatokens[datatokenAddress].poolAddress); + uint256 lpBalance = lPTokens.balanceOf(address(this)); + // uint256 balanceToTransfer = lpBalance.div(2); + lPTokens.safeTransfer( + _datatokens[datatokenAddress].publisherAddress, + lpBalance.div(2) + ); + } + + /** + * Get available vesting now + * @param datatokenAddress - datatokenAddress + + */ + function getAvailableVesting(address datatokenAddress) + public + view + returns (uint256) + { + uint256 blocksPassed; + + if (_datatokens[datatokenAddress].vestingEndBlock < block.number) { + blocksPassed = + _datatokens[datatokenAddress].vestingEndBlock - + _datatokens[datatokenAddress].vestingLastBlock; + } else { + blocksPassed = + block.number - + _datatokens[datatokenAddress].vestingLastBlock; + } + + uint256 availableVesting = blocksPassed + .mul(_datatokens[datatokenAddress].vestingAmount) + .div( + _datatokens[datatokenAddress].vestingEndBlock - + _datatokens[datatokenAddress].blockDeployed + ); + + return availableVesting; + } + + /** + * Send available vested tokens to the publisher address, can be called by anyone + * @param datatokenAddress - datatokenAddress + + */ + // called by vester to get datatokens + function getVesting(address datatokenAddress) external nonReentrant { + require(_datatokens[datatokenAddress].bound, "ERR:Invalid datatoken"); + uint256 amount = getAvailableVesting(datatokenAddress); + if ( + amount > 0 && + _datatokens[datatokenAddress].datatokenBalance >= amount + ) { + IERC20 dt = IERC20(datatokenAddress); + _datatokens[datatokenAddress].vestingLastBlock = block.number; + _datatokens[datatokenAddress].datatokenBalance -= amount; + _datatokens[datatokenAddress].vestingAmountSoFar += amount; + emit Vesting( + datatokenAddress, + _datatokens[datatokenAddress].publisherAddress, + msg.sender, + amount + ); + dt.safeTransfer( + _datatokens[datatokenAddress].publisherAddress, + amount + ); + + } + } + + /** + * Change pool fee + * @param datatokenAddress - datatokenAddress + * @param poolAddress - poolAddress + * @param swapFee - new fee + + */ + // called by ERC20 Deployer of datatoken + function setPoolSwapFee( + address datatokenAddress, + address poolAddress, + uint256 swapFee + ) external nonReentrant { + require(poolAddress != address(0), "Invalid poolAddress"); + IPool bpool = IPool(poolAddress); + require( + bpool.getController() == address(this), + "We are not the pool controller" + ); + //check if the tokens are bound + require( + bpool.getDatatokenAddress() == datatokenAddress, + "Datatoken address missmatch" + ); + IERC20Template dt = IERC20Template(datatokenAddress); + require(dt.isERC20Deployer(msg.sender), "Not ERC20 Deployer"); + bpool.setSwapFee(swapFee); + } +} diff --git a/sol080/contracts/oceanv4/templates/ERC20Template.sol b/sol080/contracts/oceanv4/templates/ERC20Template.sol new file mode 100644 index 0000000000000000000000000000000000000000..eb631ba5198897b635130289d815d21598ed3656 --- /dev/null +++ b/sol080/contracts/oceanv4/templates/ERC20Template.sol @@ -0,0 +1,1139 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "../interfaces/IERC20Template.sol"; +import "../interfaces/IERC721Template.sol"; +import "../interfaces/IFactoryRouter.sol"; +import "../utils/ERC725/ERC725Ocean.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/ERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/IERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; + +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/security/ReentrancyGuard.sol"; +import "../utils/ERC20Roles.sol"; + +/** + * @title DatatokenTemplate + * + * @dev DatatokenTemplate is an ERC20 compliant token template + * Used by the factory contract as a bytecode reference to + * deploy new Datatokens. + */ +contract ERC20Template is + ERC20("test", "testSymbol"), + ERC20Roles, + ERC20Burnable, + ReentrancyGuard +{ + using SafeMath for uint256; + using SafeERC20 for IERC20; + + string private _name; + string private _symbol; + uint256 private _cap; + uint8 private constant _decimals = 18; + address private _communityFeeCollector; + bool private initialized = false; + address private _erc721Address; + address private paymentCollector; + address private publishMarketFeeAddress; + address private publishMarketFeeToken; + uint256 private publishMarketFeeAmount; + uint256 public constant BASE = 10**18; + + // EIP 2612 SUPPORT + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + mapping(address => uint256) public nonces; + + address public router; + address[] deployedPools; + struct fixedRate{ + address contractAddress; + bytes32 id; + } + fixedRate[] fixedRateExchanges; + address[] dispensers; + + struct providerFee{ + address providerFeeAddress; + address providerFeeToken; // address of the token + uint256 providerFeeAmount; // amount to be transfered to provider + uint8 v; // v of provider signed message + bytes32 r; // r of provider signed message + bytes32 s; // s of provider signed message + uint256 validUntil; //validity expresses in unix timestamp + bytes providerData; //data encoded by provider + } + + struct consumeMarketFee{ + address consumeMarketFeeAddress; + address consumeMarketFeeToken; // address of the token marketplace wants to add fee on top + uint256 consumeMarketFeeAmount; // amount to be transfered to marketFeeCollector + } + + event OrderStarted( + address indexed consumer, + address payer, + uint256 amount, + uint256 serviceIndex, + uint256 timestamp, + address indexed publishMarketAddress, + uint256 blockNumber + ); + + event OrderReused( + bytes32 orderTxId, + address caller, + uint256 timestamp, + uint256 number + ); + + event OrderExecuted( + address indexed providerAddress, + address indexed consumerAddress, + bytes32 orderTxId, + bytes providerData, + bytes providerSignature, + bytes consumerData, + bytes consumerSignature, + uint256 timestamp, + uint256 blockNumber + ); + + event PublishMarketFeeChanged( + address caller, + address PublishMarketFeeAddress, + address PublishMarketFeeToken, + uint256 PublishMarketFeeAmount + ); + + // emited for every order + event PublishMarketFee( + address indexed PublishMarketFeeAddress, + address indexed PublishMarketFeeToken, + uint256 PublishMarketFeeAmount + ); + + // emited for every order + event ConsumeMarketFee( + address indexed consumeMarketFeeAddress, + address indexed consumeMarketFeeToken, + uint256 consumeMarketFeeAmount + ); + + // emited for every order + event ProviderFee( + address indexed providerFeeAddress, + address indexed providerFeeToken, + uint256 providerFeeAmount, + bytes providerData, + uint8 v, + bytes32 r, + bytes32 s, + uint256 validUntil + ); + + + event MinterProposed(address currentMinter, address newMinter); + + event MinterApproved(address currentMinter, address newMinter); + + event NewPool( + address poolAddress, + address ssContract, + address baseTokenAddress + ); + event VestingCreated(address indexed datatokenAddress, + address indexed publisherAddress, + uint256 vestingEndBlock, + uint256 totalVestingAmount); + event NewFixedRate(bytes32 exchangeId, address indexed owner, address exchangeContract, address indexed baseToken); + event NewDispenser(address dispenserContract); + + event NewPaymentCollector( + address indexed caller, + address indexed _newPaymentCollector, + uint256 timestamp, + uint256 blockNumber + ); + + modifier onlyNotInitialized() { + require( + !initialized, + "ERC20Template: token instance already initialized" + ); + _; + } + modifier onlyNFTOwner() { + require( + msg.sender == IERC721Template(_erc721Address).ownerOf(1), + "ERC20Template: not NFTOwner" + ); + _; + } + + modifier onlyPublishingMarketFeeAddress() { + require( + msg.sender == publishMarketFeeAddress, + "ERC20Template: not publishMarketFeeAddress" + ); + _; + } + + modifier onlyERC20Deployer() { + require( + IERC721Template(_erc721Address) + .getPermissions(msg.sender) + .deployERC20, + "ERC20Template: NOT DEPLOYER ROLE" + ); + _; + } + + /** + * @dev initialize + * Called prior contract initialization (e.g creating new Datatoken instance) + * Calls private _initialize function. Only if contract is not initialized. + * @param strings_ refers to an array of strings + * [0] = name token + * [1] = symbol + * @param addresses_ refers to an array of addresses passed by user + * [0] = minter account who can mint datatokens (can have multiple minters) + * [1] = paymentCollector initial paymentCollector for this DT + * [2] = publishing Market Address + * [3] = publishing Market Fee Token + * @param factoryAddresses_ refers to an array of addresses passed by the factory + * [0] = erc721Address + * [1] = communityFeeCollector it is the community fee collector address + * [2] = router address + * + * @param uints_ refers to an array of uints + * [0] = cap_ the total ERC20 cap + * [1] = publishing Market Fee Amount + * @param bytes_ refers to an array of bytes + * Currently not used, usefull for future templates + */ + function initialize( + string[] calldata strings_, + address[] calldata addresses_, + address[] calldata factoryAddresses_, + uint256[] calldata uints_, + bytes[] calldata bytes_ + ) external onlyNotInitialized returns (bool) { + return + _initialize( + strings_, + addresses_, + factoryAddresses_, + uints_, + bytes_ + ); + } + + /** + * @dev _initialize + * Private function called on contract initialization. + * @param strings_ refers to an array of strings + * [0] = name token + * [1] = symbol + * @param addresses_ refers to an array of addresses passed by user + * [0] = minter account who can mint datatokens (can have multiple minters) + * [1] = paymentCollector initial paymentCollector for this DT + * [2] = publishing Market Address + * [3] = publishing Market Fee Token + * @param factoryAddresses_ refers to an array of addresses passed by the factory + * [0] = erc721Address + * [1] = communityFeeCollector it is the community fee collector address + * [2] = router address + * + * @param uints_ refers to an array of uints + * [0] = cap_ the total ERC20 cap + * [1] = publishing Market Fee Amount + * @param bytes_ refers to an array of bytes + * Currently not used, usefull for future templates + */ + function _initialize( + string[] memory strings_, + address[] memory addresses_, + address[] memory factoryAddresses_, + uint256[] memory uints_, + bytes[] memory bytes_ + ) private returns (bool) { + address erc721Address = factoryAddresses_[0]; + address communityFeeCollector = factoryAddresses_[1]; + require( + erc721Address != address(0), + "ERC20Template: Invalid minter, zero address" + ); + + require( + communityFeeCollector != address(0), + "ERC20Template: Invalid community fee collector, zero address" + ); + + require(uints_[0] != 0, "DatatokenTemplate: Invalid cap value"); + _cap = uints_[0]; + _name = strings_[0]; + _symbol = strings_[1]; + _erc721Address = erc721Address; + router = factoryAddresses_[2]; + _communityFeeCollector = communityFeeCollector; + initialized = true; + // add a default minter, similar to what happens with manager in the 721 contract + _addMinter(addresses_[0]); + if (addresses_[1] != address(0)) { + _setPaymentCollector(addresses_[1]); + emit NewPaymentCollector( + msg.sender, + addresses_[1], + block.timestamp, + block.number + ); + } + publishMarketFeeAddress = addresses_[2]; + publishMarketFeeToken = addresses_[3]; + publishMarketFeeAmount = uints_[1]; + emit PublishMarketFeeChanged( + msg.sender, + publishMarketFeeAddress, + publishMarketFeeToken, + publishMarketFeeAmount + ); + uint256 chainId; + assembly { + chainId := chainid() + } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(_name)), + keccak256(bytes("1")), // version, could be any other value + chainId, + address(this) + ) + ); + + return initialized; + } + + /** + * @dev deployPool + * Function to deploy new Pool with 1SS. It also has a vesting schedule. + * This function can only be called ONCE and ONLY if no token have been minted yet. + * Requires baseToken approval + * @param ssParams params for the ssContract. + * [0] = rate (wei) + * [1] = baseToken decimals + * [2] = vesting amount (wei) + * [3] = vested blocks + * [4] = initial liquidity in baseToken for pool creation + * @param swapFees swapFees (swapFee, swapMarketFee), swapOceanFee will be set automatically later + * [0] = swapFee for LP Providers + * [1] = swapFee for marketplace runner + + . + * @param addresses refers to an array of addresses passed by user + * [0] = side staking contract address + * [1] = baseToken address for pool creation(OCEAN or other) + * [2] = baseTokenSender user which will provide the baseToken amount for initial liquidity + * [3] = publisherAddress user which will be assigned the vested amount + * [4] = marketFeeCollector marketFeeCollector address + [5] = poolTemplateAddress + */ + + function deployPool( + uint256[] memory ssParams, + uint256[] memory swapFees, + address[] memory addresses + ) external onlyERC20Deployer nonReentrant returns (address pool) { + require(totalSupply() == 0, "ERC20Template: tokens already minted"); + _addMinter(addresses[0]); + // TODO: confirm minimum number of blocks required + require( + ssParams[3] >= IFactoryRouter(router).getMinVestingPeriod(), + "ERC20Template: Vesting period too low. See FactoryRouter.minVestingPeriodInBlocks" + ); + + address[2] memory tokens = [address(this), addresses[1]]; + pool = IFactoryRouter(router).deployPool( + tokens, + ssParams, + swapFees, + addresses + ); + deployedPools.push(pool); + emit NewPool(pool, addresses[0], addresses[1]); + } + + /** + * @dev createFixedRate + * Creates a new FixedRateExchange setup. + * @param fixedPriceAddress fixedPriceAddress + * @param addresses array of addresses [baseToken,owner,marketFeeCollector] + * @param uints array of uints [baseTokenDecimals,datatokenDecimals, fixedRate, marketFee, withMint] + * @return exchangeId + */ + function createFixedRate( + address fixedPriceAddress, + address[] memory addresses, + uint256[] memory uints + ) external onlyERC20Deployer nonReentrant returns (bytes32 exchangeId) { + exchangeId = IFactoryRouter(router).deployFixedRate( + fixedPriceAddress, + addresses, + uints + ); + // add FixedPriced contract as minter if withMint == true + if (uints[4] > 0) _addMinter(fixedPriceAddress); + emit NewFixedRate(exchangeId, addresses[1], fixedPriceAddress, addresses[0]); + fixedRateExchanges.push(fixedRate(fixedPriceAddress,exchangeId)); + + } + + /** + * @dev createDispenser + * Creates a new Dispenser + * @param _dispenser dispenser contract address + * @param maxTokens - max tokens to dispense + * @param maxBalance - max balance of requester. + * @param withMint - true if we want to allow the dispenser to be a minter + * @param allowedSwapper - only account that can ask tokens. set address(0) if not required + */ + function createDispenser( + address _dispenser, + uint256 maxTokens, + uint256 maxBalance, + bool withMint, + address allowedSwapper + ) external onlyERC20Deployer nonReentrant { + IFactoryRouter(router).deployDispenser( + _dispenser, + address(this), + maxTokens, + maxBalance, + msg.sender, + allowedSwapper + ); + // add FixedPriced contract as minter if withMint == true + if (withMint) _addMinter(_dispenser); + dispensers.push(_dispenser); + emit NewDispenser(_dispenser); + } + + /** + * @dev mint + * Only the minter address can call it. + * msg.value should be higher than zero and gt or eq minting fee + * @param account refers to an address that token is going to be minted to. + * @param value refers to amount of tokens that is going to be minted. + */ + function mint(address account, uint256 value) external { + require(permissions[msg.sender].minter, "ERC20Template: NOT MINTER"); + require( + totalSupply().add(value) <= _cap, + "DatatokenTemplate: cap exceeded" + ); + _mint(account, value); + } + + /** + * @dev isMinter + * Check if an address has the minter role + * @param account refers to an address that is checked + */ + function isMinter(address account) external view returns (bool) { + return (permissions[account].minter); + } + + + /** + * @dev checkProviderFee + * Checks if a providerFee structure is valid, signed and + * transfers fee to providerAddress + * @param _providerFee providerFee structure + */ + function checkProviderFee(providerFee calldata _providerFee) internal{ + // check if they are signed + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 message = keccak256( + abi.encodePacked(prefix, + keccak256( + abi.encodePacked( + _providerFee.providerData, + _providerFee.providerFeeAddress, + _providerFee.providerFeeToken, + _providerFee.providerFeeAmount, + _providerFee.validUntil + ) + ) + ) + ); + address signer = ecrecover(message, _providerFee.v, _providerFee.r, _providerFee.s); + require(signer == _providerFee.providerFeeAddress, "Invalid provider fee"); + emit ProviderFee( + _providerFee.providerFeeAddress, + _providerFee.providerFeeToken, + _providerFee.providerFeeAmount, + _providerFee.providerData, + _providerFee.v, + _providerFee.r, + _providerFee.s, + _providerFee.validUntil + ); + // skip fee if amount == 0 or feeToken == 0x0 address or feeAddress == 0x0 address + // Requires approval for the providerFeeToken of providerFeeAmount + if ( + _providerFee.providerFeeAmount > 0 && + _providerFee.providerFeeToken != address(0) && + _providerFee.providerFeeAddress != address(0) + ) { + uint256 OPCFee = IFactoryRouter(router).getOPCProviderFee(); + uint256 OPCcut = 0; + if(OPCFee > 0) + OPCcut = _providerFee.providerFeeAmount.mul(OPCFee).div(BASE); + uint256 providerCut = _providerFee.providerFeeAmount.sub(OPCcut); + + _pullUnderlying(_providerFee.providerFeeToken,msg.sender, + address(this), + _providerFee.providerFeeAmount); + IERC20(_providerFee.providerFeeToken).safeTransfer( + _providerFee.providerFeeAddress, + providerCut + ); + if(OPCcut > 0){ + IERC20(_providerFee.providerFeeToken).safeTransfer( + _communityFeeCollector, + OPCcut + ); + } + } + } + + /** + * @dev startOrder + * called by payer or consumer prior ordering a service consume on a marketplace. + * Requires previous approval of consumeFeeToken and publishMarketFeeToken + * @param consumer is the consumer address (payer could be different address) + * @param serviceIndex service index in the metadata + * @param _providerFee provider fee + * @param _consumeMarketFee consume market fee + */ + function startOrder( + address consumer, + uint256 serviceIndex, + providerFee calldata _providerFee, + consumeMarketFee calldata _consumeMarketFee + ) external nonReentrant { + uint256 amount = 1e18; // we always pay 1 DT. No more, no less + uint256 communityFeePublish = 0; + require( + balanceOf(msg.sender) >= amount, + "Not enough datatokens to start Order" + ); + emit OrderStarted( + consumer, + msg.sender, + amount, + serviceIndex, + block.timestamp, + publishMarketFeeAddress, + block.number + ); + // publishMarketFee + // Requires approval for the publishMarketFeeToken of publishMarketFeeAmount + // skip fee if amount == 0 or feeToken == 0x0 address or feeAddress == 0x0 address + if ( + publishMarketFeeAmount > 0 && + publishMarketFeeToken != address(0) && + publishMarketFeeAddress != address(0) + ) { + _pullUnderlying(publishMarketFeeToken,msg.sender, + address(this), + publishMarketFeeAmount); + uint256 OPCFee = IFactoryRouter(router).getOPCConsumeFee(); + if(OPCFee > 0) + communityFeePublish = publishMarketFeeAmount.mul(OPCFee).div(BASE); + //send publishMarketFee + IERC20(publishMarketFeeToken).safeTransfer( + publishMarketFeeAddress, + publishMarketFeeAmount.sub(communityFeePublish) + ); + + emit PublishMarketFee( + publishMarketFeeAddress, + publishMarketFeeToken, + publishMarketFeeAmount.sub(communityFeePublish) + ); + //send fee to OPC + if (communityFeePublish > 0) { + //since both fees are in the same token, have just one transaction for both, to save gas + IERC20(publishMarketFeeToken).safeTransfer( + _communityFeeCollector, + communityFeePublish + ); + emit PublishMarketFee( + _communityFeeCollector, + publishMarketFeeToken, + communityFeePublish + ); + } + } + + // consumeMarketFee + // Requires approval for the FeeToken + // skip fee if amount == 0 or feeToken == 0x0 address or feeAddress == 0x0 address + if ( + _consumeMarketFee.consumeMarketFeeAmount > 0 && + _consumeMarketFee.consumeMarketFeeToken != address(0) && + _consumeMarketFee.consumeMarketFeeAddress != address(0) + ) { + _pullUnderlying(_consumeMarketFee.consumeMarketFeeToken,msg.sender, + address(this), + _consumeMarketFee.consumeMarketFeeAmount); + uint256 OPCFee = IFactoryRouter(router).getOPCConsumeFee(); + if(OPCFee > 0) + communityFeePublish = _consumeMarketFee.consumeMarketFeeAmount.mul(OPCFee).div(BASE); + //send publishMarketFee + IERC20(_consumeMarketFee.consumeMarketFeeToken).safeTransfer( + _consumeMarketFee.consumeMarketFeeAddress, + _consumeMarketFee.consumeMarketFeeAmount.sub(communityFeePublish) + ); + + emit ConsumeMarketFee( + _consumeMarketFee.consumeMarketFeeAddress, + _consumeMarketFee.consumeMarketFeeToken, + _consumeMarketFee.consumeMarketFeeAmount.sub(communityFeePublish) + ); + //send fee to OPC + if (communityFeePublish > 0) { + //since both fees are in the same token, have just one transaction for both, to save gas + IERC20(_consumeMarketFee.consumeMarketFeeToken).safeTransfer( + _communityFeeCollector, + communityFeePublish + ); + emit ConsumeMarketFee( + _communityFeeCollector, + _consumeMarketFee.consumeMarketFeeToken, + communityFeePublish + ); + } + } + checkProviderFee(_providerFee); + + // send datatoken to publisher + require( + transfer(getPaymentCollector(), amount), + "Failed to send DT to publisher" + ); + } + + /** + * @dev reuseOrder + * called by payer or consumer having a valid order, but with expired provider access + * Pays the provider fee again, but it will not require a new datatoken payment + * Requires previous approval of provider fee. + * @param orderTxId previous valid order + * @param _providerFee provider feee + */ + function reuseOrder( + bytes32 orderTxId, + providerFee calldata _providerFee + ) external nonReentrant { + emit OrderReused( + orderTxId, + msg.sender, + block.timestamp, + block.number + ); + checkProviderFee(_providerFee); + } + + /** + * @dev addMinter + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple minters + * @param _minter new minter address + */ + + function addMinter(address _minter) external onlyERC20Deployer { + _addMinter(_minter); + } + + /** + * @dev removeMinter + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple minters + * @param _minter minter address to remove + */ + + function removeMinter(address _minter) external onlyERC20Deployer { + _removeMinter(_minter); + } + + /** + * @dev addPaymentManager (can set who's going to collect fee when consuming orders) + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple paymentCollectors + * @param _paymentManager new minter address + */ + + function addPaymentManager(address _paymentManager) + external + onlyERC20Deployer + { + _addPaymentManager(_paymentManager); + } + + /** + * @dev removePaymentManager + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple paymentManagers + * @param _paymentManager _paymentManager address to remove + */ + + function removePaymentManager(address _paymentManager) + external + onlyERC20Deployer + { + _removePaymentManager(_paymentManager); + } + + /** + * @dev setData + * Only ERC20Deployer (at 721 level) can call it. + * This function allows to store data with a preset key (keccak256(ERC20Address)) into NFT 725 Store + * @param _value data to be set with this key + */ + + function setData(bytes calldata _value) external onlyERC20Deployer { + bytes32 key = keccak256(abi.encodePacked(address(this))); + IERC721Template(_erc721Address).setDataERC20(key, _value); + } + + /** + * @dev cleanPermissions() + * Only NFT Owner (at 721 level) can call it. + * This function allows to remove all minters, feeManagers and reset the paymentCollector + * + */ + + function cleanPermissions() external onlyNFTOwner { + _cleanPermissions(); + paymentCollector = address(0); + } + + /** + * @dev cleanFrom721() + * OnlyNFT(721) Contract can call it. + * This function allows to remove all minters, feeManagers and reset the paymentCollector + * This function is used when transferring an NFT to a new owner, + * so that permissions at ERC20level (minter,feeManager,paymentCollector) can be reset. + * + */ + function cleanFrom721() external { + require( + msg.sender == _erc721Address, + "ERC20Template: NOT 721 Contract" + ); + _cleanPermissions(); + paymentCollector = address(0); + } + + /** + * @dev setPaymentCollector + * Only feeManager can call it + * This function allows to set a newPaymentCollector (receives DT when consuming) + If not set the paymentCollector is the NFT Owner + * @param _newPaymentCollector new fee collector + */ + + function setPaymentCollector(address _newPaymentCollector) external { + //we allow _newPaymentCollector = address(0), because it means that the collector is nft owner + require( + permissions[msg.sender].paymentManager || + IERC721Template(_erc721Address) + .getPermissions(msg.sender) + .deployERC20, + "ERC20Template: NOT PAYMENT MANAGER or OWNER" + ); + _setPaymentCollector(_newPaymentCollector); + emit NewPaymentCollector( + msg.sender, + _newPaymentCollector, + block.timestamp, + block.number + ); + } + + /** + * @dev _setPaymentCollector + * @param _newPaymentCollector new fee collector + */ + + function _setPaymentCollector(address _newPaymentCollector) internal { + paymentCollector = _newPaymentCollector; + } + + /** + * @dev getPublishingMarketFee + * Get publishingMarket Fee + * This function allows to get the current fee set by the publishing market + */ + function getPublishingMarketFee() + external + view + returns ( + address, + address, + uint256 + ) + { + return ( + publishMarketFeeAddress, + publishMarketFeeToken, + publishMarketFeeAmount + ); + } + + /** + * @dev setPublishingMarketFee + * Only publishMarketFeeAddress can call it + * This function allows to set the fee required by the publisherMarket + * @param _publishMarketFeeAddress new _publishMarketFeeAddress + * @param _publishMarketFeeToken new _publishMarketFeeToken + * @param _publishMarketFeeAmount new fee amount + */ + function setPublishingMarketFee( + address _publishMarketFeeAddress, + address _publishMarketFeeToken, + uint256 _publishMarketFeeAmount + ) external onlyPublishingMarketFeeAddress { + require( + _publishMarketFeeAddress != address(0), + "Invalid _publishMarketFeeAddress address" + ); + require( + _publishMarketFeeToken != address(0), + "Invalid _publishMarketFeeToken address" + ); + publishMarketFeeAddress = _publishMarketFeeAddress; + publishMarketFeeToken = _publishMarketFeeToken; + publishMarketFeeAmount = _publishMarketFeeAmount; + emit PublishMarketFeeChanged( + msg.sender, + _publishMarketFeeAddress, + _publishMarketFeeToken, + _publishMarketFeeAmount + ); + } + + /** + * @dev getId + * Return template id in case we need different ABIs. + * If you construct your own template, please make sure to change the hardcoded value + */ + function getId() pure public returns (uint8) { + return 1; + } + + /** + * @dev name + * It returns the token name. + * @return Datatoken name. + */ + function name() public view override returns (string memory) { + return _name; + } + + /** + * @dev symbol + * It returns the token symbol. + * @return Datatoken symbol. + */ + function symbol() public view override returns (string memory) { + return _symbol; + } + + /** + * @dev getERC721Address + * It returns the parent ERC721 + * @return ERC721 address. + */ + function getERC721Address() public view returns (address) { + return _erc721Address; + } + + /** + * @dev decimals + * It returns the token decimals. + * how many supported decimal points + * @return Datatoken decimals. + */ + function decimals() public pure override returns (uint8) { + return _decimals; + } + + /** + * @dev cap + * it returns the capital. + * @return Datatoken cap. + */ + function cap() external view returns (uint256) { + return _cap; + } + + /** + * @dev isInitialized + * It checks whether the contract is initialized. + * @return true if the contract is initialized. + */ + + function isInitialized() external view returns (bool) { + return initialized; + } + + /** + * @dev permit + * used for signed approvals, see ERC20Template test for more details + * @param owner user who signed the message + * @param spender spender + * @param value token amount + * @param deadline deadline after which signed message is no more valid + * @param v parameters from signed message + * @param r parameters from signed message + * @param s parameters from signed message + */ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + require(deadline >= block.number, "ERC20DT: EXPIRED"); + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require( + recoveredAddress != address(0) && recoveredAddress == owner, + "ERC20DT: INVALID_SIGNATURE" + ); + _approve(owner, spender, value); + } + + /** + * @dev getAddressLength + * It returns the array lentgh + @param array address array we want to get length + * @return length + */ + + function getAddressLength(address[] memory array) + private + pure + returns (uint256) + { + return array.length; + } + + /** + * @dev getUintLength + * It returns the array lentgh + @param array uint array we want to get length + * @return length + */ + + function getUintLength(uint256[] memory array) + private + pure + returns (uint256) + { + return array.length; + } + + /** + * @dev getBytesLength + * It returns the array lentgh + @param array bytes32 array we want to get length + * @return length + */ + + function getBytesLength(bytes32[] memory array) + private + pure + returns (uint256) + { + return array.length; + } + + /** + * @dev getPaymentCollector + * It returns the current paymentCollector + * @return paymentCollector address + */ + + function getPaymentCollector() public view returns (address) { + if (paymentCollector == address(0)) { + return IERC721Template(_erc721Address).ownerOf(1); + } else { + return paymentCollector; + } + } + + /** + * @dev fallback function + * this is a default fallback function in which receives + * the collected ether. + */ + fallback() external payable {} + + /** + * @dev withdrawETH + * transfers all the accumlated ether the collector account + */ + function withdrawETH() external payable { + payable(getPaymentCollector()).transfer(address(this).balance); + } + + /** + * @dev isERC20Deployer + * returns true if address has deployERC20 role + */ + function isERC20Deployer(address user) public returns (bool deployer) { + deployer = IERC721Template(_erc721Address) + .getPermissions(user) + .deployERC20; + return (deployer); + } + + /** + * @dev getPools + * Returns the list of pools created for this datatoken + */ + function getPools() public view returns(address[] memory) { + return(deployedPools); + } + /** + * @dev getFixedRates + * Returns the list of fixedRateExchanges created for this datatoken + */ + function getFixedRates() public view returns(fixedRate[] memory) { + return(fixedRateExchanges); + } + /** + * @dev getDispensers + * Returns the list of dispensers created for this datatoken + */ + function getDispensers() public view returns(address[] memory) { + return(dispensers); + } + + function _pullUnderlying( + address erc20, + address from, + address to, + uint256 amount + ) internal { + uint256 balanceBefore = IERC20(erc20).balanceOf(to); + IERC20(erc20).safeTransferFrom(from, to, amount); + require(IERC20(erc20).balanceOf(to) >= balanceBefore.add(amount), + "Transfer amount is too low"); + } + + + /** + * @dev orderExecuted + * Providers should call this to prove order execution + * @param orderTxId order tx + * @param providerData provider data + * @param providerSignature provider signature + * @param consumerData consumer data + * @param consumerSignature consumer signature + * @param consumerAddress consumer address + */ + function orderExecuted( + bytes32 orderTxId, + bytes calldata providerData, + bytes calldata providerSignature, + bytes calldata consumerData, + bytes calldata consumerSignature, + address consumerAddress + ) external { + require(msg.sender != consumerAddress, "Provider cannot be the consumer"); + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 providerHash = keccak256( + abi.encodePacked(prefix, + keccak256( + abi.encodePacked( + orderTxId, + providerData + ) + ) + ) + ); + require(ecrecovery(providerHash, providerSignature) == msg.sender, "Provider signature check failed"); + bytes32 consumerHash = keccak256( + abi.encodePacked(prefix, + keccak256( + abi.encodePacked( + consumerData + ) + ) + ) + ); + require(ecrecovery(consumerHash, consumerSignature) == consumerAddress, "Consumer signature check failed"); + emit OrderExecuted(msg.sender, consumerAddress ,orderTxId, providerData, providerSignature, + consumerData, consumerSignature, block.timestamp, block.number); + } + + + + function ecrecovery(bytes32 hash, bytes memory sig) pure internal returns (address) { + bytes32 r; + bytes32 s; + uint8 v; + if (sig.length != 65) { + return address(0); + } + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := and(mload(add(sig, 65)), 255) + } + if (v < 27) { + v += 27; + } + if (v != 27 && v != 28) { + return address(0); + } + return ecrecover(hash, v, r, s); + } + +} diff --git a/sol080/contracts/oceanv4/templates/ERC20TemplateEnterprise.sol b/sol080/contracts/oceanv4/templates/ERC20TemplateEnterprise.sol new file mode 100644 index 0000000000000000000000000000000000000000..f2689fb0e1e52e8bd217aa0a006f7d24d3ffd759 --- /dev/null +++ b/sol080/contracts/oceanv4/templates/ERC20TemplateEnterprise.sol @@ -0,0 +1,1180 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "../interfaces/IERC20Template.sol"; +import "../interfaces/IERC721Template.sol"; +import "../interfaces/IFactoryRouter.sol"; +import "../interfaces/IFixedRateExchange.sol"; +import "../interfaces/IDispenser.sol"; +import "../utils/ERC725/ERC725Ocean.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/ERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/IERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/security/ReentrancyGuard.sol"; +import "../utils/ERC20Roles.sol"; + +/** + * @title DatatokenTemplate + * + * @dev ERC20TemplateEnterprise is an ERC20 compliant token template + * Used by the factory contract as a bytecode reference to + * deploy new Datatokens. + * IMPORTANT CHANGES: + * - buyFromFreAndOrder function: one call to buy a DT from the minting capable FRE, startOrder and burn the DT + * - buyFromDispenserAndOrder function: one call to fetch a DT from the Dispenser, startOrder and burn the DT + * - creation of pools is not allowed + */ +contract ERC20TemplateEnterprise is + ERC20("test", "testSymbol"), + ERC20Roles, + ERC20Burnable, + ReentrancyGuard +{ + using SafeMath for uint256; + using SafeERC20 for IERC20; + string private _name; + string private _symbol; + uint256 private _cap; + uint8 private constant _decimals = 18; + address private _communityFeeCollector; + bool private initialized = false; + address private _erc721Address; + address private paymentCollector; + address private publishMarketFeeAddress; + address private publishMarketFeeToken; + uint256 private publishMarketFeeAmount; + + uint256 public constant BASE = 10**18; + + + // EIP 2612 SUPPORT + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + mapping(address => uint256) public nonces; + address public router; + + struct fixedRate{ + address contractAddress; + bytes32 id; + } + fixedRate[] fixedRateExchanges; + address[] dispensers; + + struct providerFee{ + address providerFeeAddress; + address providerFeeToken; // address of the token + uint256 providerFeeAmount; // amount to be transfered to provider + uint8 v; // v of provider signed message + bytes32 r; // r of provider signed message + bytes32 s; // s of provider signed message + uint256 validUntil; //validity expresses in unix timestamp + bytes providerData; //data encoded by provider + } + + struct consumeMarketFee{ + address consumeMarketFeeAddress; + address consumeMarketFeeToken; // address of the token marketplace wants to add fee on top + uint256 consumeMarketFeeAmount; // amount to be transfered to marketFeeCollector + } + + event OrderStarted( + address indexed consumer, + address payer, + uint256 amount, + uint256 serviceIndex, + uint256 timestamp, + address indexed publishMarketAddress, + uint256 blockNumber + ); + + event OrderReused( + bytes32 orderTxId, + address caller, + uint256 timestamp, + uint256 number + ); + + event OrderExecuted( + address indexed providerAddress, + address indexed consumerAddress, + bytes32 orderTxId, + bytes providerData, + bytes providerSignature, + bytes consumerData, + bytes consumerSignature, + uint256 timestamp, + uint256 blockNumber + ); + + // emited for every order + event PublishMarketFee( + address indexed PublishMarketFeeAddress, + address indexed PublishMarketFeeToken, + uint256 PublishMarketFeeAmount + ); + + // emited for every order + event ConsumeMarketFee( + address indexed consumeMarketFeeAddress, + address indexed consumeMarketFeeToken, + uint256 consumeMarketFeeAmount + ); + + event PublishMarketFeeChanged( + address caller, + address PublishMarketFeeAddress, + address PublishMarketFeeToken, + uint256 PublishMarketFeeAmount + ); + + event ProviderFee( + address indexed providerFeeAddress, + address indexed providerFeeToken, + uint256 providerFeeAmount, + bytes providerData, + uint8 v, + bytes32 r, + bytes32 s, + uint256 validUntil + ); + + event MinterProposed(address currentMinter, address newMinter); + + event MinterApproved(address currentMinter, address newMinter); + + event NewFixedRate(bytes32 exchangeId, address indexed owner, address exchangeContract, address indexed baseToken); + event NewDispenser(address dispenserContract); + + event NewPaymentCollector( + address indexed caller, + address indexed _newPaymentCollector, + uint256 timestamp, + uint256 blockNumber + ); + + modifier onlyNotInitialized() { + require( + !initialized, + "ERC20Template: token instance already initialized" + ); + _; + } + modifier onlyNFTOwner() { + require( + msg.sender == IERC721Template(_erc721Address).ownerOf(1), + "ERC20Template: not NFTOwner" + ); + _; + } + + modifier onlyPublishingMarketFeeAddress() { + require( + msg.sender == publishMarketFeeAddress, + "ERC20Template: not publishMarketFeeAddress" + ); + _; + } + + modifier onlyERC20Deployer() { + require( + IERC721Template(_erc721Address) + .getPermissions(msg.sender) + .deployERC20, + "ERC20Template: NOT DEPLOYER ROLE" + ); + _; + } + + /** + * @dev initialize + * Called prior contract initialization (e.g creating new Datatoken instance) + * Calls private _initialize function. Only if contract is not initialized. + * @param strings_ refers to an array of strings + * [0] = name token + * [1] = symbol + * @param addresses_ refers to an array of addresses passed by user + * [0] = minter account who can mint datatokens (can have multiple minters) + * [1] = paymentCollector initial paymentCollector for this DT + * [2] = publishing Market Address + * [3] = publishing Market Fee Token + * @param factoryAddresses_ refers to an array of addresses passed by the factory + * [0] = erc721Address + * [1] = communityFeeCollector it is the community fee collector address + * [2] = router address + * + * @param uints_ refers to an array of uints + * [0] = cap_ the total ERC20 cap + * [1] = publishing Market Fee Amount + * @param bytes_ refers to an array of bytes + * Currently not used, usefull for future templates + */ + function initialize( + string[] calldata strings_, + address[] calldata addresses_, + address[] calldata factoryAddresses_, + uint256[] calldata uints_, + bytes[] calldata bytes_ + ) external onlyNotInitialized returns (bool) { + return + _initialize( + strings_, + addresses_, + factoryAddresses_, + uints_, + bytes_ + ); + } + + /** + * @dev _initialize + * Private function called on contract initialization. + * @param strings_ refers to an array of strings + * [0] = name token + * [1] = symbol + * @param addresses_ refers to an array of addresses passed by user + * [0] = minter account who can mint datatokens (can have multiple minters) + * [1] = paymentCollector initial paymentCollector for this DT + * [2] = publishing Market Address + * [3] = publishing Market Fee Token + * @param factoryAddresses_ refers to an array of addresses passed by the factory + * [0] = erc721Address + * [1] = communityFeeCollector it is the community fee collector address + * [2] = router address + * + * @param uints_ refers to an array of uints + * [0] = cap_ the total ERC20 cap + * [1] = publishing Market Fee Amount + * @param bytes_ refers to an array of bytes + * Currently not used, usefull for future templates + */ + function _initialize( + string[] memory strings_, + address[] memory addresses_, + address[] memory factoryAddresses_, + uint256[] memory uints_, + bytes[] memory bytes_ + ) private returns (bool) { + address erc721Address = factoryAddresses_[0]; + address communityFeeCollector = factoryAddresses_[1]; + require( + erc721Address != address(0), + "ERC20Template: Invalid minter, zero address" + ); + + require( + communityFeeCollector != address(0), + "ERC20Template: Invalid community fee collector, zero address" + ); + + require(uints_[0] != 0, "DatatokenTemplate: Invalid cap value"); + _cap = uints_[0]; + _name = strings_[0]; + _symbol = strings_[1]; + _erc721Address = erc721Address; + router = factoryAddresses_[2]; + _communityFeeCollector = communityFeeCollector; + initialized = true; + // add a default minter, similar to what happens with manager in the 721 contract + _addMinter(addresses_[0]); + if (addresses_[1] != address(0)) { + _setPaymentCollector(addresses_[1]); + emit NewPaymentCollector( + msg.sender, + addresses_[1], + block.timestamp, + block.number + ); + } + publishMarketFeeAddress = addresses_[2]; + publishMarketFeeToken = addresses_[3]; + publishMarketFeeAmount = uints_[1]; + emit PublishMarketFeeChanged( + msg.sender, + publishMarketFeeAddress, + publishMarketFeeToken, + publishMarketFeeAmount + ); + uint256 chainId; + assembly { + chainId := chainid() + } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(_name)), + keccak256(bytes("1")), // version, could be any other value + chainId, + address(this) + ) + ); + + return initialized; + } + + /** + * @dev createFixedRate + * Creates a new FixedRateExchange setup. + * @param fixedPriceAddress fixedPriceAddress + * @param addresses array of addresses [baseToken,owner,marketFeeCollector] + * @param uints array of uints [baseTokenDecimals,datatokenDecimals, fixedRate, marketFee, withMint] + * @return exchangeId + */ + function createFixedRate( + address fixedPriceAddress, + address[] memory addresses, + uint256[] memory uints + ) external onlyERC20Deployer nonReentrant returns (bytes32 exchangeId) { + //force FRE allowedSwapper to this contract address. no one else can swap + addresses[3] = address(this); + exchangeId = IFactoryRouter(router).deployFixedRate( + fixedPriceAddress, + addresses, + uints + ); + if (uints[4] > 0) _addMinter(fixedPriceAddress); + emit NewFixedRate(exchangeId, addresses[1], fixedPriceAddress, addresses[0]); + fixedRateExchanges.push(fixedRate(fixedPriceAddress,exchangeId)); + } + + /** + * @dev createDispenser + * Creates a new Dispenser + * @param _dispenser dispenser contract address + * @param maxTokens - max tokens to dispense + * @param maxBalance - max balance of requester. + * @param withMint - with MinterRole + * @param allowedSwapper - have it here for compat reasons, will be overwritten + */ + function createDispenser( + address _dispenser, + uint256 maxTokens, + uint256 maxBalance, + bool withMint, + address allowedSwapper + ) external onlyERC20Deployer nonReentrant { + IFactoryRouter(router).deployDispenser( + _dispenser, + address(this), + maxTokens, + maxBalance, + msg.sender, + address(this) + ); + // add FixedPriced contract as minter if withMint == true + if (withMint) _addMinter(_dispenser); + dispensers.push(_dispenser); + emit NewDispenser(_dispenser); + } + + /** + * @dev mint + * Only the minter address can call it. + * msg.value should be higher than zero and gt or eq minting fee + * @param account refers to an address that token is going to be minted to. + * @param value refers to amount of tokens that is going to be minted. + */ + function mint(address account, uint256 value) external { + require(permissions[msg.sender].minter, "ERC20Template: NOT MINTER"); + require( + totalSupply().add(value) <= _cap, + "DatatokenTemplate: cap exceeded" + ); + _mint(account, value); + } + + /** + * @dev isMinter + * Check if an address has the minter role + * @param account refers to an address that is checked + */ + function isMinter(address account) external view returns (bool) { + return (permissions[account].minter); + } + + + /** + * @dev checkProviderFee + * Checks if a providerFee structure is valid, signed and + * transfers fee to providerAddress + * @param _providerFee providerFee structure + */ + function checkProviderFee(providerFee calldata _providerFee) internal{ + // check if they are signed + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 message = keccak256( + abi.encodePacked(prefix, + keccak256( + abi.encodePacked( + _providerFee.providerData, + _providerFee.providerFeeAddress, + _providerFee.providerFeeToken, + _providerFee.providerFeeAmount, + _providerFee.validUntil + ) + ) + ) + ); + address signer = ecrecover(message, _providerFee.v, _providerFee.r, _providerFee.s); + require(signer == _providerFee.providerFeeAddress, "Invalid provider fee"); + emit ProviderFee( + _providerFee.providerFeeAddress, + _providerFee.providerFeeToken, + _providerFee.providerFeeAmount, + _providerFee.providerData, + _providerFee.v, + _providerFee.r, + _providerFee.s, + _providerFee.validUntil + ); + // skip fee if amount == 0 or feeToken == 0x0 address or feeAddress == 0x0 address + // Requires approval for the providerFeeToken of providerFeeAmount + if ( + _providerFee.providerFeeAmount > 0 && + _providerFee.providerFeeToken != address(0) && + _providerFee.providerFeeAddress != address(0) + ) { + uint256 OPCFee = IFactoryRouter(router).getOPCProviderFee(); + uint256 OPCcut = 0; + if(OPCFee > 0) + OPCcut = _providerFee.providerFeeAmount.mul(OPCFee).div(BASE); + uint256 providerCut = _providerFee.providerFeeAmount.sub(OPCcut); + _pullUnderlying(_providerFee.providerFeeToken,msg.sender, + address(this), + _providerFee.providerFeeAmount); + IERC20(_providerFee.providerFeeToken).safeTransfer( + _providerFee.providerFeeAddress, + providerCut + ); + if(OPCcut > 0){ + IERC20(_providerFee.providerFeeToken).safeTransfer( + _communityFeeCollector, + OPCcut + ); + } + } + } + /** + * @dev startOrder + * called by payer or consumer prior ordering a service consume on a marketplace. + * Requires previous approval of consumeFeeToken and publishMarketFeeToken + * @param consumer is the consumer address (payer could be different address) + * @param serviceIndex service index in the metadata + * @param _providerFee provider fee + * @param _consumeMarketFee consume market fee + */ + function startOrder( + address consumer, + uint256 serviceIndex, + providerFee calldata _providerFee, + consumeMarketFee calldata _consumeMarketFee + ) public { + uint256 amount = 1e18; // we always pay 1 DT. No more, no less + uint256 communityFeePublish = 0; + require( + balanceOf(msg.sender) >= amount, + "Not enough datatokens to start Order" + ); + emit OrderStarted( + consumer, + msg.sender, + amount, + serviceIndex, + block.timestamp, + publishMarketFeeAddress, + block.number + ); + // publishMarketFee + // Requires approval for the publishMarketFeeToken of publishMarketFeeAmount + // skip fee if amount == 0 or feeToken == 0x0 address or feeAddress == 0x0 address + if ( + publishMarketFeeAmount > 0 && + publishMarketFeeToken != address(0) && + publishMarketFeeAddress != address(0) + ) { + _pullUnderlying(publishMarketFeeToken,msg.sender, + address(this), + publishMarketFeeAmount); + uint256 OPCFee = IFactoryRouter(router).getOPCConsumeFee(); + if(OPCFee > 0) + communityFeePublish = publishMarketFeeAmount.mul(OPCFee).div(BASE); + //send publishMarketFee + IERC20(publishMarketFeeToken).safeTransfer( + publishMarketFeeAddress, + publishMarketFeeAmount.sub(communityFeePublish) + ); + emit PublishMarketFee( + publishMarketFeeAddress, + publishMarketFeeToken, + publishMarketFeeAmount.sub(communityFeePublish) + ); + //send fee to OPC + if (communityFeePublish > 0) { + IERC20(publishMarketFeeToken).safeTransfer( + _communityFeeCollector, + communityFeePublish + ); + emit PublishMarketFee( + _communityFeeCollector, + publishMarketFeeToken, + communityFeePublish + ); + } + } + + // consumeMarketFee + // Requires approval for the FeeToken + // skip fee if amount == 0 or feeToken == 0x0 address or feeAddress == 0x0 address + if ( + _consumeMarketFee.consumeMarketFeeAmount > 0 && + _consumeMarketFee.consumeMarketFeeToken != address(0) && + _consumeMarketFee.consumeMarketFeeAddress != address(0) + ) { + _pullUnderlying(_consumeMarketFee.consumeMarketFeeToken,msg.sender, + address(this), + _consumeMarketFee.consumeMarketFeeAmount); + uint256 OPCFee = IFactoryRouter(router).getOPCConsumeFee(); + if(OPCFee > 0) + communityFeePublish = _consumeMarketFee.consumeMarketFeeAmount.mul(OPCFee).div(BASE); + //send publishMarketFee + IERC20(_consumeMarketFee.consumeMarketFeeToken).safeTransfer( + _consumeMarketFee.consumeMarketFeeAddress, + _consumeMarketFee.consumeMarketFeeAmount.sub(communityFeePublish) + ); + + emit ConsumeMarketFee( + _consumeMarketFee.consumeMarketFeeAddress, + _consumeMarketFee.consumeMarketFeeToken, + _consumeMarketFee.consumeMarketFeeAmount.sub(communityFeePublish) + ); + //send fee to OPC + if (communityFeePublish > 0) { + //since both fees are in the same token, have just one transaction for both, to save gas + IERC20(_consumeMarketFee.consumeMarketFeeToken).safeTransfer( + _communityFeeCollector, + communityFeePublish + ); + emit ConsumeMarketFee( + _communityFeeCollector, + _consumeMarketFee.consumeMarketFeeToken, + communityFeePublish + ); + } + } + + checkProviderFee(_providerFee); + + // instead of sending datatoken to publisher, we burn them + burn(amount); + } + + /** + * @dev reuseOrder + * called by payer or consumer having a valid order, but with expired provider access + * Pays the provider fee again, but it will not require a new datatoken payment + * Requires previous approval of provider fee. + * @param orderTxId previous valid order + * @param _providerFee provider feee + */ + function reuseOrder( + bytes32 orderTxId, + providerFee calldata _providerFee + ) external { + emit OrderReused( + orderTxId, + msg.sender, + block.timestamp, + block.number + ); + checkProviderFee(_providerFee); + } + + /** + * @dev addMinter + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple minters + * @param _minter new minter address + */ + + function addMinter(address _minter) external onlyERC20Deployer { + _addMinter(_minter); + } + + /** + * @dev removeMinter + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple minters + * @param _minter minter address to remove + */ + + function removeMinter(address _minter) external onlyERC20Deployer { + _removeMinter(_minter); + } + + /** + * @dev addPaymentManager (can set who's going to collect fee when consuming orders) + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple paymentCollectors + * @param _paymentManager new minter address + */ + + function addPaymentManager(address _paymentManager) + external + onlyERC20Deployer + { + _addPaymentManager(_paymentManager); + } + + /** + * @dev removePaymentManager + * Only ERC20Deployer (at 721 level) can update. + * There can be multiple paymentManagers + * @param _paymentManager _paymentManager address to remove + */ + + function removePaymentManager(address _paymentManager) + external + onlyERC20Deployer + { + _removePaymentManager(_paymentManager); + } + + /** + * @dev setData + * Only ERC20Deployer (at 721 level) can call it. + * This function allows to store data with a preset key (keccak256(ERC20Address)) into NFT 725 Store + * @param _value data to be set with this key + */ + + function setData(bytes calldata _value) external onlyERC20Deployer { + bytes32 key = keccak256(abi.encodePacked(address(this))); + IERC721Template(_erc721Address).setDataERC20(key, _value); + } + + /** + * @dev cleanPermissions() + * Only NFT Owner (at 721 level) can call it. + * This function allows to remove all minters, feeManagers and reset the paymentCollector + * + */ + + function cleanPermissions() external onlyNFTOwner { + _cleanPermissions(); + paymentCollector = address(0); + } + + /** + * @dev cleanFrom721() + * OnlyNFT(721) Contract can call it. + * This function allows to remove all minters, feeManagers and reset the paymentCollector + * This function is used when transferring an NFT to a new owner, + * so that permissions at ERC20level (minter,feeManager,paymentCollector) can be reset. + * + */ + function cleanFrom721() external { + require( + msg.sender == _erc721Address, + "ERC20Template: NOT 721 Contract" + ); + _cleanPermissions(); + paymentCollector = address(0); + } + + /** + * @dev setPaymentCollector + * Only feeManager can call it + * This function allows to set a newPaymentCollector (receives DT when consuming) + If not set the paymentCollector is the NFT Owner + * @param _newPaymentCollector new fee collector + */ + + function setPaymentCollector(address _newPaymentCollector) external { + //we allow _newPaymentCollector = address(0), because it means that the collector is nft owner + require( + permissions[msg.sender].paymentManager || + IERC721Template(_erc721Address) + .getPermissions(msg.sender) + .deployERC20, + "ERC20Template: NOT PAYMENT MANAGER or OWNER" + ); + _setPaymentCollector(_newPaymentCollector); + emit NewPaymentCollector( + msg.sender, + _newPaymentCollector, + block.timestamp, + block.number + ); + } + + /** + * @dev _setPaymentCollector + * @param _newPaymentCollector new fee collector + */ + + function _setPaymentCollector(address _newPaymentCollector) internal { + paymentCollector = _newPaymentCollector; + } + + /** + * @dev getPublishingMarketFee + * Get publishingMarket Fee + * This function allows to get the current fee set by the publishing market + */ + function getPublishingMarketFee() + external + view + returns ( + address, + address, + uint256 + ) + { + return ( + publishMarketFeeAddress, + publishMarketFeeToken, + publishMarketFeeAmount + ); + } + + /** + * @dev setPublishingMarketFee + * Only publishMarketFeeAddress can call it + * This function allows to set the fee required by the publisherMarket + * @param _publishMarketFeeAddress new _publishMarketFeeAddress + * @param _publishMarketFeeToken new _publishMarketFeeToken + * @param _publishMarketFeeAmount new fee amount + */ + function setPublishingMarketFee( + address _publishMarketFeeAddress, + address _publishMarketFeeToken, + uint256 _publishMarketFeeAmount + ) external onlyPublishingMarketFeeAddress { + require( + _publishMarketFeeAddress != address(0), + "Invalid _publishMarketFeeAddress address" + ); + require( + _publishMarketFeeToken != address(0), + "Invalid _publishMarketFeeToken address" + ); + publishMarketFeeAddress = _publishMarketFeeAddress; + publishMarketFeeToken = _publishMarketFeeToken; + publishMarketFeeAmount = _publishMarketFeeAmount; + emit PublishMarketFeeChanged( + msg.sender, + _publishMarketFeeAddress, + _publishMarketFeeToken, + _publishMarketFeeAmount + ); + } + + /** + * @dev getId + * Return template id in case we need different ABIs. + * If you construct your own template, please make sure to change the hardcoded value + */ + function getId() pure public returns (uint8) { + return 2; + } + + /** + * @dev name + * It returns the token name. + * @return Datatoken name. + */ + function name() public view override returns (string memory) { + return _name; + } + + /** + * @dev symbol + * It returns the token symbol. + * @return Datatoken symbol. + */ + function symbol() public view override returns (string memory) { + return _symbol; + } + + /** + * @dev getERC721Address + * It returns the parent ERC721 + * @return ERC721 address. + */ + function getERC721Address() public view returns (address) { + return _erc721Address; + } + + /** + * @dev decimals + * It returns the token decimals. + * how many supported decimal points + * @return Datatoken decimals. + */ + function decimals() public pure override returns (uint8) { + return _decimals; + } + + /** + * @dev cap + * it returns the capital. + * @return Datatoken cap. + */ + function cap() external view returns (uint256) { + return _cap; + } + + /** + * @dev isInitialized + * It checks whether the contract is initialized. + * @return true if the contract is initialized. + */ + + function isInitialized() external view returns (bool) { + return initialized; + } + + /** + * @dev permit + * used for signed approvals, see ERC20Template test for more details + * @param owner user who signed the message + * @param spender spender + * @param value token amount + * @param deadline deadline after which signed message is no more valid + * @param v parameters from signed message + * @param r parameters from signed message + * @param s parameters from signed message + */ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + require(deadline >= block.number, "ERC20DT: EXPIRED"); + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require( + recoveredAddress != address(0) && recoveredAddress == owner, + "ERC20DT: INVALID_SIGNATURE" + ); + _approve(owner, spender, value); + } + + /** + * @dev getAddressLength + * It returns the array lentgh + @param array address array we want to get length + * @return length + */ + + function getAddressLength(address[] memory array) + private + pure + returns (uint256) + { + return array.length; + } + + /** + * @dev getUintLength + * It returns the array lentgh + @param array uint array we want to get length + * @return length + */ + + function getUintLength(uint256[] memory array) + private + pure + returns (uint256) + { + return array.length; + } + + /** + * @dev getBytesLength + * It returns the array lentgh + @param array bytes32 array we want to get length + * @return length + */ + + function getBytesLength(bytes32[] memory array) + private + pure + returns (uint256) + { + return array.length; + } + + /** + * @dev getPaymentCollector + * It returns the current paymentCollector + * @return paymentCollector address + */ + + function getPaymentCollector() public view returns (address) { + if (paymentCollector == address(0)) { + return IERC721Template(_erc721Address).ownerOf(1); + } else { + return paymentCollector; + } + } + + /** + * @dev fallback function + * this is a default fallback function in which receives + * the collected ether. + */ + fallback() external payable {} + + /** + * @dev withdrawETH + * transfers all the accumlated ether the collector account + */ + function withdrawETH() external payable { + payable(getPaymentCollector()).transfer(address(this).balance); + } + + struct OrderParams { + address consumer; + uint256 serviceIndex; + providerFee _providerFee; + consumeMarketFee _consumeMarketFee; + } + struct FreParams { + address exchangeContract; + bytes32 exchangeId; + uint256 maxBaseTokenAmount; + uint256 swapMarketFee; + address marketFeeAddress; + } + + /** + * @dev buyFromFreAndOrder + * Buys 1 DT from the FRE and then startsOrder, while burning that DT + */ + function buyFromFreAndOrder( + OrderParams calldata _orderParams, + FreParams calldata _freParams + ) external nonReentrant{ + // get exchange info + ( + , + address datatoken, + , + address baseToken, + , + , + , + , + , + , + , + + ) = IFixedRateExchange(_freParams.exchangeContract).getExchange( + _freParams.exchangeId + ); + require( + datatoken == address(this), + "This FixedRate is not providing this DT" + ); + // get token amounts needed + ( + uint256 baseTokenAmount + , + , + , + + ) = IFixedRateExchange(_freParams.exchangeContract) + .calcBaseInGivenOutDT( + _freParams.exchangeId, + 1e18, // we always take 1 DT + _freParams.swapMarketFee + ); + require( + baseTokenAmount <= _freParams.maxBaseTokenAmount, + "FixedRateExchange: Too many base tokens" + ); + + //transfer baseToken to us first + _pullUnderlying(baseToken,msg.sender, + address(this), + baseTokenAmount); + //approve FRE to spend baseTokens + IERC20(baseToken).safeIncreaseAllowance( + _freParams.exchangeContract, + baseTokenAmount + ); + //buy DT + IFixedRateExchange(_freParams.exchangeContract).buyDT( + _freParams.exchangeId, + 1e18, // we always take 1 dt + baseTokenAmount, + _freParams.marketFeeAddress, + _freParams.swapMarketFee + ); + require( + balanceOf(address(this)) >= 1e18, + "Unable to buy DT from FixedRate" + ); + //we need the following because startOrder expects msg.sender to have dt + _transfer(address(this), msg.sender, 1e18); + //startOrder and burn it + startOrder(_orderParams.consumer, _orderParams.serviceIndex, + _orderParams._providerFee, _orderParams._consumeMarketFee); + } + + /** + * @dev buyFromDispenserAndOrder + * Gets DT from dispenser and then startsOrder, while burning that DT + */ + function buyFromDispenserAndOrder( + OrderParams calldata _orderParams, + address dispenserContract + ) external nonReentrant { + uint256 amount = 1e18; + //get DT + IDispenser(dispenserContract).dispense( + address(this), + amount, + msg.sender + ); + require( + balanceOf(address(msg.sender)) >= amount, + "Unable to get DT from Dispenser" + ); + //startOrder and burn it + startOrder(_orderParams.consumer, _orderParams.serviceIndex, + _orderParams._providerFee, _orderParams._consumeMarketFee); + } + + /** + * @dev isERC20Deployer + * returns true if address has deployERC20 role + */ + function isERC20Deployer(address user) public returns(bool deployer){ + deployer = IERC721Template(_erc721Address).getPermissions(user).deployERC20; + return(deployer); + } + + /** + * @dev getFixedRates + * Returns the list of fixedRateExchanges created for this datatoken + */ + function getFixedRates() public view returns(fixedRate[] memory) { + return(fixedRateExchanges); + } + /** + * @dev getDispensers + * Returns the list of dispensers created for this datatoken + */ + function getDispensers() public view returns(address[] memory) { + return(dispensers); + } + + function _pullUnderlying( + address erc20, + address from, + address to, + uint256 amount + ) internal { + uint256 balanceBefore = IERC20(erc20).balanceOf(to); + IERC20(erc20).safeTransferFrom(from, to, amount); + require(IERC20(erc20).balanceOf(to) >= balanceBefore.add(amount), + "Transfer amount is too low"); + } + + /** + * @dev orderExecuted + * Providers should call this to prove order execution + * @param orderTxId order tx + * @param providerData provider data + * @param providerSignature provider signature + * @param consumerData consumer data + * @param consumerSignature consumer signature + * @param consumerAddress consumer address + */ + function orderExecuted( + bytes32 orderTxId, + bytes calldata providerData, + bytes calldata providerSignature, + bytes calldata consumerData, + bytes calldata consumerSignature, + address consumerAddress + ) external { + require(msg.sender != consumerAddress, "Provider cannot be the consumer"); + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 providerHash = keccak256( + abi.encodePacked(prefix, + keccak256( + abi.encodePacked( + orderTxId, + providerData + ) + ) + ) + ); + require(ecrecovery(providerHash, providerSignature) == msg.sender, "Provider signature check failed"); + bytes32 consumerHash = keccak256( + abi.encodePacked(prefix, + keccak256( + abi.encodePacked( + consumerData + ) + ) + ) + ); + require(ecrecovery(consumerHash, consumerSignature) == consumerAddress, "Consumer signature check failed"); + emit OrderExecuted(msg.sender, consumerAddress ,orderTxId, providerData, providerSignature, + consumerData, consumerSignature, block.timestamp, block.number); + } + + + + function ecrecovery(bytes32 hash, bytes memory sig) pure internal returns (address) { + bytes32 r; + bytes32 s; + uint8 v; + if (sig.length != 65) { + return address(0); + } + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := and(mload(add(sig, 65)), 255) + } + if (v < 27) { + v += 27; + } + if (v != 27 && v != 28) { + return address(0); + } + return ecrecover(hash, v, r, s); + } +} diff --git a/sol080/contracts/oceanv4/templates/ERC721Template.sol b/sol080/contracts/oceanv4/templates/ERC721Template.sol new file mode 100644 index 0000000000000000000000000000000000000000..4e977271797636ddc67b311337d814aa5e5f43c6 --- /dev/null +++ b/sol080/contracts/oceanv4/templates/ERC721Template.sol @@ -0,0 +1,620 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "../utils/ERC721/ERC721.sol"; +import "../utils/ERC725/ERC725Ocean.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/Create2.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/security/ReentrancyGuard.sol"; +import "../interfaces/IV3ERC20.sol"; +import "../interfaces/IFactory.sol"; +import "../interfaces/IERC20Template.sol"; +import "../utils/ERC721RolesAddress.sol"; + + +contract ERC721Template is + ERC721("Template", "TemplateSymbol"), + ERC721RolesAddress, + ERC725Ocean, + ReentrancyGuard +{ + + string private _name; + string private _symbol; + //uint256 private tokenId = 1; + bool private initialized; + bool public hasMetaData; + string public metaDataDecryptorUrl; + string public metaDataDecryptorAddress; + uint8 public metaDataState; + address private _tokenFactory; + address[] private deployedERC20List; + address public ssContract; + uint8 private constant templateId = 1; + mapping(address => bool) private deployedERC20; + + //stored here only for ABI reasons + event TokenCreated( + address indexed newTokenAddress, + address indexed templateAddress, + string name, + string symbol, + uint256 cap, + address creator + ); + event MetadataCreated( + address indexed createdBy, + uint8 state, + string decryptorUrl, + bytes flags, + bytes data, + bytes32 metaDataHash, + uint256 timestamp, + uint256 blockNumber + ); + event MetadataUpdated( + address indexed updatedBy, + uint8 state, + string decryptorUrl, + bytes flags, + bytes data, + bytes32 metaDataHash, + uint256 timestamp, + uint256 blockNumber + ); + event MetadataValidated( + address indexed validator, + bytes32 metaDataHash, + uint8 v, + bytes32 r, + bytes32 s + ); + event MetadataState( + address indexed updatedBy, + uint8 state, + uint256 timestamp, + uint256 blockNumber + ); + + event TokenURIUpdate( + address indexed updatedBy, + string tokenURI, + uint256 tokenID, + uint256 timestamp, + uint256 blockNumber + ); + + modifier onlyNFTOwner() { + require(msg.sender == ownerOf(1), "ERC721Template: not NFTOwner"); + _; + } + + /** + * @dev initialize + * Calls private _initialize function. Only if contract is not initialized. + This function mints an NFT (tokenId=1) to the owner and add owner as Manager Role + * @param owner NFT Owner + * @param name_ NFT name + * @param symbol_ NFT Symbol + * @param tokenFactory NFT factory address + + @return boolean + */ + + function initialize( + address owner, + string calldata name_, + string calldata symbol_, + address tokenFactory, + address additionalERC20Deployer, + address additionalMetaDataUpdater, + string memory tokenURI + ) external returns (bool) { + require( + !initialized, + "ERC721Template: token instance already initialized" + ); + if(additionalERC20Deployer != address(0)) + _addToCreateERC20List(additionalERC20Deployer); + if(additionalMetaDataUpdater != address(0)) + _addToMetadataList(additionalMetaDataUpdater); + bool initResult = + _initialize( + owner, + name_, + symbol_, + tokenFactory, + tokenURI + ); + return(initResult); + } + + /** + * @dev _initialize + * Calls private _initialize function. Only if contract is not initialized. + * This function mints an NFT (tokenId=1) to the owner + * and add owner as Manager Role (Roles admin) + * @param owner NFT Owner + * @param name_ NFT name + * @param symbol_ NFT Symbol + * @param tokenFactory NFT factory address + * @param tokenURI tokenURI for token 1 + + @return boolean + */ + + function _initialize( + address owner, + string memory name_, + string memory symbol_, + address tokenFactory, + string memory tokenURI + ) internal returns (bool) { + require( + owner != address(0), + "ERC721Template:: Invalid minter, zero address" + ); + + _name = name_; + _symbol = symbol_; + _tokenFactory = tokenFactory; + defaultBaseURI = ""; + initialized = true; + hasMetaData = false; + _safeMint(owner, 1); + _addManager(owner); + + // we add the nft owner to all other roles (so that doesn't need to make multiple transactions) + Roles storage user = permissions[owner]; + user.updateMetadata = true; + user.deployERC20 = true; + user.store = true; + // no need to push to auth since it has been already added in _addManager() + _setTokenURI(1, tokenURI); + + return initialized; + } + + /** + * @dev setTokenURI + * sets tokenURI for a tokenId + * @param tokenId token ID + * @param tokenURI token URI + */ + function setTokenURI(uint256 tokenId, string memory tokenURI) public { + require(msg.sender == ownerOf(tokenId), "ERC721Template: not NFTOwner"); + _setTokenURI(tokenId, tokenURI); + emit TokenURIUpdate(msg.sender, tokenURI, tokenId, + /* solium-disable-next-line */ + block.timestamp, + block.number); + } + + + + /** + * @dev setMetaDataState + * Updates metadata state + * @param _metaDataState metadata state + */ + function setMetaDataState(uint8 _metaDataState) public { + require( + permissions[msg.sender].updateMetadata, + "ERC721Template: NOT METADATA_ROLE" + ); + metaDataState = _metaDataState; + emit MetadataState(msg.sender, _metaDataState, + /* solium-disable-next-line */ + block.timestamp, + block.number); + } + + struct metaDataProof { + address validatorAddress; + uint8 v; // v of validator signed message + bytes32 r; // r of validator signed message + bytes32 s; // s of validator signed message + } + /** + * @dev setMetaData + * + Creates or update Metadata for Aqua(emit event) + Also, updates the METADATA_DECRYPTOR key + * @param _metaDataState metadata state + * @param _metaDataDecryptorUrl decryptor URL + * @param _metaDataDecryptorAddress decryptor public key + * @param flags flags used by Aquarius + * @param data data used by Aquarius + * @param _metaDataHash hash of clear data (before the encryption, if any) + * @param _metadataProofs optional signatures of entitys who validated data (before the encryption, if any) + */ + function setMetaData(uint8 _metaDataState, string calldata _metaDataDecryptorUrl + , string calldata _metaDataDecryptorAddress, bytes calldata flags, + bytes calldata data,bytes32 _metaDataHash, metaDataProof[] memory _metadataProofs) external { + require( + permissions[msg.sender].updateMetadata, + "ERC721Template: NOT METADATA_ROLE" + ); + _setMetaData(_metaDataState, _metaDataDecryptorUrl, _metaDataDecryptorAddress,flags, + data,_metaDataHash, _metadataProofs); + } + + function _setMetaData(uint8 _metaDataState, string calldata _metaDataDecryptorUrl + , string calldata _metaDataDecryptorAddress, bytes calldata flags, + bytes calldata data,bytes32 _metaDataHash, metaDataProof[] memory _metadataProofs) internal { + metaDataState = _metaDataState; + metaDataDecryptorUrl = _metaDataDecryptorUrl; + metaDataDecryptorAddress = _metaDataDecryptorAddress; + if(!hasMetaData){ + emit MetadataCreated(msg.sender, _metaDataState, _metaDataDecryptorUrl, + flags, data, _metaDataHash, + /* solium-disable-next-line */ + block.timestamp, + block.number); + hasMetaData = true; + } + else + emit MetadataUpdated(msg.sender, metaDataState, _metaDataDecryptorUrl, + flags, data, _metaDataHash, + /* solium-disable-next-line */ + block.timestamp, + block.number); + //check proofs and emit an event for each proof + require(_metadataProofs.length <= 50, 'Too Many Proofs'); + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + for (uint256 i = 0; i < _metadataProofs.length; i++) { + if(_metadataProofs[i].validatorAddress != address(0)){ + bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, _metaDataHash)); + address signer = ecrecover(prefixedHash, + _metadataProofs[i].v, _metadataProofs[i].r, _metadataProofs[i].s); + require(signer == _metadataProofs[i].validatorAddress, "Invalid proof signer"); + } + emit MetadataValidated(_metadataProofs[i].validatorAddress, + _metaDataHash, + _metadataProofs[i].v, _metadataProofs[i].r, _metadataProofs[i].s); + } + } + + struct metaDataAndTokenURI { + uint8 metaDataState; + string metaDataDecryptorUrl; + string metaDataDecryptorAddress; + bytes flags; + bytes data; + bytes32 metaDataHash; + uint256 tokenId; + string tokenURI; + metaDataProof[] metadataProofs; + } + + /** + * @dev setMetaDataAndTokenURI + * Helper function to improve UX + Calls setMetaData & setTokenURI + * @param _metaDataAndTokenURI metaDataAndTokenURI struct + */ + function setMetaDataAndTokenURI(metaDataAndTokenURI calldata _metaDataAndTokenURI) external { + require( + permissions[msg.sender].updateMetadata, + "ERC721Template: NOT METADATA_ROLE" + ); + _setMetaData(_metaDataAndTokenURI.metaDataState, _metaDataAndTokenURI.metaDataDecryptorUrl, + _metaDataAndTokenURI.metaDataDecryptorAddress, _metaDataAndTokenURI.flags, + _metaDataAndTokenURI.data, _metaDataAndTokenURI.metaDataHash, _metaDataAndTokenURI.metadataProofs); + + setTokenURI(_metaDataAndTokenURI.tokenId, _metaDataAndTokenURI.tokenURI); + + } + /** + * @dev getMetaData + * Returns metaDataState, metaDataDecryptorUrl, metaDataDecryptorAddress + */ + function getMetaData() external view returns (string memory, string memory, uint8, bool){ + return (metaDataDecryptorUrl, metaDataDecryptorAddress, metaDataState, hasMetaData); + } + + + /** + * @dev createERC20 + * ONLY user with deployERC20 permission (assigned by Manager) can call it + Creates a new ERC20 datatoken. + It also adds initial minting and fee management permissions to custom users. + + * @param _templateIndex ERC20Template index + * @param strings refers to an array of strings + * [0] = name + * [1] = symbol + * @param addresses refers to an array of addresses + * [0] = minter account who can mint datatokens (can have multiple minters) + * [1] = feeManager initial feeManager for this DT + * [2] = publishing Market Address + * [3] = publishing Market Fee Token + * @param uints refers to an array of uints + * [0] = cap_ the total ERC20 cap + * [1] = publishing Market Fee Amount + * @param bytess refers to an array of bytes + * Currently not used, usefull for future templates + + @return ERC20 token address + */ + + function createERC20( + uint256 _templateIndex, + string[] calldata strings, + address[] calldata addresses, + uint256[] calldata uints, + bytes[] calldata bytess + ) external nonReentrant returns (address ) { + require( + permissions[msg.sender].deployERC20, + "ERC721Template: NOT ERC20DEPLOYER_ROLE" + ); + + address token = IFactory(_tokenFactory).createToken( + _templateIndex, + strings, + addresses, + uints, + bytess + ); + + deployedERC20[token] = true; + + deployedERC20List.push(token); + return token; + } + + /** + * @dev isERC20Deployer + * @return true if the account has ERC20 Deploy role + */ + function isERC20Deployer(address account) external view returns (bool) { + return permissions[account].deployERC20; + } + + /** + * @dev name + * It returns the token name. + * @return Datatoken name. + */ + function name() public view override returns (string memory) { + return _name; + } + + /** + * @dev symbol + * It returns the token symbol. + * @return Datatoken symbol. + */ + function symbol() public view override returns (string memory) { + return _symbol; + } + + /** + * @dev isInitialized + * It checks whether the contract is initialized. + * @return true if the contract is initialized. + */ + + function isInitialized() external view returns (bool) { + return initialized; + } + + /** + * @dev addManager + * Only NFT Owner can add a new manager (Roles admin) + * There can be multiple minters + * @param _managerAddress new manager address + */ + + function addManager(address _managerAddress) external onlyNFTOwner { + _addManager(_managerAddress); + } + + /** + * @dev removeManager + * Only NFT Owner can remove a manager (Roles admin) + * There can be multiple minters + * @param _managerAddress new manager address + */ + + + function removeManager(address _managerAddress) external onlyNFTOwner { + _removeManager(_managerAddress); + } + + /** + * @notice Executes any other smart contract. + Is only callable by the Manager. + * + * + * @param _operation the operation to execute: CALL = 0; DELEGATECALL = 1; CREATE2 = 2; CREATE = 3; + * @param _to the smart contract or address to interact with. + * `_to` will be unused if a contract is created (operation 2 and 3) + * @param _value the value of ETH to transfer + * @param _data the call data, or the contract data to deploy + **/ + + function executeCall( + uint256 _operation, + address _to, + uint256 _value, + bytes calldata _data + ) external payable onlyManager { + execute(_operation, _to, _value, _data); + } + + + /** + * @dev setNewData + * ONLY user with store permission (assigned by Manager) can call it + This function allows to set any arbitrary key-value into the 725 standard + * There can be multiple store updaters + * @param _key key (see 725 for standard (keccak256)) + Data keys, should be the keccak256 hash of a type name. + e.g. keccak256('ERCXXXMyNewKeyType') is 0x6935a24ea384927f250ee0b954ed498cd9203fc5d2bf95c735e52e6ca675e047 + + * @param _value data to store at that key + */ + + + function setNewData(bytes32 _key, bytes calldata _value) external { + require( + permissions[msg.sender].store, + "ERC721Template: NOT STORE UPDATER" + ); + setData(_key, _value); + } + + /** + * @dev setDataERC20 + * ONLY callable FROM the ERC20Template and BY the corresponding ERC20Deployer + This function allows to store data with a preset key (keccak256(ERC20Address)) into NFT 725 Store + * @param _key keccak256(ERC20Address) see setData into ERC20Template.sol + * @param _value data to store at that key + */ + + + function setDataERC20(bytes32 _key, bytes calldata _value) external { + require( + deployedERC20[msg.sender], + "ERC721Template: NOT ERC20 Contract" + ); + setData(_key, _value); + } + + + /** + * @dev cleanPermissions + * Only NFT Owner can call it. + * This function allows to remove all ROLES at erc721 level: + * Managers, ERC20Deployer, MetadataUpdater, StoreUpdater + * Permissions at erc20 level stay. + * Even NFT Owner has to readd himself as Manager + */ + + function cleanPermissions() external onlyNFTOwner { + _cleanPermissions(); + } + + + + /** + * @dev transferFrom + * Used for transferring the NFT, can be used by an approved relayer + Even if we only have 1 tokenId, we leave it open as arguments for being a standard ERC721 + @param from nft owner + @param to nft receiver + @param tokenId tokenId (1) + */ + + function transferFrom( + address from, + address to, + uint256 tokenId + ) external { + require(tokenId == 1, "ERC721Template: Cannot transfer this tokenId"); + _cleanERC20Permissions(getAddressLength(deployedERC20List)); + _cleanPermissions(); + _addManager(to); + // we add the nft owner to all other roles (so that doesn't need to make multiple transactions) + Roles storage user = permissions[to]; + user.updateMetadata = true; + user.deployERC20 = true; + user.store = true; + // no need to push to auth since it has been already added in _addManager() + _transferFrom(from, to, tokenId); + + } + + /** + * @dev safeTransferFrom + * Used for transferring the NFT, can be used by an approved relayer + Even if we only have 1 tokenId, we leave it open as arguments for being a standard ERC721 + @param from nft owner + @param to nft receiver + @param tokenId tokenId (1) + */ + + function safeTransferFrom(address from, address to,uint256 tokenId) external { + require(tokenId == 1, "ERC721Template: Cannot transfer this tokenId"); + _cleanERC20Permissions(getAddressLength(deployedERC20List)); + _cleanPermissions(); + _addManager(to); + // we add the nft owner to all other roles (so that doesn't need to make multiple transactions) + Roles storage user = permissions[to]; + user.updateMetadata = true; + user.deployERC20 = true; + user.store = true; + // no need to push to auth since it has been already added in _addManager() + safeTransferFrom(from, to, tokenId, ""); + + } + + /** + * @dev getAddressLength + * It returns the array lentgh + @param array address array we want to get length + * @return length + */ + + + function getAddressLength(address[] memory array) + private + pure + returns (uint256) + { + return array.length; + } + + /** + * @dev _cleanERC20Permissions + * Internal function used to clean permissions at ERC20 level when transferring the NFT + @param length lentgh of the deployedERC20List + */ + + function _cleanERC20Permissions(uint256 length) internal { + for (uint256 i = 0; i < length; i++) { + IERC20Template(deployedERC20List[i]).cleanFrom721(); + } + } + + /** + * @dev getId + * Return template id in case we need different ABIs. + * If you construct your own template, please make sure to change the hardcoded value + */ + function getId() pure public returns (uint8) { + return 1; + } + /** + * @dev fallback function + * this is a default fallback function in which receives + * the collected ether. + */ + fallback() external payable {} + + /** + * @dev withdrawETH + * transfers all the accumlated ether the ownerOf + */ + function withdrawETH() + external + payable + { + payable(ownerOf(1)).transfer(address(this).balance); + } + + function getTokensList() external view returns (address[] memory) { + return deployedERC20List; + } + + function isDeployed(address datatoken) external view returns (bool) { + return deployedERC20[datatoken]; + } + + function setBaseURI(string memory _baseURI) external onlyNFTOwner { + defaultBaseURI = _baseURI; + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/test/__init__.py b/sol080/contracts/oceanv4/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol080/contracts/oceanv4/test/test_erc721_factory.py b/sol080/contracts/oceanv4/test/test_erc721_factory.py new file mode 100644 index 0000000000000000000000000000000000000000..5e7ca42263d1a3165f5857f117ce031c2bac5e88 --- /dev/null +++ b/sol080/contracts/oceanv4/test/test_erc721_factory.py @@ -0,0 +1,191 @@ +import brownie + +from sol080.contracts.oceanv4 import oceanv4util +from util.base18 import toBase18 +from util.constants import ( + BROWNIE_PROJECT080, + GOD_ACCOUNT, + ZERO_ADDRESS, + OPF_ADDRESS, +) +from util.globaltokens import fundOCEANFromAbove +from util.tx import txdict + +accounts = brownie.network.accounts +account0 = accounts[0] +address0 = account0.address + + +def test_direct(): # pylint: disable=too-many-statements + # God deploys fake OCEAN token + OCEANtoken = BROWNIE_PROJECT080.MockOcean.deploy(address0, txdict(GOD_ACCOUNT)) + OCEAN_address = OCEANtoken.address + + # God deploys templates + erc721_template = BROWNIE_PROJECT080.ERC721Template.deploy(txdict(GOD_ACCOUNT)) + erc20_template = BROWNIE_PROJECT080.ERC20Template.deploy(txdict(GOD_ACCOUNT)) + pool_template = BROWNIE_PROJECT080.BPool.deploy(txdict(GOD_ACCOUNT)) + + # God deploys Factory Router + router = BROWNIE_PROJECT080.FactoryRouter.deploy( + address0, + OCEAN_address, + pool_template.address, + OPF_ADDRESS, + [], + txdict(GOD_ACCOUNT), + ) + + # God deploys ERC721 Factory, and reports it to the router + erc721_factory = BROWNIE_PROJECT080.ERC721Factory.deploy( + erc721_template.address, + erc20_template.address, + OPF_ADDRESS, + router.address, + txdict(account0), + ) + assert erc721_factory.owner() == address0 + current_nft_count = erc721_factory.getCurrentNFTCount() + + # Publisher creates ERC721 data NFT + tx = erc721_factory.deployERC721Contract( + "dataNFT1", + "DATANFT1", + 1, + ZERO_ADDRESS, + ZERO_ADDRESS, + "https://mystorage.com/mytoken.png", + txdict(account0), + ) + assert tx.events["NFTCreated"] is not None + assert tx.events["NFTCreated"]["admin"] == address0 + dataNFT1_address = tx.events["NFTCreated"]["newTokenAddress"] + dataNFT1 = BROWNIE_PROJECT080.ERC721Template.at(dataNFT1_address) + assert dataNFT1.name() == "dataNFT1" + assert dataNFT1.symbol() == "DATANFT1" + + assert erc721_factory.getCurrentNFTCount() == current_nft_count + 1 + + nft_template = erc721_factory.getNFTTemplate(1) + assert nft_template[0] == erc721_template.address + assert nft_template[1] + + # Publisher creates ERC20 datatoken + strings = ["datatoken1", "DT1"] + DT_cap = 10000 + uints = [toBase18(DT_cap), toBase18(0.0)] + addresses = [address0] * 4 + _bytes = [] + tx = dataNFT1.createERC20(1, strings, addresses, uints, _bytes, txdict(account0)) + + DT_address = tx.events["TokenCreated"]["newTokenAddress"] + DT = BROWNIE_PROJECT080.ERC20Template.at(DT_address) + assert DT.name() == "datatoken1" + assert DT.symbol() == "DT1" + assert DT.cap() == toBase18(DT_cap) + assert DT.balanceOf(address0) == 0 + assert erc721_factory.getTokenTemplate(1)[0] == erc20_template.address + assert erc721_factory.getTokenTemplate(1)[1] + + # Now, the publisher has 3 options: + # (a) Create pool + # (b) Mint DTs and create fixed price exchange + # (c) Mint DTs and create pools on any other market + # Here, we do option (a)... + + # Publisher approves staking OCEAN + OCEAN_init_liquidity = 2000.0 + OCEANtoken.approve(router.address, toBase18(OCEAN_init_liquidity), txdict(account0)) + + # Publisher deploys 1-sided staking bot, reports info to router. + ss_bot = BROWNIE_PROJECT080.SideStaking.deploy(router.address, txdict(GOD_ACCOUNT)) + router.addSSContract(ss_bot.address, txdict(account0)) + router.addFactory(erc721_factory.address, txdict(account0)) + + # Publisher deploys pool, which includes a 1-sided staking bot + ss_params = [ + toBase18(0.1), # rate (wei) + 18, # OCEAN decimals + toBase18(0.05 * DT_cap), # vesting amount (wei) + int(2.5e6), # vested blocks + toBase18(OCEAN_init_liquidity), + ] + swap_fees = [ + toBase18(0.02), # LP swap fee + toBase18(0.01), # mkt swap fee + ] + addresses = [ + ss_bot.address, + OCEAN_address, + address0, + address0, + OPF_ADDRESS, + pool_template.address, + ] + + tx = DT.deployPool(ss_params, swap_fees, addresses, txdict(account0)) + pool_address = oceanv4util.poolAddressFromNewBPoolTx(tx) + pool = BROWNIE_PROJECT080.BPool.at(pool_address) + + assert OCEANtoken.balanceOf(pool_address) == toBase18(OCEAN_init_liquidity) + assert pool.getSwapFee() == toBase18(0.02) + assert pool.getMarketFee() == toBase18(0.01) + + +def test_createDataNFT_via_util(): + router = oceanv4util.deployRouter(account0) + (dataNFT, _) = oceanv4util.createDataNFT("dataNFT", "DATANFT", account0, router) + assert dataNFT.name() == "dataNFT" + assert dataNFT.symbol() == "DATANFT" + assert dataNFT.getPermissions(account0.address) == (True, True, True, True) + assert dataNFT.balanceOf(account0.address) == 1 + + +def test_createDT_via_util(): + router = oceanv4util.deployRouter(account0) + (dataNFT, _) = oceanv4util.createDataNFT("dataNFT", "DATANFT", account0, router) + DT = oceanv4util.createDatatokenFromDataNFT( + "DT", "DTSymbol", 10000, dataNFT, account0 + ) + assert DT.name() == "DT" + assert DT.symbol() == "DTSymbol" + assert DT.cap() == toBase18(10000) + assert DT.balanceOf(account0.address) == 0 + + +def test_createBPool_via_util(): + brownie.chain.reset() + router = oceanv4util.deployRouter(account0) + (dataNFT, erc721_factory) = oceanv4util.createDataNFT( + "dataNFT", "DATANFT", account0, router + ) + DT = oceanv4util.createDatatokenFromDataNFT( + "DT", "DTSymbol", 10000, dataNFT, account0 + ) + + fundOCEANFromAbove(address0, toBase18(10000.0)) + OCEAN = oceanv4util.OCEANtoken() + + OCEAN_init_liquidity = 2000.0 + DT_OCEAN_rate = 0.1 + DT_vest_amt = 100 + DT_vest_num_blocks = 600 + LP_swap_fee = 0.03 + mkt_swap_fee = 0.01 + pool = oceanv4util.createBPoolFromDatatoken( + DT, + erc721_factory, + account0, + OCEAN_init_liquidity, + DT_OCEAN_rate, + DT_vest_amt, + DT_vest_num_blocks, + LP_swap_fee, + mkt_swap_fee, + ) + pool_address = pool.address + + assert pool.getBaseTokenAddress() == OCEAN.address + assert OCEAN.balanceOf(pool_address) < toBase18(10000.0) + assert pool.getMarketFee() == toBase18(mkt_swap_fee) + assert pool.getSwapFee() == toBase18(LP_swap_fee) diff --git a/sol080/contracts/oceanv4/test/test_oceanv4util.py b/sol080/contracts/oceanv4/test/test_oceanv4util.py new file mode 100644 index 0000000000000000000000000000000000000000..ee3a3105c24f7fa625f2593c333e5073d6a38048 --- /dev/null +++ b/sol080/contracts/oceanv4/test/test_oceanv4util.py @@ -0,0 +1,3 @@ +# oceanv4util.py is tested piecewise by other test files in this directory, +# such as test_erc721_factory.py. Therefore we don't need to repeat ourselves and +# have tests in this file. We keep this file, and these comments, as a reminder. diff --git a/sol080/contracts/oceanv4/test/test_side_staking.py b/sol080/contracts/oceanv4/test/test_side_staking.py new file mode 100644 index 0000000000000000000000000000000000000000..91f3aa7e3d56a56257b0a44d0b9e6fba7351f368 --- /dev/null +++ b/sol080/contracts/oceanv4/test/test_side_staking.py @@ -0,0 +1,361 @@ +import brownie + +from util.base18 import toBase18, fromBase18 +from util.constants import BROWNIE_PROJECT080 +from util.globaltokens import fundOCEANFromAbove, OCEANtoken +from sol080.contracts.oceanv4 import oceanv4util +from util.tx import txdict + +accounts = brownie.network.accounts + +account0 = accounts[0] +address0 = account0.address + +account1 = accounts[1] +address1 = account1.address + + +def test_sideStaking_properties(): + brownie.chain.reset() + OCEAN = OCEANtoken() + + OCEAN_base_funding = 10000 + do_extra_funding = False + OCEAN_extra_funding = 10000 + + OCEAN_init_liquidity = 2000 # initial liquidity in OCEAN on pool creation + DT_OCEAN_rate = 0.1 + + DT_cap = 10000 + DT_vest_amt = 1000 + DT_vest_num_blocks = 600 + (DT, pool, ssbot) = _deployBPool( + OCEAN_base_funding, + do_extra_funding, + OCEAN_extra_funding, + OCEAN_init_liquidity, + DT_OCEAN_rate, + DT_cap, + DT_vest_amt, + DT_vest_num_blocks, + ) + + # basic tests + assert pool.getBaseTokenAddress() == OCEAN.address + assert ssbot.getBaseTokenAddress(DT.address) == OCEAN.address + assert ssbot.getBaseTokenBalance(DT.address) == 0 + + assert ssbot.getPublisherAddress(DT.address) == address0 + assert ssbot.getPoolAddress(DT.address) == pool.address + + assert fromBase18(ssbot.getvestingAmount(DT.address)) == 1000 + + # ssbot manages DTs in two specific ways: + # 1. Linear vesting of DTs to publisher over a fixed # blocks + # 2. When OCEAN liquidity is added to the pool, it + # moves DT from its balance into the pool (ultimately, DT circ supply) + # such that DT:OCEAN ratio stays constant + + # Therefore: (DT_vested) + (ssbot_DT_balance + DT_circ_supply) = DT_cap + + # No blocks have passed since pool creation. Therefore no vesting yet + assert ssbot.getvestingAmountSoFar(DT.address) == 0 + + # We start out with a given OCEAN_init_liquidity. And, DT_OCEAN_rate is + # the initial DT:OCEAN ratio. + # Therefore we know how many DT the bot was supposed to add to the pool: + # OCEAN_init_liquidity * DT_OCEAN_rate = 2000*0.1 = 200 + # Since no one's swapped for DT, then this is also DT circulating supply + assert fromBase18(ssbot.getDatatokenCirculatingSupply(DT.address)) == 200 + + # The ssbot holds the rest of the DT. Rerrange formula above to see amt. + # So,ssbot_DT_balance = DT_cap - DT_vested - DT_circ_supply + # = 10000 - 0 - 200 + # = 9800 + assert fromBase18(DT.balanceOf(ssbot.address)) == 9800 + + # ======================================================== + # Test vesting... (recall DT_vest_amt = 1000) + assert fromBase18(ssbot.getvestingAmount(DT.address)) == 1000 + + # datatoken.deployPool() + # -> call BFactory.newBPool() + # -> call SideStaking.newDatatokenCreated() + # -> emit VestingCreated(datatokenAddress, publisherAddress, + # _datatokens[datatokenAddress].vestingEndBlock, + # _datatokens[datatokenAddress].vestingAmount) + + assert ssbot.getAvailableVesting(DT.address) == 0 + assert ssbot.getvestingAmountSoFar(DT.address) == 0 + + # Pass enough time to make all vesting happen! + brownie.chain.mine(blocks=DT_vest_num_blocks) + + # Test key numbers + block_number = len(brownie.chain) + vesting_end_block = ssbot.getvestingEndBlock(DT.address) + vesting_last_block = ssbot.getvestingLastBlock(DT.address) + blocks_passed = vesting_end_block - vesting_last_block + available_vesting = fromBase18(ssbot.getAvailableVesting(DT.address)) + + assert block_number >= 616 + assert vesting_end_block >= 615 + assert vesting_last_block >= 15 # last block when tokens were vested + assert blocks_passed >= 600 + assert available_vesting == 1000 + + # Publisher hasn't claimed yet, so no vesting or DTs + assert ssbot.getvestingAmountSoFar(DT.address) == 0 + assert DT.balanceOf(account0) == 0 + + # Publisher claims. Now he has vesting and DTs + ssbot.getVesting(DT.address, txdict(account0)) # claim! + assert fromBase18(ssbot.getvestingAmountSoFar(DT.address)) == 1000 + assert fromBase18(DT.balanceOf(account0)) == 1000 + + # The ssbot's holdings are updated too + # ssbot_DT_balance = DT_cap - DT_vested - DT_circ_supply + # = 10000 - 1000 - 200 + # = 8800 + assert fromBase18(ssbot.getDatatokenBalance(DT.address)) == 8800 + + # + assert ssbot.getvestingLastBlock(DT.address) == block_number >= 616 + + +def test_swapExactAmountIn(): + brownie.chain.reset() + OCEAN = OCEANtoken() + (DT, pool, _) = _deployBPool(do_extra_funding=True) + + tokenInOutMarket = [OCEAN.address, DT.address, address0] + # [tokenIn,tokenOut,marketFeeAddress] + amountsInOutMaxFee = [toBase18(100), toBase18(1), toBase18(100), 0] + # [exactAmountIn,minAmountOut,maxPrice,_swapMarketFee] + + assert DT.balanceOf(address0) == 0 + pool.swapExactAmountIn(tokenInOutMarket, amountsInOutMaxFee, txdict(account0)) + assert DT.balanceOf(address0) > 0 + + # swaps some DT back to Ocean swapExactAmountIn + dt_balance_before = DT.balanceOf(address0) + ocean_balance_before = OCEAN.balanceOf(address0) + + DT.approve(pool.address, toBase18(1000), txdict(account0)) + tokenInOutMarket = [DT.address, OCEAN.address, address0] + # [tokenIn,tokenOut,marketFeeAddress] + amountsInOutMaxFee = [toBase18(1), toBase18(1), toBase18(100), 0] + # [exactAmountIn,minAmountOut,maxPrice,_swapMarketFee] + pool.swapExactAmountIn(tokenInOutMarket, amountsInOutMaxFee, txdict(account0)) + + assert DT.balanceOf(address0) < dt_balance_before + assert OCEAN.balanceOf(address0) > ocean_balance_before + + +def test_swapExactAmountOut(): + brownie.chain.reset() + OCEAN = OCEANtoken() + (DT, pool, _) = _deployBPool(do_extra_funding=True) + + tokenInOutMarket = [ + OCEAN.address, + DT.address, + address0, + ] # // [tokenIn,tokenOut,marketFeeAddress] + amountsInOutMaxFee = [toBase18(1000), toBase18(10), toBase18(1000), 0] + + assert DT.balanceOf(address0) == 0 + pool.swapExactAmountOut(tokenInOutMarket, amountsInOutMaxFee, txdict(account0)) + assert DT.balanceOf(address0) > 0 + + +def test_joinPool_addTokens(): + brownie.chain.reset() + OCEAN = OCEANtoken() + (DT, pool, ssbot) = _deployBPool(do_extra_funding=True) + + tokenInOutMarket = [OCEAN.address, DT.address, address0] + # [tokenIn,tokenOut,marketFeeAddress] + amountsInOutMaxFee = [toBase18(100), toBase18(1), toBase18(100), 0] + # [exactAmountIn,minAmountOut,maxPrice,_swapMarketFee] + + assert DT.balanceOf(address0) == 0 + pool.swapExactAmountIn(tokenInOutMarket, amountsInOutMaxFee, txdict(account0)) + + account0_DT_balance = DT.balanceOf(address0) + account0_OCEAN_balance = OCEAN.balanceOf(address0) + account0_BPT_balance = pool.balanceOf(address0) + ssContractDTbalance = DT.balanceOf(ssbot.address) + ssContractBPTbalance = pool.balanceOf(ssbot.address) + + BPTAmountOut = toBase18(0.01) + maxAmountsIn = [toBase18(50), toBase18(50)] + DT.approve(pool.address, toBase18(50), txdict(account0)) + tx = pool.joinPool(BPTAmountOut, maxAmountsIn, txdict(account0)) + + assert tx.events["LOG_JOIN"][0]["tokenIn"] == DT.address + assert tx.events["LOG_JOIN"][1]["tokenIn"] == OCEAN.address + + assert ( + tx.events["LOG_JOIN"][0]["tokenAmountIn"] + DT.balanceOf(address0) + == account0_DT_balance + ) + assert ( + tx.events["LOG_JOIN"][1]["tokenAmountIn"] + OCEAN.balanceOf(address0) + == account0_OCEAN_balance + ) + assert account0_BPT_balance + BPTAmountOut == pool.balanceOf(address0) + + # check ssContract BPT and DT balance didn't change + assert ssContractBPTbalance == pool.balanceOf(ssbot.address) + assert ssContractDTbalance == DT.balanceOf(ssbot.address) + + +def test_joinswapExternAmountIn_addOCEAN(): + brownie.chain.reset() + OCEAN = OCEANtoken() + (DT, pool, ssbot) = _deployBPool(do_extra_funding=True) + + account0_DT_balance = DT.balanceOf(address0) + ssContractDTbalance = DT.balanceOf(ssbot.address) + ssContractBPTbalance = pool.balanceOf(ssbot.address) + + oceanAmountIn = toBase18(100) + minBPTOut = toBase18(0.1) + + tx = pool.joinswapExternAmountIn(oceanAmountIn, minBPTOut, txdict(account0)) + + assert tx.events["LOG_JOIN"][0]["tokenIn"] == OCEAN.address + assert tx.events["LOG_JOIN"][0]["tokenAmountIn"] == oceanAmountIn + + assert tx.events["LOG_JOIN"][1]["tokenIn"] == DT.address + ssbotAmountIn = ssContractDTbalance - DT.balanceOf(ssbot.address) + assert ssbotAmountIn == tx.events["LOG_JOIN"][1]["tokenAmountIn"] + + # we check ssContract actually moved DT and got back BPT + assert ( + DT.balanceOf(ssbot.address) + == ssContractDTbalance - tx.events["LOG_JOIN"][1]["tokenAmountIn"] + ) + assert ( + pool.balanceOf(ssbot.address) + == ssContractBPTbalance + tx.events["LOG_BPT"]["bptAmount"] + ) + + # no datatoken where taken from account0 + assert account0_DT_balance == DT.balanceOf(address0) + + +def test_exitPool_receiveTokens(): + brownie.chain.reset() + OCEAN = OCEANtoken() + (DT, pool, ssbot) = _deployBPool(do_extra_funding=True) + + account0_DT_balance = DT.balanceOf(address0) + account0_OCEAN_balance = OCEAN.balanceOf(address0) + account0_BPT_balance = pool.balanceOf(address0) + ssContractDTbalance = DT.balanceOf(ssbot.address) + ssContractBPTbalance = pool.balanceOf(ssbot.address) + + BPTAmountIn = toBase18(0.5) + minAmountOut = [toBase18(1), toBase18(1)] + tx = pool.exitPool(BPTAmountIn, minAmountOut, txdict(account0)) + + assert tx.events["LOG_EXIT"][0]["tokenOut"] == DT.address + assert tx.events["LOG_EXIT"][1]["tokenOut"] == OCEAN.address + + assert tx.events["LOG_EXIT"][0][ + "tokenAmountOut" + ] + account0_DT_balance == DT.balanceOf(address0) + assert tx.events["LOG_EXIT"][1][ + "tokenAmountOut" + ] + account0_OCEAN_balance == OCEAN.balanceOf(address0) + assert pool.balanceOf(address0) + BPTAmountIn == account0_BPT_balance + + # check the ssContract BPT and DT balance didn"t change + assert ssContractBPTbalance == pool.balanceOf(ssbot.address) + assert ssContractDTbalance == DT.balanceOf(ssbot.address) + + +def test_exitswapPoolAmountIn_receiveOcean(): + brownie.chain.reset() + OCEAN = OCEANtoken() + (DT, pool, ssbot) = _deployBPool(do_extra_funding=True) + + account0_DT_balance = DT.balanceOf(address0) + account0_OCEAN_balance = OCEAN.balanceOf(address0) + account0_BPT_balance = pool.balanceOf(address0) + ssContractDTbalance = DT.balanceOf(ssbot.address) + ssContractBPTbalance = pool.balanceOf(ssbot.address) + + BPTAmountIn = toBase18(0.5) + minOceanOut = toBase18(0.5) + + tx = pool.exitswapPoolAmountIn(BPTAmountIn, minOceanOut, txdict(account0)) + + assert DT.balanceOf(address0) == account0_DT_balance + + # check event argument + assert tx.events["LOG_EXIT"][0]["caller"] == address0 + assert tx.events["LOG_EXIT"][0]["tokenOut"] == OCEAN.address + assert tx.events["LOG_EXIT"][1]["tokenOut"] == DT.address + + # check user ocean balance + assert tx.events["LOG_EXIT"][0][ + "tokenAmountOut" + ] + account0_OCEAN_balance == OCEAN.balanceOf(address0) + + # check BPT balance + assert account0_BPT_balance == pool.balanceOf(address0) + BPTAmountIn + + # check the ssContract BPT balance + assert ssContractBPTbalance == pool.balanceOf(ssbot.address) + BPTAmountIn + + # ssContract got back his dt when redeeeming BPT + assert ssContractDTbalance + tx.events["LOG_EXIT"][1][ + "tokenAmountOut" + ] == DT.balanceOf(ssbot.address) + + +def _deployBPool( + OCEAN_base_funding=10000, + do_extra_funding: bool = True, + OCEAN_extra_funding=10000, + OCEAN_init_liquidity=2000, + DT_OCEAN_rate=0.1, + DT_cap=10000, + DT_vest_amt=1000, + DT_vest_num_blocks=600, +): # pylint: disable=too-many-arguments + + fundOCEANFromAbove(address0, toBase18(OCEAN_base_funding)) + + router = oceanv4util.deployRouter(account0) + + (data_NFT, erc721_factory) = oceanv4util.createDataNFT( + "dataNFT", "DATANFTSYMBOL", account0, router + ) + + DT = oceanv4util.createDatatokenFromDataNFT( + "DT", "DTSYMBOL", DT_cap, data_NFT, account0 + ) + + pool = oceanv4util.createBPoolFromDatatoken( + DT, + erc721_factory, + account0, + OCEAN_init_liquidity, + DT_OCEAN_rate, + DT_vest_amt, + DT_vest_num_blocks, + ) + + ssbot_address = pool.getController() + ssbot = BROWNIE_PROJECT080.SideStaking.at(ssbot_address) + + if do_extra_funding: + fundOCEANFromAbove(address0, toBase18(OCEAN_base_funding)) + OCEAN = OCEANtoken() + OCEAN.approve(pool.address, toBase18(OCEAN_extra_funding), txdict(account0)) + + return (DT, pool, ssbot) diff --git a/sol080/contracts/oceanv4/test/test_swap_fees.py b/sol080/contracts/oceanv4/test/test_swap_fees.py new file mode 100644 index 0000000000000000000000000000000000000000..ac90204a60a3cb2ca8420b1a95d0c49bbb43e921 --- /dev/null +++ b/sol080/contracts/oceanv4/test/test_swap_fees.py @@ -0,0 +1,84 @@ +import brownie + +from sol080.contracts.oceanv4 import oceanv4util +from util.base18 import toBase18 +from util.constants import BROWNIE_PROJECT080 +from util.globaltokens import fundOCEANFromAbove +from util.tx import txdict + +accounts = brownie.network.accounts + +account0 = accounts[0] +address0 = account0.address + +account1 = accounts[1] +address1 = account1.address + + +def test_exactAmountIn_fee(): + pool = _deployBPool() + OCEAN = oceanv4util.OCEANtoken() + fundOCEANFromAbove(address0, toBase18(10000)) + OCEAN.approve(pool.address, toBase18(10000), txdict(account0)) + datatoken = BROWNIE_PROJECT080.ERC20Template.at(pool.getDatatokenAddress()) + + assert datatoken.balanceOf(address0) == 0 + account0_DT_balance = datatoken.balanceOf(address0) + account0_Ocean_balance = OCEAN.balanceOf(address0) + + tokenInOutMarket = [OCEAN.address, datatoken.address, address1] + # [tokenIn,tokenOut,marketFeeAddress] + amountsInOutMaxFee = [toBase18(100), toBase18(1), toBase18(100), toBase18(0.001)] + # [exactAmountIn,minAmountOut,maxPrice,_swapMarketFee=0.1%] + + tx = pool.swapExactAmountIn(tokenInOutMarket, amountsInOutMaxFee, txdict(account0)) + assert datatoken.balanceOf(address0) > 0 + + assert tx.events["SWAP_FEES"][0]["marketFeeAmount"] == toBase18( + 0.01 * 100 + ) # 0.01: mkt_swap_fee in oceanv4util.create_BPool_from_datatoken, 100: exactAmountIn + assert tx.events["SWAP_FEES"][0]["tokenFeeAddress"] == OCEAN.address + + # account1 received fee + assert OCEAN.balanceOf(address1) == toBase18(0.001 * 100) + + # account0 ocean balance decreased + assert ( + OCEAN.balanceOf(address0) + tx.events["LOG_SWAP"][0]["tokenAmountIn"] + ) == account0_Ocean_balance + + # account0 DT balance increased + assert account0_DT_balance + tx.events["LOG_SWAP"][0][ + "tokenAmountOut" + ] == datatoken.balanceOf(address0) + + +def _deployBPool(): + brownie.chain.reset() + router = oceanv4util.deployRouter(account0) + fundOCEANFromAbove(address0, toBase18(100000)) + + (dataNFT, erc721_factory) = oceanv4util.createDataNFT( + "dataNFT", "DATANFTSYMBOL", account0, router + ) + + DT_cap = 10000 + datatoken = oceanv4util.createDatatokenFromDataNFT( + "DT", "DTSYMBOL", DT_cap, dataNFT, account0 + ) + + OCEAN_init_liquidity = 80000 + DT_OCEAN_rate = 0.1 + DT_vest_amt = 1000 + DT_vest_num_blocks = 600 + pool = oceanv4util.createBPoolFromDatatoken( + datatoken, + erc721_factory, + account0, + OCEAN_init_liquidity, + DT_OCEAN_rate, + DT_vest_amt, + DT_vest_num_blocks, + ) + + return pool diff --git a/sol080/contracts/oceanv4/test/test_vesting.py b/sol080/contracts/oceanv4/test/test_vesting.py new file mode 100644 index 0000000000000000000000000000000000000000..ccf8fb225bd35003682016fdd374534af21eb635 --- /dev/null +++ b/sol080/contracts/oceanv4/test/test_vesting.py @@ -0,0 +1,53 @@ +import brownie + +from sol080.contracts.oceanv4 import oceanv4util +from util.base18 import toBase18 +from util.constants import BROWNIE_PROJECT080 +from util.globaltokens import fundOCEANFromAbove + +accounts = brownie.network.accounts + +account0 = accounts[0] +address0 = account0.address + +account1 = accounts[1] +address1 = account1.address + + +def test_vesting_amount(): + pool = _deployBPool() + datatoken = BROWNIE_PROJECT080.ERC20Template.at(pool.getDatatokenAddress()) + sideStakingAddress = pool.getController() + sideStaking = BROWNIE_PROJECT080.SideStaking.at(sideStakingAddress) + assert sideStaking.getvestingAmount(datatoken.address) == toBase18(1000) + + +def _deployBPool(): + brownie.chain.reset() + router = oceanv4util.deployRouter(account0) + fundOCEANFromAbove(address0, toBase18(10000)) + + (dataNFT, erc721_factory) = oceanv4util.createDataNFT( + "dataNFT", "DATANFTSYMBOL", account0, router + ) + + DT_cap = 10000 + datatoken = oceanv4util.createDatatokenFromDataNFT( + "DT", "DTSYMBOL", DT_cap, dataNFT, account0 + ) + + OCEAN_init_liquidity = 2000 + DT_OCEAN_rate = 0.1 + DT_vest_amt = 1000 + DT_vest_num_blocks = 600 + pool = oceanv4util.createBPoolFromDatatoken( + datatoken, + erc721_factory, + account0, + OCEAN_init_liquidity, + DT_OCEAN_rate, + DT_vest_amt, + DT_vest_num_blocks, + ) + + return pool diff --git a/sol080/contracts/oceanv4/utils/Deployer.sol b/sol080/contracts/oceanv4/utils/Deployer.sol new file mode 100644 index 0000000000000000000000000000000000000000..af2661fd8ab822be88f21aee0d1d03a044f6983b --- /dev/null +++ b/sol080/contracts/oceanv4/utils/Deployer.sol @@ -0,0 +1,48 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +/** + * @title Deployer Contract + * @author Ocean Protocol Team + * + * @dev Contract Deployer + * This contract allowes factory contract + * to deploy new contract instances using + * the same library pattern in solidity. + * the logic it self is deployed only once, but + * executed in the context of the new storage + * contract (new contract instance) + */ +contract Deployer { + event InstanceDeployed(address instance); + + // /** + // * @dev deploy + // * deploy new contract instance + // * @param _logic the logic contract address + // * @return address of the new instance + // */ + function deploy( + address _logic + ) + internal + returns (address instance) + { + bytes20 targetBytes = bytes20(_logic); + // solhint-disable-next-line max-line-length + // Follows OpenZeppelin Implementation https://github.com/OpenZeppelin/openzeppelin-sdk/blob/71c9ad77e0326db079e6a643eca8568ab316d4a9/packages/lib/contracts/upgradeability/ProxyFactory.sol + // solhint-disable-next-line max-line-length + // Adapted from https://github.com/optionality/clone-factory/blob/32782f82dfc5a00d103a7e61a17a5dedbd1e8e9d/contracts/CloneFactory.sol + /* solium-disable-next-line security/no-inline-assembly */ + assembly { + let clone := mload(0x40) + mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(clone, 0x14), targetBytes) + mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + instance := create(0, clone, 0x37) + } + emit InstanceDeployed(address(instance)); + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/ERC20Roles.sol b/sol080/contracts/oceanv4/utils/ERC20Roles.sol new file mode 100644 index 0000000000000000000000000000000000000000..27f4d21c7e5ea32dfd03d7a6ac2cfa8f053657e8 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC20Roles.sol @@ -0,0 +1,99 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +contract ERC20Roles { + + + mapping(address => RolesERC20) public permissions; + + address[] public authERC20; + + struct RolesERC20 { + bool minter; + bool paymentManager; + } + + event AddedMinter( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + event RemovedMinter( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + + function getPermissions(address user) public view returns (RolesERC20 memory) { + return permissions[user]; + } + + function _addMinter(address _minter) internal { + RolesERC20 storage user = permissions[_minter]; + require(user.minter == false, "ERC20Roles: ALREADY A MINTER"); + user.minter = true; + authERC20.push(_minter); + emit AddedMinter(_minter,msg.sender,block.timestamp,block.number); + } + + function _removeMinter(address _minter) internal { + RolesERC20 storage user = permissions[_minter]; + user.minter = false; + emit RemovedMinter(_minter,msg.sender,block.timestamp,block.number); + } + + event AddedPaymentManager( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + event RemovedPaymentManager( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + function _addPaymentManager(address _paymentCollector) internal { + RolesERC20 storage user = permissions[_paymentCollector]; + require(user.paymentManager == false, "ERC20Roles: ALREADY A FEE MANAGER"); + user.paymentManager = true; + authERC20.push(_paymentCollector); + emit AddedPaymentManager(_paymentCollector,msg.sender,block.timestamp,block.number); + } + + function _removePaymentManager(address _paymentCollector) internal { + RolesERC20 storage user = permissions[_paymentCollector]; + user.paymentManager = false; + emit RemovedPaymentManager(_paymentCollector,msg.sender,block.timestamp,block.number); + } + + + + + + event CleanedPermissions( + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + + + function _cleanPermissions() internal { + + for (uint256 i = 0; i < authERC20.length; i++) { + RolesERC20 storage user = permissions[authERC20[i]]; + user.minter = false; + user.paymentManager = false; + + } + + delete authERC20; + emit CleanedPermissions(msg.sender,block.timestamp,block.number); + + } +} diff --git a/sol080/contracts/oceanv4/utils/ERC721/Address.sol b/sol080/contracts/oceanv4/utils/ERC721/Address.sol new file mode 100644 index 0000000000000000000000000000000000000000..91c9cd80ead620c677ba1b180683a276834d09c4 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/Address.sol @@ -0,0 +1,195 @@ + +pragma solidity 0.8.10; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { size := extcodesize(account) } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{ value: amount }(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain`call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{ value: value }(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.staticcall(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + require(isContract(target), "Address: delegate call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.delegatecall(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } + + +} + + +// File OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/Context.sol@v4.0.0 + diff --git a/sol080/contracts/oceanv4/utils/ERC721/Context.sol b/sol080/contracts/oceanv4/utils/ERC721/Context.sol new file mode 100644 index 0000000000000000000000000000000000000000..9fe04868688a09107a95cfb6781bbf3a34ec5196 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/Context.sol @@ -0,0 +1,23 @@ + +pragma solidity 0.8.10; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +/* + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/ERC721/ERC721.sol b/sol080/contracts/oceanv4/utils/ERC721/ERC721.sol new file mode 100644 index 0000000000000000000000000000000000000000..e5a21db83b34d4cf2ff8c81705c7fc6a8ed235b2 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/ERC721.sol @@ -0,0 +1,389 @@ +pragma solidity 0.8.10; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/Context.sol"; +import "./IERC721.sol"; +import "./IERC721Metadata.sol"; +import "./Address.sol"; +import "./Strings.sol"; +import "./IERC721Receiver.sol"; +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension, but not including the Enumerable extension, which is available separately as + * {ERC721Enumerable}. + */ +contract ERC721 is Context, IERC721, IERC721Metadata { + using Address for address; + using Strings for uint256; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // baseURI + string internal defaultBaseURI; + + // Mapping from token ID to owner address + mapping (uint256 => address) private _owners; + + // Mapping owner address to token count + mapping (address => uint256) private _balances; + + // Mapping from token ID to approved address + mapping (uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping (address => mapping (address => bool)) private _operatorApprovals; + + // Optional mapping for token URIs + mapping (uint256 => string) private _tokenURIs; + + /** + * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + constructor (string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view virtual override returns (uint256) { + require(owner != address(0), "ERC721: balance query for the zero address"); + return _balances[owner]; + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + address owner = _owners[tokenId]; + require(owner != address(0), "ERC721: owner query for nonexistent token"); + return owner; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { + require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); + _tokenURIs[tokenId] = _tokenURI; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); + string memory _tokenURI = _tokenURIs[tokenId]; + string memory base = _baseURI(); + + // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + if (bytes(_tokenURI).length > 0) { + return string(abi.encodePacked(base, _tokenURI)); + } + else{ + return _tokenURI; + } + } + + /** + * @dev Base URI for computing {tokenURI}. Empty by default, can be overriden + * in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return defaultBaseURI; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ERC721.ownerOf(tokenId); + require(to != owner, "ERC721: approval to current owner"); + + require(_msgSender() == owner || ERC721.isApprovedForAll(owner, _msgSender()), + "ERC721: approve caller is not owner nor approved for all" + ); + + _approve(to, tokenId); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view virtual override returns (address) { + require(_exists(tokenId), "ERC721: approved query for nonexistent token"); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + require(operator != _msgSender(), "ERC721: approve to caller"); + + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function _transferFrom(address from, address to, uint256 tokenId) internal virtual { + //solhint-disable-next-line max-line-length + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); + + safeTransferFrom(from, to, tokenId, ""); + } + + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function _safeTransferFrom(address from, address to, uint256 tokenId) internal virtual { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) internal virtual { + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); + _safeTransfer(from, to, tokenId, _data); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * `_data` is additional data, it has no specified format and it is sent in call to `to`. + * + * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. + * implement alternative mechanisms to perform token transfer, such as signature-based. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual { + _transfer(from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + * and stop existing when they are burned (`_burn`). + */ + function _exists(uint256 tokenId) internal view virtual returns (bool) { + return _owners[tokenId] != address(0); + } + + /** + * @dev Returns whether `spender` is allowed to manage `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { + require(_exists(tokenId), "ERC721: operator query for nonexistent token"); + address owner = ERC721.ownerOf(tokenId); + return (spender == owner || getApproved(tokenId) == spender || ERC721.isApprovedForAll(owner, spender)); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual { + _mint(to, tokenId); + require(_checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 tokenId) internal virtual { + require(to != address(0), "ERC721: mint to the zero address"); + require(!_exists(tokenId), "ERC721: token already minted"); + + _beforeTokenTransfer(address(0), to, tokenId); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal virtual { + address owner = ERC721.ownerOf(tokenId); + + _beforeTokenTransfer(owner, address(0), tokenId); + + // Clear approvals + _approve(address(0), tokenId); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) internal virtual { + require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own"); + require(to != address(0), "ERC721: transfer to the zero address"); + + _beforeTokenTransfer(from, to, tokenId); + + // Clear approvals from the previous owner + _approve(address(0), tokenId); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + function _approve(address to, uint256 tokenId) internal virtual { + _tokenApprovals[tokenId] = to; + emit Approval(ERC721.ownerOf(tokenId), to, tokenId); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. + * The call is not executed if the target address is not a contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) + private returns (bool) + { + if (to.isContract()) { + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC721: transfer to non ERC721Receiver implementer"); + } else { + // solhint-disable-next-line no-inline-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @dev Hook that is called before any token transfer. This includes minting + * and burning. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual { } +} + + diff --git a/sol080/contracts/oceanv4/utils/ERC721/IERC721.sol b/sol080/contracts/oceanv4/utils/ERC721/IERC721.sol new file mode 100644 index 0000000000000000000000000000000000000000..f8d466ad8fa24ad985d24dd4691a188cea04bd59 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/IERC721.sol @@ -0,0 +1,130 @@ +// File OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC721/IERC721.sol@v4.0.0 + +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + //function safeTransferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + //function transferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + //function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; +} + + diff --git a/sol080/contracts/oceanv4/utils/ERC721/IERC721Enumerable.sol b/sol080/contracts/oceanv4/utils/ERC721/IERC721Enumerable.sol new file mode 100644 index 0000000000000000000000000000000000000000..17b09b653728f659c2487ad47c9545a49d467520 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/IERC721Enumerable.sol @@ -0,0 +1,26 @@ +pragma solidity 0.8.10; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +import "./IERC721.sol"; +/** + * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721Enumerable is IERC721 { + + /** + * @dev Returns the total amount of tokens stored by the contract. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns a token ID owned by `owner` at a given `index` of its token list. + * Use along with {balanceOf} to enumerate all of ``owner``'s tokens. + */ + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId); + + /** + * @dev Returns a token ID at a given `index` of all the tokens stored by the contract. + * Use along with {totalSupply} to enumerate all tokens. + */ + function tokenByIndex(uint256 index) external view returns (uint256); +} diff --git a/sol080/contracts/oceanv4/utils/ERC721/IERC721Metadata.sol b/sol080/contracts/oceanv4/utils/ERC721/IERC721Metadata.sol new file mode 100644 index 0000000000000000000000000000000000000000..75fe90988d1691b5abd039260a5cb5eb234e262d --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/IERC721Metadata.sol @@ -0,0 +1,24 @@ +pragma solidity 0.8.10; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +import "./IERC721.sol"; +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721Metadata is IERC721 { + + /** + * @dev Returns the token collection name. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the token collection symbol. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/sol080/contracts/oceanv4/utils/ERC721/IERC721Receiver.sol b/sol080/contracts/oceanv4/utils/ERC721/IERC721Receiver.sol new file mode 100644 index 0000000000000000000000000000000000000000..f94f92326bd9455fbc955cdebfa4fda4ef3b6d3e --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/IERC721Receiver.sol @@ -0,0 +1,19 @@ +pragma solidity 0.8.10; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721Receiver { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + * + * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. + */ + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/ERC721/Strings.sol b/sol080/contracts/oceanv4/utils/ERC721/Strings.sol new file mode 100644 index 0000000000000000000000000000000000000000..dffffa60718330bbd7ebf1a3594e8c18b7d8eaa3 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721/Strings.sol @@ -0,0 +1,83 @@ + +pragma solidity 0.8.10; +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant alphabet = "0123456789abcdef"; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = alphabet[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + +} + + +// File OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC721/ERC721.sol@v4.0.0 + + + + + + + + + + + + + diff --git a/sol080/contracts/oceanv4/utils/ERC721RolesAddress.sol b/sol080/contracts/oceanv4/utils/ERC721RolesAddress.sol new file mode 100644 index 0000000000000000000000000000000000000000..4b0c1357b249d44954cf744955af9343a1cc2f76 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC721RolesAddress.sol @@ -0,0 +1,193 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +contract ERC721RolesAddress { + mapping(address => Roles) internal permissions; + + address[] public auth; + + struct Roles { + bool manager; + bool deployERC20; + bool updateMetadata; + bool store; + } + + + + function getPermissions(address user) public view returns (Roles memory) { + return permissions[user]; + } + + modifier onlyManager() { + require( + permissions[msg.sender].manager == true, + "ERC721RolesAddress: NOT MANAGER" + ); + _; + } + + event AddedTo725StoreList( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + event RemovedFrom725StoreList( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + + function addTo725StoreList(address _allowedAddress) public onlyManager { + Roles storage user = permissions[_allowedAddress]; + user.store = true; + auth.push(_allowedAddress); + emit AddedTo725StoreList(_allowedAddress,msg.sender,block.timestamp,block.number); + } + + function removeFrom725StoreList(address _allowedAddress) public { + if(permissions[msg.sender].manager == true || + (msg.sender == _allowedAddress && permissions[msg.sender].store == true) + ){ + Roles storage user = permissions[_allowedAddress]; + user.store = false; + emit RemovedFrom725StoreList(_allowedAddress,msg.sender,block.timestamp,block.number); + } + else{ + revert("ERC721RolesAddress: Not enough permissions to remove from 725StoreList"); + } + + } + + + event AddedToCreateERC20List( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + event RemovedFromCreateERC20List( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + function addToCreateERC20List(address _allowedAddress) public onlyManager { + Roles storage user = permissions[_allowedAddress]; + user.deployERC20 = true; + auth.push(_allowedAddress); + emit AddedToCreateERC20List(_allowedAddress,msg.sender,block.timestamp,block.number); + } + + //it's only called internally, so is without checking onlyManager + function _addToCreateERC20List(address _allowedAddress) internal { + Roles storage user = permissions[_allowedAddress]; + user.deployERC20 = true; + auth.push(_allowedAddress); + emit AddedToCreateERC20List(_allowedAddress,msg.sender,block.timestamp,block.number); + } + + function removeFromCreateERC20List(address _allowedAddress) + public + { + if(permissions[msg.sender].manager == true || + (msg.sender == _allowedAddress && permissions[msg.sender].deployERC20 == true) + ){ + Roles storage user = permissions[_allowedAddress]; + user.deployERC20 = false; + emit RemovedFromCreateERC20List(_allowedAddress,msg.sender,block.timestamp,block.number); + } + else{ + revert("ERC721RolesAddress: Not enough permissions to remove from ERC20List"); + } + } + + event AddedToMetadataList( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + event RemovedFromMetadataList( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + function addToMetadataList(address _allowedAddress) public onlyManager { + Roles storage user = permissions[_allowedAddress]; + user.updateMetadata = true; + auth.push(_allowedAddress); + emit AddedToMetadataList(_allowedAddress,msg.sender,block.timestamp,block.number); + } + //it's only called internally, so is without checking onlyManager + function _addToMetadataList(address _allowedAddress) internal { + Roles storage user = permissions[_allowedAddress]; + user.updateMetadata = true; + auth.push(_allowedAddress); + emit AddedToMetadataList(_allowedAddress,msg.sender,block.timestamp,block.number); + } + + function removeFromMetadataList(address _allowedAddress) + public + { + if(permissions[msg.sender].manager == true || + (msg.sender == _allowedAddress && permissions[msg.sender].updateMetadata == true) + ){ + Roles storage user = permissions[_allowedAddress]; + user.updateMetadata = false; + emit RemovedFromMetadataList(_allowedAddress,msg.sender,block.timestamp,block.number); + } + else{ + revert("ERC721RolesAddress: Not enough permissions to remove from metadata list"); + } + } + + event AddedManager( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + event RemovedManager( + address indexed user, + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + function _addManager(address _managerAddress) internal { + Roles storage user = permissions[_managerAddress]; + user.manager = true; + auth.push(_managerAddress); + emit AddedManager(_managerAddress,msg.sender,block.timestamp,block.number); + } + + function _removeManager(address _managerAddress) internal { + Roles storage user = permissions[_managerAddress]; + user.manager = false; + emit RemovedManager(_managerAddress,msg.sender,block.timestamp,block.number); + } + + + event CleanedPermissions( + address indexed signer, + uint256 timestamp, + uint256 blockNumber + ); + function _cleanPermissions() internal { + for (uint256 i = 0; i < auth.length; i++) { + Roles storage user = permissions[auth[i]]; + user.manager = false; + user.deployERC20 = false; + user.updateMetadata = false; + user.store = false; + } + + delete auth; + emit CleanedPermissions(msg.sender,block.timestamp,block.number); + } +} diff --git a/sol080/contracts/oceanv4/utils/ERC725/ERC725Ocean.sol b/sol080/contracts/oceanv4/utils/ERC725/ERC725Ocean.sol new file mode 100644 index 0000000000000000000000000000000000000000..9adcf6a3c82efebb50baeef6f1f4f28f8e942d79 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/ERC725/ERC725Ocean.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.10; +// interfaces +import "../../interfaces/IERC725X.sol"; +import "../../interfaces/IERC725Y.sol"; +// modules +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/introspection/ERC165Storage.sol"; + +// libraries +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/Create2.sol"; +import "GNSPS/solidity-bytes-utils@0.8.0/contracts/BytesLib.sol"; + + +/** + * @title ERC725 X / ERC725 Y executor + * @dev Implementation of a contract module which provides the ability to call arbitrary functions at any other smart contract and itself, + * including using `delegatecall`, as well creating contracts using `create` and `create2`. + * This is the basis for a smart contract based account system, but could also be used as a proxy account system. + * + * `execute` MUST only be called by the owner of the contract set via ERC173. + * + * @author Fabian Vogelsteller <fabian@lukso.network> + */ +contract ERC725Ocean is ERC165Storage, IERC725X, IERC725Y { + + bytes4 internal constant _INTERFACE_ID_ERC725X = 0x44c028fe; + + uint256 constant OPERATION_CALL = 0; + uint256 constant OPERATION_DELEGATECALL = 1; + uint256 constant OPERATION_CREATE2 = 2; + uint256 constant OPERATION_CREATE = 3; + + bytes4 internal constant _INTERFACE_ID_ERC725Y = 0x2bd57b73; + + mapping(bytes32 => bytes) internal store; + + + constructor() { + + + _registerInterface(_INTERFACE_ID_ERC725X); + _registerInterface(_INTERFACE_ID_ERC725Y); + } + + /* Public functions */ + + /** + * @notice Executes any other smart contract. Is only callable by the owner. + * + * + * @param _operation the operation to execute: CALL = 0; DELEGATECALL = 1; CREATE2 = 2; CREATE = 3; + * @param _to the smart contract or address to interact with. `_to` will be unused if a contract is created (operation 2 and 3) + * @param _value the value of ETH to transfer + * @param _data the call data, or the contract data to deploy + */ + function execute(uint256 _operation, address _to, uint256 _value, bytes calldata _data) + internal + { + // emit event + emit Executed(_operation, _to, _value, _data); + + uint256 txGas = gasleft() - 2500; + + // CALL + if (_operation == OPERATION_CALL) { + executeCall(_to, _value, _data, txGas); + + // DELEGATE CALL + // TODO: risky as storage slots can be overridden, remove? +// } else if (_operation == OPERATION_DELEGATECALL) { +// address currentOwner = owner(); +// executeDelegateCall(_to, _data, txGas); +// // Check that the owner was not overridden +// require(owner() == currentOwner, "Delegate call is not allowed to modify the owner!"); + + // CREATE + } else if (_operation == OPERATION_CREATE) { + performCreate(_value, _data); + + // CREATE2 + } else if (_operation == OPERATION_CREATE2) { + bytes32 salt = BytesLib.toBytes32(_data, _data.length - 32); + bytes memory data = BytesLib.slice(_data, 0, _data.length - 32); + + address contractAddress = Create2.deploy(_value, salt, data); + + emit ContractCreated(contractAddress); + + } else { + revert("Wrong operation type"); + } + } + + /* Internal functions */ + + // Taken from GnosisSafe + // https://github.com/gnosis/safe-contracts/blob/development/contracts/base/Executor.sol + function executeCall(address to, uint256 value, bytes memory data, uint256 txGas) + internal + returns (bool success) + { + // solium-disable-next-line security/no-inline-assembly + assembly { + success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) + } + } + + // Taken from GnosisSafe + // https://github.com/gnosis/safe-contracts/blob/development/contracts/base/Executor.sol + function executeDelegateCall(address to, bytes memory data, uint256 txGas) + internal + returns (bool success) + { + // solium-disable-next-line security/no-inline-assembly + assembly { + success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) + } + } + + // Taken from GnosisSafe + // https://github.com/gnosis/safe-contracts/blob/development/contracts/libraries/CreateCall.sol + function performCreate(uint256 value, bytes memory deploymentData) + internal + returns (address newContract) + { + // solium-disable-next-line security/no-inline-assembly + assembly { + newContract := create(value, add(deploymentData, 0x20), mload(deploymentData)) + } + require(newContract != address(0), "Could not deploy contract"); + emit ContractCreated(newContract); + } + + + /* Public functions */ + + /** + * @notice Gets data at a given `key` + * @param _key the key which value to retrieve + * @return _value The data stored at the key + */ + function getData(bytes32 _key) + public + view + override + virtual + returns (bytes memory _value) + { + return store[_key]; + } + + /** + * @notice Sets data at a given `key` + * @param _key the key which value to retrieve + * @param _value the bytes to set. + */ + function setData(bytes32 _key, bytes calldata _value) + internal + virtual + { + store[_key] = _value; + emit DataChanged(_key, _value); + } + + +} diff --git a/sol080/contracts/oceanv4/utils/Ownable.sol b/sol080/contracts/oceanv4/utils/Ownable.sol new file mode 100644 index 0000000000000000000000000000000000000000..99ef75be9216108262f813061e530391e08e7098 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/Ownable.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import "./ERC721/Context.sol"; +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor () { + address msgSender = _msgSender(); + _owner = msgSender; + emit OwnershipTransferred(address(0), msgSender); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + emit OwnershipTransferred(_owner, address(0)); + _owner = address(0); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + emit OwnershipTransferred(_owner, newOwner); + _owner = newOwner; + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/SafeERC20.sol b/sol080/contracts/oceanv4/utils/SafeERC20.sol new file mode 100644 index 0000000000000000000000000000000000000000..a34b9492cc280d3fdac40b0ca50130eff47151a6 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/SafeERC20.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../interfaces/IERC20.sol"; +import "./ERC721/Address.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using Address for address; + + function safeTransfer( + IERC20 token, + address to, + uint256 value + ) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value + ) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove( + IERC20 token, + address spender, + uint256 value + ) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + + function safeIncreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender) + value; + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + + function safeDecreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + unchecked { + uint256 oldAllowance = token.allowance(address(this), spender); + require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); + uint256 newAllowance = oldAllowance - value; + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); + if (returndata.length > 0) { + // Return data is optional + require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + } +} diff --git a/sol080/contracts/oceanv4/utils/UtilsLib.sol b/sol080/contracts/oceanv4/utils/UtilsLib.sol new file mode 100644 index 0000000000000000000000000000000000000000..390d07b8aeea09fb603891bb3589638109d32377 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/UtilsLib.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + * @title Solidity Utils + * @author Fabian Vogelsteller <fabian@lukso.network> + * + * @dev Utils functions + */ + +pragma solidity 0.8.10; + + +library UtilsLib { + + /** + * @dev Internal function to determine if an address is a contract + * @param _target The address being queried + * + * @return result Returns TRUE if `_target` is a contract + */ + function isContract(address _target) internal view returns(bool result) { + // solium-disable-next-line security/no-inline-assembly + assembly { + result := gt(extcodesize(_target), 0) + } + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/mock/MockERC20.sol b/sol080/contracts/oceanv4/utils/mock/MockERC20.sol new file mode 100644 index 0000000000000000000000000000000000000000..b10af365c76ab84260a99c3430e517d247c6f044 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/mock/MockERC20.sol @@ -0,0 +1,16 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + + +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20{ + + + constructor(address owner, string memory name, string memory symbol) ERC20(name,symbol) { + _mint(owner, 1e23); + } + +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/mock/MockERC20Decimals.sol b/sol080/contracts/oceanv4/utils/mock/MockERC20Decimals.sol new file mode 100644 index 0000000000000000000000000000000000000000..bd9e5ef77c7d8fcb60c99f39cd83ebf334766218 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/mock/MockERC20Decimals.sol @@ -0,0 +1,359 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "../../interfaces/IERC20.sol"; + +import "../ERC721/Context.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract MockERC20Decimals is Context, IERC20 { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * The default value of {decimals} is 18. To select a different value for + * {decimals} you should overload it. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_, uint8 decimals_) { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + _mint(msg.sender,10e22); + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless this function is + * overridden; + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public override view virtual returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * Requirements: + * + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + + uint256 currentAllowance = _allowances[sender][_msgSender()]; + require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance"); + unchecked { + _approve(sender, _msgSender(), currentAllowance - amount); + } + + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + uint256 currentAllowance = _allowances[_msgSender()][spender]; + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(_msgSender(), spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `sender` to `recipient`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(sender, recipient, amount); + + uint256 senderBalance = _balances[sender]; + require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[sender] = senderBalance - amount; + } + _balances[recipient] += amount; + + emit Transfer(sender, recipient, amount); + + _afterTokenTransfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + _balances[account] += amount; + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + } + _totalSupply -= amount; + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/mock/MockExchange.sol b/sol080/contracts/oceanv4/utils/mock/MockExchange.sol new file mode 100644 index 0000000000000000000000000000000000000000..81dc505c8ea5050cb14e58536c92d033e1dd5c3c --- /dev/null +++ b/sol080/contracts/oceanv4/utils/mock/MockExchange.sol @@ -0,0 +1,36 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import "../../interfaces/IERC20Template.sol"; + + +contract MockExchange { + + + + function depositWithPermit( + address _token, + uint256 _amount, + uint256 _deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + IERC20Template(_token).permit( + msg.sender, + address(this), + _amount, + _deadline, + v, + r, + s + ); + IERC20Template(_token).transferFrom(msg.sender, address(this), _amount); + } + + function deposit(address _token, uint256 _amount) external { + IERC20Template(_token).transferFrom(msg.sender, address(this), _amount); + } +} diff --git a/sol080/contracts/oceanv4/utils/mock/MockOcean.sol b/sol080/contracts/oceanv4/utils/mock/MockOcean.sol new file mode 100644 index 0000000000000000000000000000000000000000..621f14b83057e30a1b54f3000b2cc051a7989f16 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/mock/MockOcean.sol @@ -0,0 +1,16 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + + +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/ERC20.sol"; + +contract MockOcean is ERC20("Ocean","Ocean"){ + + + constructor(address owner) { + _mint(owner, 1e23); + } + +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/utils/mock/MockOldDT.sol b/sol080/contracts/oceanv4/utils/mock/MockOldDT.sol new file mode 100644 index 0000000000000000000000000000000000000000..93a37e9735a8a52fed49bd0dc4abd5b205ad4844 --- /dev/null +++ b/sol080/contracts/oceanv4/utils/mock/MockOldDT.sol @@ -0,0 +1,339 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +//import '../interfaces/IERC20Template.sol'; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/ERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; + +/** +* @title DatatokenTemplate +* +* @dev DatatokenTemplate is an ERC20 compliant token template +* Used by the factory contract as a bytecode reference to +* deploy new Datatokens. +*/ +contract MockOldDT is ERC20('Test','TESTSYMBOL') { + using SafeMath for uint256; + + string private _name = 'MOCKV3DT'; + string private _symbol = 'V3DT'; + string private _blob = 'blob'; + uint256 private _cap = 1e21; + uint8 private constant _decimals = 18; + address private _communityFeeCollector = address(0); + bool private initialized = false; + address private _minter; + address private _proposedMinter; + uint256 public constant BASE = 10**18; + uint256 public constant BASE_COMMUNITY_FEE_PERCENTAGE = BASE / 1000; + uint256 public constant BASE_MARKET_FEE_PERCENTAGE = BASE / 1000; + + event OrderStarted( + address indexed consumer, + address indexed payer, + uint256 amount, + uint256 serviceId, + uint256 timestamp, + address indexed mrktFeeCollector, + uint256 marketFee + ); + + event OrderFinished( + bytes32 orderTxId, + address indexed consumer, + uint256 amount, + uint256 serviceId, + address indexed provider, + uint256 timestamp + ); + + event MinterProposed( + address currentMinter, + address newMinter + ); + + event MinterApproved( + address currentMinter, + address newMinter + ); + + modifier onlyNotInitialized() { + require( + !initialized, + 'DatatokenTemplate: token instance already initialized' + ); + _; + } + + modifier onlyMinter() { + require( + msg.sender == _minter, + 'DatatokenTemplate: invalid minter' + ); + _; + } + + constructor() + + { + _initialize( + _name, + _symbol, + msg.sender, + _cap, + _blob, + msg.sender + ); + } + + + function initialize( + string calldata name, + string calldata symbol, + address minterAddress, + uint256 cap_, + string calldata blob_, + address feeCollector + ) + external + onlyNotInitialized + returns(bool) + { + return _initialize( + name, + symbol, + minterAddress, + cap_, + blob_, + feeCollector + ); + } + + + function _initialize( + string memory name, + string memory symbol, + address minterAddress, + uint256 cap_, + string memory blob_, + address feeCollector + ) + private + returns(bool) + { + require( + minterAddress != address(0), + 'DatatokenTemplate: Invalid minter, zero address' + ); + + require( + _minter == address(0), + 'DatatokenTemplate: Invalid minter, zero address' + ); + + require( + feeCollector != address(0), + 'DatatokenTemplate: Invalid community fee collector, zero address' + ); + + require( + cap_ != 0, + 'DatatokenTemplate: Invalid cap value' + ); + _cap = cap_; + _name = name; + _blob = blob_; + _symbol = symbol; + _minter = minterAddress; + _communityFeeCollector = feeCollector; + initialized = true; + return initialized; + } + + + function mint( + address account, + uint256 value + ) + external + onlyMinter + { + require( + totalSupply().add(value) <= _cap, + 'DatatokenTemplate: cap exceeded' + ); + _mint(account, value); + } + + function startOrder( + address consumer, + uint256 amount, + uint256 serviceId, + address mrktFeeCollector + ) + external + { + uint256 marketFee = 0; + uint256 communityFee = calculateFee( + amount, + BASE_COMMUNITY_FEE_PERCENTAGE + ); + transfer(_communityFeeCollector, communityFee); + if(mrktFeeCollector != address(0)){ + marketFee = calculateFee( + amount, + BASE_MARKET_FEE_PERCENTAGE + ); + transfer(mrktFeeCollector, marketFee); + } + uint256 totalFee = communityFee.add(marketFee); + transfer(_minter, amount.sub(totalFee)); + emit OrderStarted( + consumer, + msg.sender, + amount, + serviceId, + /* solium-disable-next-line */ + block.timestamp, + mrktFeeCollector, + marketFee + ); + } + + + function finishOrder( + bytes32 orderTxId, + address consumer, + uint256 amount, + uint256 serviceId + ) + external + { + if ( amount != 0 ) + require( + transfer(consumer, amount), + 'DatatokenTemplate: failed to finish order' + ); + + emit OrderFinished( + orderTxId, + consumer, + amount, + serviceId, + msg.sender, + /* solium-disable-next-line */ + block.timestamp + ); + } + + /** + * @dev proposeMinter + * It proposes a new token minter address. + * Only the current minter can call it. + * @param newMinter refers to a new token minter address. + */ + function proposeMinter(address newMinter) + external + onlyMinter + { + _proposedMinter = newMinter; + emit MinterProposed( + msg.sender, + _proposedMinter + ); + } + + /** + * @dev approveMinter + * It approves a new token minter address. + * Only the current minter can call it. + */ + function approveMinter() + external + { + require( + msg.sender == _proposedMinter, + 'DatatokenTemplate: invalid proposed minter address' + ); + emit MinterApproved( + _minter, + _proposedMinter + ); + _minter = _proposedMinter; + _proposedMinter = address(0); + } + + + + /** + * @dev blob + * It returns the blob (e.g https://123.com). + * @return Datatoken blob. + */ + function blob() external view returns(string memory) { + return _blob; + } + + + /** + * @dev cap + * it returns the capital. + * @return Datatoken cap. + */ + function cap() external view returns (uint256) { + return _cap; + } + + /** + * @dev isMinter + * It takes the address and checks whether it has a minter role. + * @param account refers to the address. + * @return true if account has a minter role. + */ + function isMinter(address account) external view returns(bool) { + return (_minter == account); + } + + /** + * @dev minter + * @return minter's address. + */ + function minter() + external + view + returns(address) + { + return _minter; + } + + /** + * @dev isInitialized + * It checks whether the contract is initialized. + * @return true if the contract is initialized. + */ + function isInitialized() external view returns(bool) { + return initialized; + } + + /** + * @dev calculateFee + * giving a fee percentage, and amount it calculates the actual fee + * @param amount the amount of token + * @param feePercentage the fee percentage + * @return the token fee. + */ + function calculateFee( + uint256 amount, + uint256 feePercentage + ) + public + pure + returns(uint256) + { + if(amount == 0) return 0; + if(feePercentage == 0) return 0; + return amount.mul(feePercentage).div(BASE); + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/v3/IDataTokenTemplate.sol b/sol080/contracts/oceanv4/v3/IDataTokenTemplate.sol new file mode 100644 index 0000000000000000000000000000000000000000..59c029f538a0aad7e4b6c0e9df90084df4f34e3a --- /dev/null +++ b/sol080/contracts/oceanv4/v3/IDataTokenTemplate.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Unknown +pragma solidity 0.8.10; + +interface IDataTokenTemplate { + function initialize( + string calldata name, + string calldata symbol, + address minter, + uint256 cap, + string calldata blob, + address collector + ) external returns (bool); + + function mint(address account, uint256 value) external; + function minter() external view returns(address); + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function cap() external view returns (uint256); + function isMinter(address account) external view returns (bool); + function isInitialized() external view returns (bool); + function allowance(address owner, address spender) + external + view + returns (uint256); + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function proposeMinter(address newMinter) external; + function approveMinter() external; +} diff --git a/sol080/contracts/oceanv4/v3/V3DTFactory.sol b/sol080/contracts/oceanv4/v3/V3DTFactory.sol new file mode 100644 index 0000000000000000000000000000000000000000..4335c8fddc53bb515c1f400c7baaee4f9793f96e --- /dev/null +++ b/sol080/contracts/oceanv4/v3/V3DTFactory.sol @@ -0,0 +1,126 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import '../utils/Deployer.sol'; +import './IDataTokenTemplate.sol'; + +/** + * @title DTFactory contract + * @author Ocean Protocol Team + * + * @dev Implementation of Ocean DataTokens Factory + * + * DTFactory deploys DataToken proxy contracts. + * New DataToken proxy contracts are links to the template contract's bytecode. + * Proxy contract functionality is based on Ocean Protocol custom implementation of ERC1167 standard. + */ +contract V3DTFactory is Deployer { + address private tokenTemplate; + address private communityFeeCollector; + uint256 private currentTokenCount = 1; + + event TokenCreated( + address indexed newTokenAddress, + address indexed templateAddress, + string indexed tokenName + ); + + event TokenRegistered( + address indexed tokenAddress, + string tokenName, + string tokenSymbol, + uint256 tokenCap, + address indexed registeredBy, + string indexed blob + ); + + /** + * @dev constructor + * Called on contract deployment. Could not be called with zero address parameters. + * @param _template refers to the address of a deployed DataToken contract. + * @param _collector refers to the community fee collector address + */ + constructor( + address _template, + address _collector + ) public { + require( + _template != address(0) && + _collector != address(0), + 'DTFactory: Invalid template token/community fee collector address' + ); + tokenTemplate = _template; + communityFeeCollector = _collector; + } + + /** + * @dev Deploys new DataToken proxy contract. + * Template contract address could not be a zero address. + * @param blob any string that hold data/metadata for the new token + * @param name token name + * @param symbol token symbol + * @param cap the maximum total supply + * @return token address of a new proxy DataToken contract + */ + function createToken( + string memory blob, + string memory name, + string memory symbol, + uint256 cap + ) + public + returns (address token) + { + require( + cap != 0, + 'DTFactory: zero cap is not allowed' + ); + + token = deploy(tokenTemplate); + + require( + token != address(0), + 'DTFactory: Failed to perform minimal deploy of a new token' + ); + IDataTokenTemplate tokenInstance = IDataTokenTemplate(token); + require( + tokenInstance.initialize( + name, + symbol, + msg.sender, + cap, + blob, + communityFeeCollector + ), + 'DTFactory: Unable to initialize token instance' + ); + emit TokenCreated(token, tokenTemplate, name); + emit TokenRegistered( + token, + name, + symbol, + cap, + msg.sender, + blob + ); + currentTokenCount += 1; + } + + /** + * @dev get the current token count. + * @return the current token count + */ + function getCurrentTokenCount() external view returns (uint256) { + return currentTokenCount; + } + + /** + * @dev get the token template address + * @return the template address + */ + function getTokenTemplate() external view returns (address) { + return tokenTemplate; + } +} \ No newline at end of file diff --git a/sol080/contracts/oceanv4/v3/V3DataTokenTemplate.sol b/sol080/contracts/oceanv4/v3/V3DataTokenTemplate.sol new file mode 100644 index 0000000000000000000000000000000000000000..db2d0dab8afe7d11eb2a7ca0513e49e667601958 --- /dev/null +++ b/sol080/contracts/oceanv4/v3/V3DataTokenTemplate.sol @@ -0,0 +1,423 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import '../interfaces/IERC20Template.sol'; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/token/ERC20/ERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.2.0/contracts/utils/math/SafeMath.sol"; + +/** +* @title DataTokenTemplate +* +* @dev DataTokenTemplate is an ERC20 compliant token template +* Used by the factory contract as a bytecode reference to +* deploy new DataTokens. +*/ +contract V3DataTokenTemplate is ERC20("test", "testSymbol"){ + using SafeMath for uint256; + + string private _name; + string private _symbol; + string private _blob; + uint256 private _cap; + uint8 private constant _decimals = 18; + address private _communityFeeCollector; + bool private initialized = false; + address private _minter; + address private _proposedMinter; + uint256 public constant BASE = 10**18; + uint256 public constant BASE_COMMUNITY_FEE_PERCENTAGE = BASE / 1000; + uint256 public constant BASE_MARKET_FEE_PERCENTAGE = BASE / 1000; + + event OrderStarted( + address indexed consumer, + address indexed payer, + uint256 amount, + uint256 serviceId, + uint256 timestamp, + address indexed mrktFeeCollector, + uint256 marketFee + ); + + event OrderFinished( + bytes32 orderTxId, + address indexed consumer, + uint256 amount, + uint256 serviceId, + address indexed provider, + uint256 timestamp + ); + + event MinterProposed( + address currentMinter, + address newMinter + ); + + event MinterApproved( + address currentMinter, + address newMinter + ); + + modifier onlyNotInitialized() { + require( + !initialized, + 'DataTokenTemplate: token instance already initialized' + ); + _; + } + + modifier onlyMinter() { + require( + msg.sender == _minter, + 'DataTokenTemplate: invalid minter' + ); + _; + } + + /** + * @dev constructor + * Called prior contract deployment + * @param name refers to a template DataToken name + * @param symbol refers to a template DataToken symbol + * @param minterAddress refers to an address that has minter role + * @param cap the total ERC20 cap + * @param blob data string refering to the resolver for the metadata + * @param feeCollector it is the community fee collector address + */ + constructor( + string memory name, + string memory symbol, + address minterAddress, + uint256 cap, + string memory blob, + address feeCollector + ) + public + { + _initialize( + name, + symbol, + minterAddress, + cap, + blob, + feeCollector + ); + } + + /** + * @dev initialize + * Called prior contract initialization (e.g creating new DataToken instance) + * Calls private _initialize function. Only if contract is not initialized. + * @param name refers to a new DataToken name + * @param symbol refers to a nea DataToken symbol + * @param minterAddress refers to an address that has minter rights + * @param cap the total ERC20 cap + * @param blob data string refering to the resolver for the metadata + * @param feeCollector it is the community fee collector address + */ + function initialize( + string calldata name, + string calldata symbol, + address minterAddress, + uint256 cap, + string calldata blob, + address feeCollector + ) + external + onlyNotInitialized + returns(bool) + { + return _initialize( + name, + symbol, + minterAddress, + cap, + blob, + feeCollector + ); + } + + /** + * @dev _initialize + * Private function called on contract initialization. + * @param name refers to a new DataToken name + * @param symbol refers to a nea DataToken symbol + * @param minterAddress refers to an address that has minter rights + * @param cap the total ERC20 cap + * @param blob data string refering to the resolver for the metadata + * @param feeCollector it is the community fee collector address + */ + function _initialize( + string memory name, + string memory symbol, + address minterAddress, + uint256 cap, + string memory blob, + address feeCollector + ) + private + returns(bool) + { + require( + minterAddress != address(0), + 'DataTokenTemplate: Invalid minter, zero address' + ); + + require( + _minter == address(0), + 'DataTokenTemplate: Invalid minter, zero address' + ); + + require( + feeCollector != address(0), + 'DataTokenTemplate: Invalid community fee collector, zero address' + ); + + require( + cap != 0, + 'DataTokenTemplate: Invalid cap value' + ); + _cap = cap; + _name = name; + _blob = blob; + _symbol = symbol; + _minter = minterAddress; + _communityFeeCollector = feeCollector; + initialized = true; + return initialized; + } + + /** + * @dev mint + * Only the minter address can call it. + * msg.value should be higher than zero and gt or eq minting fee + * @param account refers to an address that token is going to be minted to. + * @param value refers to amount of tokens that is going to be minted. + */ + function mint( + address account, + uint256 value + ) + external + onlyMinter + { + require( + totalSupply().add(value) <= _cap, + 'DataTokenTemplate: cap exceeded' + ); + _mint(account, value); + } + + /** + * @dev startOrder + * called by payer or consumer prior ordering a service consume on a marketplace. + * @param consumer is the consumer address (payer could be different address) + * @param amount refers to amount of tokens that is going to be transfered. + * @param serviceId service index in the metadata + * @param mrktFeeCollector marketplace fee collector + */ + function startOrder( + address consumer, + uint256 amount, + uint256 serviceId, + address mrktFeeCollector + ) + external + { + uint256 marketFee = 0; + uint256 communityFee = calculateFee( + amount, + BASE_COMMUNITY_FEE_PERCENTAGE + ); + transfer(_communityFeeCollector, communityFee); + if(mrktFeeCollector != address(0)){ + marketFee = calculateFee( + amount, + BASE_MARKET_FEE_PERCENTAGE + ); + transfer(mrktFeeCollector, marketFee); + } + uint256 totalFee = communityFee.add(marketFee); + transfer(_minter, amount.sub(totalFee)); + emit OrderStarted( + consumer, + msg.sender, + amount, + serviceId, + /* solium-disable-next-line */ + block.timestamp, + mrktFeeCollector, + marketFee + ); + } + + /** + * @dev finishOrder + * called by provider prior completing service delivery only + * if there is a partial or full refund. + * @param orderTxId refers to the transaction Id of startOrder acts + * as a payment reference. + * @param consumer refers to an address that has consumed that service. + * @param amount refers to amount of tokens that is going to be transfered. + * @param serviceId service index in the metadata. + */ + function finishOrder( + bytes32 orderTxId, + address consumer, + uint256 amount, + uint256 serviceId + ) + external + { + if ( amount != 0 ) + require( + transfer(consumer, amount), + 'DataTokenTemplate: failed to finish order' + ); + + emit OrderFinished( + orderTxId, + consumer, + amount, + serviceId, + msg.sender, + /* solium-disable-next-line */ + block.timestamp + ); + } + + /** + * @dev proposeMinter + * It proposes a new token minter address. + * Only the current minter can call it. + * @param newMinter refers to a new token minter address. + */ + function proposeMinter(address newMinter) + external + onlyMinter + { + _proposedMinter = newMinter; + emit MinterProposed( + msg.sender, + _proposedMinter + ); + } + + /** + * @dev approveMinter + * It approves a new token minter address. + * Only the current minter can call it. + */ + function approveMinter() + external + { + require( + msg.sender == _proposedMinter, + 'DataTokenTemplate: invalid proposed minter address' + ); + emit MinterApproved( + _minter, + _proposedMinter + ); + _minter = _proposedMinter; + _proposedMinter = address(0); + } + + /** + * @dev name + * It returns the token name. + * @return DataToken name. + */ + function name() public view override returns (string memory) { + return _name; + } + + /** + * @dev symbol + * It returns the token symbol. + * @return DataToken symbol. + */ + function symbol() public view override returns (string memory) { + return _symbol; + } + + /** + * @dev decimals + * It returns the token decimals. + * how many supported decimal points + * @return DataToken decimals. + */ + function decimals() public view override returns (uint8) { + return _decimals; + } + + /** + * @dev cap + * it returns the capital. + * @return DataToken cap. + */ + function cap() external view returns (uint256) { + return _cap; + } + + /** + * @dev blob + * It returns the blob (e.g https://123.com). + * @return DataToken blob. + */ + function blob() external view returns(string memory) { + return _blob; + } + + /** + * @dev isMinter + * It takes the address and checks whether it has a minter role. + * @param account refers to the address. + * @return true if account has a minter role. + */ + function isMinter(address account) external view returns(bool) { + return (_minter == account); + } + + /** + * @dev minter + * @return minter's address. + */ + function minter() + external + view + returns(address) + { + return _minter; + } + + /** + * @dev isInitialized + * It checks whether the contract is initialized. + * @return true if the contract is initialized. + */ + function isInitialized() external view returns(bool) { + return initialized; + } + + /** + * @dev calculateFee + * giving a fee percentage, and amount it calculates the actual fee + * @param amount the amount of token + * @param feePercentage the fee percentage + * @return the token fee. + */ + function calculateFee( + uint256 amount, + uint256 feePercentage + ) + public + pure + returns(uint256) + { + if(amount == 0) return 0; + if(feePercentage == 0) return 0; + return amount.mul(feePercentage).div(BASE); + } +} diff --git a/sol080/contracts/oceanv4/v3/V3MetaData.sol b/sol080/contracts/oceanv4/v3/V3MetaData.sol new file mode 100644 index 0000000000000000000000000000000000000000..8f3c3cd7c911f19b5df083366319aeb66bbd1d43 --- /dev/null +++ b/sol080/contracts/oceanv4/v3/V3MetaData.sol @@ -0,0 +1,87 @@ +pragma solidity 0.8.10; +// Copyright BigchainDB GmbH and Ocean Protocol contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import './IDataTokenTemplate.sol'; + + +/** +* @title Metadata +* +* @dev Metadata stands for Decentralized Document. It allows publishers +* to publish their dataset metadata in decentralized way. +* It follows the Ocean DID Document standard: +* https://github.com/oceanprotocol/OEPs/blob/master/7/v0.2/README.md +*/ +contract V3Metadata { + + event MetadataCreated( + address indexed dataToken, + address indexed createdBy, + bytes flags, + bytes data + ); + event MetadataUpdated( + address indexed dataToken, + address indexed updatedBy, + bytes flags, + bytes data + ); + + modifier onlyDataTokenMinter(address dataToken) + { + IDataTokenTemplate token = IDataTokenTemplate(dataToken); + require( + token.minter() == msg.sender, + 'Metadata: Invalid DataToken Minter' + ); + _; + } + + /** + * @dev create + * creates/publishes new metadata/DDO document on-chain. + * @param dataToken refers to data token address + * @param flags special flags associated with metadata + * @param data referes to the actual metadata + */ + function create( + address dataToken, + bytes calldata flags, + bytes calldata data + ) + external + onlyDataTokenMinter(dataToken) + { + emit MetadataCreated( + dataToken, + msg.sender, + flags, + data + ); + } + + /** + * @dev update + * allows only datatoken minter(s) to update the DDO/metadata content + * @param dataToken refers to data token address + * @param flags special flags associated with metadata + * @param data referes to the actual metadata + */ + function update( + address dataToken, + bytes calldata flags, + bytes calldata data + ) + external + onlyDataTokenMinter(dataToken) + { + emit MetadataUpdated( + dataToken, + msg.sender, + flags, + data + ); + } +} \ No newline at end of file diff --git a/sol080/contracts/scheduler080/VestingWallet080.sol b/sol080/contracts/scheduler080/VestingWallet080.sol new file mode 100644 index 0000000000000000000000000000000000000000..a023d1299f0511f43f0e0cfcb96a4be598443e45 --- /dev/null +++ b/sol080/contracts/scheduler080/VestingWallet080.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.0 (finance/VestingWallet.sol) +pragma solidity ^0.8.0; + +import "OpenZeppelin/openzeppelin-contracts@4.0.0/contracts/token/ERC20/utils/SafeERC20.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.0.0/contracts/utils/Address.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.0.0/contracts/utils/Context.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.0.0/contracts/utils/math/SafeMath.sol"; + +/** + * @title VestingWallet080 + * @dev This contract handles the vesting of Eth and ERC20 tokens for a given beneficiary. Custody of multiple tokens + * can be given to this contract, which will release the token to the beneficiary following a given vesting schedule. + * The vesting schedule is customizable through the {vestedAmount} function. + * + * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. + * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) + * be immediately releasable. + */ +contract VestingWallet080 is Context { + event EtherReleased(uint256 amount); + event ERC20Released(address indexed token, uint256 amount); + + uint256 private _released; + mapping(address => uint256) private _erc20Released; + address private immutable _beneficiary; + uint256 private immutable _startBlock; + uint256 private immutable _numBlocksDuration; + + /** + * @dev Set the beneficiary, start blockNumber and vesting duration of the vesting wallet. + */ + constructor( + address beneficiaryAddress, + uint256 startBlock, + uint256 numBlocksDuration + ) { + require(beneficiaryAddress != address(0), "VestingWallet080: beneficiary is zero address"); + _beneficiary = beneficiaryAddress; + _startBlock = startBlock; + _numBlocksDuration = numBlocksDuration; + } + + /** + * @dev The contract should be able to receive Eth. + */ + receive() external payable virtual {} + + /** + * @dev Getter for the beneficiary address. + */ + function beneficiary() public view virtual returns (address) { + return _beneficiary; + } + + /** + * @dev Getter for the start blockNumber. + */ + function startBlock() public view virtual returns (uint256) { + return _startBlock; + } + + /** + * @dev Getter for the vesting duration. + */ + function numBlocksDuration() public view virtual returns (uint256) { + return _numBlocksDuration; + } + + /** + * @dev Amount of eth already released + */ + function released() public view virtual returns (uint256) { + return _released; + } + + /** + * @dev Amount of token already released + */ + function released(address token) public view virtual returns (uint256) { + return _erc20Released[token]; + } + + /** + * @dev Release the native token (ether) that have already vested. + * + * Emits a {TokensReleased} event. + */ + function release() public virtual { + uint256 releasable = vestedAmount(uint256(block.number*10**18)) - released(); + _released += releasable; + emit EtherReleased(releasable); + Address.sendValue(payable(beneficiary()), releasable); + } + + /** + * @dev Release the tokens that have already vested. + * + * Emits a {TokensReleased} event. + */ + function release(address token) public virtual { + uint256 releasable = vestedAmount(token, uint256(block.number*10**18)) - released(token); + _erc20Released[token] += releasable; + emit ERC20Released(token, releasable); + SafeERC20.safeTransfer(IERC20(token), beneficiary(), releasable); + } + + /** + * @dev Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. + */ + function vestedAmount(uint256 blockNumber) public view virtual returns (uint256) { + return _vestingSchedule(address(this).balance + released(), blockNumber); + } + + /** + * @dev Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve. + */ + function vestedAmount(address token, uint256 blockNumber) public view virtual returns (uint256) { + return _vestingSchedule(IERC20(token).balanceOf(address(this)) + released(token), blockNumber); + } + + /** + * @dev Virtual implementation of the vesting formula. This returns the amout vested, as a function of time, for + * an asset given its total historical allocation. + */ + function _vestingSchedule(uint256 totalAllocation, uint256 blockNumber) internal view virtual returns (uint256) { + if (blockNumber < startBlock()) { + return 0; + } else if (blockNumber > (startBlock() + numBlocksDuration())) { + return totalAllocation; + } else { + return ( totalAllocation * (blockNumber - startBlock()) / numBlocksDuration()); + } + } +} diff --git a/sol080/contracts/scheduler080/__init__.py b/sol080/contracts/scheduler080/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol080/contracts/scheduler080/test/__init__.py b/sol080/contracts/scheduler080/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sol080/contracts/scheduler080/test/test_VestingWallet080.py b/sol080/contracts/scheduler080/test/test_VestingWallet080.py new file mode 100644 index 0000000000000000000000000000000000000000..188d10b384c256f8910ff0191f07faa38bb5878c --- /dev/null +++ b/sol080/contracts/scheduler080/test/test_VestingWallet080.py @@ -0,0 +1,202 @@ +import brownie +from pytest import approx + +from util.base18 import toBase18 +from util.constants import BROWNIE_PROJECT057, BROWNIE_PROJECT080, GOD_ACCOUNT +from util.tx import txdict, transferETH + +accounts = brownie.network.accounts +account0, account1, account2, account3 = ( + accounts[0], + accounts[1], + accounts[2], + accounts[3], +) +address0, address1, address2 = account0.address, account1.address, account2.address +chain = brownie.network.chain + +# This code has evolved from sol057: +# -DONE: time is # blocks not unix time +# -TODO: fix 500M issue, exponential vesting, ratchet + + +def test_basic(): + print("test_VestingWallet080:test_basic 1") + n_blocks = len(chain) + beneficiary = address1 + start_block = n_blocks + 1 + num_blocks_duration = 4 + + # constructor + vesting_wallet = BROWNIE_PROJECT080.VestingWallet080.deploy( + beneficiary, + toBase18(start_block), + toBase18(num_blocks_duration), + txdict(account0), + ) + + print("test_VestingWallet080:test_basic 2") + assert vesting_wallet.beneficiary() == beneficiary + start_block_measured = int(vesting_wallet.startBlock() / 1e18) + assert start_block_measured in [start_block - 1, start_block, start_block + 1] + assert int(vesting_wallet.numBlocksDuration() / 1e18) == 4 + assert vesting_wallet.released() == 0 + + # time passes + print("test_VestingWallet080:test_basic 3") + chain.mine(blocks=15, timedelta=1) + assert vesting_wallet.released() == 0 # haven't released anything + + # call release + print("test_VestingWallet080:test_basic 4") + vesting_wallet.release(txdict(account0)) + assert vesting_wallet.released() == 0 # wallet never got funds to release! + + print("test_VestingWallet080:test_basic 5") + + +def test_ethFunding(): + # ensure each account has exactly 30 ETH + for account in [account0, account1, account2]: + transferETH(account, GOD_ACCOUNT, account.balance()) + transferETH(GOD_ACCOUNT, account, toBase18(30.0)) + + # account0 should be able to freely transfer ETH + transferETH(account0, account1, toBase18(1.0)) + transferETH(account1, account0, toBase18(1.0)) + assert account0.balance() / 1e18 == approx(30.0) + assert account1.balance() / 1e18 == approx(30.0) + + # set up vesting wallet (account). It vests all ETH/tokens that it receives. + # where beneficiary is account1 + start_block = 4 + num_blocks_duration = 5 + wallet = BROWNIE_PROJECT080.VestingWallet080.deploy( + address1, + toBase18(start_block), + toBase18(num_blocks_duration), + txdict(account0), + ) + assert wallet.balance() == 0 + + # send ETH to the wallet. It has a function: + # receive() external payable virtual {} + # which allows it to receive ETH. It's called for plain ETH transfers, + # ie every call with empty calldata. + # https://medium.com/coinmonks/solidity-v0-6-0-is-here-things-you-should-know-7d4ab5bca5f1 + transferETH(account0, wallet.address, toBase18(30.0)) + assert wallet.balance() / 1e18 == approx(30.0) + assert account0.balance() / 1e18 == approx(0.0) + assert account1.balance() / 1e18 == approx(30.0) # unchanged so far + assert wallet.vestedAmount(toBase18(4)) == 0 + assert wallet.vestedAmount(toBase18(10)) > 0.0 + assert wallet.released() == 0 + + # make enough time pass for everything to vest + chain.mine(blocks=14, timedelta=100) + + assert wallet.vestedAmount(toBase18(1)) == 0 + assert wallet.vestedAmount(toBase18(2)) == 0 + assert wallet.vestedAmount(toBase18(3)) == 0 + assert wallet.vestedAmount(toBase18(4)) == 0 + assert wallet.vestedAmount(toBase18(5)) / 1e18 == approx(6.0) + assert wallet.vestedAmount(toBase18(6)) / 1e18 == approx(12.0) + assert wallet.vestedAmount(toBase18(7)) / 1e18 == approx(18.0) + assert wallet.vestedAmount(toBase18(8)) / 1e18 == approx(24.0) + assert wallet.vestedAmount(toBase18(9)) / 1e18 == approx(30.0) + assert wallet.vestedAmount(toBase18(10)) / 1e18 == approx(30.0) + assert wallet.vestedAmount(toBase18(11)) / 1e18 == approx(30.0) + + assert wallet.released() == 0 + assert account1.balance() / 1e18 == approx(30.0) # not released yet! + + # release the ETH. Anyone can call it + wallet.release(txdict(account2)) + assert wallet.released() / 1e18 == approx(30.0) # now it's released! + assert account1.balance() / 1e18 == approx(30.0 + 30.0) # beneficiary richer + + # put some new ETH into wallet. It's immediately vested, but not released + transferETH(account2, wallet.address, toBase18(10.0)) + assert wallet.vestedAmount(toBase18(len(chain))) / 1e18 == approx(30.0 + 10.0) + assert wallet.released() / 1e18 == approx(30.0 + 0.0) # not released yet! + + # release the new ETH + wallet.release(txdict(account3)) + assert wallet.released() / 1e18 == approx(30.0 + 10.0) # new ETH is released! + assert account1.balance() / 1e18 == approx(30.0 + 30.0 + 10.0) # +10 eth to ben + + +def test_tokenFunding(): + # accounts 0, 1, 2 should each start with 100 TOK + token = BROWNIE_PROJECT057.Simpletoken.deploy( + "TOK", "Test Token", 18, toBase18(300.0), txdict(account0) + ) + token.transfer(account1, toBase18(100.0), txdict(account0)) + token.transfer(account2, toBase18(100.0), txdict(account0)) + taddress = token.address + + assert token.balanceOf(account0) / 1e18 == approx(100.0) + assert token.balanceOf(account1) / 1e18 == approx(100.0) + assert token.balanceOf(account2) / 1e18 == approx(100.0) + + # account0 should be able to freely transfer TOK + token.transfer(account1, toBase18(10.0), txdict(account0)) + assert token.balanceOf(account0) / 1e18 == approx(90.0) + assert token.balanceOf(account1) / 1e18 == approx(110.0) + + # set up vesting wallet (account). It vests all ETH/tokens that it receives. + start_block = 4 + num_blocks_duration = 5 + wallet = BROWNIE_PROJECT080.VestingWallet080.deploy( + address1, + toBase18(start_block), + toBase18(num_blocks_duration), + txdict(account0), + ) + assert token.balanceOf(wallet) == 0 + + # send TOK to the wallet + token.transfer(wallet.address, toBase18(30.0), txdict(account0)) + assert token.balanceOf(wallet) / 1e18 == approx(30.0) + assert token.balanceOf(account0) / 1e18 == approx(60.0) + assert token.balanceOf(account1) / 1e18 == approx(110.0) + assert wallet.vestedAmount(taddress, toBase18(4)) == 0 + assert wallet.vestedAmount(taddress, toBase18(10)) > 0.0 + assert wallet.released(taddress) == 0 + + # make enough time pass for everything to vest + chain.mine(blocks=14, timedelta=100) + + assert wallet.vestedAmount(taddress, toBase18(1)) == 0 + assert wallet.vestedAmount(taddress, toBase18(2)) == 0 + assert wallet.vestedAmount(taddress, toBase18(3)) == 0 + assert wallet.vestedAmount(taddress, toBase18(4)) == 0 + assert wallet.vestedAmount(taddress, toBase18(5)) / 1e18 == approx(6.0) + assert wallet.vestedAmount(taddress, toBase18(6)) / 1e18 == approx(12.0) + assert wallet.vestedAmount(taddress, toBase18(7)) / 1e18 == approx(18.0) + assert wallet.vestedAmount(taddress, toBase18(8)) / 1e18 == approx(24.0) + assert wallet.vestedAmount(taddress, toBase18(9)) / 1e18 == approx(30.0) + assert wallet.vestedAmount(taddress, toBase18(10)) / 1e18 == approx(30.0) + assert wallet.vestedAmount(taddress, toBase18(11)) / 1e18 == approx(30.0) + + assert wallet.released(taddress) == 0 + assert token.balanceOf(account1) / 1e18 == approx(110.0) # not released yet + + # release the TOK. Anyone can call it + wallet.release(taddress, txdict(account2)) + assert wallet.released(taddress) / 1e18 == approx(30.0) # released! + assert token.balanceOf(account1) / 1e18 == approx( + 110.0 + 30.0 + ) # beneficiary richer + + # put some new TOK into wallet. It's immediately vested, but not released + token.transfer(wallet.address, toBase18(10.0), txdict(account2)) + assert wallet.vestedAmount(taddress, toBase18(11)) / 1e18 == approx(30.0 + 10.0) + assert wallet.released(taddress) / 1e18 == approx(30.0) # not released yet + + # release the new TOK + wallet.release(taddress, txdict(account3)) + assert wallet.released(taddress) / 1e18 == approx(30.0 + 10.0) # TOK released! + assert token.balanceOf(account1) / 1e18 == approx( + 110 + 30 + 10.0 + ) # beneficiary richer diff --git a/tokenspice.ini b/tokenspice.ini new file mode 100644 index 0000000000000000000000000000000000000000..7b0ea8d94545b1bef2ed1a169386c3e6556f0b16 --- /dev/null +++ b/tokenspice.ini @@ -0,0 +1,30 @@ + +[general] + +NETWORK = ganache +ARTIFACTS_PATH = ../contracts/artifacts + +#set to True for safety (via type-checking), and False for speed. +SAFETY = True + +#set to True for cleaner stdout, and False for better debugging +SILENT = True + +#Brownie uses http://127.0.0.1:8545 +GANACHE_URL = http://127.0.0.1:8545 + +[ganache] +#NOTE: *not* 'development' because brownie reverts changes. So treat ganache as a live network. + +#eth address: 0x66aB6D9362d4F35596279692F0251Db635165871 +TEST_PRIVATE_KEY1 = 0xbbfbee4961061d506ffbb11dfea64eba16355cbf1d9c29613126ba7fec0aed5d + +#eth address: 0x33A4622B82D4c04a53e170c638B944ce27cffce3 +TEST_PRIVATE_KEY2 = 0x804365e293b9fab9bd11bddd39082396d56d30779efbb3ffb0a6089027902c4a + +#to deploy Factory (DataTokenTemplate), BFactory (BPool), and MFactory (MPool) +#eth address: 0xc920aBcE3Adb7A54ce740D7EE39Cf023d99e7da2 +FACTORY_DEPLOYER_PRIVATE_KEY = 0x904365e293b9fab9bd11bddd39082396d56d30779efbb3ffb0a6089027902c4a + +GAS_PRICE = 1 + diff --git a/tsp b/tsp new file mode 100755 index 0000000000000000000000000000000000000000..c9d58ee0cbed63e95bc08576281ad2a7b581f444 --- /dev/null +++ b/tsp @@ -0,0 +1,260 @@ +#!/usr/bin/env python + +import cProfile +import importlib +import logging +import pstats +import os +import sys + +import brownie + +# ======================================================================== +# tsp help +HELP_MAIN = """ +TokenSPICE - EVM agent-based token simulator + +Usage: tsp compile|ganache|run|plot|showstats + tsp compile -- compile Solidity code + tsp ganache -- run local chain + tsp run -- run simulation + tsp plot -- plot results + tsp showstats -- see run stats +""" + +def do_help(): + print(HELP_MAIN) + +# ======================================================================== +# tsp ganache +def do_ganache(): + os.system("./ganache.py") + +# ======================================================================== +# tsp compile +def do_compile(): + os.system("./compile.sh") + +# ======================================================================== +# tsp run + +HELP_RUN = """ +Usage: tsp run NETLIST OUTPUT_DIR [DO_PROFILE] + + NETLIST -- string -- pathname for netlist + OUTPUT_DIR -- string -- output directory for csv file. + DO_PROFILE -- bool -- if True, profile. Otherwise don't. Default=False. +""" + + +def do_run(): + if len(sys.argv) not in [4, 5]: + print(HELP_RUN) + sys.exit(0) + + # extract inputs + assert sys.argv[1] == "run" + netlist_str = sys.argv[2] + output_dir = sys.argv[3] + do_profile = False + if len(sys.argv) == 5 and sys.argv[4] == "True": + do_profile = True + + print( + f"Arguments: NETLIST={netlist_str}, OUTPUT_DIR={output_dir}," + f" DO_PROFILE={do_profile}" + ) + + # handle corner cases + if os.path.exists(output_dir): + print(f"\nOutput path '{output_dir}' already exists. Exiting.\n") + sys.exit(0) + + # make directory + os.mkdir(output_dir) + + # go + netlist_module = _importNetlistModule(netlist_str) + netlist_state = netlist_module.SimState() + netlist_log_func = netlist_module.netlist_createLogData + + from engine.SimEngine import SimEngine #pylint: disable=import-outside-toplevel + engine = SimEngine(netlist_state, output_dir, netlist_log_func) + if not do_profile: + engine.run() + else: + stats_filename = os.path.join(output_dir, "stats") + cProfile.run("engine.run()", stats_filename) + print( + f"Output stats file: {stats_filename}. To see: tsp showstats " + f"outdir_csv/stats 20 cumulative" + ) + print(f"Output directory: {output_dir}") + + if brownie.network.is_connected(): + brownie.network.disconnect() + + +def _importNetlistModule(netlist_str: str): + module_str = netlist_str.replace("/", ".").replace(".py", "") + netlist_module = importlib.import_module(module_str) + return netlist_module + + +# ========================================================================== +# tsp plot + +HELP_PLOT = """ +Usage: tsp plot NETLIST INPUT_CSV_DIR OUTPUT_PNG_DIR + + NETLIST -- string -- pathname for netlist + INPUT_CSV_DIR -- string -- input directory for csv file. + OUTPUT_PNG_DIR -- string -- output directory for png files. Can't exist yet. +""" + + +def do_plot(): + # got the right number of args? If not, output help + if len(sys.argv) not in [5]: + print(HELP_PLOT) + sys.exit(0) + + # extract inputs + assert sys.argv[1] == "plot" + netlist_str = sys.argv[2] + input_csv_dir = sys.argv[3] + output_png_dir = sys.argv[4] + + # + base_input_csv_filename = "data.csv" # magic number. Set in engine/SimEngine.py + input_csv_filename = os.path.join(input_csv_dir, base_input_csv_filename) + + print( + f"Arguments: NETLIST={netlist_str}, INPUT_CSV_DIR={input_csv_dir}, " + f"OUTPUT_PNG_DIR={output_png_dir}" + ) + print(f"Base input filename: '{base_input_csv_filename}' (hardcoded)") + print(f"Full input filename: '{input_csv_filename}'") + print() + + # corner cases + if not os.path.exists(input_csv_dir): + print(f"Input directory '{input_csv_dir}' does not exist. Exiting.") + sys.exit(0) + + if not os.path.exists(input_csv_filename): + print(f"Input filename '{input_csv_filename}' does not exist. Exiting.") + sys.exit(0) + + if os.path.exists(output_png_dir): + print(f"Output path '{output_png_dir}' already exists. Exiting.") + sys.exit(0) + + # get netlist module + netlist_module = _importNetlistModule(netlist_str) + netlist_plot_instrs_func = netlist_module.netlist_plotInstructions + + # main work + from util.plotutil import csvToPngs #pylint: disable=import-outside-toplevel + csvToPngs(input_csv_filename, output_png_dir, netlist_plot_instrs_func) + + print("Done") + + +# ======================================================================== +# tsp showstats + +HELP_SHOWSTATS = """ +Usage: tsp showstats FILENAME NUM_SHOW SORT_BY + + FILENAME -- string -- stats filename (including path) + NUM_SHOW -- int -- only show the top 'NUM_SHOW' functions + SORT_BY -- string -- one of 'cumulative', 'time' + cumulative -- cumulative time by a function and its callees. To understand what functions take the most time + internal -- time spent *within* a function, but not callees. To understand what functions were looping a lot, and taking a lot of time. + internal_callers -- which funcs called the ones above + +Example: tsp showstats outdir_csv/stats 20 cumulative +""" + + +def do_showstats(): + if len(sys.argv) not in [5]: + print(HELP_SHOWSTATS) + sys.exit(0) + + # extract inputs + assert sys.argv[1] == "showstats" + filename = sys.argv[2] + num_show = int(eval(sys.argv[3])) # pylint: disable=eval-used + sort_by = sys.argv[4] + + print(f"Argument FILENAME: '{filename}'") + print(f"Argument NUM_SHOW: '{num_show}'") + print(f"Argument SORT_BY: '{sort_by}'") + print() + + # corner cases + if not os.path.exists(filename): + print(f"Input file '{filename}' does not exist. Exiting.") + sys.exit(0) + if num_show < 1: + print("Input NUM_SHOW is invalid. Exiting.") + sys.exit(0) + + # do work + # Note: assumes python3.6. Different in 3.7+. + p = pstats.Stats(filename) + + print("=" * 80) + if sort_by == "cumulative": + print("Highest-impact by cumulative time in a function and its callees") + print("=" * 80) + p.sort_stats("cumulative").print_stats(num_show) + + elif sort_by == "internal": + print("Highest-impact by time spent *within* a function") + print("=" * 80) + p.sort_stats("time", "cumulative").print_stats(num_show) + + elif sort_by == "internal_callers": + print("Highest-impact by functions looping a lot _and_ taking time") + print("=" * 80) + p.sort_stats("time").print_stats(num_show).print_callers(num_show) + + else: + print("Input sort_by is invalid. Exiting.") + sys.exit(0) + + print("Done") + + +# ======================================================================== +# main +def do_main(): + logging.basicConfig() + logging.getLogger("master").setLevel(logging.INFO) + + if len(sys.argv) == 1: + do_help() + + elif sys.argv[1] == "compile": + do_compile() + + elif sys.argv[1] == "ganache": + do_ganache() + + elif sys.argv[1] == "run": + do_run() + + elif sys.argv[1] == "plot": + do_plot() + + elif sys.argv[1] == "showstats": + do_showstats() + + else: + do_help() + +if __name__ == "__main__": + do_main() diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3af8d15f8847b08d37952f0df6f8c02cba56c1b3 --- /dev/null +++ b/util/__init__.py @@ -0,0 +1,2 @@ +"""empty (and it should be for clean namespaces) +""" diff --git a/util/base18.py b/util/base18.py new file mode 100644 index 0000000000000000000000000000000000000000..f376e6a5d8959b3248290cd3488e5a51cc8d0935 --- /dev/null +++ b/util/base18.py @@ -0,0 +1,6 @@ +def toBase18(amt: float) -> int: + return int(amt * 1e18) + + +def fromBase18(amt_base: int) -> float: + return amt_base / 1e18 diff --git a/util/configutil.py b/util/configutil.py new file mode 100644 index 0000000000000000000000000000000000000000..52a374f61c9455ae0fd7c1c6d0f689559d48c617 --- /dev/null +++ b/util/configutil.py @@ -0,0 +1,11 @@ +import configparser +import os + +CONF_FILE_PATH = "./tokenspice.ini" + + +def confFileValue(section: str, key: str) -> str: + conf = configparser.ConfigParser() + path = os.path.expanduser(CONF_FILE_PATH) + conf.read(path) + return conf[section][key] diff --git a/util/constants.py b/util/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..c6998733cb56f26d9d9b582b77e4a857f1dfa069 --- /dev/null +++ b/util/constants.py @@ -0,0 +1,67 @@ +# Note: this file no longer contains magic numbers, just useful +# numbers for running TokenSPICE. Magic numbers have been +# moved into netlists. + +import configparser +import math +import os + +import brownie +from brownie._config import CONFIG # pylint: disable=no-name-in-module +from enforce_typing import enforce_types # pylint: disable=unused-import + +# from sol080.contracts.oceanv4.test.test_SideStaking import ZERO_ADDRESS # pylint: disable=unused-import + +from util.configutil import CONF_FILE_PATH + +config = configparser.ConfigParser() +config.read(os.path.expanduser(CONF_FILE_PATH)) + +BROWNIE_PROJECT057 = brownie.project.load("./sol057/", name="Project057") +BROWNIE_PROJECT080 = brownie.project.load("./sol080/", name="Project080") + +# brownie auto-reverts in "development". If needed, set to "ganache" +brownie.network.connect("development") + +GOD_ACCOUNT = brownie.network.accounts[9] + +SAFETY = config["general"].getboolean("SAFETY") +assert SAFETY is not None + +if not SAFETY: + # do nothing, just return the original function + def noop(f): + return f + + enforce_types = noop + +SILENT = config["general"].getboolean("SILENT") +assert SILENT is not None + +CONFIG.argv["silent"] = SILENT # brownie config + +# big numbers + +INF = math.inf +HUGEINT = 2**255 # biggest int that can be passed into contracts + +# number of seconds in an hour, etc. +S_PER_MIN = 60 +S_PER_HOUR = S_PER_MIN * 60 +S_PER_DAY = S_PER_HOUR * 24 +S_PER_WEEK = S_PER_DAY * 7 +S_PER_MONTH = S_PER_DAY * 30 +S_PER_YEAR = S_PER_DAY * 365 + +# Number of half-lives that bitcoin stops after. +# https://en.bitcoin.it/wiki/Controlled_supply#Projected_Bitcoins_Long_Term +BITCOIN_NUM_HALF_LIVES = 34 + +# evm stuff +GASLIMIT_DEFAULT = 5000000 +BURN_ADDRESS = "0x000000000000000000000000000000000000dEaD" + +OPF_ACCOUNT = brownie.network.accounts[8] +OPF_ADDRESS = OPF_ACCOUNT.address + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" diff --git a/util/globaltokens.py b/util/globaltokens.py new file mode 100644 index 0000000000000000000000000000000000000000..ec226d3869f0ed7dbd6e4bf334d8aaf3cacb5017 --- /dev/null +++ b/util/globaltokens.py @@ -0,0 +1,34 @@ +import brownie +from enforce_typing import enforce_types + +from util.constants import BROWNIE_PROJECT057, GOD_ACCOUNT +from util.base18 import toBase18 +from util.tx import txdict + +_OCEAN_TOKEN = None + + +@enforce_types +def OCEANtoken(): + global _OCEAN_TOKEN # pylint: disable=global-statement + try: + token = _OCEAN_TOKEN # may trigger failure + if token is not None: + x = token.address # "" # pylint: disable=unused-variable + except brownie.exceptions.ContractNotFound: + token = None + if token is None: + token = _OCEAN_TOKEN = BROWNIE_PROJECT057.Simpletoken.deploy( + "OCEAN", "OCEAN", 18, toBase18(1e9), txdict(GOD_ACCOUNT) + ) + return token + + +@enforce_types +def OCEAN_address() -> str: + return OCEANtoken().address + + +@enforce_types +def fundOCEANFromAbove(dst_address: str, amount_base: int): + OCEANtoken().transfer(dst_address, amount_base, txdict(GOD_ACCOUNT)) diff --git a/util/mathutil.py b/util/mathutil.py new file mode 100644 index 0000000000000000000000000000000000000000..651069e67b16599421c716e0acb206b1038279f9 --- /dev/null +++ b/util/mathutil.py @@ -0,0 +1,46 @@ +from math import log10, floor +import random +import re +from typing import Union + +from enforce_typing import enforce_types + +from util.strutil import StrMixin + + +def isNumber(x) -> bool: + return isinstance(x, (int, float)) + + +@enforce_types +def intInStr(s: str) -> int: + int_s = re.sub("[^0-9]", "", s) + return int(int_s) + + +@enforce_types +class Range(StrMixin): + def __init__(self, min_: float, max_: Union[float, None] = None): + assert (max_ is None) or (max_ >= min_) + self.min_: float = min_ + self.max_: Union[float, None] = max_ + + def drawRandomPoint(self) -> float: + if self.max_ is None: + return self.min_ + return randunif(self.min_, self.max_) + + +@enforce_types +def randunif(mn: float, mx: float) -> float: + """Return a uniformly-distributed random number in range [mn, mx]""" + assert mx >= mn + if mn == mx: + return mn + return mn + random.random() * (mx - mn) + + +@enforce_types +def round_sig(x: Union[int, float], sig: int) -> Union[int, float]: + """Return a number with the specified # significant bits""" + return round(x, sig - int(floor(log10(abs(x)))) - 1) diff --git a/util/plotutil.py b/util/plotutil.py new file mode 100644 index 0000000000000000000000000000000000000000..fbbb269267661f22bdfb3c72e36987ff7b096705 --- /dev/null +++ b/util/plotutil.py @@ -0,0 +1,273 @@ +import copy +import csv +import math +import os +from typing import Any, List + +from enforce_typing import enforce_types +import matplotlib +from matplotlib import pyplot +import numpy + +LINEAR, LOG, BOTH = 0, 1, 2 # pyplot.yscale interprets 1st 2 +MULT1, MULT100, DIV1M, DIV1B = 0, 1, 2, 3 # multiply or divide the value? +COUNT, DOLLAR, PERCENT = 0, 1, 2 + + +@enforce_types +class YParam: + """Defines what to plot for y-values. + See example usage in netlist_headerValuesToXY. + """ + + def __init__( # pylint: disable=too-many-arguments + self, + y_header_names: List[str], + labels: List[str], + y_pretty_name: str, + y_scale: int, # one of LINEAR, LOG, BOTH + mult: int, # one of MULT1, MULT100, DIV1M, DIV1B + unit: int, # one of COUNT, DOLLAR, PERCENT + ): + + self.y_header_names = y_header_names + self.labels = labels + self.y_pretty_name = y_pretty_name + self.y_scale = y_scale + self.mult = mult + self.unit = unit + + @property + def y_scale_str(self): + if self.y_scale == LINEAR: + return "LINEAR" + if self.y_scale == LOG: + return "LOG" + if self.y_scale == BOTH: + return "BOTH" + raise ValueError(self.y_scale) + + +@enforce_types +def arrayToFloatList(x_array) -> List[float]: + """Convert array to list of float. + + Args: + x_array: array[Any] -- + + Returns: + List[float] + """ + return [float(x_item) for x_item in x_array] + + +@enforce_types +def _applyMult(y: List[float], mult: int) -> List[float]: + """Apply multiplier according to enum specified by 'mult' + + Args: + y -- values + mult -- e.g. MULT1, DIV1B, .. + + Returns: + y multiplied + """ + if mult == MULT1: + return list(numpy.array(y) * 1.0) + if mult == MULT100: + return list(numpy.array(y) * 100.0) + if mult == DIV1M: + return list(numpy.array(y) / 1e6) + if mult == DIV1B: + return list(numpy.array(y) / 1e9) + raise ValueError(mult) + + +@enforce_types +def _multUnitStr( # pylint: disable=too-many-return-statements + mult: int, unit: int +) -> str: + """String describing units of a given enum multiplier + + Args: + mult -- e.g. MULT1, DIV1B, .. + unit -- e.g. DOLLAR, COUNT + + Returns: + e.g. "$", "count, in billions" + """ + if mult == MULT1 and unit == DOLLAR: + return "$" + if mult == DIV1M and unit == DOLLAR: + return "$M" + if mult == DIV1B and unit == DOLLAR: + return "$B" + if mult == MULT1 and unit == COUNT: + return "count" + if mult == DIV1M and unit == COUNT: + return "count, in millions" + if mult == DIV1B and unit == COUNT: + return "count, in billions" + if mult == MULT100 and unit == PERCENT: + return "%" + raise ValueError(f"can't handle mult={mult} with unit={unit}") + + +@enforce_types +def _expandBOTHinY(y_params: List[YParam]) -> List[YParam]: + """Any y_param that has a BOTH gets expanded to a LOG & a LINEAR entry + + Args: + y_params -- incoming list of what to plot + + Returns: + y_params2 -- revised list + """ + # replace BOTH with 2 entries + y_params2 = [] + for p in y_params: + if p.y_scale in [LINEAR, BOTH]: + p2 = copy.copy(p) + p2.y_scale = LINEAR + y_params2.append(p2) + if p.y_scale in [LOG, BOTH]: + p2 = copy.copy(p) + p2.y_scale = LOG + y_params2.append(p2) + return y_params2 + + +@enforce_types +def _xyToPngs( # pylint: disable=too-many-branches, too-many-locals, too-many-arguments + header: List[str], + values, + x_label: str, + x: List[float], + y_params: List[YParam], + output_png_dir: str, +): + """Plot. + + Given actual data (header, values) and what to plot (x, y_params), + create plots and store as .png files in output_png_dir + + Args: + header -- e.g. 'Tick', 'Second', ... + values: 2d array of float [tick_i, valuetype_i] -- + x_label -- e.g. "Day", "Month", "Year" + x -- x-axis info on how to plot + y_params -- y-axis info on how to plot + output_png_dir -- path of output png to be created and filled + """ + + assert not os.path.exists(output_png_dir) + os.mkdir(output_png_dir) + + for p in y_params: + ys = [ + arrayToFloatList(values[:, header.index(name)]) for name in p.y_header_names + ] + + ys = [_applyMult(y, p.mult) for y in ys] + + _, ax = pyplot.subplots() + + ax.set_xlabel(x_label) + + for y, label in zip(ys, p.labels): + if label == "": + ax.plot(x, y) + else: + ax.plot(x, y, label=label) + if len(p.labels) > 1: + ax.legend() + + mult_unit_s = _multUnitStr(p.mult, p.unit) + ax.set_ylabel(f"{p.y_pretty_name} ({mult_unit_s})") + + ax.set_title(f"{p.y_pretty_name}" + f" ({p.y_scale_str})") + + pyplot.yscale(p.y_scale_str) + + if p.y_scale == LOG: # turn off exponential notation + ax.get_yaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter()) + + ax.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter("%.2g")) + max_y = max([max(y) for y in ys]) # pylint: disable=consider-using-generator + if max_y < 1000.0: + ax.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter("%.2f")) + + max_x = max(10, math.ceil(max(x))) + if max_x < 12: + xticks = list(range(max_x + 1)) + elif max_x < 22: + xticks = [i for i in range(max_x + 1) if (i % 2) == 0] + elif max_x < 52: + xticks = [i for i in range(max_x + 1) if (i % 5) == 0] + elif max_x < 152: + xticks = [i for i in range(max_x + 1) if (i % 10) == 0] + elif max_x < 202: + xticks = [i for i in range(max_x + 1) if (i % 20) == 0] + elif max_x < 1000: + xticks = [i for i in range(max_x + 1) if (i % 100) == 0] + elif max_x < 5000: + xticks = [i for i in range(max_x + 1) if (i % 500) == 0] + else: + raise ValueError(f"don't yet have behavior for max_x={max_x}") + pyplot.xticks(xticks) + + # pyplot.show() #popup + + base_output_filename = ( + f"{p.y_pretty_name}_{p.y_scale_str}.png".replace("/", "_per_") + .replace(" ", "_") + .replace(",", "_") + .replace("'", "") + .replace("__", "_") + ) + full_output_filename = os.path.join(output_png_dir, base_output_filename) + pyplot.savefig(full_output_filename, bbox_inches="tight") + print(f"Created '{full_output_filename}'") + + +@enforce_types +def _csvToHeaderValues(input_csv_filename: str): + """Given csv file from a TokenSPICE run, creates (header, values). + + Args: + input_csv_filename -- absolute path of input csv file + + Returns: + header: List[str] -- e.g. 'Tick', 'Second', ... + values: 2d array of float [tick_i, valuetype_i] -- + """ + assert os.path.exists(input_csv_filename) + header: Any = None + values: Any = [] + with open(input_csv_filename, newline="", encoding="utf-8") as csvfile: + csvreader = csv.reader(csvfile, delimiter=",") + for row in csvreader: # row = ['Tick', 'Second', ..] or [1.0, 100.0, ..] + if header is None: + header = row + header = [param.strip() for param in header] + else: + values.append(row) + values = numpy.array(values) # [tick_i, valuetype_i] + return (header, values) + + +@enforce_types +def csvToPngs(input_csv_filename: str, output_png_dir: str, netlist_plot_instrs_func): + """Main plotting routine, delegates subtasks to worker routines. + + Args: + input_csv_filename -- absolute path of input csv file + output_png_dir -- path of output png to be created and filled + netlist_plot_instrs -- function to convert to (x, y_params) + """ + (header, values) = _csvToHeaderValues(input_csv_filename) + + (x_label, x, y_params) = netlist_plot_instrs_func(header, values) + y_params = _expandBOTHinY(y_params) + + _xyToPngs(header, values, x_label, x, y_params, output_png_dir) diff --git a/util/strutil.py b/util/strutil.py new file mode 100644 index 0000000000000000000000000000000000000000..248d5fd0f89172d596a80513674f029e7bd26a95 --- /dev/null +++ b/util/strutil.py @@ -0,0 +1,124 @@ +import inspect + +from enforce_typing import enforce_types + + +@enforce_types +class StrMixin: + def __str__(self) -> str: + class_name = self.__class__.__name__ + + newline = False + if hasattr(self, "__STR_GIVES_NEWLINE__"): + newline = self.__STR_GIVES_NEWLINE__ # type: ignore + + s = [] + s += [f"{class_name}={{"] + if newline: + s += ["\n"] + + obj = self + + short_attrs, long_attrs = [], [] + for attr in dir(obj): + if "__" in attr: + continue + attr_obj = getattr(obj, attr) + if inspect.ismethod(attr_obj): + continue + if isinstance(attr_obj, (int, float, str)) or (attr_obj is None): + short_attrs.append(attr) + else: + long_attrs.append(attr) + attrs = short_attrs + long_attrs # print short attrs first + + for i, attr in enumerate(attrs): + attr_obj = getattr(obj, attr) + s += [f"{attr}="] + if isinstance(attr_obj, dict): + s += [dictStr(attr_obj, newline)] + else: + s += [str(attr_obj)] + + if i < (len(attrs) - 1): + s += [","] + s += [" "] + if newline: + s += ["\n"] + + s += [f"/{class_name}}}"] + return "".join(s) + + +@enforce_types +def dictStr(d: dict, newline=False) -> str: + if not d: + return "{}" + s = ["dict={"] + for i, (k, v) in enumerate(d.items()): + s += [f"'{k}':{v}"] + if i < (len(d) - 1): + s += [","] + s += [" "] + if newline: + s += ["\n"] + s += ["/dict}"] + return "".join(s) + + +@enforce_types +def asCurrency(amount, decimals: bool = True) -> str: + """Ref: https://stackoverflow.com/questions/21208376/converting-float-to-dollars-and-cents""" + if decimals: + if amount >= 0: + return f"${amount:,.2f}" + return f"-${-amount:,.2f}".format(-amount) + + if amount >= 0: + return f"${amount:,.0f}" + + return f"-${-amount:,.0f}" + + +@enforce_types +def prettyBigNum(amount, remove_zeroes: bool = True) -> str: + """Prints, for example: + 1.23e12, 123.4B, 1.23B, 123M, 1.23M, 123K, 1.23K, 123, + 1.23, 0.12, 1.23e-3, + 1e12, 100B, 1B, 100M, 1M, 100K, 1K, 100, 1 + + Remove zeros True vs False: 1.00M vs 1M + """ + if remove_zeroes: + amount = float(f"{amount:.2e}") # reduce to 3 sig figs + if amount == 0: + return "0" + + a = abs(amount) + + if a >= 1e12 or a < 1e-1: + s = format(a, ".2e").replace("e+", "e").replace("e0", "e").replace("e-0", "e-") + base = "e" + s = s.replace("e", "X") + elif a >= 1e9: + s = f"{a/1e9:.2f}X" + base = "B" + elif a >= 1e6: + s = f"{a/1e6:.2f}X" + base = "M" + elif a >= 1e3: + s = f"{a/1e3:.2f}X" + base = "K" + else: + s = f"{a:.2f}X" + base = "" + + if remove_zeroes: + s = s.replace("0X", "X").replace(".0X", "X") + + s = s.replace("X", base) + + if amount < 0: + s = "-" + s + + return s diff --git a/util/test/__init__.py b/util/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/util/test/test_base18.py b/util/test/test_base18.py new file mode 100644 index 0000000000000000000000000000000000000000..8f95d0ef003ed3e48a04dac799af8af10f76611a --- /dev/null +++ b/util/test/test_base18.py @@ -0,0 +1,15 @@ +from util.base18 import toBase18, fromBase18 + + +def test_toBase18(): + # assert toBase18(1.23e6) == 1230000000000000000000000 #not quite + assert toBase18(1.23) == 1230000000000000000 + assert toBase18(3e-18) == 3 + assert toBase18(1e-19) == 0 + + +def test_fromBase18(): + assert fromBase18(1230000000000000000000000) == 1.23e6 + assert fromBase18(1230000000000000000) == 1.23 + assert fromBase18(3) == 3e-18 + assert fromBase18(0) == 0 diff --git a/util/test/test_configutil.py b/util/test/test_configutil.py new file mode 100644 index 0000000000000000000000000000000000000000..a43a09269c7fb66f5da0a65893b2e48a005dc64c --- /dev/null +++ b/util/test/test_configutil.py @@ -0,0 +1,5 @@ +from util import configutil + + +def test1(): + assert isinstance(configutil.CONF_FILE_PATH, str) diff --git a/util/test/test_constants.py b/util/test/test_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..1241e9263962d5e8759ec45d952f1a34529860d4 --- /dev/null +++ b/util/test/test_constants.py @@ -0,0 +1,44 @@ +import brownie + +from util.constants import ( + INF, + S_PER_MIN, + S_PER_HOUR, + S_PER_DAY, + S_PER_WEEK, + S_PER_MONTH, + S_PER_YEAR, +) + + +def testINF(): + assert INF > 1.0 + + +def testSeconds(): + assert ( + 0 < S_PER_MIN < S_PER_HOUR < S_PER_DAY < S_PER_WEEK < S_PER_MONTH < S_PER_YEAR + ) + assert S_PER_HOUR == (60 * 60) + assert S_PER_WEEK == (60 * 60 * 24 * 7) + assert S_PER_YEAR == (60 * 60 * 24 * 365) + + assert isinstance(S_PER_HOUR, int) + assert isinstance(S_PER_DAY, int) + assert isinstance(S_PER_WEEK, int) + assert isinstance(S_PER_MONTH, int) + assert isinstance(S_PER_YEAR, int) + + +def test_network_connected(): + """ + Importing from util.constants includes: + brownie.network.connect("development") + """ + assert brownie.network.is_connected() + + +def test_have_accounts(): + assert brownie.network.is_connected() + # by default, brownie should have 10 accounts + assert brownie.network.accounts diff --git a/util/test/test_globaltokens.py b/util/test/test_globaltokens.py new file mode 100644 index 0000000000000000000000000000000000000000..601e1bb3a0face68dff615b7307a2b0d6a7cc080 --- /dev/null +++ b/util/test/test_globaltokens.py @@ -0,0 +1,6 @@ +from util import globaltokens + + +def test_OCEAN(): + assert globaltokens.OCEANtoken().symbol() == "OCEAN" + assert globaltokens.OCEAN_address()[:2] == "0x" diff --git a/util/test/test_mathutil.py b/util/test/test_mathutil.py new file mode 100644 index 0000000000000000000000000000000000000000..0128d83b2572f72699b256702dfc890e1afd8a18 --- /dev/null +++ b/util/test/test_mathutil.py @@ -0,0 +1,137 @@ +from enforce_typing import enforce_types + +import pytest + +from util.mathutil import ( + isNumber, + intInStr, + Range, + randunif, + round_sig, +) + + +@enforce_types +def testIsNumber(): + for x in [-2, 0, 2, 20000, -2.1, -2.0, 0.0, 2.0, 2.1, 2e6]: + assert isNumber(x) + + for x in [[], [1, 2], {}, {1: 2, 2: 3}, None, "", "foo"]: + assert not isNumber(x) + + +@enforce_types +def testIntInStr(): + assert intInStr("123") == 123 + assert intInStr("sdds12") == 12 + assert intInStr("sdds12afdsf3zz") == 123 + assert intInStr("sdds12afdsf39sf#@#@9fdsj!!49sd") == 1239949 + + assert intInStr("34.56") == 3456 + assert intInStr("0.00006") == 6 + assert intInStr("10.00006") == 1000006 + + with pytest.raises(ValueError): + intInStr("") + for v in [32, None, {}, []]: + with pytest.raises(TypeError): + intInStr(v) + + +@enforce_types +def testRange(): + r = Range(2.2) + p = r.drawRandomPoint() + assert p == 2.2 + + r = Range(-1.5, 2.5) + for _ in range(20): + p = r.drawRandomPoint() + assert -1.5 <= p <= 2.5 + + r = Range(2.3, None) + p = r.drawRandomPoint() + assert p == 2.3 + + r = Range(2.3, 2.3) + p = r.drawRandomPoint() + assert p == 2.3 + + with pytest.raises(AssertionError): + Range(3.0, 1.0) + + with pytest.raises(TypeError): + Range(3) + with pytest.raises(TypeError): + Range("foo") + with pytest.raises(TypeError): + Range(3.0, "foo") + + +@enforce_types +def testRangeStr(): + r = Range(2.2) + s = str(r) + assert "Range={" in s + assert "min_" in s + assert "2.2" in s + assert "Range}" in s + + +@enforce_types +def testRandunif(): + for _ in range(20): + # happy path + p = randunif(-1.5, 2.5) + assert -1.5 <= p <= 2.5 + + p = randunif(-1.5, -0.5) + assert -1.5 <= p <= -0.5 + + p = randunif(0.0, 100.0) + assert 0.0 <= p <= 100.0 + + # min = max + p = randunif(-2.0, -2.0) + assert p == -2.0 + + p = randunif(0.0, 0.0) + assert p == 0.0 + + p = randunif(2.0, 2.0) + assert p == 2.0 + + # exceptions + with pytest.raises(AssertionError): + p = randunif(0.0, -1.0) + + with pytest.raises(TypeError): + randunif(0.0, 3) + with pytest.raises(TypeError): + randunif(0, 3.0) + with pytest.raises(TypeError): + randunif(3.0, "foo") + + +@enforce_types +def test_round_sig(): + assert round_sig(123456, 1) == 100000 + assert round_sig(123456, 2) == 120000 + assert round_sig(123456, 3) == 123000 + assert round_sig(123456, 4) == 123500 + assert round_sig(123456, 5) == 123460 + assert round_sig(123456, 6) == 123456 + + assert round_sig(1.23456, 1) == 1.00000 + assert round_sig(1.23456, 2) == 1.20000 + assert round_sig(1.23456, 3) == 1.23000 + assert round_sig(1.23456, 4) == 1.23500 + assert round_sig(1.23456, 5) == 1.23460 + assert round_sig(1.23456, 6) == 1.23456 + + assert round_sig(1.23456e9, 1) == 1.00000e9 + assert round_sig(1.23456e9, 2) == 1.20000e9 + assert round_sig(1.23456e9, 3) == 1.23000e9 + assert round_sig(1.23456e9, 4) == 1.23500e9 + assert round_sig(1.23456e9, 5) == 1.23460e9 + assert round_sig(1.23456e9, 6) == 1.23456e9 diff --git a/util/test/test_plotutil.py b/util/test/test_plotutil.py new file mode 100644 index 0000000000000000000000000000000000000000..53bb42f2891b2bfc34d7778c2ed017950140b421 --- /dev/null +++ b/util/test/test_plotutil.py @@ -0,0 +1,85 @@ +from enforce_typing import enforce_types +import pytest + +from util.plotutil import ( + YParam, + _applyMult, + _multUnitStr, + _expandBOTHinY, + LINEAR, + LOG, + BOTH, + MULT1, + MULT100, + DIV1M, + DIV1B, + COUNT, + DOLLAR, + PERCENT, +) + + +@enforce_types +def test_yparam1(): + yparam = YParam( + ["headername1", "headername2"], + ["Header 1", "Header 2"], + "Header 1 and 2", + LINEAR, + MULT1, + COUNT, + ) + + assert yparam.y_header_names == ["headername1", "headername2"] + assert yparam.labels == ["Header 1", "Header 2"] + assert yparam.y_pretty_name == "Header 1 and 2" + assert yparam.y_scale == LINEAR + assert yparam.mult == MULT1 + assert yparam.unit == COUNT + assert yparam.y_scale_str == "LINEAR" + + +def test_yparam2_yscale(): + for (y_scale, y_scale_str) in [(LINEAR, "LINEAR"), (LOG, "LOG"), (BOTH, "BOTH")]: + + yparam = YParam(["foo1"], ["foo 1"], "Foo 1", y_scale, MULT1, COUNT) + assert yparam.y_scale == y_scale + assert yparam.y_scale_str == y_scale_str + + BAD_VALUE = 99 + yparam = YParam(["foo1"], ["foo 1"], "Foo 1", BAD_VALUE, MULT1, COUNT) + with pytest.raises(ValueError): + yparam.y_scale_str # pylint: disable=pointless-statement + + +@enforce_types +def test_applyMult(): + assert _applyMult([1.1, 1.201], MULT1) == [1.1, 1.201] + + assert pytest.approx(_applyMult([1.201], MULT1)[0]) == 1.201 + assert pytest.approx(_applyMult([1.201], MULT100)[0]) == 120.1 + assert pytest.approx(_applyMult([1.201], DIV1M)[0]) == 1.201e-6 + assert pytest.approx(_applyMult([1.201], DIV1B)[0]) == 1.201e-9 + + +@enforce_types +def test_multUnitStr(): + assert _multUnitStr(MULT1, DOLLAR) == "$" + assert _multUnitStr(DIV1M, DOLLAR) == "$M" + assert _multUnitStr(DIV1B, DOLLAR) == "$B" + assert _multUnitStr(MULT1, COUNT) == "count" + assert _multUnitStr(DIV1M, COUNT) == "count, in millions" + assert _multUnitStr(DIV1B, COUNT) == "count, in billions" + assert _multUnitStr(MULT100, PERCENT) == "%" + + +def test_expandBOTHinY(): + yparams = [YParam(["foo1"], ["foo 1"], "Foo 1", BOTH, MULT1, COUNT)] + yparams2 = _expandBOTHinY(yparams) + assert len(yparams2) == 2 + assert sorted([yparams2[0].y_scale, yparams2[1].y_scale]) == sorted([LINEAR, LOG]) + + +# not currently tested: +# _xyToPngs() +# csvToPngs() diff --git a/util/test/test_strutil.py b/util/test/test_strutil.py new file mode 100644 index 0000000000000000000000000000000000000000..530334dae35fd5af3d310c09b0835da2c704f09c --- /dev/null +++ b/util/test/test_strutil.py @@ -0,0 +1,332 @@ +import random + +from util import mathutil +from util.strutil import ( + StrMixin, + dictStr, + asCurrency, + prettyBigNum, +) + + +def testStrMixin(): + class Foo(StrMixin): + def __init__(self): + self.x = 1 + self.y = 2 + self.d = {"a": 3, "b": 4} + self.d2 = {} + self.__ignoreVal = "ignoreVal" # pylint: disable=unused-private-member + + def ignoreMethod(self): + pass + + f = Foo() + s = str(f) + s2 = s.replace(" ", "") + assert "Foo={" in s + assert "x=1" in s2 + assert "y=2" in s2 + assert "d=dict={" in s2 + assert "d2={}" in s2 + assert "'a':3" in s2 + assert "'b':4" in s2 + assert "Foo}" in s + assert "ignoreVal" not in s + assert "ignoreMethod" not in s + + +def testDictStr(): + d = {"a": 3, "b": 4} + s = dictStr(d) + s2 = s.replace(" ", "") + assert "dict={" in s + assert "'a':3" in s2 + assert "'b':4" in s2 + assert "dict}" in s + + +def testEmptyDictStr(): + d = {} + s = dictStr(d) + assert s == ("{}") + + +def testAsCurrency(): + assert asCurrency(0) == "$0.00" + assert asCurrency(0.0) == "$0.00" + assert asCurrency(10) == "$10.00" + assert asCurrency(10.0) == "$10.00" + assert asCurrency(1234.567) == "$1,234.57" + assert asCurrency(2e6) == "$2,000,000.00" + assert asCurrency(2e6 + 0.03) == "$2,000,000.03" + + assert asCurrency(0, decimals=False) == "$0" + assert asCurrency(0.0, False) == "$0" + assert asCurrency(10, False) == "$10" + assert asCurrency(10.0, False) == "$10" + assert asCurrency(1234.567, False) == "$1,235" + assert asCurrency(2e6, False) == "$2,000,000" + assert asCurrency(2e6 + 0.03, False) == "$2,000,000" + + +def testPrettyBigNum1_DoRemoveZeros_decimalsNeeded(): + assert prettyBigNum(1.23456e13) == "1.23e13" + assert prettyBigNum(1.23456e12) == "1.23e12" + assert prettyBigNum(1.23456e11) == "123B" + assert prettyBigNum(1.23456e10) == "12.3B" + assert prettyBigNum(1.23456e9) == "1.23B" + assert prettyBigNum(1.23456e8) == "123M" + assert prettyBigNum(1.23456e7) == "12.3M" + assert prettyBigNum(1.23456e6) == "1.23M" + assert prettyBigNum(1.23456e5) == "123K" + assert prettyBigNum(1.23456e4) == "12.3K" + assert prettyBigNum(1.23456e3) == "1.23K" + assert prettyBigNum(1.23456e2) == "123" + assert prettyBigNum(1.23456e1) == "12.3" + assert prettyBigNum(1.23456e0) == "1.23" + assert prettyBigNum(1.23456e-1) == "0.12" + assert prettyBigNum(1.23456e-2) == "1.23e-2" + assert prettyBigNum(1.23456e-3) == "1.23e-3" + assert prettyBigNum(1.23456e-10) == "1.23e-10" + + +def testPrettyBigNum1_DoRemoveZeros_decimalsNotNeeded(): + assert prettyBigNum(1e13) == "1e13" + assert prettyBigNum(1e12) == "1e12" + assert prettyBigNum(1e11) == "100B" + assert prettyBigNum(1e10) == "10B" + assert prettyBigNum(1e9) == "1B" + assert prettyBigNum(1e8) == "100M" + assert prettyBigNum(1e7) == "10M" + assert prettyBigNum(1e6) == "1M" + assert prettyBigNum(1e5) == "100K" + assert prettyBigNum(1e4) == "10K" + assert prettyBigNum(1e3) == "1K" + assert prettyBigNum(1e2) == "100" + assert prettyBigNum(1e1) == "10" + assert prettyBigNum(1) == "1" + assert prettyBigNum(1e-1) == "0.1" + assert prettyBigNum(1e-2) == "1e-2" + assert prettyBigNum(1e-3) == "1e-3" + assert prettyBigNum(1e-10) == "1e-10" + + +def testPrettyBigNum1_DoRemoveZeros_catchRoundoff(): + assert prettyBigNum(57.02e10) == "570B" + assert prettyBigNum(57.02e9) == "57B" + assert prettyBigNum(57.02e8) == "5.7B" + assert prettyBigNum(57.02e7) == "570M" + assert prettyBigNum(57.02e6) == "57M" + assert prettyBigNum(57.02e5) == "5.7M" + assert prettyBigNum(57.02e4) == "570K" + assert prettyBigNum(57.02e3) == "57K" + assert prettyBigNum(57.02) == "57" + assert prettyBigNum(27.02) == "27" + + +def testPrettyBigNum1_DoRemoveZeros_zero(): + assert prettyBigNum(0) == "0" + assert prettyBigNum(0.0) == "0" + + +def testPrettyBigNum1_DoRemoveZeros_negative(): + assert prettyBigNum(-1.23456e13) == "-1.23e13" + assert prettyBigNum(-1.23456e11) == "-123B" + assert prettyBigNum(-1.23456e7) == "-12.3M" + assert prettyBigNum(-1.23456e3) == "-1.23K" + assert prettyBigNum(-1.23456e2) == "-123" + assert prettyBigNum(-1.23456e-1) == "-0.12" + assert prettyBigNum(-1.23456e-3) == "-1.23e-3" + + assert prettyBigNum(-1e13) == "-1e13" + assert prettyBigNum(-1e10) == "-10B" + assert prettyBigNum(-1e7) == "-10M" + assert prettyBigNum(-1e5) == "-100K" + assert prettyBigNum(-1e1) == "-10" + assert prettyBigNum(-1e-1) == "-0.1" + assert prettyBigNum(-1e-10) == "-1e-10" + + +def generatePairsForPrettyBigNum2_Random_DoRemoveZeroes(): + for _ in range(100): + power = random.choice(list(range(-4, 14))) + sigfigs = random.choice([1, 2, 3, 4, 5]) + x = random.random() * pow(10, power) + x = mathutil.round_sig(x, sigfigs) + + s = prettyBigNum(x) # prettyBigNum(x, remove_zeroes=False) + print(f" ({x:s}, '{s:s}')," % (x, s)) + + +def testPrettyBigNum2_Random_DoRemoveZeroes(): + # these are generated via method above, then manually fixed as needed + x_s_pairs = [ + (1200.0, "1.2K"), + (5.284, "5.28"), + (2380000000000.0, "2.38e12"), + (0.071, "7.1e-2"), + (86000.0, "86K"), + (49300000.0, "49.3M"), + (4020000.0, "4.02M"), + (9600000000.0, "9.6B"), + (4800000000000.0, "4.8e12"), + (3000000.0, "3M"), + (6.256, "6.26"), + (89500.0, "89.5K"), + (156170000000.0, "156B"), + (80000.0, "80K"), + (710000000.0, "710M"), + (0.65312, "0.65"), + (553000000000.0, "553B"), + (0.04, "4e-2"), + (6.03e-05, "6.03e-5"), + (90300000.0, "90.3M"), + (828000000000.0, "828B"), + (0.09939, "9.94e-2"), + (5552000.0, "5.55M"), + (0.0004, "4e-4"), + (10000.0, "10K"), + (513000.0, "513K"), + (0.00097, "9.7e-4"), + (52325.0, "52.3K"), + (90000000.0, "90M"), + (0.00266, "2.66e-3"), + (400000.0, "400K"), + (400000.0, "400K"), + (107480000.0, "107M"), + (6785200000000.0, "6.79e12"), + (33680000.0, "33.7M"), + (625000.0, "625K"), + (52790000000.0, "52.8B"), + (51354000000.0, "51.4B"), + (71660.0, "71.7K"), + (2726000000000.0, "2.73e12"), + (671.6, "672"), + (10000000.0, "10M"), + (3415000000.0, "3.42B"), + (0.00272, "2.72e-3"), + (3000000.0, "3M"), + (0.0004171, "4.17e-4"), + (0.002181, "2.18e-3"), + (400000.0, "400K"), + (20000000000.0, "20B"), + (1.8458e-05, "1.85e-5"), + (403000.0, "403K"), + (3.81e-05, "3.81e-5"), + (2e-05, "2e-5"), + (6800000000.0, "6.8B"), + (1000000000000.0, "1e12"), + (4405300000000.0, "4.41e12"), + (0.0048122, "4.81e-3"), + (891000.0, "891K"), + (99000000.0, "99M"), + (50.0, "50"), + (0.128, "0.13"), + (23440000000.0, "23.4B"), + (41000.0, "41K"), + (7271100000000.0, "7.27e12"), + (3230000000000.0, "3.23e12"), + (64.99, "65"), + (740000000.0, "740M"), + (217000.0, "217K"), + (900.0, "900"), + (6.0, "6"), + (0.7631, "0.76"), + (0.04, "4e-2"), + (61700000.0, "61.7M"), + (0.0449, "4.49e-2"), + (737360000.0, "737M"), + (3415000000.0, "3.42B"), + (81244000000.0, "81.2B"), + (4.9e-05, "4.9e-5"), + (9493000.0, "9.49M"), + ] + for (x, target_s) in x_s_pairs: + assert prettyBigNum(x) == target_s + + +def testPrettyBigNum3_Random_DontRemoveZeros(): + # these are generated via method above, then manually fixed as needed + x_s_pairs = [ + (1200.0, "1.20K"), + (5.284, "5.28"), + (2380000000000.0, "2.38e12"), + (0.071, "7.10e-2"), + (86000.0, "86.00K"), + (49300000.0, "49.30M"), + (4020000.0, "4.02M"), + (9600000000.0, "9.60B"), + (4800000000000.0, "4.80e12"), + (3000000.0, "3.00M"), + (6.256, "6.26"), + (89500.0, "89.50K"), + (156170000000.0, "156.17B"), + (80000.0, "80.00K"), + (710000000.0, "710.00M"), + (0.65312, "0.65"), + (553000000000.0, "553.00B"), + (0.04, "4.00e-2"), + (6.03e-05, "6.03e-5"), + (90300000.0, "90.30M"), + (828000000000.0, "828.00B"), + (0.09939, "9.94e-2"), + (5552000.0, "5.55M"), + (0.0004, "4.00e-4"), + (10000.0, "10.00K"), + (513000.0, "513.00K"), + (0.00097, "9.70e-4"), + (52325.0, "52.33K"), + (90000000.0, "90.00M"), + (0.00266, "2.66e-3"), + (400000.0, "400.00K"), + (107480000.0, "107.48M"), + (6785200000000.0, "6.79e12"), + (33680000.0, "33.68M"), + (625000.0, "625.00K"), + (52790000000.0, "52.79B"), + (51354000000.0, "51.35B"), + (71660.0, "71.66K"), + (2726000000000.0, "2.73e12"), + (671.6, "671.60"), + (10000000.0, "10.00M"), + (3415000000.0, "3.42B"), + (0.00272, "2.72e-3"), + (3000000.0, "3.00M"), + (0.0004171, "4.17e-4"), + (0.002181, "2.18e-3"), + (400000.0, "400.00K"), + (20000000000.0, "20.00B"), + (1.8458e-05, "1.85e-5"), + (403000.0, "403.00K"), + (3.81e-05, "3.81e-5"), + (2e-05, "2.00e-5"), + (6800000000.0, "6.80B"), + (1000000000000.0, "1.00e12"), + (4405300000000.0, "4.41e12"), + (0.0048122, "4.81e-3"), + (891000.0, "891.00K"), + (99000000.0, "99.00M"), + (50.0, "50.00"), + (0.128, "0.13"), + (23440000000.0, "23.44B"), + (41000.0, "41.00K"), + (7271100000000.0, "7.27e12"), + (3230000000000.0, "3.23e12"), + (64.99, "64.99"), + (740000000.0, "740.00M"), + (217000.0, "217.00K"), + (900.0, "900.00"), + (6.0, "6.00"), + (0.7631, "0.76"), + (0.04, "4.00e-2"), + (61700000.0, "61.70M"), + (0.0449, "4.49e-2"), + (737360000.0, "737.36M"), + (3415000000.0, "3.42B"), + (81244000000.0, "81.24B"), + (4.9e-05, "4.90e-5"), + (9493000.0, "9.49M"), + ] + for (x, target_s) in x_s_pairs: + assert prettyBigNum(x, False) == target_s diff --git a/util/test/test_tx.py b/util/test/test_tx.py new file mode 100644 index 0000000000000000000000000000000000000000..66b78aed91d30b9669eba1bfd694c4e9766ea22f --- /dev/null +++ b/util/test/test_tx.py @@ -0,0 +1,13 @@ +import brownie + +from util.tx import txdict + + +def test_txdict(): + if not brownie.network.is_connected(): + brownie.network.connect("development") + + from_account = "foo_account" + d = txdict(from_account) + assert set(d.keys()) == set(["priority_fee", "max_fee", "from"]) + assert d["from"] == from_account diff --git a/util/test/test_valuation.py b/util/test/test_valuation.py new file mode 100644 index 0000000000000000000000000000000000000000..511cfc3e81fdd156dca447b95387651429e7ec4f --- /dev/null +++ b/util/test/test_valuation.py @@ -0,0 +1,5 @@ +from util.valuation import OCEANprice + + +def test_OCEANprice(): + assert OCEANprice(10e9, 1e9) == 10.0 diff --git a/util/tx.py b/util/tx.py new file mode 100644 index 0000000000000000000000000000000000000000..f55003e841b6c30cc80b01b75459fab0c56f2705 --- /dev/null +++ b/util/tx.py @@ -0,0 +1,31 @@ +"""tx utilities""" +import brownie +from brownie.network import chain + + +def txdict(from_account) -> dict: + """Return a tx dict that includes priority_fee and max_fee for EIP1559""" + priority_fee, max_fee = _fees() + return { + "from": from_account, + "priority_fee": priority_fee, + "max_fee": max_fee, + } + + +def transferETH(from_account, to_account, amount): + """ + Transfer ETH accounting for priority_fee and max_fee, for EIP1559. + Returns a TransactionReceipt instance. + """ + priority_fee, max_fee = _fees() + return from_account.transfer( + to_account, amount, priority_fee=priority_fee, max_fee=max_fee + ) + + +def _fees() -> tuple: + assert brownie.network.is_connected() + priority_fee = chain.priority_fee + max_fee = chain.base_fee + 2 * chain.priority_fee + return (priority_fee, max_fee) diff --git a/util/valuation.py b/util/valuation.py new file mode 100644 index 0000000000000000000000000000000000000000..53ad821154702846a505f86d603726193cee7ed6 --- /dev/null +++ b/util/valuation.py @@ -0,0 +1,8 @@ +from enforce_typing import enforce_types + + +@enforce_types +def OCEANprice(firm_valuation: float, OCEAN_supply: float) -> float: + """Return price of OCEAN token, in USD""" + assert OCEAN_supply > 0 + return firm_valuation / OCEAN_supply