diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b3c5706..3e3bb0f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,7 @@ -# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster -ARG VARIANT=3-bullseye -FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} +ARG VARIANT=3.11-bookworm +# requires rosetta 2 on apple chips +ARG PLATFORM=linux/amd64 +FROM --platform=${PLATFORM} mcr.microsoft.com/devcontainers/python:${VARIANT} # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="16" @@ -31,13 +32,19 @@ RUN echo 'eval "$(mcfly init zsh)"' >> ~/.zshrc \ # Install brownie & other devtools -# COPY requirements.txt /tmp/pip-tmp/ -RUN wget https://raw.githubusercontent.com/eth-brownie/brownie/master/requirements.txt -O /tmp/pip-requirements.txt +# The vyper dependency in the brownie requirements file currently breaks the devcontainer build. +# Should this change in the future consider to re-enable the line below +# RUN wget https://raw.githubusercontent.com/eth-brownie/brownie/master/requirements.txt -O /tmp/pip-requirements.txt -RUN pip install -r /tmp/pip-requirements.txt +RUN mkdir -p /tmp/brownie +COPY .devcontainer/requirements.txt /tmp/brownie +RUN pip install -r /tmp/brownie/requirements.txt RUN pip install eth-brownie \ && pip install fastapi \ - && pip install uvicorn + && pip install uvicorn \ + && pip install onepassword-sdk \ + && pip install simple_term_menu \ + && pip install questionary # [Optional] Uncomment this line to install global node packages. RUN npm install -g ganache solhint prettier prettier-plugin-solidity solhint-plugin-prettier diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cc41558..f223bf3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,15 +3,31 @@ { "name": "gif-contracts", "dockerComposeFile": "docker-compose.yaml", - "service": "brownie", - "workspaceFolder": "/workspace", + "service": "brownie", + "workspaceFolder": "/workspace", // Set *default* container specific settings.json values on container create. - "settings": { - //"terminal.integrated.shell.linux": "/bin/bash" - "editor.fontFamily": "'JetBrainsMono Nerd Font Mono', Menlo, Monaco, 'Courier New', monospace", - "editor.fontSize": 13, + "customizations": { + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "juanblanco.solidity", + "tintinweb.solidity-visual-auditor", + "github.vscode-pull-request-github", + "github.copilot", + "mhutchie.git-graph", + "eamodio.gitlens", + "gruntfuggly.todo-tree", + "oderwat.indent-rainbow", + "2gua.rainbow-brackets", + "johnpapa.vscode-peacock", + "vikas.code-navigation", + "ms-azuretools.vscode-docker", + "ms-python.black-formatter" + ] + } }, - // "features": { // // "github-cli": "latest", // "docker-from-docker": { @@ -19,30 +35,16 @@ // "moby": true // } // }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "juanblanco.solidity", - "tintinweb.solidity-visual-auditor", - "github.vscode-pull-request-github", - "github.copilot", - "mhutchie.git-graph", - "eamodio.gitlens", - "gruntfuggly.todo-tree", - "oderwat.indent-rainbow", - "2gua.rainbow-brackets", - "johnpapa.vscode-peacock", - "vikas.code-navigation", - ], - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [8000], - + "forwardPorts": [ + 8000 + ], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "touch .env && brownie compile --all && python --version", - + // This reads the 1password environment variables from the .env.op file and exports them to the container. + "postCreateCommand": "touch .env && brownie compile --all && python --version && grep -v '^#' /home/vscode/.env.op | sed 's/^/export /' >> /home/vscode/.zshrc", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + "remoteUser": "vscode", + "mounts": [ + "source=${localEnv:HOME}/.env.op,target=/home/vscode/.env.op,type=bind,consistency=cached" + ] } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index ff1ad09..5c1c7b9 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -9,7 +9,8 @@ services: context: .. dockerfile: .devcontainer/Dockerfile args: - VARIANT: 3.9-bullseye + # VARIANT: 3.9-bullseye + VARIANT: 3.11-bookworm USER_UID: 1000 USER_GID: 1000 INSTALL_NODE: "true" diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt new file mode 100644 index 0000000..6d08031 --- /dev/null +++ b/.devcontainer/requirements.txt @@ -0,0 +1,245 @@ +# Pip file for brownie with vyper package commented out. +# File content from line below. +# RUN wget https://raw.githubusercontent.com/eth-brownie/brownie/master/requirements.txt -O /tmp/pip-requirements.txt + + +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile requirements.in +# +aiohttp==3.9.3 + # via web3 +aiosignal==1.3.1 + # via aiohttp +asttokens==2.4.1 + # via vyper +attrs==23.2.0 + # via + # aiohttp + # hypothesis + # jsonschema + # pytest + # referencing +bitarray==2.9.2 + # via eth-account +black==24.2.0 + # via -r requirements.in +cbor2==5.6.2 + # via vyper +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via black +cytoolz==0.12.3 + # via eth-utils +dataclassy==0.11.1 + # via eip712 +eip712==0.2.4 + # via -r requirements.in +eth-abi==5.0.0 + # via + # -r requirements.in + # eip712 + # eth-account + # eth-event + # web3 +eth-account==0.10.0 + # via + # -r requirements.in + # eip712 + # web3 +eth-event==1.2.5 + # via -r requirements.in +eth-hash[pycryptodome]==0.6.0 + # via + # -r requirements.in + # eip712 + # eth-event + # eth-utils + # web3 +eth-keyfile==0.7.0 + # via eth-account +eth-keys==0.5.0 + # via + # eth-account + # eth-keyfile +eth-rlp==1.0.1 + # via eth-account +eth-typing==3.5.2 + # via + # eip712 + # eth-abi + # eth-keys + # eth-utils + # web3 +eth-utils==2.3.1 + # via + # -r requirements.in + # eip712 + # eth-abi + # eth-account + # eth-event + # eth-keyfile + # eth-keys + # eth-rlp + # rlp + # web3 +execnet==2.0.2 + # via pytest-xdist +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +hexbytes==0.3.1 + # via + # -r requirements.in + # eip712 + # eth-account + # eth-event + # eth-rlp + # web3 +hypothesis==6.27.3 + # via -r requirements.in +idna==3.6 + # via + # requests + # yarl +importlib-metadata==7.0.1 + # via vyper +iniconfig==2.0.0 + # via pytest +jsonschema==4.21.1 + # via web3 +jsonschema-specifications==2023.12.1 + # via jsonschema +lazy-object-proxy==1.10.0 + # via -r requirements.in +lru-dict==1.2.0 + # via web3 +multidict==6.0.5 + # via + # aiohttp + # yarl +mypy-extensions==1.0.0 + # via black +packaging==23.2 + # via + # black + # pytest + # vyper +parsimonious==0.9.0 + # via eth-abi +pathspec==0.12.1 + # via black +platformdirs==4.2.0 + # via black +pluggy==1.4.0 + # via pytest +prompt-toolkit==3.0.43 + # via -r requirements.in +protobuf==4.25.3 + # via web3 +psutil==5.9.8 + # via -r requirements.in +py==1.11.0 + # via + # -r requirements.in + # pytest + # pytest-forked +py-solc-ast==1.2.10 + # via -r requirements.in +py-solc-x==1.1.1 + # via -r requirements.in +pycryptodome==3.20.0 + # via + # eth-hash + # eth-keyfile + # vyper +pygments==2.17.2 + # via + # -r requirements.in + # pygments-lexer-solidity +pygments-lexer-solidity==0.7.0 + # via -r requirements.in +pytest==6.2.5 + # via + # -r requirements.in + # pytest-forked + # pytest-xdist +pytest-forked==1.6.0 + # via pytest-xdist +pytest-xdist==1.34.0 + # via -r requirements.in +python-dotenv==0.16.0 + # via -r requirements.in +pyunormalize==15.1.0 + # via web3 +pyyaml==6.0.1 + # via -r requirements.in +referencing==0.33.0 + # via + # jsonschema + # jsonschema-specifications +regex==2023.12.25 + # via parsimonious +requests==2.31.0 + # via + # -r requirements.in + # py-solc-x + # vvm + # web3 +rlp==4.0.0 + # via + # -r requirements.in + # eth-account + # eth-rlp +rpds-py==0.18.0 + # via + # jsonschema + # referencing +semantic-version==2.10.0 + # via + # -r requirements.in + # py-solc-x + # vvm +six==1.16.0 + # via + # asttokens + # pytest-xdist +sortedcontainers==2.4.0 + # via hypothesis +toml==0.10.2 + # via pytest +toolz==0.12.1 + # via cytoolz +tqdm==4.66.2 + # via -r requirements.in +typing-extensions==4.9.0 + # via + # eth-rlp + # eth-typing + # web3 +urllib3==2.2.1 + # via requests +vvm==0.1.0 + # via -r requirements.in +# vyper==0.3.10 + # via -r requirements.in +wcwidth==0.2.13 + # via prompt-toolkit +web3==6.15.1 + # via -r requirements.in +websockets==12.0 + # via web3 +wheel==0.42.0 + # via vyper +wrapt==1.16.0 + # via -r requirements.in +yarl==1.9.4 + # via aiohttp +zipp==3.17.0 + # via importlib-metadata diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5221af5..8953ca8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,14 +13,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.x uses: actions/setup-python@v4 with: python-version: '3.x' - name: Setup node environment - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16 diff --git a/.solhint.json b/.solhint.json index 3fdeddd..1523887 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,8 +2,23 @@ "extends": "solhint:recommended", "plugins": [], "rules": { - "compiler-version": ["error", "0.8.2"], - "func-visibility": ["warn", {"ignoreConstructors": true}], - "reason-string": ["warn",{"maxLength": 64}] + "compiler-version": [ + "error", + "0.8.2" + ], + "func-visibility": [ + "warn", + { + "ignoreConstructors": true + } + ], + "reason-string": [ + "warn", + { + "maxLength": 64 + } + ], + "custom-errors": "off", + "no-global-import": "off" } -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 735d636..065e081 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,5 +31,13 @@ "titleBar.activeForeground": "#e7e7e7", "titleBar.inactiveBackground": "#1d3c4399", "titleBar.inactiveForeground": "#e7e7e799" - } + }, + "security.olympix.project.includePath": "/contracts", + "solidity.defaultCompiler": "remote", + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "editor.formatOnSave": true + } \ No newline at end of file diff --git a/README.md b/README.md index 8064b5e..3a3ef9c 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,34 @@ In this case replace the `auto` keyword in the command with the number of execut Deployments to live networks can be done with brownie console as well. -Example for the deployment to Polygon test +### Setup Deployment Secrets + +Deployments typically use wallets etc and we need to ensure that secrets are not exposed. +We use 1password to store secrets in vaults, which are accessed programatically via the onepassword-sdk. +The access tokens are stored in environment variables which are not part of the devcontainer, but +are imported when the devcontainer is built. +The solution uses two environment variables: +* `OP_VAULT` designates the vault under which the secrets are stored in 1password +* `OP_SERVICE_ACCOUNT_TOKEN` designates the service account token which is used to access the secrets. + +To use this system, create a `.env.op` file in your local home folder. In the file, store the two values: +``` +OP_VAULT="MY VAULT" +OP_SERVICE_ACCOUNT_TOKEN="mysecrettoken..." +``` +When the devcontainer is rebuild, the values are read from this file and stored as environment variables in your devcontainer. + +In the vault, create items for each chain you will work with. +The item name needs to follow this pattern: + +` secrets` + +where `` is the output of the brownie `network.show_active()` command. + +In the item, store each Mnemonic and each Address to be used in the deployment in two different sections "Mnemonics" and "Addresses". + + +### Example for the deployment to Polygon test ```bash brownie console --network polygon-test diff --git a/brownie-config.yaml b/brownie-config.yaml index 298a54a..02e14b7 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -23,7 +23,8 @@ compiler: - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.7.3" - "@chainlink=smartcontractkit/chainlink@1.6.0" - "@etherisc/gif-interface=etherisc/gif-interface@3b0002a" - + metadata: + useLiteralContent: true # packages below will be added to brownie # you may use 'brownie pm list' after 'brownie compile' # to list the packages installed via the dependency list below @@ -37,22 +38,22 @@ dependencies: # exclude open zeppeling contracts when calculating test coverage # https://eth-brownie.readthedocs.io/en/v1.10.3/config.html#exclude_paths reports: - exclude_contracts: - # chainlink - - ChainlinkClient - - Operator - # openzeppelin - - AccessControl - - AccessControlEnumerable - - Context - - Ownable - - EnumerableMap - - EnumerableSet - - ERC1967Proxy - - ERC20 - - ERC721 - - IERC20 - - IERC721 - - Initializable - - SafeERC20 - - Strings + exclude_contracts: + # chainlink + - ChainlinkClient + - Operator + # openzeppelin + - AccessControl + - AccessControlEnumerable + - Context + - Ownable + - EnumerableMap + - EnumerableSet + - ERC1967Proxy + - ERC20 + - ERC721 + - IERC20 + - IERC721 + - Initializable + - SafeERC20 + - Strings diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol deleted file mode 100644 index 4e0c8ad..0000000 --- a/contracts/Migrations.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.2; - -contract Migrations { - address public owner; - uint256 public last_completed_migration; // solhint-disable-line - - constructor() { - owner = msg.sender; - } - - modifier restricted() { - if (msg.sender == owner) _; - } - - function setCompleted(uint256 _completed) public restricted { - last_completed_migration = _completed; - } - - function upgrade(address _newAddress) public restricted { - Migrations upgraded = Migrations(_newAddress); - upgraded.setCompleted(last_completed_migration); - } -} diff --git a/contracts/examples/AyiiClf.sol b/contracts/examples/AyiiClf.sol new file mode 100644 index 0000000..b1bd721 --- /dev/null +++ b/contracts/examples/AyiiClf.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.2; + +interface AyiiClf { + function sendClfRequest( + bytes calldata input + ) external returns (bytes32 requestId); +} diff --git a/contracts/examples/AyiiOracle.clf b/contracts/examples/AyiiOracle.clf new file mode 100644 index 0000000..2bc9f95 --- /dev/null +++ b/contracts/examples/AyiiOracle.clf @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +// NEU: mit Chainlink Funktion + +import "./strings.sol"; + +// import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; +import "@etherisc/gif-interface/contracts/components/Oracle.sol"; + +contract AyiiOracle is Oracle, FunctionsClient, ConfirmedOwner { + using strings for bytes32; + using FunctionsRequest for FunctionsRequest.Request; + + mapping(bytes32 /* Chainlink request ID */ => uint256 /* GIF request ID */) + public gifRequests; + // DON Network parameters + // router and donID can be found on https://docs.chain.link/chainlink-functions/supported-networks + // subscriptionId can be found on https://functions.chain.link/ after loggin in with the chainlink subscription account + // gasLimit should be estimated + // source is the code text of the javascript function to be executed by the DON + address public router; + uint64 public subscriptionId; + uint32 public gasLimit; + bytes32 public donID; + string public source; + + event LogAyiiRequest(uint256 requestId, bytes32 chainlinkRequestId); + + event LogAyiiFulfill( + uint256 requestId, + bytes32 chainlinkRequestId, + bytes response + ); + constructor( + bytes32 _name, + address _registry, + address _router, + uint64 _subscriptionId, + uint32 _gasLimit, + bytes32 _donID, + string memory _source + ) + Oracle(_name, _registry) + FunctionsClient(_router) + ConfirmedOwner(msg.sender) + { + router = _router; + subscriptionId = _subscriptionId; + gasLimit = _gasLimit; + donID = _donID; + source = _source; + } + + function updateChainlinkFunctionParameters( + address _router, + uint64 _subscriptionId, + uint32 _gasLimit, + bytes32 _donID, + string memory _source + ) external onlyOwner { + router = _router; + subscriptionId = _subscriptionId; + gasLimit = _gasLimit; + donID = _donID; + source = _source; + } + + function request( + uint256 gifRequestId, + bytes calldata input + ) external override onlyQuery { + FunctionsRequest.Request memory req; + + (bytes32 projectId, bytes32 uaiId, bytes32 cropId) = abi.decode( + input, + (bytes32, bytes32, bytes32) + ); + + bytes[] memory bytesArgs = [ + projectId.toB32String(), + uaiId.toB32String(), + cropId.toB32String() + ]; + + req.initializeRequestForInlineJavaScript(source); + req.setBytesArgs(bytesArgs); + + bytes32 chainlinkRequestId = _sendRequest( + req.encodeCBOR(), + subscriptionId, + gasLimit, + donID + ); + + gifRequests[chainlinkRequestId] = gifRequestId; + emit LogAyiiRequest(gifRequestId, chainlinkRequestId); + } + /* +function fulfillRequest( + bytes32 requestId, + bytes memory response, + bytes memory err + ) internal override { + if (s_lastRequestId != requestId) { + revert UnexpectedRequestID(requestId); + } + s_lastResponse = response; + s_lastError = err; + emit Response(requestId, s_lastResponse, s_lastError); + } +*/ + function fulfillRequest( + bytes32 requestId, + bytes memory response, + bytes memory err + ) internal override { + uint256 gifRequest = gifRequests[requestId]; + if (gifRequest == 0) { + revert("Unexpected Request Id"); + } + _respond(gifRequest, response); + + delete gifRequests[chainlinkRequestId]; + emit LogAyiiFulfill(gifRequest, requestId, response); + } +} diff --git a/contracts/examples/AyiiOracle.sol b/contracts/examples/AyiiOracle.sol index c6f6049..6a87e08 100644 --- a/contracts/examples/AyiiOracle.sol +++ b/contracts/examples/AyiiOracle.sol @@ -1,151 +1,133 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.2; -import "./strings.sol"; +// new: Implementation with Chainlink Functions. -import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; +import "./strings.sol"; +import "./AyiiClf.sol"; import "@etherisc/gif-interface/contracts/components/Oracle.sol"; -contract AyiiOracle is - Oracle, ChainlinkClient -{ +/// @title AyiiOracle +/// @notice This contract is the implementation of the Oracle contract that uses the Chainlink DON network. +/// @custom:security +/// - We assume the security of the Chainlink DON network and the AyiiClfConsumer contract. +/// - The contract owner should be the `oracleProvider` account. +/// - We assume that the owner of this contract is trustworthy, +/// as they can set the address of the AyiiClfConsumer contract anytime. +/// - The contract is automatically registered in the registry +/// when deployed by inheriting the Oracle contract. +/// - Only the contract owner can set the address of the AyiiClfConsumer contract. +/// - The request function can only be called by the GIF framework, i.e. the Query module. +/// - The fulfillClfRequest function can only be called by the AyiiClfConsumer contract, +/// which in turn can only be called by the Chainlink DON framework. +/// @custom:deployment +/// - The contract is deployed by the `oracleProvider` account. +/// - Deployment sequence: +/// The contract must be deployed before the deployment of the product contract, +/// because the product contract deployment requires the address of the oracle contract. +/// - The address of the AyiiClfConsumer contract must be set after deployment, this +/// can be done anytime after deployment, but before the first request is sent. + +contract AyiiOracle is Oracle { using strings for bytes32; - using Chainlink for Chainlink.Request; - - mapping(bytes32 /* Chainlink request ID */ => uint256 /* GIF request ID */) public gifRequests; - bytes32 public jobId; - uint256 public payment; - - event LogAyiiRequest(uint256 requestId, bytes32 chainlinkRequestId); - - event LogAyiiFulfill( - uint256 requestId, - bytes32 chainlinkRequestId, - bytes32 projectId, - bytes32 uaiId, - bytes32 cropId, - uint256 aaay - ); - - constructor( - bytes32 _name, - address _registry, - address _chainLinkToken, - address _chainLinkOperator, - bytes32 _jobId, - uint256 _payment - ) - Oracle(_name, _registry) - { - updateRequestDetails( - _chainLinkToken, - _chainLinkOperator, - _jobId, - _payment); - } - function updateRequestDetails( - address _chainLinkToken, - address _chainLinkOperator, - bytes32 _jobId, - uint256 _payment - ) - public - onlyOwner - { - if (_chainLinkToken != address(0)) { setChainlinkToken(_chainLinkToken); } - if (_chainLinkOperator != address(0)) { setChainlinkOracle(_chainLinkOperator); } - - jobId = _jobId; - payment = _payment; + AyiiClf public ayiiClf; + modifier onlyAyiiClf() { + require( + _msgSender() == address(ayiiClf), + "ERROR:AYII-001:ACCESS_DENIED" + ); + _; } - function request(uint256 gifRequestId, bytes calldata input) - external override - onlyQuery - { - Chainlink.Request memory request_ = buildChainlinkRequest( - jobId, - address(this), - this.fulfill.selector - ); + /// @dev Mapping of Chainlink request IDs to GIF request IDs. + mapping(bytes32 /* Chainlink request ID */ => uint256 /* GIF request ID */) + public gifRequests; - ( - bytes32 projectId, - bytes32 uaiId, - bytes32 cropId - ) = abi.decode(input, (bytes32, bytes32, bytes32)); + event LogAyiiRequest(uint256 requestId, bytes32 chainlinkRequestId); - request_.add("projectId", projectId.toB32String()); - request_.add("uaiId", uaiId.toB32String()); - request_.add("cropId", cropId.toB32String()); + event LogAyiiFulfill( + uint256 requestId, + bytes32 chainlinkRequestId, + bytes response + ); - bytes32 chainlinkRequestId = sendChainlinkRequest(request_, payment); + event LogAyiiFulfillError( + uint256 requestId, + bytes32 chainlinkRequestId, + bytes err + ); - gifRequests[chainlinkRequestId] = gifRequestId; - emit LogAyiiRequest(gifRequestId, chainlinkRequestId); + constructor(bytes32 _name, address _registry) Oracle(_name, _registry) { + // NOOP } - function fulfill( - bytes32 chainlinkRequestId, - bytes32 projectId, - bytes32 uaiId, - bytes32 cropId, - uint256 aaay - ) - public recordChainlinkFulfillment(chainlinkRequestId) - { - uint256 gifRequest = gifRequests[chainlinkRequestId]; - bytes memory data = abi.encode(projectId, uaiId, cropId, aaay); - _respond(gifRequest, data); - - delete gifRequests[chainlinkRequestId]; - emit LogAyiiFulfill(gifRequest, chainlinkRequestId, projectId, uaiId, cropId, aaay); - } + /// @dev Here we set the address of the AyiiClfConsumer contract. + /// The AyiiClfConsumer contract is the one that will send the request to the Chainlink DON network. + /// AyiiClfConsumer is maintained in a separate repository because it uses the 0.8.19 version of Solidity + /// which is incompatible with this repo, which uses the 0.8.2 version of Solidity. + /// @param _ayiiClf The address of the AyiiClfConsumer contract. - function cancel(uint256 requestId) - external override - onlyOwner - { - // TODO mid/low priority - // cancelChainlinkRequest(_requestId, _payment, _callbackFunctionId, _expiration); + function setClfGateWay(address _ayiiClf) external onlyOwner { + ayiiClf = AyiiClf(_ayiiClf); } - // only used for testing of chainlink operator - function encodeFulfillParameters( - bytes32 chainlinkRequestId, - bytes32 projectId, - bytes32 uaiId, - bytes32 cropId, - uint256 aaay - ) - external - pure - returns(bytes memory parameterData) - { - return abi.encode( - chainlinkRequestId, - projectId, - uaiId, - cropId, - aaay + /// @dev This function is called by the GIF framework and sends a request to the Chainlink DON network. + /// @param gifRequestId The ID of the request. + /// @param input The input data for the request. + /// input data is abi encoded (bytes32, bytes32, bytes32) where: + /// - the first bytes32 is the projectId e.g. "1234" + /// - the second bytes32 is the uaiId e.g. "134" + /// - the third bytes32 is the cropId e.g. "Greengrams" + /// The abi encoding is already done in the product contract so we don't need to do it here. + function request( + uint256 gifRequestId, + bytes calldata input + ) external override onlyQuery { + require( + address(ayiiClf) != address(0), + "ERROR:AYII-002:GATEWAY_NOT_SET" ); - } + bytes32 chainlinkRequestId = ayiiClf.sendClfRequest(input); - function getChainlinkJobId() external view returns(bytes32 chainlinkJobId) { - return jobId; + gifRequests[chainlinkRequestId] = gifRequestId; + emit LogAyiiRequest(gifRequestId, chainlinkRequestId); } - function getChainlinkPayment() external view returns(uint256 paymentAmount) { - return payment; + /// @dev This function is called by the AyiiClfConsumer contract when it receives a response from the Chainlink DON network. + /// @param requestId The chainlink functions request ID of the request. + /// @param response The response from the Chainlink DON network. + /// @param err The error message if the request failed. + /// @notice Either response or err will be filled. + /// In case of an error, the error message will be logged and the function will return so the request can be retried. + /// In this case, the request has to be cancelled in the product with the cancelOracleRequest function. + function fulfillClfRequest( + bytes32 requestId, + bytes memory response, + bytes memory err + ) external onlyAyiiClf { + uint256 gifRequest = gifRequests[requestId]; + if (gifRequest == 0) { + revert("ERROR:AYII-003:UNEXPECTED_REQUEST_ID"); + } + if (err.length > 0) { + // don't revert so we can log the error + emit LogAyiiFulfillError(gifRequest, requestId, err); + return; + } + _respond(gifRequest, response); + + delete gifRequests[requestId]; + emit LogAyiiFulfill(gifRequest, requestId, response); } - function getChainlinkToken() external view returns(address linkTokenAddress) { - return chainlinkTokenAddress(); - } + /// @dev This function needs to exist due to the interface, but it is not used. + /// not implemented / no implementation needed + /// to cancel a request on our side, use product.cancelOracleRequest(policyId) + /// to cancel a request on the Chainlink side, use functions.chainlink.com + /// Chainlink functions request run in a timeout after 5 minutes, so no need to cancel them; + /// the timeout needs to be confirmed in the Functions web interface. - function getChainlinkOperator() external view returns(address operator) { - return chainlinkOracleAddress(); - } + // solhint-disable-next-line no-empty-blocks + function cancel(uint256 requestId) external override onlyQuery {} } - diff --git a/contracts/examples/UsdcAccounting.sol b/contracts/examples/UsdcAccounting.sol new file mode 100644 index 0000000..035a4a0 --- /dev/null +++ b/contracts/examples/UsdcAccounting.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; + +contract UsdcAccounting is ERC20Permit { + + string public constant NAME = "USD Coin - Accounting Token"; + string public constant SYMBOL = "USDC-AT"; + uint8 public constant DECIMALS = 6; + uint256 public constant INITIAL_SUPPLY = 10**24; + + constructor() + ERC20(NAME, SYMBOL) + ERC20Permit(NAME) + { + _mint( + _msgSender(), + INITIAL_SUPPLY + ); + } + + function decimals() public view virtual override returns (uint8) { + return DECIMALS; + } +} diff --git a/contracts/generic/GenericOracle.sol b/contracts/generic/GenericOracle.sol new file mode 100644 index 0000000..a21e8db --- /dev/null +++ b/contracts/generic/GenericOracle.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import "@etherisc/gif-interface/contracts/components/Oracle.sol"; + +contract GenericOracle is Oracle { + constructor(bytes32 _name, address _registry) Oracle(_name, _registry) { + // not implemented + } + + function request( + uint256 gifRequestId, + bytes calldata input + ) external override { + // not implemented + } + + function cancel(uint256 requestId) external override { + // not implemented + } +} diff --git a/contracts/generic/GenericProduct.sol b/contracts/generic/GenericProduct.sol new file mode 100644 index 0000000..2ba2fac --- /dev/null +++ b/contracts/generic/GenericProduct.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +import "@etherisc/gif-interface/contracts/components/Product.sol"; + +contract GenericProduct is Product { + bytes32 public constant POLICY_FLOW = ""; + + constructor( + bytes32 productName, + address registry, + address token, + uint256 riskpoolId, + address insurer + ) Product(productName, token, POLICY_FLOW, riskpoolId, registry) {} +} diff --git a/contracts/generic/GenericRiskPool.sol b/contracts/generic/GenericRiskPool.sol new file mode 100644 index 0000000..5773ee6 --- /dev/null +++ b/contracts/generic/GenericRiskPool.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.2; + +import "@etherisc/gif-interface/contracts/components/BasicRiskpool.sol"; + +contract GenericRiskpool is BasicRiskpool { + constructor( + bytes32 name, + uint256 collateralization, + address erc20Token, + address wallet, + address registry + ) BasicRiskpool(name, collateralization, 0, erc20Token, wallet, registry) { + // not implemented + } + + function bundleMatchesApplication( + IBundle.Bundle memory bundle, + IPolicy.Application memory application + ) public pure override returns (bool isMatching) { + isMatching = true; + } +} diff --git a/contracts/generic/ProtectedMulticall.sol b/contracts/generic/ProtectedMulticall.sol new file mode 100644 index 0000000..1f2e8b9 --- /dev/null +++ b/contracts/generic/ProtectedMulticall.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.2; + +/// @title Multicall - Aggregate results from multiple read-only function calls +/// @author Michael Elliot +/// @author Joshua Levine +/// @author Nick Johnson +/// @notice added onlyOwner so we can restrict access to this contract +contract ProtectedMulticall { + address public owner; + + modifier onlyOwner() { + require(msg.sender == owner, "ProtectedMulticall: only owner"); + _; + } + struct Call { + address target; + bytes callData; + } + + constructor() { + owner = msg.sender; + } + function aggregate( + Call[] calldata calls + ) + public + onlyOwner + returns (uint256 blockNumber, bytes[] memory returnData) + { + blockNumber = block.number; + returnData = new bytes[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory ret) = calls[i].target.call( + calls[i].callData + ); + require(success); + returnData[i] = ret; + } + } +} diff --git a/scripts/ayii_product.py b/scripts/ayii_product.py index d195172..cbbfc19 100644 --- a/scripts/ayii_product.py +++ b/scripts/ayii_product.py @@ -6,8 +6,9 @@ from brownie.network.account import Account from brownie import ( + history, Wei, - Contract, + Contract, PolicyController, OracleService, ComponentOwnerService, @@ -15,11 +16,12 @@ AyiiRiskpool, AyiiProduct, AyiiOracle, - ChainlinkOperator, - ChainlinkToken, + ChainlinkOperator, + ChainlinkToken, ) from scripts.util import ( + wait_for_confirmations, get_account, encode_function_data, # s2h, @@ -31,40 +33,45 @@ from scripts.instance import GifInstance -RISKPOOL_NAME = 'AyiiRiskpool' -ORACLE_NAME = 'AyiiOracle' -PRODUCT_NAME = 'AyiiProduct' +RISKPOOL_NAME = "AyiiRiskpool" +ORACLE_NAME = "AyiiOracle" +PRODUCT_NAME = "AyiiProduct" + class GifAyiiRiskpool(object): - def __init__(self, - instance: GifInstance, + def __init__( + self, + instance: GifInstance, erc20Token: Account, - riskpoolKeeper: Account, + riskpoolKeeper: Account, riskpoolWallet: Account, investor: Account, - collateralization:int, - name=RISKPOOL_NAME, - publishSource=False + collateralization: int, + name=RISKPOOL_NAME, + publishSource=False, ): instanceService = instance.getInstanceService() instanceOperatorService = instance.getInstanceOperatorService() componentOwnerService = instance.getComponentOwnerService() riskpoolService = instance.getRiskpoolService() - print('------ setting up riskpool ------') + print("------ setting up riskpool ------") riskpoolKeeperRole = instanceService.getRiskpoolKeeperRole() - print('1) grant riskpool keeper role {} to riskpool keeper {}'.format( - riskpoolKeeperRole, riskpoolKeeper)) + print( + "1) grant riskpool keeper role {} to riskpool keeper {}".format( + riskpoolKeeperRole, riskpoolKeeper + ) + ) instanceOperatorService.grantRole( - riskpoolKeeperRole, - riskpoolKeeper, - {'from': instance.getOwner()}) + riskpoolKeeperRole, + riskpoolKeeper, + {"from": instanceOperatorService.owner()}, + ) - print('2) deploy riskpool by riskpool keeper {}'.format( - riskpoolKeeper)) + print("2) deploy riskpool by riskpool keeper {}".format(riskpoolKeeper)) self.riskpool = AyiiRiskpool.deploy( s2b32(name), @@ -72,168 +79,174 @@ def __init__(self, erc20Token, riskpoolWallet, instance.getRegistry(), - {'from': riskpoolKeeper}, - publish_source=publishSource) - - print('3) investor role granting to investor {} by riskpool keeper {}'.format( - investor, riskpoolKeeper)) + {"from": riskpoolKeeper}, + publish_source=publishSource, + ) + + tx = history[-1] + wait_for_confirmations(tx) + + print( + "3) investor role granting to investor {} by riskpool keeper {}".format( + investor, riskpoolKeeper + ) + ) self.riskpool.grantInvestorRole( investor, - {'from': riskpoolKeeper}, + {"from": riskpoolKeeper}, ) - print('4) riskpool {} proposing to instance by riskpool keeper {}'.format( - self.riskpool, riskpoolKeeper)) - - componentOwnerService.propose( - self.riskpool, - {'from': riskpoolKeeper}) + print( + "4) riskpool {} proposing to instance by riskpool keeper {}".format( + self.riskpool, riskpoolKeeper + ) + ) + + componentOwnerService.propose(self.riskpool, {"from": riskpoolKeeper}) + + tx = history[-1] + wait_for_confirmations(tx) + + print( + "5) approval of riskpool id {} by instance operator {}".format( + self.riskpool.getId(), instance.getOwner() + ) + ) - print('5) approval of riskpool id {} by instance operator {}'.format( - self.riskpool.getId(), instance.getOwner())) - instanceOperatorService.approve( - self.riskpool.getId(), - {'from': instance.getOwner()}) + self.riskpool.getId(), {"from": instanceOperatorService.owner()} + ) + + print( + "6) riskpool wallet {} set for riskpool id {} by instance operator {}".format( + riskpoolWallet, self.riskpool.getId(), instance.getOwner() + ) + ) - print('6) riskpool wallet {} set for riskpool id {} by instance operator {}'.format( - riskpoolWallet, self.riskpool.getId(), instance.getOwner())) - instanceOperatorService.setRiskpoolWallet( self.riskpool.getId(), riskpoolWallet, - {'from': instance.getOwner()}) + {"from": instanceOperatorService.owner()}, + ) # 7) setup capital fees - fixedFee = 42 - fractionalFee = instanceService.getFeeFractionFullUnit() / 20 # corresponds to 5% - print('7) creating capital fee spec (fixed: {}, fractional: {}) for riskpool id {} by instance operator {}'.format( - fixedFee, fractionalFee, self.riskpool.getId(), instance.getOwner())) - + fixedFee = 0 + fractionalFee = ( + 0 # instanceService.getFeeFractionFullUnit() / 20 # corresponds to 5% + ) + print( + "7) creating capital fee spec (fixed: {}, fractional: {}) for riskpool id {} by instance operator {}".format( + fixedFee, fractionalFee, self.riskpool.getId(), instance.getOwner() + ) + ) + feeSpec = instanceOperatorService.createFeeSpecification( self.riskpool.getId(), fixedFee, fractionalFee, - b'', - {'from': instance.getOwner()}) + b"", + {"from": instanceOperatorService.owner()}, + ) + + print( + "8) setting capital fee spec by instance operator {}".format( + instance.getOwner() + ) + ) - print('8) setting capital fee spec by instance operator {}'.format( - instance.getOwner())) - instanceOperatorService.setCapitalFees( - feeSpec, - {'from': instance.getOwner()}) - + feeSpec, {"from": instanceOperatorService.owner()} + ) + def getId(self) -> int: return self.riskpool.getId() - + def getContract(self) -> AyiiRiskpool: return self.riskpool class GifAyiiOracle(object): - def __init__(self, - instance: GifInstance, - oracleProvider: Account, - chainlinkNodeOperator: Account, - name=ORACLE_NAME, - publishSource=False + def __init__( + self, + instance: GifInstance, + oracleProvider: Account, + name=ORACLE_NAME, + publishSource=False, ): instanceService = instance.getInstanceService() instanceOperatorService = instance.getInstanceOperatorService() componentOwnerService = instance.getComponentOwnerService() - oracleService = instance.getOracleService() - print('------ setting up oracle ------') + print("------ setting up oracle ------") providerRole = instanceService.getOracleProviderRole() - print('1) grant oracle provider role {} to oracle provider {}'.format( - providerRole, oracleProvider)) + print( + "1) grant oracle provider role {} to oracle provider {}".format( + providerRole, oracleProvider + ) + ) instanceOperatorService.grantRole( - providerRole, - oracleProvider, - {'from': instance.getOwner()}) - - - clTokenOwner = oracleProvider - clTokenSupply = 10**20 - print('2) deploy chainlink (mock) token with token owner (=oracle provider) {} by oracle provider {}'.format( - clTokenOwner, oracleProvider)) - - self.chainlinkToken = ChainlinkToken.deploy( - clTokenOwner, - clTokenSupply, - {'from': oracleProvider}, - publish_source=publishSource) - - print('3) deploy chainlink (mock) operator by oracle provider {}'.format( - oracleProvider)) - - self.chainlinkOperator = ChainlinkOperator.deploy( - {'from': oracleProvider}, - publish_source=publishSource) - - print('4) set node operator list [{}] as authorized sender by oracle provider {}'.format( - chainlinkNodeOperator, oracleProvider)) - - self.chainlinkOperator.setAuthorizedSenders([chainlinkNodeOperator]) - - # 2c) oracle provider creates oracle - chainLinkTokenAddress = self.chainlinkToken.address - chainLinkOracleAddress = self.chainlinkOperator.address - chainLinkJobId = s2b32('1') - chainLinkPaymentAmount = 0 - print('5) deploy oracle by oracle provider {}'.format( - oracleProvider)) - + providerRole, oracleProvider, {"from": instance.getOwner()} + ) + + print("5) deploy oracle by oracle provider {}".format(oracleProvider)) + self.oracle = AyiiOracle.deploy( - s2b32('AyiiOracle'), + s2b32(name), instance.getRegistry(), - chainLinkTokenAddress, - chainLinkOracleAddress, - chainLinkJobId, - chainLinkPaymentAmount, - {'from': oracleProvider}, - publish_source=publishSource) + {"from": oracleProvider}, + publish_source=publishSource, + ) - print('6) oracle {} proposing to instance by oracle provider {}'.format( - self.oracle, oracleProvider)) + tx = history[-1] + wait_for_confirmations(tx) - componentOwnerService.propose( - self.oracle, - {'from': oracleProvider}) + print( + "6) oracle {} proposing to instance by oracle provider {}".format( + self.oracle, oracleProvider + ) + ) + + componentOwnerService.propose(self.oracle, {"from": oracleProvider}) - print('7) approval of oracle id {} by instance operator {}'.format( - self.oracle.getId(), instance.getOwner())) + tx = history[-1] + wait_for_confirmations(tx) + + print( + "7) approval of oracle id {} by instance operator {}".format( + self.oracle.getId(), instance.getOwner() + ) + ) instanceOperatorService.approve( - self.oracle.getId(), - {'from': instance.getOwner()}) - + self.oracle.getId(), {"from": instance.getOwner()} + ) + def getId(self) -> int: return self.oracle.getId() - + def getClOperator(self) -> ChainlinkOperator: return self.chainlinkOperator - + def getContract(self) -> AyiiOracle: return self.oracle class GifAyiiProduct(object): - def __init__(self, - instance: GifInstance, - erc20Token, - productOwner: Account, - insurer: Account, - oracle: GifAyiiOracle, - riskpool: GifAyiiRiskpool, - name=PRODUCT_NAME, - publishSource=False + def __init__( + self, + instance: GifInstance, + erc20Token, + productOwner: Account, + insurer: Account, + oracle: GifAyiiOracle, + riskpool: GifAyiiRiskpool, + name=PRODUCT_NAME, + publishSource=False, ): self.policy = instance.getPolicy() self.oracle = oracle @@ -245,72 +258,97 @@ def __init__(self, componentOwnerService = instance.getComponentOwnerService() registry = instance.getRegistry() - print('------ setting up product ------') + print("------ setting up product ------") productOwnerRole = instanceService.getProductOwnerRole() - print('1) grant product owner role {} to product owner {}'.format( - productOwnerRole, productOwner)) + print( + "1) grant product owner role {} to product owner {}".format( + productOwnerRole, productOwner + ) + ) instanceOperatorService.grantRole( - productOwnerRole, - productOwner, - {'from': instance.getOwner()}) + productOwnerRole, productOwner, {"from": instance.getOwner()} + ) + + print("2) deploy product by product owner {}".format(productOwner)) - print('2) deploy product by product owner {}'.format( - productOwner)) - self.product = AyiiProduct.deploy( - s2b32('AyiiProduct'), + s2b32(name), registry, erc20Token.address, oracle.getId(), riskpool.getId(), insurer, - {'from': productOwner}, - publish_source=publishSource) - - print('3) product {} proposing to instance by product owner {}'.format( - self.product, productOwner)) - - componentOwnerService.propose( - self.product, - {'from': productOwner}) - - print('4) approval of product id {} by instance operator {}'.format( - self.product.getId(), instance.getOwner())) - + {"from": productOwner}, + publish_source=publishSource, + ) + + tx = history[-1] + wait_for_confirmations(tx) + + print( + "3) product {} proposing to instance by product owner {}".format( + self.product, productOwner + ) + ) + + componentOwnerService.propose(self.product, {"from": productOwner}) + + tx = history[-1] + wait_for_confirmations(tx) + + print( + "4) approval of product id {} by instance operator {}".format( + self.product.getId(), instance.getOwner() + ) + ) + instanceOperatorService.approve( - self.product.getId(), - {'from': instance.getOwner()}) + self.product.getId(), {"from": instance.getOwner()} + ) - print('5) setting erc20 product token {} for product id {} by instance operator {}'.format( - erc20Token, self.product.getId(), instance.getOwner())) + print( + "5) setting erc20 product token {} for product id {} by instance operator {}".format( + erc20Token, self.product.getId(), instance.getOwner() + ) + ) instanceOperatorService.setProductToken( - self.product.getId(), - erc20Token, - {'from': instance.getOwner()}) + self.product.getId(), erc20Token, {"from": instance.getOwner()} + ) + + fixedFee = 0 + fractionalFee = ( + 0 # instanceService.getFeeFractionFullUnit() / 10 # corresponds to 10% + ) + + # # set fees to zero + # fixedFee = 0 + # fractionalFee = 0 + + print( + "6) creating premium fee spec (fixed: {}, fractional: {}) for product id {} by instance operator {}".format( + fixedFee, fractionalFee, self.product.getId(), instance.getOwner() + ) + ) - fixedFee = 3 - fractionalFee = instanceService.getFeeFractionFullUnit() / 10 # corresponds to 10% - print('6) creating premium fee spec (fixed: {}, fractional: {}) for product id {} by instance operator {}'.format( - fixedFee, fractionalFee, self.product.getId(), instance.getOwner())) - feeSpec = instanceOperatorService.createFeeSpecification( self.product.getId(), fixedFee, fractionalFee, - b'', - {'from': instance.getOwner()}) + b"", + {"from": instance.getOwner()}, + ) - print('7) setting premium fee spec by instance operator {}'.format( - instance.getOwner())) + print( + "7) setting premium fee spec by instance operator {}".format( + instance.getOwner() + ) + ) - instanceOperatorService.setPremiumFees( - feeSpec, - {'from': instance.getOwner()}) + instanceOperatorService.setPremiumFees(feeSpec, {"from": instance.getOwner()}) - def getId(self) -> int: return self.product.getId() @@ -322,7 +360,7 @@ def getOracle(self) -> GifAyiiOracle: def getRiskpool(self) -> GifAyiiRiskpool: return self.riskpool - + def getContract(self) -> AyiiProduct: return self.product @@ -332,54 +370,48 @@ def getPolicy(self, policyId: str): class GifAyiiProductComplete(object): - def __init__(self, - instance: GifInstance, - productOwner: Account, + def __init__( + self, + instance: GifInstance, + productOwner: Account, insurer: Account, - oracleProvider: Account, - chainlinkNodeOperator: Account, - riskpoolKeeper: Account, + oracleProvider: Account, + riskpoolKeeper: Account, investor: Account, erc20Token: Account, riskpoolWallet: Account, - baseName='Ayii', - publishSource=False + collateralizationLevel: int, + baseName="Ayii", + publishSource=False, ): - instanceService = instance.getInstanceService() - instanceOperatorService = instance.getInstanceOperatorService() - componentOwnerService = instance.getComponentOwnerService() - registry = instance.getRegistry() self.token = erc20Token self.riskpool = GifAyiiRiskpool( - instance, - erc20Token, - riskpoolKeeper, - riskpoolWallet, - investor, - instanceService.getFullCollateralizationLevel(), - '{}Riskpool'.format(baseName), - publishSource) + instance, + erc20Token, + riskpoolKeeper, + riskpoolWallet, + investor, + collateralizationLevel, + "{}Riskpool".format(baseName), + publishSource, + ) self.oracle = GifAyiiOracle( - instance, - oracleProvider, - oracleProvider, - # TODO analyze how to set a separate chainlink operator node account - # chainlinkNodeOperator, - '{}Oracle'.format(baseName), - publishSource) + instance, oracleProvider, "{}Oracle".format(baseName), publishSource + ) self.product = GifAyiiProduct( - instance, - erc20Token, - productOwner, - insurer, - self.oracle, + instance, + erc20Token, + productOwner, + insurer, + self.oracle, self.riskpool, - '{}Product'.format(baseName), - publishSource) + "{}Product".format(baseName), + publishSource, + ) def getToken(self): return self.token diff --git a/scripts/component.py b/scripts/component.py index b7b50e7..23a8218 100644 --- a/scripts/component.py +++ b/scripts/component.py @@ -7,8 +7,8 @@ from brownie import ( Wei, - interface, - Contract, + interface, + Contract, PolicyController, OracleService, ComponentOwnerService, @@ -27,8 +27,9 @@ class GifComponent(object): - def __init__(self, - componentAddress: Account, + def __init__( + self, + componentAddress: Account, ): self.component = contractFromAddress(interface.IComponent, componentAddress) self.instance = GifInstance(registryAddress=self.component.getRegistry()) @@ -39,4 +40,4 @@ def __init__(self, riskpoolService = self.instance.getRiskpoolService() self.component = contractFromAddress(interface.IComponent, componentAddress) - self.instance = GifInstance(registryAddress=self.component.getRegistry()) \ No newline at end of file + self.instance = GifInstance(registryAddress=self.component.getRegistry()) diff --git a/scripts/const.py b/scripts/const.py index 50880a5..b5c6920 100644 --- a/scripts/const.py +++ b/scripts/const.py @@ -3,54 +3,56 @@ # === GIF platform ========================================================== # # GIF release -GIF_RELEASE = '2.0.0' +GIF_RELEASE = "2.0.0" # GIF modules -ACCESS_NAME = 'Access' -BUNDLE_NAME = 'Bundle' -COMPONENT_NAME = 'Component' +ACCESS_NAME = "Access" +BUNDLE_NAME = "Bundle" +COMPONENT_NAME = "Component" -REGISTRY_CONTROLLER_NAME = 'RegistryController' -REGISTRY_NAME = 'Registry' +REGISTRY_CONTROLLER_NAME = "RegistryController" +REGISTRY_NAME = "Registry" -ACCESS_CONTROLLER_NAME = 'AccessController' -ACCESS_NAME = 'Access' +ACCESS_CONTROLLER_NAME = "AccessController" +ACCESS_NAME = "Access" -LICENSE_CONTROLLER_NAME = 'LicenseController' -LICENSE_NAME = 'License' +LICENSE_CONTROLLER_NAME = "LicenseController" +LICENSE_NAME = "License" -POLICY_CONTROLLER_NAME = 'PolicyController' -POLICY_NAME = 'Policy' +POLICY_CONTROLLER_NAME = "PolicyController" +POLICY_NAME = "Policy" -POLICY_DEFAULT_FLOW_NAME = 'PolicyDefaultFlow' -POOL_NAME = 'Pool' +POLICY_DEFAULT_FLOW_NAME = "PolicyDefaultFlow" +POOL_NAME = "Pool" -QUERY_NAME = 'Query' +QUERY_NAME = "Query" -RISKPOOL_CONTROLLER_NAME = 'RiskpoolController' -RISKPOOL_NAME = 'Riskpool' -TREASURY_NAME = 'Treasury' +RISKPOOL_CONTROLLER_NAME = "RiskpoolController" +RISKPOOL_NAME = "Riskpool" +TREASURY_NAME = "Treasury" # GIF services -COMPONENT_OWNER_SERVICE_NAME = 'ComponentOwnerService' -PRODUCT_SERVICE_NAME = 'ProductService' -RISKPOOL_SERVICE_NAME = 'RiskpoolService' -ORACLE_SERVICE_NAME = 'OracleService' -INSTANCE_OPERATOR_SERVICE_NAME = 'InstanceOperatorService' -INSTANCE_SERVICE_NAME = 'InstanceService' +COMPONENT_OWNER_SERVICE_NAME = "ComponentOwnerService" +PRODUCT_SERVICE_NAME = "ProductService" +RISKPOOL_SERVICE_NAME = "RiskpoolService" +ORACLE_SERVICE_NAME = "OracleService" +INSTANCE_OPERATOR_SERVICE_NAME = "InstanceOperatorService" +INSTANCE_SERVICE_NAME = "InstanceService" # === GIF testing =========================================================== # # ZERO_ADDRESS = accounts.at('0x0000000000000000000000000000000000000000') -ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -COMPROMISED_ADDRESS = '0x0000000000000000000000000000000000000013' +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +COMPROMISED_ADDRESS = "0x0000000000000000000000000000000000000013" # TEST account values -ACCOUNTS_MNEMONIC = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' +ACCOUNTS_MNEMONIC = ( + "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" +) # TEST oracle/rikspool/product values -PRODUCT_NAME = 'Test.Product' -RISKPOOL_NAME = 'Test.Riskpool' -ORACLE_NAME = 'Test.Oracle' -ORACLE_INPUT_FORMAT = '(bytes input)' -ORACLE_OUTPUT_FORMAT = '(bool output)' +PRODUCT_NAME = "Test.Product" +RISKPOOL_NAME = "Test.Riskpool" +ORACLE_NAME = "Test.Oracle" +ORACLE_INPUT_FORMAT = "(bytes input)" +ORACLE_OUTPUT_FORMAT = "(bool output)" diff --git a/scripts/context.py b/scripts/context.py new file mode 100644 index 0000000..31eebcc --- /dev/null +++ b/scripts/context.py @@ -0,0 +1,186 @@ +import asyncio +import os +from prompt_toolkit.shortcuts.progress_bar import ProgressBar + +from brownie import ( + network, + accounts, + UsdcAccounting, + AyiiProduct, + AyiiRiskpool, + ProtectedMulticall, + Wei, +) + +from scripts.onepassword import signIn, getItem, getSecret +from scripts.deploy_ayii import from_registry +from scripts.util import contract_from_address, decodeEnum + + +class Context: + + networkName = network.show_active() + vault = os.getenv("OP_VAULT") + stakeholders = [ + "fundsOwner", + "instanceOperator", + "instanceWallet", + "oracleProvider", + "chainlinkNodeOperator", + "riskpoolKeeper", + "riskpoolWallet", + "investor", + "productOwner", + "insurer", + "customer1", + "customer2", + "escrow", + ] + feeCapitalFix = 0 + feeCapitalPercentage = 0 + feePremiumFix = 0 + feePremiumPercentage = 0 + + def __init__(self): + self.initialize() + + def initialize(self): + print("Initializing context, please wait...") + self.secretsItem = f"{self.networkName} secrets" + + asyncio.run(signIn()) + asyncio.run(getItem(self.vault, self.secretsItem)) + + self.registry = getSecret("Addresses", "registry") + self.usdcAccountingToken = getSecret("Addresses", "usdc_accounting_token") + self.clfConsumer = getSecret("Addresses", "clf_consumer") + self.multicallAddress = getSecret("Addresses", "multicall") + self.usdc = contract_from_address(UsdcAccounting, self.usdcAccountingToken) + self.multicall = contract_from_address( + ProtectedMulticall, self.multicallAddress + ) + self.decimals = self.usdc.decimals() + + self.accounts = { + s: accounts.from_mnemonic(getSecret("Mnemonics", s)) + for s in self.stakeholders + } + self.instanceOperator = self.accounts["instanceOperator"] + self.insurer = self.accounts["insurer"] + self.escrow = self.accounts["escrow"] + self.farmer_hd_base = "insurer" + + (instance, product, oracle, riskpool, componentController) = from_registry( + self.registry + ) + self.instanceService = instance.getInstanceService() + self.instance = instance + self.treasury = instance.getTreasury() + self.product = product + self.oracle = oracle + self.riskpool = riskpool + self.riskpoolId = riskpool.getId() + self.oracleId = oracle.getId() + self.productId = product.getId() + self.componentController = componentController + + assert self.instanceService.getInstanceOperator() == self.instanceOperator + assert self.riskpool.getFullCollateralizationLevel() == 1000000000000000000 + self.fullCollateralizationLevel = self.riskpool.getFullCollateralizationLevel() + self.noPrint = ["accounts", "stakeholders", "noPrint", "components"] + self.components = [] + + def setContext(self, key, value): + setattr(self, key, value) + + def getHdWallet(self, name, index): + return accounts.from_mnemonic(getSecret("Mnemonics", name), 1, index) + + def printContext(self): + print("Context:") + for k, v in self.__dict__.items(): + if k not in self.noPrint: + print(f"{k.ljust(30)}: {v}") + + def printAccounts(self): + print("Accounts:") + for k, v in self.accounts.items(): + print( + ( + f"{k.ljust(30)}: " + f"{v.address[:8]} " + f'{str(Wei(v.balance()).to("ether"))[-22:-16].rjust(8)} ' + f"{str(round(self.usdc.balanceOf(v)/10**self.decimals,2)).rjust(12)}" + ) + ) + + def loadComponents(self): + componentController = self.componentController + components = componentController.components() + result = [] + with ProgressBar() as pb: + for componentIndex in pb( + range(1, components + 1), label="Fetching components..." + ): + cAddress = componentController.getComponent(componentIndex) + cType = componentController.getComponentType(componentIndex) + cState = componentController.getComponentState(componentIndex) + componentData = { + "index": componentIndex, + "address": cAddress, + "type": cType, + "state": cState, + "typeStr": decodeEnum("ComponentType", cType), + "stateStr": decodeEnum("ComponentState", cState), + } + try: + if cType == 0: # Oracle + componentData["additionalData"] = {} + elif cType == 1: # Product + product = contract_from_address(AyiiProduct, cAddress) + risks = product.risks() + policies = 0 + for riskIndex in range(0, risks): + riskId = product.getRiskId(riskIndex) + policies += product.policies(riskId) + componentData["additionalData"] = { + "erc20Token": product.getToken(), + "riskpoolId": product.getRiskpoolId(), + "risks": product.risks(), + "policies": policies, + "applications": product.applications(), + } + elif cType == 2: # Riskpool + riskpool = contract_from_address(AyiiRiskpool, cAddress) + componentData["additionalData"] = { + "erc20Token": riskpool.getErc20Token(), + "capital": riskpool.getCapital(), + "bundles": riskpool.bundles(), + } + except Exception as e: + componentData["error"] = str(e) + + result.append(componentData) + + self.components = result + return result + + def getComponents(self, type=None, reload=False): + if reload or not hasattr(self, "components") or len(self.components) == 0: + self.loadComponents() + if type is not None: + return list(filter(lambda x: x["type"] == type, self.components)) + return self.components + + +context = Context() + +# Convenience: + +usdc = context.usdc +a = context.accounts +iop = context.instanceOperator +insurer = context.insurer +escrow = context.escrow +getHdWallet = context.getHdWallet +multicall = context.multicall diff --git a/scripts/deploy_ayii.py b/scripts/deploy_ayii.py index ee1fc16..65da73b 100644 --- a/scripts/deploy_ayii.py +++ b/scripts/deploy_ayii.py @@ -1,84 +1,138 @@ +from datetime import datetime + from brownie.network import accounts from brownie.network.account import Account from brownie import ( + web3, interface, network, - TestCoin, InstanceService, InstanceOperatorService, ComponentOwnerService, AyiiProduct, AyiiOracle, - AyiiRiskpool + AyiiRiskpool, + ComponentController, ) from scripts.ayii_product import GifAyiiProductComplete from scripts.instance import GifInstance -from scripts.util import contract_from_address, s2b32 - -INSTANCE_OPERATOR = 'instanceOperator' -INSTANCE_WALLET = 'instanceWallet' -ORACLE_PROVIDER = 'oracleProvider' -NODE_OPERATOR = 'chainlinkNodeOperator' -RISKPOOL_KEEPER = 'riskpoolKeeper' -RISKPOOL_WALLET = 'riskpoolWallet' -INVESTOR = 'investor' -PRODUCT_OWNER = 'productOwner' -INSURER = 'insurer' -CUSTOMER1 = 'customer1' -CUSTOMER2 = 'customer2' - -ERC20_TOKEM = 'erc20Token' -INSTANCE = 'instance' -INSTANCE_SERVICE = 'instanceService' -INSTANCE_OPERATOR_SERVICE = 'instanceOperatorService' -COMPONENT_OWNER_SERVICE = 'componentOwnerService' -PRODUCT = 'product' -ORACLE = 'oracle' -RISKPOOL = 'riskpool' - -RISK_ID1 = 'riskId1' -RISK_ID2 = 'riskId2' -PROCESS_ID1 = 'processId1' -PROCESS_ID2 = 'processId2' - -REQUIRED_FUNDS_S = 50000000000000000 -REQUIRED_FUNDS_M = 150000000000000000 -REQUIRED_FUNDS_L = 1500000000000000000 +from scripts.util import ( + contract_from_address, + s2b32, + getChainName, + utcStr, + decodeEnum, + fromWei, +) +from scripts.prompt import confirm + +INSTANCE_OPERATOR = "instanceOperator" +INSTANCE_WALLET = "instanceWallet" +ORACLE_PROVIDER = "oracleProvider" +NODE_OPERATOR = "chainlinkNodeOperator" +RISKPOOL_KEEPER = "riskpoolKeeper" +RISKPOOL_WALLET = "riskpoolWallet" +INVESTOR = "investor" +PRODUCT_OWNER = "productOwner" +INSURER = "insurer" +CUSTOMER1 = "customer1" +CUSTOMER2 = "customer2" + +ERC20_TOKEM = "erc20Token" +INSTANCE = "instance" +INSTANCE_SERVICE = "instanceService" +INSTANCE_OPERATOR_SERVICE = "instanceOperatorService" +COMPONENT_OWNER_SERVICE = "componentOwnerService" +PRODUCT = "product" +ORACLE = "oracle" +RISKPOOL = "riskpool" + +RISK_ID1 = "riskId1" +RISK_ID2 = "riskId2" +PROCESS_ID1 = "processId1" +PROCESS_ID2 = "processId2" + +GAS_PRICE = web3.eth.gas_price +GAS_PRICE_SAFETY_FACTOR = 1.25 + +GAS_S = 2000000 +GAS_M = 3 * GAS_S +GAS_L = 10 * GAS_M + +REQUIRED_FUNDS_S = int(GAS_PRICE * GAS_PRICE_SAFETY_FACTOR * GAS_S) +REQUIRED_FUNDS_M = int(GAS_PRICE * GAS_PRICE_SAFETY_FACTOR * GAS_M) +REQUIRED_FUNDS_L = int(GAS_PRICE * GAS_PRICE_SAFETY_FACTOR * GAS_L) + +INITIAL_ERC20_BUNDLE_FUNDING = 100000 REQUIRED_FUNDS = { INSTANCE_OPERATOR: REQUIRED_FUNDS_L, - INSTANCE_WALLET: REQUIRED_FUNDS_S, - PRODUCT_OWNER: REQUIRED_FUNDS_M, - INSURER: REQUIRED_FUNDS_M, - ORACLE_PROVIDER: REQUIRED_FUNDS_M, - RISKPOOL_KEEPER: REQUIRED_FUNDS_M, - RISKPOOL_WALLET: REQUIRED_FUNDS_S, - INVESTOR: REQUIRED_FUNDS_S, - CUSTOMER1: REQUIRED_FUNDS_S, - CUSTOMER2: REQUIRED_FUNDS_S, + INSTANCE_WALLET: REQUIRED_FUNDS_S, + PRODUCT_OWNER: REQUIRED_FUNDS_M, + INSURER: REQUIRED_FUNDS_M, + ORACLE_PROVIDER: int(1.2 * REQUIRED_FUNDS_M), + RISKPOOL_KEEPER: REQUIRED_FUNDS_M, + RISKPOOL_WALLET: REQUIRED_FUNDS_S, + INVESTOR: REQUIRED_FUNDS_S, + CUSTOMER1: REQUIRED_FUNDS_S, + CUSTOMER2: REQUIRED_FUNDS_S, } + +def help(): + print("from scripts.util import s2b, b2s, contract_from_address") + print( + "from scripts.deploy_ayii import stakeholders_accounts_ganache, check_funds, amend_funds, deploy, deploy_product_riskpool, from_registry, from_component, verify_deploy" + ) + print() + print("#--- deploy ganache setup ---------------------------------------#") + print("a = stakeholders_accounts_ganache()") + print("instance_operator = a['instanceOperator']") + print() + print("check_funds(a, token)") + print("# amend_funds(a)") + print("d = deploy(a, token, False)") + print() + print( + "(instance, product, oracle, riskpool) = from_registry(d['instance'].getRegistry())" + ) + print("verify_deploy(a, token, instance.getRegistry())") + print() + print("#--- deploy to existing instance --------------------------------#") + print("check_funds(a, token)") + print("registry_address = d['instance'].getRegistry()") + print("registry_address = instance.getRegistry().address") + print("collateralization_level = 0") + print() + print("product_old = product") + print("oracle_old = oracle") + print("riskpool_old = riskpool") + print() + print( + "(instance, product, oracle, riskpool) = deploy_product_riskpool(registry_address, a, token, collateralization_level)" + ) + print(f"You are on chain {network.show_active()}") + + def stakeholders_accounts_ganache(): - # define stakeholder accounts - instanceOperator=accounts[0] - instanceWallet=accounts[1] - oracleProvider=accounts[2] - chainlinkNodeOperator=accounts[3] - riskpoolKeeper=accounts[4] - riskpoolWallet=accounts[5] - investor=accounts[6] - productOwner=accounts[7] - insurer=accounts[8] - customer=accounts[9] - customer2=accounts[10] + # define stakeholder accounts + instanceOperator = accounts[0] + instanceWallet = accounts[1] + oracleProvider = accounts[2] + riskpoolKeeper = accounts[4] + riskpoolWallet = accounts[5] + investor = accounts[6] + productOwner = accounts[7] + insurer = accounts[8] + customer = accounts[9] + customer2 = accounts[10] return { INSTANCE_OPERATOR: instanceOperator, INSTANCE_WALLET: instanceWallet, ORACLE_PROVIDER: oracleProvider, - NODE_OPERATOR: chainlinkNodeOperator, RISKPOOL_KEEPER: riskpoolKeeper, RISKPOOL_WALLET: riskpoolWallet, INVESTOR: investor, @@ -89,38 +143,103 @@ def stakeholders_accounts_ganache(): } -def check_funds(stakeholders_accounts): +def check_funds(stakeholders_accounts, erc20_token): + _print_constants() + a = stakeholders_accounts + + native_token_success = True fundsMissing = 0 for accountName, requiredAmount in REQUIRED_FUNDS.items(): if a[accountName].balance() >= REQUIRED_FUNDS[accountName]: - print('{} funding ok'.format(accountName)) + print("{} funding ok".format(accountName)) else: fundsMissing += REQUIRED_FUNDS[accountName] - a[accountName].balance() - print('{} needs {} but has {}'.format( - accountName, - REQUIRED_FUNDS[accountName], - a[accountName].balance() - )) - + print( + "{} needs {} but has {}".format( + accountName, REQUIRED_FUNDS[accountName], a[accountName].balance() + ) + ) + if fundsMissing > 0: - if a[INSTANCE_OPERATOR].balance() >= REQUIRED_FUNDS[INSTANCE_OPERATOR] + fundsMissing: - print('{} sufficiently funded to cover missing funds'.format(INSTANCE_OPERATOR)) + native_token_success = False + + if ( + a[INSTANCE_OPERATOR].balance() + >= REQUIRED_FUNDS[INSTANCE_OPERATOR] + fundsMissing + ): + print( + "{} sufficiently funded with native token to cover missing funds".format( + INSTANCE_OPERATOR + ) + ) else: - print('{} needs additional funding of {} to cover missing funds'.format( + additionalFunds = ( + REQUIRED_FUNDS[INSTANCE_OPERATOR] + + fundsMissing + - a[INSTANCE_OPERATOR].balance() + ) + print( + "{} needs additional funding of {} ({} ETH) with native token to cover missing funds".format( + INSTANCE_OPERATOR, additionalFunds, additionalFunds / 10**18 + ) + ) + else: + native_token_success = True + + erc20_success = False + if erc20_token: + erc20_success = check_erc20_funds(a, erc20_token) + else: + print("WARNING: no erc20 token defined, skipping erc20 funds checking") + + return native_token_success & erc20_success + + +def check_erc20_funds(a, erc20_token): + if erc20_token.balanceOf(a[INSTANCE_OPERATOR]) >= INITIAL_ERC20_BUNDLE_FUNDING: + print("{} ERC20 funding ok".format(INSTANCE_OPERATOR)) + return True + else: + print( + "{} needs additional ERC20 funding of {} to cover missing funds".format( INSTANCE_OPERATOR, - REQUIRED_FUNDS[INSTANCE_OPERATOR] + fundsMissing - a[INSTANCE_OPERATOR].balance() - )) + INITIAL_ERC20_BUNDLE_FUNDING + - erc20_token.balanceOf(a[INSTANCE_OPERATOR]), + ) + ) + print("IMPORTANT: manual transfer needed to ensure ERC20 funding") + return False def amend_funds(stakeholders_accounts): + if not confirm("amend funds"): + return + a = stakeholders_accounts for accountName, requiredAmount in REQUIRED_FUNDS.items(): if a[accountName].balance() < REQUIRED_FUNDS[accountName]: missingAmount = REQUIRED_FUNDS[accountName] - a[accountName].balance() - print('funding {} with {}'.format(accountName, missingAmount)) + print("funding {} with {}".format(accountName, missingAmount)) a[INSTANCE_OPERATOR].transfer(a[accountName], missingAmount) + print("re-run check_funds() to verify funding before deploy") + + +def _print_constants(): + chainId = web3.eth.chain_id + print("chain id: {} {}".format(chainId, getChainName(chainId))) + print("gas price [Mwei]: {}".format(GAS_PRICE / 10**6)) + print("gas price safety factor: {}".format(GAS_PRICE_SAFETY_FACTOR)) + + print("gas S: {}".format(GAS_S)) + print("gas M: {}".format(GAS_M)) + print("gas L: {}".format(GAS_L)) + + print("required S [ETH]: {}".format(REQUIRED_FUNDS_S / 10**18)) + print("required M [ETH]: {}".format(REQUIRED_FUNDS_M / 10**18)) + print("required L [ETH]: {}".format(REQUIRED_FUNDS_L / 10**18)) + def _get_balances(stakeholders_accounts): balance = {} @@ -132,83 +251,284 @@ def _get_balances(stakeholders_accounts): def _get_balances_delta(balances_before, balances_after): - balance_delta = { 'total': 0 } + balance_delta = {"total": 0} for accountName, account in balances_before.items(): - balance_delta[accountName] = balances_before[accountName] - balances_after[accountName] - balance_delta['total'] += balance_delta[accountName] - + balance_delta[accountName] = ( + balances_before[accountName] - balances_after[accountName] + ) + balance_delta["total"] += balance_delta[accountName] + return balance_delta def _pretty_print_delta(title, balances_delta): - print('--- {} ---'.format(title)) - + print("--- {} ---".format(title)) + gasPrice = network.gas_price() - print('gas price: {}'.format(gasPrice)) + print("gas price: {}".format(gasPrice)) for accountName, amount in balances_delta.items(): - if accountName != 'total': - if gasPrice != 'auto': - print('account {}: gas {}'.format(accountName, amount / gasPrice)) + if accountName != "total": + if gasPrice != "auto": + print("account {}: gas {}".format(accountName, amount / gasPrice)) else: - print('account {}: amount {}'.format(accountName, amount)) - - print('-----------------------------') - if gasPrice != 'auto': - print('account total: gas {}'.format(balances_delta['total'] / gasPrice)) + print("account {}: amount {}".format(accountName, amount)) + + print("-----------------------------") + if gasPrice != "auto": + print("account total: gas {}".format(balances_delta["total"] / gasPrice)) else: - print('account total: amount {}'.format(balances_delta['total'])) - print('=============================') + print("account total: amount {}".format(balances_delta["total"])) + print("=============================") def deploy_setup_including_token( - stakeholders_accounts, - publishSource=False + stakeholders_accounts, erc20_token, publishSource=False ): - return deploy(stakeholders_accounts, None) + return deploy(stakeholders_accounts, erc20_token, None) -def deploy( - stakeholders_accounts, +def verify_deploy( + stakeholders_accounts, erc20_token, - publishSource=False + registry_address, + riskpoolId=0, + oracleId=0, + productId=0, ): + # define stakeholder accounts + a = stakeholders_accounts + instanceOperator = a[INSTANCE_OPERATOR] + instanceWallet = a[INSTANCE_WALLET] + oracleProvider = a[ORACLE_PROVIDER] + riskpoolKeeper = a[RISKPOOL_KEEPER] + riskpoolWallet = a[RISKPOOL_WALLET] + productOwner = a[PRODUCT_OWNER] + + (instance, product, oracle, riskpool, componentController) = from_registry( + registry_address, riskpoolId=riskpoolId, oracleId=oracleId, productId=productId + ) + + instanceService = instance.getInstanceService() + + verify_element("Registry", instanceService.getRegistry(), registry_address) + verify_element( + "InstanceOperator", instanceService.getInstanceOperator(), instanceOperator + ) + verify_element( + "InstanceWallet", instanceService.getInstanceWallet(), instanceWallet + ) + + verify_element("RiskpoolId", riskpool.getId(), riskpoolId) + verify_element("RiskpoolType", instanceService.getComponentType(riskpoolId), 2) + verify_element("RiskpoolState", instanceService.getComponentState(riskpoolId), 3) + verify_element("RiskpoolKeeper", riskpool.owner(), riskpoolKeeper) + verify_element( + "RiskpoolWallet", instanceService.getRiskpoolWallet(riskpoolId), riskpoolWallet + ) + verify_element( + "RiskpoolBalance", + instanceService.getBalance(riskpoolId), + erc20_token.balanceOf(riskpoolWallet), + ) + verify_element("RiskpoolToken", riskpool.getErc20Token(), erc20_token.address) + + verify_element("OracleId", oracle.getId(), oracleId) + verify_element("OracleType", instanceService.getComponentType(oracleId), 0) + verify_element("OracleState", instanceService.getComponentState(oracleId), 3) + verify_element("OracleProvider", oracle.owner(), oracleProvider) + + verify_element("ProductId", product.getId(), productId) + verify_element("ProductType", instanceService.getComponentType(productId), 1) + verify_element("ProductState", instanceService.getComponentState(productId), 3) + verify_element("ProductOwner", product.owner(), productOwner) + verify_element("ProductToken", product.getToken(), erc20_token.address) + verify_element("ProductRiskpool", product.getRiskpoolId(), riskpoolId) + + print( + "InstanceWalletBalance {:.2f}".format( + erc20_token.balanceOf(instanceService.getInstanceWallet()) + / 10 ** erc20_token.decimals() + ) + ) + print( + "RiskpoolWalletTVL {:.2f}".format( + instanceService.getTotalValueLocked(riskpoolId) + / 10 ** erc20_token.decimals() + ) + ) + print( + "RiskpoolWalletCapacity {:.2f}".format( + instanceService.getCapacity(riskpoolId) / 10 ** erc20_token.decimals() + ) + ) + print( + "RiskpoolWalletBalance {:.2f}".format( + erc20_token.balanceOf(instanceService.getRiskpoolWallet(riskpoolId)) + / 10 ** erc20_token.decimals() + ) + ) + print("RiskpoolBundles {}".format(riskpool.bundles())) + + # bundle_id = riskpool.getBundleId(0) + printBundle(riskpool, 0) + print("ProductRisks {}".format(product.risks())) + print("ProductApplications {}".format(product.applications())) + + +def printBundle(riskpool, bundleId): + for key, value in riskpool.getBundle(bundleId).dict().items(): + if key in ["createdAt", "updatedAt"]: # Apply conversion for specific keys + value = utcStr(value) + if key in ["state"]: + value = decodeEnum("BundleState", value) + if key in ["balance", "lockedCapital", "capital"]: + value = fromWei(value, 6) + print(f" {key:<15}: {value}") + + +def verify_element(element, value, expected_value): + if value == expected_value: + print("{} OK {}".format(element, value)) + else: + print("{} ERROR {} expected {}".format(element, value, expected_value)) + + +def deploy_product_with_oracle_riskpool( + registry_address, + stakeholders_accounts, + erc20_token, + collateralizationLevel, + publishSource=False, +): + if not confirm(f"deployment with product and riskpool"): + return + + # define stakeholder accounts + a = stakeholders_accounts + instanceOperator = a[INSTANCE_OPERATOR] + oracleProvider = a[ORACLE_PROVIDER] + riskpoolKeeper = a[RISKPOOL_KEEPER] + riskpoolWallet = a[RISKPOOL_WALLET] + investor = a[INVESTOR] + productOwner = a[PRODUCT_OWNER] + insurer = a[INSURER] + + # create basename including unix timestamp + baseName = "Ayii_{}_".format(int(datetime.now().timestamp())) + + if not check_funds(a, erc20_token): + print("ERROR: insufficient funding, aborting deploy") + return + + if not erc20_token: + print("ERROR: no erc20 defined, aborting deploy") + return + + print("====== setting erc20 token to {} ======".format(erc20_token)) + erc20Token = erc20_token + + print( + "====== getting instance from registry address {} ======".format( + registry_address + ) + ) + (instance, product, oracle, riskpool, _) = from_registry(registry_address) + + print( + "====== deploy ayii product /w base name '{}' and riskpool collateralization level {} ======".format( + baseName, collateralizationLevel + ) + ) + ayiiDeploy = GifAyiiProductComplete( + instance, + productOwner, + insurer, + oracleProvider, + riskpoolKeeper, + investor, + erc20Token, + riskpoolWallet, + collateralizationLevel, + baseName=baseName, + publishSource=publishSource, + ) + + ayiiProduct = ayiiDeploy.getProduct() + ayiiOracle = ayiiProduct.getOracle() + ayiiRiskpool = ayiiProduct.getRiskpool() + + product = ayiiProduct.getContract() + oracle = ayiiOracle.getContract() + riskpool = ayiiRiskpool.getContract() + + print("====== create bundle for investor {} ======".format(investor)) + initial_funding = 10 ** erc20Token.decimals() + bundle_filter = b"" + erc20Token.transfer(investor, initial_funding, {"from": instanceOperator}) + erc20Token.approve( + instance.getTreasury().address, initial_funding, {"from": investor} + ) + riskpool.createBundle(bundle_filter, initial_funding, {"from": investor}) + + return (instance, product, oracle, riskpool) + + +def deploy(stakeholders_accounts, erc20_token, publishSource=False): + if not confirm("full deployment"): + return # define stakeholder accounts a = stakeholders_accounts - instanceOperator=a[INSTANCE_OPERATOR] - instanceWallet=a[INSTANCE_WALLET] - oracleProvider=a[ORACLE_PROVIDER] - chainlinkNodeOperator=a[NODE_OPERATOR] - riskpoolKeeper=a[RISKPOOL_KEEPER] - riskpoolWallet=a[RISKPOOL_WALLET] - investor=a[INVESTOR] - productOwner=a[PRODUCT_OWNER] - insurer=a[INSURER] - customer=a[CUSTOMER1] - customer2=a[CUSTOMER2] + instanceOperator = a[INSTANCE_OPERATOR] + instanceWallet = a[INSTANCE_WALLET] + oracleProvider = a[ORACLE_PROVIDER] + riskpoolKeeper = a[RISKPOOL_KEEPER] + riskpoolWallet = a[RISKPOOL_WALLET] + investor = a[INVESTOR] + productOwner = a[PRODUCT_OWNER] + insurer = a[INSURER] + customer = a[CUSTOMER1] + customer2 = a[CUSTOMER2] + + if not check_funds(a, erc20_token): + print("ERROR: insufficient funding, aborting deploy") + return # assess balances at beginning of deploy balances_before = _get_balances(stakeholders_accounts) if not erc20_token: - print('====== deploy erc20 test token ======') - erc20Token = TestCoin.deploy({'from': instanceOperator}) - else: - print('====== setting erc20 token to {} ======'.format(erc20_token)) - erc20Token = erc20_token + print("ERROR: no erc20 defined, aborting deploy") + return + print("====== setting erc20 token to {} ======".format(erc20_token)) + erc20Token = erc20_token - print('====== deploy gif instance ======') - instance = GifInstance(instanceOperator, instanceWallet=instanceWallet) + print("====== deploy gif instance ======") + instance = GifInstance( + instanceOperator, instanceWallet=instanceWallet, publishSource=publishSource + ) instanceService = instance.getInstanceService() instanceOperatorService = instance.getInstanceOperatorService() componentOwnerService = instance.getComponentOwnerService() - print('====== deploy ayii product ======') - ayiiDeploy = GifAyiiProductComplete(instance, productOwner, insurer, oracleProvider, chainlinkNodeOperator, riskpoolKeeper, investor, erc20Token, riskpoolWallet) + print("====== deploy ayii product ======") + collateralizationLevel = instanceService.getFullCollateralizationLevel() + ayiiDeploy = GifAyiiProductComplete( + instance, + productOwner, + insurer, + oracleProvider, + riskpoolKeeper, + investor, + erc20Token, + riskpoolWallet, + collateralizationLevel, + publishSource=publishSource, + ) # assess balances at beginning of deploy balances_after_deploy = _get_balances(stakeholders_accounts) @@ -221,80 +541,95 @@ def deploy( oracle = ayiiOracle.getContract() riskpool = ayiiRiskpool.getContract() - print('====== create initial setup ======') + print("====== create initial setup ======") - bundleInitialFunding=1000000 - print('1) investor {} funding (transfer/approve) with {} token for erc20 {}'.format( - investor, bundleInitialFunding, erc20Token)) - - erc20Token.transfer(investor, bundleInitialFunding, {'from': instanceOperator}) - erc20Token.approve(instance.getTreasury(), bundleInitialFunding, {'from': investor}) + bundleInitialFunding = INITIAL_ERC20_BUNDLE_FUNDING + print( + "1) investor {} funding (transfer/approve) with {} token for erc20 {}".format( + investor, bundleInitialFunding, erc20Token + ) + ) - maxUint256 = 2**256-1 - print('2) riskpool wallet {} approval for instance treasury {}'.format( - riskpoolWallet, instance.getTreasury())) - - erc20Token.approve(instance.getTreasury(), maxUint256, {'from': riskpoolWallet}) + erc20Token.transfer(investor, bundleInitialFunding, {"from": instanceOperator}) + erc20Token.approve(instance.getTreasury(), bundleInitialFunding, {"from": investor}) - print('3) riskpool bundle creation by investor {}'.format( - investor)) + print( + "2) riskpool wallet {} approval for instance treasury {}".format( + riskpoolWallet, instance.getTreasury() + ) + ) + + erc20Token.approve( + instance.getTreasury(), bundleInitialFunding, {"from": riskpoolWallet} + ) + + print("3) riskpool bundle creation by investor {}".format(investor)) applicationFilter = bytes(0) - riskpool.createBundle( - applicationFilter, - bundleInitialFunding, - {'from': investor}) + riskpool.createBundle(applicationFilter, bundleInitialFunding, {"from": investor}) # create risks - projectId = s2b32('2022.kenya.wfp.ayii') - uaiId = [s2b32('1234'), s2b32('2345')] - cropId = s2b32('mixed') - + projectId = s2b32("2022.kenya.wfp.ayii") + uaiId = [s2b32("1234"), s2b32("2345")] + cropId = s2b32("mixed") + triggerFloat = 0.75 exitFloat = 0.1 tsiFloat = 0.9 aphFloat = [2.0, 1.8] - + multiplier = product.getPercentageMultiplier() trigger = multiplier * triggerFloat exit_ = multiplier * exitFloat tsi = multiplier * tsiFloat aph = [multiplier * aphFloat[0], multiplier * aphFloat[1]] - print('4) risk creation (2x) by insurer {}'.format( - insurer)) + print("4) risk creation (2x) by insurer {}".format(insurer)) tx = [None, None] - tx[0] = product.createRisk(projectId, uaiId[0], cropId, trigger, exit_, tsi, aph[0], {'from': insurer}) - tx[1] = product.createRisk(projectId, uaiId[1], cropId, trigger, exit_, tsi, aph[1], {'from': insurer}) + tx[0] = product.createRisk( + projectId, uaiId[0], cropId, trigger, exit_, tsi, aph[0], {"from": insurer} + ) + tx[1] = product.createRisk( + projectId, uaiId[1], cropId, trigger, exit_, tsi, aph[1], {"from": insurer} + ) - riskId1 = tx[0].events['LogAyiiRiskDataCreated']['riskId'] - riskId2 = tx[1].events['LogAyiiRiskDataCreated']['riskId'] + riskId1 = tx[0].events["LogAyiiRiskDataCreated"]["riskId"] + riskId2 = tx[1].events["LogAyiiRiskDataCreated"]["riskId"] - customerFunding=1000 - print('5) customer {} funding (transfer/approve) with {} token for erc20 {}'.format( - investor, customerFunding, erc20Token)) + customerFunding = 1000 + print( + "5) customer {} funding (transfer/approve) with {} token for erc20 {}".format( + customer, customerFunding, erc20Token + ) + ) - erc20Token.transfer(customer, customerFunding, {'from': instanceOperator}) - erc20Token.approve(instance.getTreasury(), customerFunding, {'from': customer}) + erc20Token.transfer(customer, customerFunding, {"from": instanceOperator}) + erc20Token.approve(instance.getTreasury(), customerFunding, {"from": customer}) # policy creation premium = [300, 400] sumInsured = [2000, 3000] - print('6) policy creation (2x) for customers {}, {} by insurer {}'.format( - customer, customer2, insurer)) + print( + "6) policy creation (2x) for customers {}, {} by insurer {}".format( + customer, customer2, insurer + ) + ) - tx[0] = product.applyForPolicy(customer, premium[0], sumInsured[0], riskId1, {'from': insurer}) - tx[1] = product.applyForPolicy(customer2, premium[1], sumInsured[1], riskId2, {'from': insurer}) + tx[0] = product.applyForPolicy( + customer, premium[0], sumInsured[0], riskId1, {"from": insurer} + ) + tx[1] = product.applyForPolicy( + customer2, premium[1], sumInsured[1], riskId2, {"from": insurer} + ) - processId1 = tx[0].events['LogAyiiPolicyCreated']['policyId'] - processId2 = tx[1].events['LogAyiiPolicyCreated']['policyId'] + processId1 = tx[0].events["LogAyiiPolicyCreated"]["policyId"] + processId2 = tx[1].events["LogAyiiPolicyCreated"]["policyId"] deploy_result = { INSTANCE_OPERATOR: instanceOperator, INSTANCE_WALLET: instanceWallet, ORACLE_PROVIDER: oracleProvider, - NODE_OPERATOR: chainlinkNodeOperator, RISKPOOL_KEEPER: riskpoolKeeper, RISKPOOL_WALLET: riskpoolWallet, INVESTOR: investor, @@ -302,11 +637,15 @@ def deploy( INSURER: insurer, CUSTOMER1: customer, CUSTOMER2: customer2, - ERC20_TOKEM: contract_from_address(TestCoin, erc20Token), + ERC20_TOKEM: contract_from_address(interface.ERC20, erc20Token), INSTANCE: instance, INSTANCE_SERVICE: contract_from_address(InstanceService, instanceService), - INSTANCE_OPERATOR_SERVICE: contract_from_address(InstanceOperatorService, instanceOperatorService), - COMPONENT_OWNER_SERVICE: contract_from_address(ComponentOwnerService, componentOwnerService), + INSTANCE_OPERATOR_SERVICE: contract_from_address( + InstanceOperatorService, instanceOperatorService + ), + COMPONENT_OWNER_SERVICE: contract_from_address( + ComponentOwnerService, componentOwnerService + ), PRODUCT: contract_from_address(AyiiProduct, product), ORACLE: contract_from_address(AyiiOracle, oracle), RISKPOOL: contract_from_address(AyiiRiskpool, riskpool), @@ -316,39 +655,39 @@ def deploy( PROCESS_ID2: processId2, } - print('deploy_result: {}'.format(deploy_result)) + print("deploy_result: {}".format(deploy_result)) - print('====== deploy and setup creation complete ======') - print('') + print("====== deploy and setup creation complete ======") + print("") # check balances at end of setup balances_after_setup = _get_balances(stakeholders_accounts) - print('--------------------------------------------------------------------') - print('inital balances: {}'.format(balances_before)) - print('after deploy balances: {}'.format(balances_after_deploy)) - print('end of setup balances: {}'.format(balances_after_setup)) + print("--------------------------------------------------------------------") + print("inital balances: {}".format(balances_before)) + print("after deploy balances: {}".format(balances_after_deploy)) + print("end of setup balances: {}".format(balances_after_setup)) delta_deploy = _get_balances_delta(balances_before, balances_after_deploy) delta_setup = _get_balances_delta(balances_after_deploy, balances_after_setup) delta_total = _get_balances_delta(balances_before, balances_after_setup) - print('--------------------------------------------------------------------') - print('total deploy {}'.format(delta_deploy['total'])) - print('deploy {}'.format(delta_deploy)) + print("--------------------------------------------------------------------") + print("total deploy {}".format(delta_deploy["total"])) + print("deploy {}".format(delta_deploy)) - print('--------------------------------------------------------------------') - print('total setup after deploy {}'.format(delta_setup['total'])) - print('setup after deploy {}'.format(delta_setup)) + print("--------------------------------------------------------------------") + print("total setup after deploy {}".format(delta_setup["total"])) + print("setup after deploy {}".format(delta_setup)) - print('--------------------------------------------------------------------') - print('total deploy + setup{}'.format(delta_total['total'])) - print('deploy + setup{}'.format(delta_total)) + print("--------------------------------------------------------------------") + print("total deploy + setup{}".format(delta_total["total"])) + print("deploy + setup{}".format(delta_total)) - print('--------------------------------------------------------------------') + print("--------------------------------------------------------------------") - _pretty_print_delta('gas usage deploy', delta_deploy) - _pretty_print_delta('gas usage total', delta_total) + _pretty_print_delta("gas usage deploy", delta_deploy) + _pretty_print_delta("gas usage total", delta_total) return deploy_result @@ -358,8 +697,11 @@ def from_component(componentAddress): return from_registry(component.getRegistry()) -def from_registry(registryAddress): +def from_registry( + registryAddress, productId=0, oracleId=0, riskpoolId=0, verbose=False +): instance = GifInstance(registryAddress=registryAddress) + registry = instance.getRegistry() instanceService = instance.getInstanceService() products = instanceService.products() @@ -371,84 +713,118 @@ def from_registry(registryAddress): riskpool = None if products >= 1: - if products > 1: - print('1 product expected, {} product available'.format(products)) - print('returning last product available') - - componentId = instanceService.getProductId(products-1) - component = instanceService.getComponent(componentId) - product = contract_from_address(AyiiProduct, component) - else: - print('1 product expected, no producta available') - print('no product returned (None)') + if productId > 0: + componentId = productId + else: + componentId = instanceService.getProductId(products - 1) + + if verbose and products > 1: + print("1 product expected, {} products available".format(products)) + print("returning last product available") + + componentAddress = instanceService.getComponent(componentId) + product = contract_from_address(AyiiProduct, componentAddress) + + if product.getType() != 1: + product = None + if verbose: + print( + "component (type={}) with id {} is not product".format( + product.getType(), componentId + ) + ) + print("no product returned (None)") + elif verbose: + print("1 product expected, no product available") + print("no product returned (None)") if oracles >= 1: - if oracles > 1: - print('1 oracle expected, {} oracles available'.format(oracles)) - print('returning last oracle available') - - componentId = instanceService.getOracleId(oracles-1) - component = instanceService.getComponent(componentId) - oracle = contract_from_address(AyiiOracle, component) - else: - print('1 oracle expected, no oracles available') - print('no oracle returned (None)') + if oracleId > 0: + componentId = oracleId + else: + componentId = instanceService.getOracleId(oracles - 1) + + if verbose and oracles > 1: + print("1 oracle expected, {} oracles available".format(oracles)) + print("returning last oracle available") + + componentAddress = instanceService.getComponent(componentId) + oracle = contract_from_address(AyiiOracle, componentAddress) + + if oracle.getType() != 0: + if verbose: + print( + "component (type={}) with id {} is not oracle".format( + oracle.getType(), componentId + ) + ) + print("no oracle returned (None)") + oracle = None + elif verbose: + print("1 oracle expected, no oracles available") + print("no oracle returned (None)") if riskpools >= 1: - if riskpools > 1: - print('1 riskpool expected, {} riskpools available'.format(riskpools)) - print('returning last riskpool available') - - componentId = instanceService.getRiskpoolId(riskpools-1) - component = instanceService.getComponent(componentId) - riskpool = contract_from_address(AyiiRiskpool, component) - else: - print('1 riskpool expected, no riskpools available') - print('no riskpool returned (None)') + if riskpoolId > 0: + componentId = riskpoolId + else: + componentId = instanceService.getRiskpoolId(riskpools - 1) + + if verbose and riskpools > 1: + print("1 riskpool expected, {} riskpools available".format(riskpools)) + print("returning last riskpool available") + + componentAddress = instanceService.getComponent(componentId) + riskpool = contract_from_address(AyiiRiskpool, componentAddress) + + if riskpool.getType() != 2: + if verbose: + print( + "component (type={}) with id {} is not riskpool".format( + riskpool.getType(), componentId + ) + ) + print("no riskpool returned (None)") + riskpool = None + elif verbose: + print("1 riskpool expected, no riskpools available") + print("no riskpool returned (None)") + + componentController = contract_from_address( + ComponentController, registry.getContract(s2b32("Component")) + ) - return (instance, product, oracle, riskpool) + return (instance, product, oracle, riskpool, componentController) def dry_run_create_risks(product, insurer): - project = '2022.kenya.wfp.ayii' - crop = 'maize' + if not confirm("dry run create risks"): + return + + project = "2022.kenya.wfp.ayii" + crop = "maize" trigger = 0.75 tsi = 0.9 - exit_ = 0.1 - - aez = [ - 22, - 23, - 26, - 29, - 30, - 32, - 34, - 38, - 39, - 40] - - aph = [ - 2.28, - 2.42, - 2.14, - 2.01, - 3.03, - 3.05, - 2.38, - 1.84, - 2.60, - 2.30] - + exit_ = 0.1 + + aez = [22, 23, 26, 29, 30, 32, 34, 38, 39, 40] + + aph = [2.28, 2.42, 2.14, 2.01, 3.03, 3.05, 2.38, 1.84, 2.60, 2.30] + print("project, aez, crop, trigger, exit, tsi, aph, riskId") for i in range(len(aez)): - riskId = create_risk(product, insurer, project, aez[i], crop, trigger, exit_, tsi, aph[i]) + riskId = create_risk( + product, insurer, project, aez[i], crop, trigger, exit_, tsi, aph[i] + ) print(project, aez[i], crop, trigger, exit_, tsi, aph[i], riskId) def create_risk(product, insurer, project, uai, crop, trigger, exit_, tsi, aph): - + + if not confirm("create risks"): + return + multiplier = product.getPercentageMultiplier() triggerInt = multiplier * trigger exitInt = multiplier * exit_ @@ -459,11 +835,21 @@ def create_risk(product, insurer, project, uai, crop, trigger, exit_, tsi, aph): s2b32(project), s2b32(str(uai)), s2b32(crop), - triggerInt, - exitInt, - tsiInt, + triggerInt, + exitInt, + tsiInt, aphInt, - {'from': insurer} + {"from": insurer}, ) - return tx.events['LogAyiiRiskDataCreated']['riskId'] + return tx.events["LogAyiiRiskDataCreated"]["riskId"] + + +def setClfConsumer(context): + if not confirm("set clf consumer in oracle"): + return + + oracleProvider = context.accounts["oracleProvider"] + clfConsumer = context.clfConsumer + tx = context.oracle.setClfGateWay(clfConsumer, {"from": oracleProvider}) + print(tx) diff --git a/scripts/farmerFunding.py b/scripts/farmerFunding.py new file mode 100644 index 0000000..8da182a --- /dev/null +++ b/scripts/farmerFunding.py @@ -0,0 +1,446 @@ +from brownie import accounts, web3, Contract +from eth_account.messages import encode_typed_data +from eth_account import Account +from eth_utils import to_bytes +from time import time + +from scripts.context import context, a, usdc, getHdWallet, multicall +from scripts.prompt import prompt + +# Multicall contract, see https://www.multicall3.com/ + + +class FarmerFunding: + + partners = ["Lemonade", "WFP", "Farmer"] + currencies = ["KES", "USDC"] + token_spender_names = ["escrow", "treasury"] + + def __init__(self): + self.usdc_conversion_rate = 130 + self.instanceOperator = a["instanceOperator"] + self.insurer = a["insurer"] + self.dry_run = True + self.token_spenders = [ + getattr(context, spender) for spender in self.token_spender_names + ] + self.prompt_parameters() + + def conv_kes_usdc(self, kes): + return round(kes * 10 ** usdc.decimals() / self.usdc_conversion_rate) + + def set_dry_run(self): + self.dry_run = prompt.enterBool("Dry run?") + + def prompt_parameters(self): + self.farmer_count = prompt.enterNumber( + "Enter the number of farmers", + default=5052, + len=42, + ) + self.farmer_hd_start_index = prompt.enterNumber( + "Enter the Farmer HD wallet start index", + default=20000, + len=42, + ) + self.escrow_hd_start_index = prompt.enterNumber( + "Enter the Escrow HD wallet start index", + default=10, + len=42, + ) + self.batch_size = prompt.enterNumber( + "Enter the batch size for permits", + default=100, + len=42, + ) + self.approval_amount = prompt.enterNumber( + "Enter the approval amount for permits", + default=1000 * 10 ** usdc.decimals(), + len=42, + ) + + self.farmer_hd_last_index = None + self.farmer_hd_max_index = self.farmer_hd_start_index + self.farmer_count + + self.partnerWallets = { + partner: { + "index": index + self.escrow_hd_start_index, + "wallet": getHdWallet("escrow", index + self.escrow_hd_start_index), + } + for index, partner in enumerate(self.partners) + } + + self.funding = { + "single": { + "KES": { + "Lemonade": prompt.enterNumber( + "Enter the Lemonade funding amount in KES", + default=1000, + len=42, + ), + "WFP": prompt.enterNumber( + "Enter the WFP funding amount in KES", + default=1000, + len=42, + ), + "Farmer": prompt.enterNumber( + "Enter the Farmer funding amount in KES", + default=500, + len=42, + ), + } + } + } + self.funding["single"]["USDC"] = { + key: self.conv_kes_usdc(value) + for key, value in self.funding["single"]["KES"].items() + } + self.funding["total"] = { + "farmer": { + keyCurr: sum(value for value in valueCurr.values()) + for keyCurr, valueCurr in self.funding["single"].items() + }, + "partner": { + keyCurr: { + keyPartner: value * self.farmer_count + for keyPartner, value in valueCurr.items() + } + for keyCurr, valueCurr in self.funding["single"].items() + }, + } + + def print_parameters(self): + data = self.funding + """ + Prints a formatted table of the funding parameters. + """ + + # Extracting and processing data + rows = [] + + # Processing 'single' category + for currency, entities in data["single"].items(): + for entity, value in entities.items(): + rows.append(["single", "", currency, entity, value]) + + # Processing 'total' category + for category, currencies in data["total"].items(): + for currency, entities in currencies.items(): + if isinstance(entities, int): # Extract value directly + rows.append(["total", category, currency, "", entities]) + else: + for entity, value in entities.items(): + rows.append(["total", category, currency, entity, value]) + + # Find max widths for each column for alignment + col_widths = [ + max( + len(str(row[i])) + for row in rows + + [["Category", "Subcategory", "Currency", "Entity", "Value"]] + ) + for i in range(5) + ] + + # Print header + print( + f"{'Category':<{col_widths[0]}} {'Subcategory':<{col_widths[1]}} {'Currency':<{col_widths[2]}} {'Entity':<{col_widths[3]}} {'Value':>{col_widths[4]}}" + ) + print("-" * (sum(col_widths) + 10)) # Line separator + + # Print rows with optimized display (avoiding duplicates) + prev_category, prev_subcategory, prev_currency = None, None, None + for category, subcategory, currency, entity, value in rows: + category_str = ( + category if category != prev_category else "" + ) # Print only if different + subcategory_str = ( + subcategory if subcategory != prev_subcategory else "" + ) # Print only if different + currency_str = ( + currency if currency != prev_currency else "" + ) # Print only if different + print( + f"{category_str:<{col_widths[0]}} {subcategory_str:<{col_widths[1]}} {currency_str:<{col_widths[2]}} {entity:<{col_widths[3]}} {value:>{col_widths[4]}}" + ) + prev_category, prev_subcategory, prev_currency = ( + category, + subcategory, + currency, + ) + + def check_parameters(self): + return ( + all( + hasattr(self, element) + for element in [ + "farmer_count", + "farmer_hd_start_index", + "escrow_hd_start_index", + "funding", + ] + ) + and all( + hasattr(self.funding, keySingleTotal) + for keySingleTotal in ["single", "total"] + ) + and all( + [ + hasattr(self.funding[keySingleTotal], keyCurr) + for keyCurr in self.currencies + for keySingleTotal in ["single", "total"] + ] + ) + and all( + [ + hasattr( + self.funding["single"][keyCurr], + keyPartner, + ) + for keyPartner in self.partners + for keyCurr in self.currencies + ] + ) + and all( + [ + hasattr(self.funding["total"][keyCurr], keyPartner) + for keyCurr in self.currencies + for keyPartner in self.partners + ] + ) + ) + + def fund_escrows(self): + if self.check_parameters(): + for i in range( + self.hd_start_index, self.hd_start_index + self.farmer_count + ): + pass + + else: + print("Please set the parameters first.") + return + + def fund_escrows(self): + for partner, wallet in self.partnerWallets.items(): + targetFunding = self.funding["total"]["partner"]["USDC"][partner] + balance = usdc.balanceOf(wallet["wallet"]) + if balance < targetFunding: + print( + f"Insufficient funds in {partner} wallet. Current balance: {balance}, required: {targetFunding}" + ) + if self.dry_run: + print( + ( + f"Dry run: Transfer " + f"{targetFunding - balance} USDC to {partner} wallet" + ) + ) + continue + usdc.transfer( + wallet["wallet"], + targetFunding - balance, + {"from": self.instanceOperator}, + ) + else: + print( + ( + f"{partner} wallet has sufficient funds" + f" (target = {targetFunding}, balance: {balance})." + ) + ) + + def approve_escrows(self): + spender = context.multicallAddress + for partner, wallet in self.partnerWallets.items(): + targetFunding = self.funding["total"]["partner"]["USDC"][partner] + print( + ( + f"{'Dry run: ' if self.dry_run else ''}Approve {partner} wallet " + f"for {targetFunding} USDC to {spender}" + ) + ) + if self.dry_run: + continue + + if wallet["wallet"].balance() < 0.05 * 10**18: + print(f"Insufficient funds in {partner} wallet - funding") + self.instanceOperator.transfer(wallet["wallet"], 0.05 * 10**18) + + usdc.approve(spender, targetFunding, {"from": wallet["wallet"]}) + + # Generate EIP-712 Permit signature + def get_permit_calldata(self, wallet, spender, amount, nonce, deadline): + + domain = { + "name": usdc.name(), + "version": "1", + "chainId": web3.eth.chain_id, + "verifyingContract": usdc.address, + } + + types = { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + {"name": "verifyingContract", "type": "address"}, + ], + "Permit": [ + {"name": "owner", "type": "address"}, + {"name": "spender", "type": "address"}, + {"name": "value", "type": "uint256"}, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + ], + } + + message = { + "owner": wallet.address, + "spender": spender.address, + "value": amount, + "nonce": nonce, + "deadline": deadline, + } + + # Encode and sign the permit data + typed_data = { + "types": types, + "domain": domain, + "primaryType": "Permit", + "message": message, + } + + signature = Account.sign_message( + encode_typed_data(full_message=typed_data), wallet.private_key + ) + + calldata = usdc.permit.encode_input( + wallet.address, + spender.address, + self.approval_amount, + deadline, + signature.v, + signature.r, + signature.s, + ) + + return calldata + + def relayCalls(self, multicall_data): + tx = multicall.aggregate(multicall_data, {"from": self.insurer}) + tx.wait(1) + + # Send batched permits via multicall + def batch_process(self, num_wallets=1, get_call_data=lambda x: None): + multicall_data = [] + if self.farmer_hd_last_index is not None: + self.farmer_hd_start_index = self.farmer_hd_last_index + 1 + else: + self.farmer_hd_last_index = self.farmer_hd_start_index + last_index = self.farmer_hd_start_index + try: + for w_idx in range( + self.farmer_hd_start_index, + min( + self.farmer_hd_start_index + num_wallets, + self.farmer_hd_max_index + 1, + ), + ): + self.farmer_hd_last_index = w_idx + + multicall_data.extend(get_call_data(w_idx)) + + # Break into batches + if len(multicall_data) >= self.batch_size: + print( + ( + f"Sending batch of {len(multicall_data)} permits " + f"(starting from {last_index} to {w_idx})" + ) + ) + last_index = w_idx + self.relayCalls(multicall_data) + multicall_data = [] # Reset batch + + self.farmer_hd_last_index = w_idx + + # Send remaining transactions + if len(multicall_data) > 0: + print( + ( + f"Sending batch of {len(multicall_data)} permits " + f"(starting from {self.farmer_hd_last_index} to {w_idx})" + ) + ) + self.relayCalls(multicall_data) + + except Exception as e: + print(f"Error: {e}") + + def get_multi_permit_calldata(self, index): + wallet = getHdWallet(context.farmer_hd_base, index) + multicall_data = [] + nonce = usdc.nonces(wallet.address) # Get current nonce + for s_idx, spender in enumerate(self.token_spenders): + deadline = int(time()) + 3600 # 1 hour from now + + # Encode permit call + permit_calldata = self.get_permit_calldata( + wallet, spender, self.approval_amount, nonce + s_idx, deadline + ) + + multicall_data.append((usdc.address, permit_calldata)) + return multicall_data + + def get_airdrop_calldata(self, index): + wallet = getHdWallet(context.farmer_hd_base, index) + multicall_data = [] + for partner, funding in self.funding["single"]["USDC"].items(): + calldata = usdc.transferFrom.encode_input( + self.partnerWallets[partner]["wallet"].address, wallet.address, funding + ) + multicall_data.append((usdc.address, calldata)) + return multicall_data + + def batch_permits(self, num_wallets=1): + self.batch_process(num_wallets, self.get_multi_permit_calldata) + + def batch_airdrop(self, num_wallets=1): + self.batch_process(num_wallets, self.get_airdrop_calldata) + + def check_allowance(self, start, stop, verbose=False): + if verbose: + print( + (f"{'Allowances:'.ljust(40)}" f"{' '.join(self.token_spender_names)}") + ) + for i in range(start, stop): + + wallet = getHdWallet(context.farmer_hd_base, i) + values = [ + usdc.allowance(wallet.address, spender.address) + for spender in self.token_spenders + ] + allowances = " ".join([str(value).ljust(20) for value in values]) + if verbose: + print(f"Allowance for wallet {wallet.address} ({i}): {allowances}") + else: + if i % 100 == 0: + print(f"Checking wallet {i}-{i+100}...") + if any(value != self.approval_amount for value in values): + print(f"Allowance for wallet {wallet.address} ({i}): {allowances}") + + def check_balances(self, start, stop, verbose=False): + for i in range(start, stop): + wallet = getHdWallet(context.farmer_hd_base, i) + bal = usdc.balanceOf(wallet.address) + if verbose: + print(f"Balance for wallet {wallet.address} ({i}): {bal}") + else: + if i % 100 == 0: + print(f"Checking wallet {i}-{i+100}...") + if bal != self.funding["total"]["farmer"]["USDC"]: + print(f"Balance for wallet {wallet.address} ({i}): {bal}") + + +farmer_funding = FarmerFunding() diff --git a/scripts/instance.py b/scripts/instance.py index a3a54b0..c166b94 100644 --- a/scripts/instance.py +++ b/scripts/instance.py @@ -9,7 +9,7 @@ from brownie import ( Wei, - Contract, + Contract, BundleToken, RiskpoolToken, CoreProxy, @@ -30,7 +30,7 @@ PolicyDefaultFlow, InstanceOperatorService, InstanceService, - network + network, ) from scripts.const import ( @@ -50,38 +50,42 @@ contractFromAddress, ) + class GifRegistry(object): - def __init__( - self, - owner: Account, - publishSource: bool = False - ): + def __init__(self, owner: Account, publishSource: bool = False): controller = RegistryController.deploy( - {'from': owner}, - publish_source=publishSource) + {"from": owner}, publish_source=publishSource + ) encoded_initializer = encode_function_data( - s2b32(GIF_RELEASE), - initializer=controller.initializeRegistry) + s2b32(GIF_RELEASE), initializer=controller.initializeRegistry + ) proxy = CoreProxy.deploy( controller.address, - encoded_initializer, - {'from': owner}, - publish_source=publishSource) + encoded_initializer, + {"from": owner}, + publish_source=publishSource, + ) self.owner = owner self.registry = contractFromAddress(RegistryController, proxy.address) - print('owner {}'.format(owner)) - print('controller.address {}'.format(controller.address)) - print('proxy.address {}'.format(proxy.address)) - print('registry.address {}'.format(self.registry.address)) - print('registry.getContract(InstanceOperatorService) {}'.format(self.registry.getContract(s2h("InstanceOperatorService")))) - - self.registry.register(s2b32("Registry"), proxy.address, {'from': owner}) - self.registry.register(s2b32("RegistryController"), controller.address, {'from': owner}) + print("owner {}".format(owner)) + print("controller.address {}".format(controller.address)) + print("proxy.address {}".format(proxy.address)) + print("registry.address {}".format(self.registry.address)) + print( + "registry.getContract(InstanceOperatorService) {}".format( + self.registry.getContract(s2h("InstanceOperatorService")) + ) + ) + + self.registry.register(s2b32("Registry"), proxy.address, {"from": owner}) + self.registry.register( + s2b32("RegistryController"), controller.address, {"from": owner} + ) def getOwner(self) -> Account: return self.owner @@ -93,73 +97,109 @@ def getRegistry(self) -> RegistryController: class GifInstance(GifRegistry): def __init__( - self, - owner: Account = None, - instanceWallet: Account = None, - registryAddress = None, + self, + owner: Account = None, + instanceWallet: Account = None, + registryAddress=None, publishSource: bool = False, - setInstanceWallet: bool = True + setInstanceWallet: bool = True, ): if registryAddress: self.fromRegistryAddress(registryAddress) - self.owner=owner - + self.owner = owner + elif owner: - super().__init__( - owner, - publishSource) - - self.deployWithRegistry( - self.registry, - owner, - publishSource) - + super().__init__(owner, publishSource) + + self.deployWithRegistry(self.registry, owner, publishSource) + if setInstanceWallet: self.instanceOperatorService.setInstanceWallet( - instanceWallet, - {'from': owner}) - + instanceWallet, {"from": owner} + ) + else: - raise ValueError('either owner or registry_address need to be provided') + raise ValueError("either owner or registry_address need to be provided") + def __str__(self): + return "GifInstance class with registry = {}".format(self.registry.address) def deployWithRegistry( - self, - registry: GifRegistry, - owner: Account, - publishSource: bool + self, registry: GifRegistry, owner: Account, publishSource: bool ): # gif instance tokens - self.bundleToken = deployGifToken("BundleToken", BundleToken, registry, owner, publishSource) - self.riskpoolToken = deployGifToken("RiskpoolToken", RiskpoolToken, registry, owner, publishSource) + self.bundleToken = deployGifToken( + "BundleToken", BundleToken, registry, owner, publishSource + ) + self.riskpoolToken = deployGifToken( + "RiskpoolToken", RiskpoolToken, registry, owner, publishSource + ) # modules (need to be deployed first) # deploy order needs to respect module dependencies - self.access = deployGifModuleV2("Access", AccessController, registry, owner, publishSource) - self.component = deployGifModuleV2("Component", ComponentController, registry, owner, publishSource) - self.query = deployGifModuleV2("Query", QueryModule, registry, owner, publishSource) - self.license = deployGifModuleV2("License", LicenseController, registry, owner, publishSource) - self.policy = deployGifModuleV2("Policy", PolicyController, registry, owner, publishSource) - self.bundle = deployGifModuleV2("Bundle", BundleController, registry, owner, publishSource) - self.pool = deployGifModuleV2("Pool", PoolController, registry, owner, publishSource) - self.treasury = deployGifModuleV2("Treasury", TreasuryModule, registry, owner, publishSource) + self.access = deployGifModuleV2( + "Access", AccessController, registry, owner, publishSource + ) + self.component = deployGifModuleV2( + "Component", ComponentController, registry, owner, publishSource + ) + self.query = deployGifModuleV2( + "Query", QueryModule, registry, owner, publishSource + ) + self.license = deployGifModuleV2( + "License", LicenseController, registry, owner, publishSource + ) + self.policy = deployGifModuleV2( + "Policy", PolicyController, registry, owner, publishSource + ) + self.bundle = deployGifModuleV2( + "Bundle", BundleController, registry, owner, publishSource + ) + self.pool = deployGifModuleV2( + "Pool", PoolController, registry, owner, publishSource + ) + self.treasury = deployGifModuleV2( + "Treasury", TreasuryModule, registry, owner, publishSource + ) # TODO these contracts do not work with proxy pattern - self.policyFlow = deployGifService(PolicyDefaultFlow, registry, owner, publishSource) + self.policyFlow = deployGifService( + PolicyDefaultFlow, registry, owner, publishSource + ) # services - self.instanceService = deployGifModuleV2("InstanceService", InstanceService, registry, owner, publishSource) - self.componentOwnerService = deployGifModuleV2("ComponentOwnerService", ComponentOwnerService, registry, owner, publishSource) - self.oracleService = deployGifModuleV2("OracleService", OracleService, registry, owner, publishSource) - self.riskpoolService = deployGifModuleV2("RiskpoolService", RiskpoolService, registry, owner, publishSource) + self.instanceService = deployGifModuleV2( + "InstanceService", InstanceService, registry, owner, publishSource + ) + self.componentOwnerService = deployGifModuleV2( + "ComponentOwnerService", + ComponentOwnerService, + registry, + owner, + publishSource, + ) + self.oracleService = deployGifModuleV2( + "OracleService", OracleService, registry, owner, publishSource + ) + self.riskpoolService = deployGifModuleV2( + "RiskpoolService", RiskpoolService, registry, owner, publishSource + ) # TODO these contracts do not work with proxy pattern - self.productService = deployGifService(ProductService, registry, owner, publishSource) + self.productService = deployGifService( + ProductService, registry, owner, publishSource + ) - # needs to be the last module to register as it will - # perform some post deploy wirings and changes the address + # needs to be the last module to register as it will + # perform some post deploy wirings and changes the address # of the instance operator service to its true address - self.instanceOperatorService = deployGifModuleV2("InstanceOperatorService", InstanceOperatorService, registry, owner, publishSource) + self.instanceOperatorService = deployGifModuleV2( + "InstanceOperatorService", + InstanceOperatorService, + registry, + owner, + publishSource, + ) # post deploy wiring steps # self.bundleToken.setBundleModule(self.bundle) @@ -167,7 +207,6 @@ def deployWithRegistry( # ensure that the instance has 32 contracts when freshly deployed assert 32 == registry.contracts() - def fromRegistryAddress(self, registry_address): self.registry = contractFromAddress(RegistryController, registry_address) self.access = self.contractFromGifRegistry(AccessController, "Access") @@ -180,25 +219,39 @@ def fromRegistryAddress(self, registry_address): self.pool = self.contractFromGifRegistry(PoolController, "Pool") self.treasury = self.contractFromGifRegistry(TreasuryModule, "Treasury") - self.instanceService = self.contractFromGifRegistry(InstanceService, "InstanceService") - self.oracleService = self.contractFromGifRegistry(OracleService, "OracleService") - self.riskpoolService = self.contractFromGifRegistry(RiskpoolService, "RiskpoolService") - self.productService = self.contractFromGifRegistry(ProductService, "ProductService") - - self.policyFlow = self.contractFromGifRegistry(PolicyDefaultFlow, "PolicyDefaultFlow") + self.instanceService = self.contractFromGifRegistry( + InstanceService, "InstanceService" + ) + self.oracleService = self.contractFromGifRegistry( + OracleService, "OracleService" + ) + self.riskpoolService = self.contractFromGifRegistry( + RiskpoolService, "RiskpoolService" + ) + self.productService = self.contractFromGifRegistry( + ProductService, "ProductService" + ) + + self.policyFlow = self.contractFromGifRegistry( + PolicyDefaultFlow, "PolicyDefaultFlow" + ) self.componentOwnerService = self.contractFromGifRegistry(ComponentOwnerService) - self.instanceOperatorService = self.contractFromGifRegistry(InstanceOperatorService) - + self.instanceOperatorService = self.contractFromGifRegistry( + InstanceOperatorService + ) def contractFromGifRegistry(self, contractClass, name=None): if not name: nameB32 = s2b32(contractClass._name) else: nameB32 = s2b32(name) - + address = self.registry.getContract(nameB32) return contractFromAddress(contractClass, address) + def getOwner(self): + return self.instanceOperatorService.owner() + def getRegistry(self) -> GifRegistry: return self.registry @@ -219,7 +272,7 @@ def getLicense(self) -> LicenseController: def getPolicy(self) -> PolicyController: return self.policy - + def getPolicyDefaultFlow(self) -> PolicyDefaultFlow: return self.policyFlow @@ -237,28 +290,28 @@ def getInstanceOperatorService(self) -> InstanceOperatorService: def getInstanceService(self) -> InstanceService: return self.instanceService - + def getRiskpoolService(self) -> RiskpoolService: return self.riskpoolService - + def getProductService(self) -> ProductService: return self.productService - + def getComponentOwnerService(self) -> ComponentOwnerService: return self.componentOwnerService - + def getOracleService(self) -> OracleService: return self.oracleService def dump_sources(registryAddress=None): - dump_sources_summary_dir = './dump_sources/{}'.format(network.show_active()) - dump_sources_summary_file = '{}/contracts.txt'.format(dump_sources_summary_dir) + dump_sources_summary_dir = "./dump_sources/{}".format(network.show_active()) + dump_sources_summary_file = "{}/contracts.txt".format(dump_sources_summary_dir) # create parent dir try: - os.mkdir('./dump_sources') + os.mkdir("./dump_sources") except OSError: pass @@ -267,12 +320,12 @@ def dump_sources(registryAddress=None): os.mkdir(dump_sources_summary_dir) except OSError: pass - + instance = None - + if registryAddress: instance = GifInstance(registryAddress=registryAddress) - + contracts = [] contracts.append(dump_single(CoreProxy, "Registry", instance)) contracts.append(dump_single(RegistryController, "RegistryController", instance)) @@ -307,47 +360,59 @@ def dump_sources(registryAddress=None): contracts.append(dump_single(PolicyDefaultFlow, "PolicyDefaultFlow", instance)) contracts.append(dump_single(CoreProxy, "InstanceService", instance)) - contracts.append(dump_single(InstanceService, "InstanceServiceController", instance)) + contracts.append( + dump_single(InstanceService, "InstanceServiceController", instance) + ) contracts.append(dump_single(CoreProxy, "ComponentOwnerService", instance)) - contracts.append(dump_single(ComponentOwnerService, "ComponentOwnerServiceController", instance)) + contracts.append( + dump_single(ComponentOwnerService, "ComponentOwnerServiceController", instance) + ) contracts.append(dump_single(CoreProxy, "OracleService", instance)) contracts.append(dump_single(OracleService, "OracleServiceController", instance)) contracts.append(dump_single(CoreProxy, "RiskpoolService", instance)) - contracts.append(dump_single(RiskpoolService, "RiskpoolServiceController", instance)) + contracts.append( + dump_single(RiskpoolService, "RiskpoolServiceController", instance) + ) contracts.append(dump_single(ProductService, "ProductService", instance)) contracts.append(dump_single(CoreProxy, "InstanceOperatorService", instance)) - contracts.append(dump_single(InstanceOperatorService, "InstanceOperatorServiceController", instance)) + contracts.append( + dump_single( + InstanceOperatorService, "InstanceOperatorServiceController", instance + ) + ) - with open(dump_sources_summary_file,'w') as f: - f.write('\n'.join(contracts)) - f.write('\n') + with open(dump_sources_summary_file, "w") as f: + f.write("\n".join(contracts)) + f.write("\n") - print('\n'.join(contracts)) - print('\nfor contract json files see directory {}'.format(dump_sources_summary_dir)) + print("\n".join(contracts)) + print("\nfor contract json files see directory {}".format(dump_sources_summary_dir)) def dump_single(contract, registryName, instance=None) -> str: info = contract.get_verification_info() netw = network.show_active() - compiler = info['compiler_version'] - optimizer = info['optimizer_enabled'] - runs = info['optimizer_runs'] - license = info['license_identifier'] - address = 'no_address' - name = info['contract_name'] + compiler = info["compiler_version"] + optimizer = info["optimizer_enabled"] + runs = info["optimizer_runs"] + license = info["license_identifier"] + address = "no_address" + name = info["contract_name"] if instance: nameB32 = s2b32(registryName) address = instance.registry.getContract(nameB32) - dump_sources_contract_file = './dump_sources/{}/{}.json'.format(netw, name) - with open(dump_sources_contract_file,'w') as f: - f.write(json.dumps(contract.get_verification_info()['standard_json_input'])) + dump_sources_contract_file = "./dump_sources/{}/{}.json".format(netw, name) + with open(dump_sources_contract_file, "w") as f: + f.write(json.dumps(contract.get_verification_info()["standard_json_input"])) - return '{} {} {} {} {} {} {}'.format(netw, compiler, optimizer, runs, license, address, name) + return "{} {} {} {} {} {} {}".format( + netw, compiler, optimizer, runs, license, address, name + ) diff --git a/scripts/interactive copy.py b/scripts/interactive copy.py new file mode 100644 index 0000000..9a39b0f --- /dev/null +++ b/scripts/interactive copy.py @@ -0,0 +1,130 @@ +import asyncio +import os + +from brownie import ( + interface, + network, + accounts, + InstanceService, + InstanceOperatorService, + ComponentOwnerService, + AyiiProduct, + AyiiOracle, + AyiiRiskpool, + ComponentController, + UsdcAccounting, +) + +from scripts.onepassword import signIn, getItem, getSecret, getLastItem +from scripts.deploy_ayii import ( + confirm, + stakeholders_accounts_ganache, + check_funds, + amend_funds, + deploy, + from_registry, + from_component, + deploy_product_riskpool, + verify_deploy, +) +from scripts.util import s2b, b2s, h2sLeft, contract_from_address, decodeEnum + +################################################################################# +# +# Step 6 - avax-test end-to-end testing +# +# +# Step 6.1 - setup stakeholders + amounts +# +""" instanceOperator = a['instanceOperator'] +investor = a['investor'] +funding = 1000 * 10 ** usdc.decimals() # 1000$ + +insurer = a['insurer'] +customer = a['customer1'] +""" +# +# Step 6. 2 - create risk (see https://lccc.etherisc.com/risks for risk data) +# +""" +projectId = s2b('5413') +uaiId = s2b('125') +cropId = s2b('greengrams') + +mul = product.getPercentageMultiplier() +(trigger, exit, tsi, aph) = (mul * 0.7, mul * 0, mul * 0.7, mul * 0.17) +tx = product.createRisk(projectId, uaiId, cropId, trigger, + exit, tsi, aph, {'from': insurer}) +riskId = dict(tx.events['LogAyiiRiskDataCreated'])['riskId'] +""" +# +# Step 6.3 - fund riskpool (bundle) +# +""" +usdc.transfer(investor, funding, {'from': instanceOperator}) +usdc.approve(instanceService.getTreasuryAddress(), funding, {'from': investor}) + +bundleId = riskpool.getActiveBundleId(0) +riskpool.fundBundle(bundleId, funding, {'from': investor}) +""" +# +# Step 6.4 - create policy +# +""" +usdc.transfer(customer, premium, {'from': instanceOperator}) +usdc.approve(instanceService.getTreasuryAddress(), premium, {'from': customer}) + +tx = product.applyForPolicy( + customer, premium, sumInsured, riskId, {'from': insurer}) +policyId = dict(tx.events['LogAyiiPolicyCreated'])['policyId'] +""" +# +# Step 6.5 - trigger oracle request +# +""" +tx = product.triggerOracle(policyId, {'from': insurer}) +requestId = dict(tx.events['LogAyiiRiskDataRequested'])['requestId'] +""" +# +# Step 6.6 - check risk (aaay value according to expectations, responseAt > 0) +# +""" +product.getRisk(riskId).dict() + +""" +# Step 6.6 - check policies linked to risk +# + +product.policies(riskId) +product.getPolicyId(riskId, 0) +instanceService.getPolicy(product.getPolicyId(riskId, 0)).dict() + +# +# Step 6.7 - check riskpool funds and allowance +# + +rpw = instanceService.getRiskpoolWallet(riskpool.getId()) +usdc.balanceOf(rpw) / 10 ** usdc.decimals() +usdc.allowance(rpw, instanceService.getTreasuryAddress()) / 10 ** usdc.decimals() + +# +# Step 6.8 - set/increase allowance for treasury if necessary +# + +usdc.approve(instanceService.getTreasuryAddress(), sumInsured, {"from": rpw}) + +# +# Step 6.9 - process policies for risk (bach size = 1) +# + + +product.processPoliciesForRisk(riskId, 1, {"from": insurer}) + +# +# Step 6.10 - check payout +# + +policyId = dict(history[-1].events["LogAyiiPolicyProcessed"])["policyId"] +instanceService.getApplication(policyId).dict() +instanceService.getPolicy(policyId).dict() +usdc.balanceOf(customer) / 10 ** usdc.decimals() diff --git a/scripts/interactive.py b/scripts/interactive.py new file mode 100644 index 0000000..79be6a4 --- /dev/null +++ b/scripts/interactive.py @@ -0,0 +1,547 @@ +from brownie import ( + AyiiProduct, + AyiiRiskpool, + GenericOracle, + GenericRiskpool, + GenericProduct, +) + +from scripts.deploy_ayii import verify_deploy, printBundle, setClfConsumer +from scripts.util import contract_from_address, decodeEnum, b2s, s2b, utcStr, fromWei +from scripts.context import context, a +from scripts.prompt import prompt + + +def listComponents(): + components = context.getComponents() + for index, componentData in enumerate(components): + info = ( + f'{componentData["index"]:>3} : ' + f'{componentData["typeStr"].ljust(12)}\n' + f' Address: {componentData["address"]}\n' + f' Status: {componentData["stateStr"].ljust(12)}' + ) + if componentData["type"] == 0: # Oracle + pass + elif componentData["type"] == 1: # Product + info += ( + "\n" + f' Risks: {componentData["additionalData"]["risks"]}\n' + f' RiskpoolId: {componentData["additionalData"]["riskpoolId"]}\n' + f' Token: {componentData["additionalData"]["erc20Token"]}\n' + f' Policies: {componentData["additionalData"]["policies"]}\n' + f' Applications: {componentData["additionalData"]["applications"]}' + ) + elif componentData["type"] == 2: # Riskpool + info += ( + "\n" + f' Token: {componentData["additionalData"]["erc20Token"]}\n' + f' Capital(wei): {componentData["additionalData"]["capital"]}\n' + f' Bundles: {componentData["additionalData"]["bundles"]}' + ) + + print(info) + + +def selectComponent(type): + components = context.getComponents(type) + ct = [context.oracle.address, context.product.address, context.riskpool.address] + options = [ + f"{i:>3}: {component['address']} {'< in context' if component['address'] in ct else ''}" + for i, component in enumerate(components, 1) + ] + options.append("Cancel") + print(f'Select {decodeEnum("ComponentType", type)}:') + selection = options.index(prompt.menu(options)) + if selection == len(options) - 1: + return None + return components[selection] + + +def selectComponentType(): + options = [f"{i}: {decodeEnum('ComponentType', i)}" for i in range(3)] + options.append("Cancel") + print("Select Component Type:") + selection = options.index(prompt.menu(options)) + if selection == len(options) - 1: + return None + return selection + + +def selectTypeAndComponent(): + type = selectComponentType() + if type is None: + return + component = selectComponent(type) + if component is None: + return + print( + ( + f'Selected: {component["typeStr"]} ' + f'Address: {component["address"]} ' + f'State: {component["stateStr"]}' + ) + ) + if component["type"] == 0: # Oracle + context.oracle = contract_from_address(GenericOracle, component["address"]) + elif component["type"] == 1: # Product + context.product = contract_from_address(AyiiProduct, component["address"]) + elif component["type"] == 2: # Riskpool + context.riskpool = contract_from_address(AyiiRiskpool, component["address"]) + + +def verifyDeploy(): + verify_deploy( + context.accounts, + context.usdc, + context.registry, + context.riskpoolId.getId(), + context.oracle.getId(), + context.product.getId(), + ) + + +def setFeeCapital(): + + instanceOperatorService = context.instanceOperatorService + feeRiskpool = instanceOperatorService.createFeeSpecification( + context.riskpool.getId(), + context.feeCapitalFix, + context.feeCapitalPercentage, + b"", + ) + instanceOperatorService.setCapitalFees( + feeRiskpool, {"from": context.instanceOperator} + ) + + +def setFeePremium(): + + instanceOperatorService = context.instanceOperatorService + feeProduct = instanceOperatorService.createFeeSpecification( + context.product.getId(), + context.feePremiumFix, + context.feePremiumPercentage, + b"", + ) + + instanceOperatorService.setPremiumFees( + feeProduct, {"from": context.instanceOperator} + ) + + +def getFeeSpecification(): + print(dict(context.treasury.getFeeSpecification(context.riskpoolId))) + print(dict(context.treasury.getFeeSpecification(context.productId))) + + +def getComponent(componentId): + address = context.instanceService.getComponent(componentId) + type = context.instanceService.getComponentType(componentId) + contract = [GenericOracle, GenericRiskpool, GenericProduct][type] + return contract_from_address(contract, address) + + +def createRisk(projectId, uaiId, cropId, trigger, exit, tsi, aph): + insurer = a["insurer"] + mul = context.product.getPercentageMultiplier() + (trigger, exit, tsi, aph) = (mul * trigger, mul * exit, mul * tsi, mul * aph) + tx = context.product.createRisk( + s2b(projectId), + s2b(uaiId), + s2b(cropId), + trigger, + exit, + tsi, + aph, + {"from": insurer}, + ) + riskId = dict(tx.events["LogAyiiRiskDataCreated"])["riskId"] + print(f"Risk created with ID: {riskId}") + return riskId + + +def queryRiskParameters(): + projectId = prompt.enterString("Enter the project ID:", r"^\d{1,10}$") + uaiId = prompt.enterString("Enter the UAI ID :", r"^\d{1,10}$") + cropId = prompt.enterString("Enter the crop ID :", r"^[a-zA-Z]+$") + trigger = prompt.enterFloat( + "Enter the trigger :", lowerBound=0, upperBound=1, default=0.7 + ) + exit = prompt.enterFloat( + "Enter the exit :", lowerBound=0, upperBound=1, default=0.0 + ) + tsi = prompt.enterFloat( + "Enter the TSI :", lowerBound=0, upperBound=1, default=0.7 + ) + aph = prompt.enterFloat( + "Enter the APH :", lowerBound=0, upperBound=1, default=0.9876 + ) + return (projectId, uaiId, cropId, trigger, exit, tsi, aph) + + +def createRiskInteractive(): + (projectId, uaiId, cropId, trigger, exit, tsi, aph) = queryRiskParameters() + print("Creating risk:") + print(f"projectId: {projectId}") + print(f"uaiId : {uaiId}") + print(f"cropId : {cropId}") + print(f"Trigger : {trigger}") + print(f"Exit : {exit}") + print(f"TSI : {tsi}") + print(f"APH : {aph}") + if not prompt.confirm("create risk"): + return + createRisk(projectId, uaiId, cropId, trigger, exit, tsi, aph) + + +def getRisks(): + product = context.product + riskCount = product.risks() + result = [] + for riskIndex in range(0, riskCount): + riskId = product.getRiskId(riskIndex) + risk = product.getRisk(riskId) + mul = product.getPercentageMultiplier() + result.append( + { + "riskId": riskId, + "risk": risk, + "mul": mul, + "trigger": risk["trigger"] / mul, + "exit": risk["exit"] / mul, + "tsi": risk["tsi"] / mul, + "aph": risk["aph"] / mul, + "aaay": risk["aaay"] / mul, + "payoutPercentage": risk["payoutPercentage"] / mul, + } + ) + return result + + +def listRisks(): + riskData = getRisks() + riskCount = len(riskData) + if riskCount == 0: + print("No risks available") + return + for riskIndex in range(0, riskCount): + data = riskData[riskIndex] + risk = data["risk"] + print( + ( + f"Index: {riskIndex}\n" + f'RiskId: {data["riskId"]}\n' + f'ProjectId: {b2s(risk["projectId"]).ljust(10)}\n' + f'UAI: {b2s(risk["uaiId"]).ljust(10)}\n' + f'CropId: {b2s(risk["cropId"]).ljust(10)}\n' + f'Trigger: {data["trigger"]:.5f}\n' + f'Exit: {data["exit"]:.5f}\n' + f'TSI: {data["tsi"]:.5f}\n' + f'APH: {data["aph"]:.5f}\n' + f'AAAY: {data["aaay"]:.5f}\n' + f'Payout Pct: {data["payoutPercentage"]:.5f}\n' + f'Triggered: {risk["requestTriggered"]}\n' + f'RequestId: {risk["requestId"]}\n' + f'Response at: {utcStr(risk["responseAt"])}\n' + f'Created: {utcStr(risk["createdAt"])}\n' + f'Updated: {utcStr(risk["updatedAt"])}\n' + ) + ) + + +def listRiskShort(filter=lambda x: True): + riskData = getRisks() + return { + ( + f"{i:>3} :" + f'{b2s(risk["risk"]["projectId"]).ljust(10)}:' + f'{b2s(risk["risk"]["uaiId"]).ljust(10)}:' + f'{b2s(risk["risk"]["cropId"]).ljust(10)}:' + f'{risk["trigger"]:.5f}:' + f'{risk["exit"]:.5f}:' + f'{risk["tsi"]:.5f}:' + f'{risk["aph"]:.5f}:' + f'{risk["aaay"]:.5f}:' + f'{risk["risk"]["requestTriggered"]}:' + f'{utcStr(risk["risk"]["responseAt"])}:' + f'{utcStr(risk["risk"]["createdAt"])}:' + f'{utcStr(risk["risk"]["updatedAt"])}:' + ): risk["riskId"] + for i, risk in enumerate(riskData) + if filter(risk) + } + + +def getPolicies(): + product = context.product + riskCount = product.risks() + result = [] + for riskIndex in range(0, riskCount): + riskId = product.getRiskId(riskIndex) + policyCount = product.policies(riskId) + for policyIndex in range(0, policyCount): + policyId = product.getPolicyId(riskId, policyIndex) + policy = context.instanceService.getPolicy(policyId) + result.append({"policyId": policyId, "policy": policy, "riskId": riskId}) + return result + + +def selectPolicy(): + policies = getPolicies() + if len(policies) == 0: + print("No policies available") + return None + options = [f"{i:>3}: {p['policyId']}" for i, p in enumerate(policies)] + options.append("Cancel") + print("Select Policy:") + selection = options.index(prompt.menu(options)) + if selection == len(options) - 1: + return None + return policies[selection] + + +def selectBundle(): + bundles = context.riskpool.bundles() + if bundles == 0: + print("No bundles available") + return None + elif bundles == 1: + print("Only one bundle available - selecting it") + return 0 + options = [f"{i:>3}: {context.riskpool.getBundle(i)}" for i in range(bundles)] + options.append("Cancel") + print("Select Bundle:") + selection = options.index(prompt.menu(options)) + if selection == len(options) - 1: + return None + return selection + + +def selectRisk(): + risks = context.product.risks() + if risks == 0: + print("No risks available") + return None + elif risks == 1: + print("Only one risk available - selecting it") + return context.product.getRiskId(0) + options = listRiskShort() + options["Cancel"] = None + print("Select Risk:") + selection = prompt.dictMenu(options) + if selection is None: + return None + return context.product.getRiskId(selection) + + +def fundBundle(): + bundleId = selectBundle() + if bundleId is None: + return + printBundle(context.riskpool, bundleId) + + bundle = context.riskpool.getBundle(bundleId) + if bundle["state"] != 0: + print("Bundle is not active - aborting") + return + + funding = prompt.enterCurrency( + "Enter the funding amount:", + context.usdc.decimals(), + lowerBound=0, + upperBound=10000000, + default=0.0, + ) + + if funding == 0: + return + if not prompt.confirm( + f"Fund bundle with {fromWei(funding, context.usdc.decimals())} USDC" + ): + return + + investor = context.accounts["investor"] + if context.usdc.balanceOf(investor) < funding: + print("Insufficient funds at investor - supplying more") + context.usdc.transfer(investor, funding, {"from": context.instanceOperator}) + if ( + context.usdc.allowance(investor, context.instanceService.getTreasuryAddress()) + < funding + ): + print("Approving USDC transfer from investor to treasury") + context.usdc.approve( + context.instanceService.getTreasuryAddress(), funding, {"from": investor} + ) + + context.riskpool.fundBundle(bundle["id"], funding, {"from": investor}) + print("Bundle successfully funded:") + printBundle(context.riskpool, bundleId) + + +def createPolicy(riskId, premium, sumInsured): + + customer = context.accounts["customer1"] + insurer = context.accounts["insurer"] + + if context.usdc.balanceOf(customer) < premium: + print("Insufficient funds at customer - supplying more") + context.usdc.transfer(customer, premium, {"from": context.instanceOperator}) + if ( + context.usdc.allowance(customer, context.instanceService.getTreasuryAddress()) + < premium + ): + print("Approving USDC transfer from customer to treasury") + context.usdc.approve( + context.instanceService.getTreasuryAddress(), premium, {"from": customer} + ) + + print(f"Creating policy for riskId: {riskId}") + tx = context.product.applyForPolicy( + customer, premium, sumInsured, riskId, {"from": insurer} + ) + policyId = dict(tx.events["LogAyiiPolicyCreated"])["policyId"] + print(f"Policy created with ID: {policyId}") + return policyId + + +def queryPolicyParameters(): + premium = prompt.enterCurrency( + "Enter the premium amount:", context.usdc.decimals(), lowerBound=0, default=15.0 + ) + if premium == 0: + return + + sumInsured = prompt.enterCurrency( + "Enter the sum insured amount:", + context.usdc.decimals(), + lowerBound=0, + default=150.0, + ) + if sumInsured == 0: + return + return (premium, sumInsured) + + +def createPolicyInteractive(): + riskId = selectRisk() + if riskId is None: + return + (premium, sumInsured) = queryPolicyParameters() + createPolicy(riskId, premium, sumInsured) + + +def triggerOracle(policyId): + print(f"Triggering oracle request for policyId: {policyId}") + insurer = context.accounts["insurer"] + tx = context.product.triggerOracle( + policyId, + { + "from": insurer + # "gas_limit": 1000000, + # "allow_revert": True + }, + ) + requestId = dict(tx.events["LogAyiiRiskDataRequested"])["requestId"] + print(f"Oracle request triggered with requestId: {requestId}") + + +def triggerOracleInteractive(): + risks = listRiskShort( + lambda r: not r["risk"]["requestTriggered"] and r["risk"]["responseAt"] == 0 + ) + if len(risks.keys()) == 0: + print("No oracle requests available") + return + options = risks + options["Cancel"] = None + print("Select Risk Request:") + print( + ( + " Idx |ProjectId | UaId | " + "CropId | Trig | Exit | TSI | " + "APH | AAAY |Trig|Response | " + "Created | Updated" + ) + ) + riskId = prompt.dictMenu(options) + if riskId is None: + return + try: + policyCount = context.product.policies(riskId) + if policyCount == 0: + print("No policies available for this risk") + return + + policyId = context.product.getPolicyId(riskId, 0) + + if not prompt.confirm(f"Trigger oracle request for policyId: {policyId}"): + return + print(f"Triggering oracle request for policyId: {policyId}") + triggerOracle(policyId) + + except Exception as e: + print(f"Error triggering oracle request: {e}") + + +def doSetClfConsumer(): + setClfConsumer(context) + + +def fullCycle(): + count = prompt.enterNumber("Enter the number of policies to create:", 1, 100) + (projectId, uaiId, cropId, trigger, exit, tsi, aph) = queryRiskParameters() + (premium, sumInsured) = queryPolicyParameters() + + for i in range(count): + uaiId2 = f"{int(uaiId)+i}" + print( + f"Creating risk with projectId: {projectId} / uaiId: {uaiId2} / cropId: {cropId}" + ) + riskId = createRisk(projectId, uaiId2, cropId, trigger, exit, tsi, aph) + policyId = createPolicy(riskId, premium, sumInsured) + triggerOracle(policyId) + + +def cancelOracleRequestInteractive(): + risks = listRiskShort( + lambda r: r["risk"]["requestTriggered"] and r["risk"]["responseAt"] == 0 + ) + if len(risks.keys()) == 0: + print("No oracle requests available") + return + options = risks + options["Cancel"] = None + print("Select Oracle Request:") + print( + ( + " Idx |ProjectId | UaId | " + "CropId | Trig | Exit | TSI | " + "APH | AAAY |Trig|Response | " + "Created | Updated" + ) + ) + riskId = prompt.dictMenu(options) + if riskId is None: + return + + try: + policyCount = context.product.policies(riskId) + if policyCount == 0: + print("No policies available for this risk") + return + policyId = context.product.getPolicyId(riskId, 0) + if not prompt.confirm(f"Cancel oracle request for policyId: {policyId}"): + return + tx = context.product.cancelOracleRequest( + policyId, {"from": context.accounts["insurer"]} + ) + print(f"Oracle request for policyId: {policyId} canceled") + print(tx) + + except Exception as e: + print(f"Error canceling oracle request: {e}") + + +def initializeContext(): + context.initialize() diff --git a/scripts/logger.py b/scripts/logger.py new file mode 100644 index 0000000..d4da38d --- /dev/null +++ b/scripts/logger.py @@ -0,0 +1,28 @@ +class LoggerWrapper: + def __init__(self, base_instance): + """ + Initialize the wrapper with the base instance. + :param base_instance: Instance of the base class to wrap. + """ + self._base_instance = base_instance + + def __getattr__(self, name): + """ + Intercept attribute access and add logging for methods. + :param name: Name of the attribute being accessed. + :return: The wrapped method or the original attribute. + """ + attr = getattr(self._base_instance, name) + + # If it's a callable (method), wrap it with logging + if callable(attr): + + def logged_method(*args, **kwargs): + print(f"Calling method: {name}") + print(f"Arguments: {args}, Keyword Arguments: {kwargs}") + result = attr(*args, **kwargs) + print(f"Result: {result}") + return result + + return logged_method + return attr diff --git a/scripts/menu.py b/scripts/menu.py new file mode 100644 index 0000000..e7b0651 --- /dev/null +++ b/scripts/menu.py @@ -0,0 +1,99 @@ +from scripts.prompt import prompt +from scripts.interactive import ( + context, + listComponents, + createRiskInteractive, + listRisks, + listRiskShort, + selectTypeAndComponent, + fundBundle, + getFeeSpecification, + createPolicyInteractive, + triggerOracleInteractive, + cancelOracleRequestInteractive, + doSetClfConsumer, + fullCycle, + initializeContext, +) + +from scripts.deploy_ayii import ( + check_funds, + amend_funds, + deploy, + deploy_product_with_oracle_riskpool, + verify_deploy, +) + + +def executeFunctionOrObject(param): + if callable(param): + # If param is a function, call it without parameters + return param() + elif isinstance(param, dict) and "function" in param and "args" in param: + # If param is a dictionary with 'function' and 'args', execute the function with arguments + func = param["function"] + args = param["args"] + + if not callable(func): + raise ValueError("'function' in the dictionary must be callable") + if not isinstance(args, (list, tuple)): + raise ValueError("'args' in the dictionary must be a list or tuple") + + return func(*args) + + +def menu(): + run = True + while run: + options = { + "Check Funds": { + "function": check_funds, + "args": [context.accounts, context.usdc], + }, + "Amend Funds": {"function": amend_funds, "args": [context.accounts]}, + "Deploy": {"function": deploy, "args": [context.accounts, context.usdc]}, + "Deploy Product, Oracle and Riskpool": { + "function": deploy_product_with_oracle_riskpool, + "args": [ + context.registry, + context.accounts, + context.usdc, + context.fullCollateralizationLevel, + ], + }, + "Verify Deploy": { + "function": verify_deploy, + "args": [ + context.accounts, + context.usdc, + context.registry, + context.riskpoolId, + context.oracleId, + context.productId, + ], + }, + "Context": context.printContext, + "Initialize Context": initializeContext, + "Accounts": context.printAccounts, + "List Components": listComponents, + "Create Risk": createRiskInteractive, + "List Risks": listRisks, + "List Risk Short": listRiskShort, + "Select Type and Component": selectTypeAndComponent, + "Fund Bundle": fundBundle, + "Create Policy": createPolicyInteractive, + "Trigger Oracle": triggerOracleInteractive, + "Cancel Oracle Request": cancelOracleRequestInteractive, + "Show Fee Specification": getFeeSpecification, + "Set Clf Consumer": doSetClfConsumer, + "Full Cycle": fullCycle, + "Exit": None, + } + selection = prompt.dictMenu(options) + if selection is None: + run = False + else: + executeFunctionOrObject(selection) + + +menu() diff --git a/scripts/onepassword.py b/scripts/onepassword.py new file mode 100644 index 0000000..ebea5b3 --- /dev/null +++ b/scripts/onepassword.py @@ -0,0 +1,65 @@ +import os +from onepassword.client import Client +from onepassword import ( + ItemCreateParams, + ItemCategory, + ItemSection, + ItemField, + ItemFieldType, +) + +client = None +lastItem = None + + +async def signIn(): + global client + # Gets your service account token from the OP_SERVICE_ACCOUNT_TOKEN environment variable. + token = os.getenv("OP_SERVICE_ACCOUNT_TOKEN") + # print(token) + # Connects to 1Password. Fill in your own integration name and version. + client = await Client.authenticate( + auth=token, integration_name="Test", integration_version="v1.0.0" + ) + + +async def listVaults(): + vaults = await client.vaults.list_all() + async for vault in vaults: + print(vault.title, vault.id) + + +async def getVaultId(vault): + vaults = await client.vaults.list_all() + async for v in vaults: + if v.title == vault: + return v.id + + +async def listItems(vault): + items = await client.items.list_all(await getVaultId(vault)) + async for item in items: + print(dict(item)) + + +async def getItem(vault, item): + global lastItem + vault_id = await getVaultId(vault) + items = await client.items.list_all(vault_id) + async for i in items: + if i.title == item: + item_id = i.id + lastItem = await client.items.get(vault_id, item_id) + + +def getLastItem(): + return lastItem + + +def getSecret(section, title): + for s in lastItem.sections: + if s.title == section: + for f in lastItem.fields: + if f.section_id == s.id and f.title == title: + return f.value + return None diff --git a/scripts/product.py b/scripts/product.py index 25137e3..fa4e820 100644 --- a/scripts/product.py +++ b/scripts/product.py @@ -7,7 +7,7 @@ from brownie import ( Wei, - Contract, + Contract, PolicyController, OracleService, ComponentOwnerService, @@ -41,15 +41,16 @@ class GifTestRiskpool(object): - def __init__(self, - instance: GifInstance, - riskpoolKeeper: Account, + def __init__( + self, + instance: GifInstance, + riskpoolKeeper: Account, erc20Token: Account, - riskpoolWallet: Account, - collateralization:int, - name=RISKPOOL_NAME, + riskpoolWallet: Account, + collateralization: int, + name=RISKPOOL_NAME, publishSource=False, - setRiskpoolWallet=True + setRiskpoolWallet=True, ): instanceService = instance.getInstanceService() operatorService = instance.getInstanceOperatorService() @@ -59,68 +60,65 @@ def __init__(self, # 1) add role to keeper keeperRole = instanceService.getRiskpoolKeeperRole() operatorService.grantRole( - keeperRole, - riskpoolKeeper, - {'from': instance.getOwner()}) + keeperRole, riskpoolKeeper, {"from": instance.getOwner()} + ) # 2) keeper deploys riskpool if not setRiskpoolWallet: - name += '_NO_WALLET' - + name += "_NO_WALLET" + self.riskpool = TestRiskpool.deploy( s2b32(name), collateralization, erc20Token, riskpoolWallet, instance.getRegistry(), - {'from': riskpoolKeeper}, - publish_source=publishSource) + {"from": riskpoolKeeper}, + publish_source=publishSource, + ) # 3) riskpool keeperproposes oracle to instance - componentOwnerService.propose( - self.riskpool, - {'from': riskpoolKeeper}) + componentOwnerService.propose(self.riskpool, {"from": riskpoolKeeper}) # 4) instance operator approves riskpool - operatorService.approve( - self.riskpool.getId(), - {'from': instance.getOwner()}) + operatorService.approve(self.riskpool.getId(), {"from": instance.getOwner()}) # 5) instance operator assigns riskpool wallet if setRiskpoolWallet: operatorService.setRiskpoolWallet( - self.riskpool.getId(), - riskpoolWallet, - {'from': instance.getOwner()}) + self.riskpool.getId(), riskpoolWallet, {"from": instance.getOwner()} + ) # 6) setup capital fees fixedFee = 42 - fractionalFee = instanceService.getFeeFractionFullUnit() / 20 # corresponds to 5% + fractionalFee = ( + instanceService.getFeeFractionFullUnit() / 20 + ) # corresponds to 5% feeSpec = operatorService.createFeeSpecification( self.riskpool.getId(), fixedFee, fractionalFee, - b'', - {'from': instance.getOwner()}) + b"", + {"from": instance.getOwner()}, + ) + + operatorService.setCapitalFees(feeSpec, {"from": instance.getOwner()}) - operatorService.setCapitalFees( - feeSpec, - {'from': instance.getOwner()}) - def getId(self) -> int: return self.riskpool.getId() - + def getContract(self) -> TestRiskpool: return self.riskpool class GifTestOracle(object): - def __init__(self, - instance: GifInstance, - oracleOwner: Account, - name=ORACLE_NAME, - publishSource=False + def __init__( + self, + instance: GifInstance, + oracleOwner: Account, + name=ORACLE_NAME, + publishSource=False, ): instanceService = instance.getInstanceService() operatorService = instance.getInstanceOperatorService() @@ -130,45 +128,42 @@ def __init__(self, # 1) add oracle provider role to owner providerRole = instanceService.getOracleProviderRole() operatorService.grantRole( - providerRole, - oracleOwner, - {'from': instance.getOwner()}) + providerRole, oracleOwner, {"from": instance.getOwner()} + ) # 2) oracle provider creates oracle self.oracle = TestOracle.deploy( s2b32(name), instance.getRegistry(), - {'from': oracleOwner}, - publish_source=publishSource) + {"from": oracleOwner}, + publish_source=publishSource, + ) # 3) oracle owner proposes oracle to instance - componentOwnerService.propose( - self.oracle, - {'from': oracleOwner}) + componentOwnerService.propose(self.oracle, {"from": oracleOwner}) # 4) instance operator approves oracle - operatorService.approve( - self.oracle.getId(), - {'from': instance.getOwner()}) - + operatorService.approve(self.oracle.getId(), {"from": instance.getOwner()}) + def getId(self) -> int: return self.oracle.getId() - + def getContract(self) -> TestOracle: return self.oracle class GifTestProduct(object): - def __init__(self, - instance: GifInstance, - token: Account, - capitalOwner: Account, - productOwner: Account, - oracle: GifTestOracle, - riskpool: GifTestRiskpool, - name=PRODUCT_NAME, - publishSource=False + def __init__( + self, + instance: GifInstance, + token: Account, + capitalOwner: Account, + productOwner: Account, + oracle: GifTestOracle, + riskpool: GifTestRiskpool, + name=PRODUCT_NAME, + publishSource=False, ): self.policy = instance.getPolicy() self.oracle = oracle @@ -182,9 +177,8 @@ def __init__(self, # 1) add oracle provider role to owner ownerRole = instanceService.getProductOwnerRole() operatorService.grantRole( - ownerRole, - productOwner, - {'from': instance.getOwner()}) + ownerRole, productOwner, {"from": instance.getOwner()} + ) # 2) product owner creates product self.product = TestProduct.deploy( @@ -194,40 +188,36 @@ def __init__(self, oracle.getId(), riskpool.getId(), instance.getRegistry(), - {'from': productOwner}, - publish_source=publishSource) + {"from": productOwner}, + publish_source=publishSource, + ) # 3) product owner proposes product to instance - componentOwnerService.propose( - self.product, - {'from': productOwner}) + componentOwnerService.propose(self.product, {"from": productOwner}) # 4) instance operator approves product - operatorService.approve( - self.product.getId(), - {'from': instance.getOwner()}) + operatorService.approve(self.product.getId(), {"from": instance.getOwner()}) # 5) instance owner sets token in treasury operatorService.setProductToken( - self.product.getId(), - token, - {'from': instance.getOwner()}) + self.product.getId(), token, {"from": instance.getOwner()} + ) # 5) instance owner creates and sets product fee spec fixedFee = 3 - fractionalFee = instanceService.getFeeFractionFullUnit() / 10 # corresponds to 10% + fractionalFee = ( + instanceService.getFeeFractionFullUnit() / 10 + ) # corresponds to 10% feeSpec = operatorService.createFeeSpecification( self.product.getId(), fixedFee, fractionalFee, - b'', - {'from': instance.getOwner()}) + b"", + {"from": instance.getOwner()}, + ) - operatorService.setPremiumFees( - feeSpec, - {'from': instance.getOwner()}) + operatorService.setPremiumFees(feeSpec, {"from": instance.getOwner()}) - def getId(self) -> int: return self.product.getId() @@ -236,7 +226,7 @@ def getOracle(self) -> GifTestOracle: def getRiskpool(self) -> GifTestRiskpool: return self.riskpool - + def getContract(self) -> TestProduct: return self.product diff --git a/scripts/prompt.py b/scripts/prompt.py new file mode 100644 index 0000000..c9cfb0a --- /dev/null +++ b/scripts/prompt.py @@ -0,0 +1,170 @@ +import questionary +import click +import re +from simple_term_menu import TerminalMenu +from brownie import network + + +class Prompt: + + INTERACTIVE = True + + def menu(self, options): + terminal_menu = TerminalMenu(options) + menu_entry_index = terminal_menu.show() + selection = options[menu_entry_index] + return selection + + def dictMenu(self, dictOptions): + # Convert keys to list and make a menu + selection = self.menu(list(dictOptions.keys())) + # Get the method that corresponds to the selected key + selected = dictOptions.get(selection) + return selected + + def qText(self, prompt, validate, default="", len=20, fill=" "): + response = questionary.text( + self.r_just_prompt(prompt, len, fill), validate=validate, default=default + ).ask() + return response + + def r_just_prompt(self, prompt, len=20, fill=" "): + return prompt.ljust(len, fill) + ": " + + def enterCurrency( + self, + prompt, + scale=1, + lowerBound=0.1, + upperBound=100000, + default=0, + len=20, + fill=" ", + ): + def validateCurrency(value): + try: + # Convert the input to a number + number = float(value) + # Check if the number is within the range + if lowerBound <= number <= upperBound: + return True + else: + return ( + f"Please enter a number between {lowerBound} and {upperBound}." + ) + except ValueError: + return "Invalid input. Please enter a valid number." + + response = self.qText( + prompt, + validate=validateCurrency, + default=f"{default:.2f}", + len=len, + fill=fill, + ) + return int(round(float(response) * 10**scale)) + + def enterNumber( + self, + prompt, + lowerBound=0, + upperBound=2**64, + default=0, + len=20, + fill=" ", + ): + def validateNumber(value): + try: + # Convert the input to a number + number = int(value) + # Check if the number is within the range + if lowerBound <= number <= upperBound: + return True + else: + return ( + f"Please enter a number between {lowerBound} and {upperBound}." + ) + except ValueError: + return "Invalid input. Please enter a valid number." + + response = self.qText( + prompt, + validate=validateNumber, + default=f"{default}", + len=len, + fill=fill, + ) + return int(response) + + def enterFloat( + self, + prompt, + lowerBound=0.0, + upperBound=10**9, + default=0.0, + len=20, + fill=" ", + ): + def validateFloat(value): + try: + # Convert the input to a number + number = float(value) + # Check if the number is within the range + if lowerBound <= number <= upperBound: + return True + else: + return ( + f"Please enter a number between {lowerBound} and {upperBound}." + ) + except ValueError: + return "Invalid input. Please enter a valid number." + + response = self.qText( + prompt, validate=validateFloat, default=f"{default:.2f}", len=len, fill=fill + ) + return float(response) + + def enterString(self, prompt, regex=r".*", default=""): + def validateString(value): + r = re.compile(regex) + if r.match(value): + return True + return "Invalid input. Please enter a valid string." + + response = self.qText( + prompt, validate=validateString, default=default, len=20, fill=" " + ) + return response + + def enterBool( + self, + prompt, + default=True, + len=20, + fill=" ", + ): + response = questionary.confirm( + self.r_just_prompt(prompt, len=len, fill=fill), default=default + ).ask() + return response + + def setInteractive(self, interactive): + self.INTERACTIVE = interactive + + def confirm(self, text): + if self.INTERACTIVE: + return click.confirm( + ( + f"This action will alter the state of " + f"the blockchain <{network.show_active()}>. \n" + f"The action is '{text}'. \n" + f"Do you want to proceed?" + ) + ) + else: + return True + + +# Prompt object +prompt = Prompt() +confirm = prompt.confirm diff --git a/scripts/setup.py b/scripts/setup.py index 18cfb1a..3d7689d 100644 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -8,74 +8,65 @@ from scripts.instance import GifInstance from scripts.ayii_product import GifAyiiProductComplete + def fund_riskpool( - instance: GifInstance, + instance: GifInstance, owner: Account, capitalOwner: Account, riskpool, bundleOwner: Account, coin, amount: int, - createBundle: bool = True + createBundle: bool = True, ): # transfer funds to riskpool keeper and create allowance safetyFactor = 2 - coin.transfer(bundleOwner, safetyFactor * amount, {'from': owner}) - coin.approve(instance.getTreasury(), safetyFactor * amount, {'from': bundleOwner}) + coin.transfer(bundleOwner, safetyFactor * amount, {"from": owner}) + coin.approve(instance.getTreasury(), safetyFactor * amount, {"from": bundleOwner}) # create approval for treasury from capital owner to allow for withdrawls - maxUint256 = 2**256-1 - coin.approve(instance.getTreasury(), maxUint256, {'from': capitalOwner}) + maxUint256 = 2**256 - 1 + coin.approve(instance.getTreasury(), maxUint256, {"from": capitalOwner}) applicationFilter = bytes(0) bundleId = None - if (createBundle): - tx = riskpool.createBundle( - applicationFilter, - amount, - {'from': bundleOwner}) + if createBundle: + tx = riskpool.createBundle(applicationFilter, amount, {"from": bundleOwner}) bundleId = tx.return_value - + return bundleId def fund_customer( - instance: GifInstance, - owner: Account, - account: Account, - coin, - amount: int + instance: GifInstance, owner: Account, account: Account, coin, amount: int ): - coin.transfer(account, amount, {'from': owner}) - coin.approve(instance.getTreasury(), amount, {'from': account}) + coin.transfer(account, amount, {"from": owner}) + coin.approve(instance.getTreasury(), amount, {"from": account}) def apply_for_policy( - instance: GifInstance, + instance: GifInstance, owner: Account, - product, + product, customer: Account, coin, premium: int, - sumInsured: int + sumInsured: int, ): # transfer premium funds to customer and create allowance - coin.transfer(customer, premium, {'from': owner}) - coin.approve(instance.getTreasury(), premium, {'from': customer}) + coin.transfer(customer, premium, {"from": owner}) + coin.approve(instance.getTreasury(), premium, {"from": customer}) # create minimal policy application metaData = bytes(0) applicationData = bytes(0) tx = product.applyForPolicy( - premium, - sumInsured, - metaData, - applicationData, - {'from': customer}) - + premium, sumInsured, metaData, applicationData, {"from": customer} + ) + # print(tx.events) # returns policy id diff --git a/scripts/util.py b/scripts/util.py index fe87357..468f3a7 100644 --- a/scripts/util.py +++ b/scripts/util.py @@ -1,7 +1,15 @@ +import json +import re +import requests +from datetime import datetime, timezone +from decimal import Decimal + from web3 import Web3 from brownie import ( - Contract, + web3, + network, + Contract, CoreProxy, ) @@ -9,37 +17,72 @@ from brownie.network import accounts from brownie.network.account import Account +CHAIN_ID_MUMBAI = 80001 +CHAIN_ID_FUJI = 43113 +CHAIN_ID_GOERLI = 5 + +CHAIN_ID_AVAX = 43114 +CHAIN_ID_MAINNET = 1 + +CHAIN_IDS_REQUIRING_CONFIRMATIONS = [ + CHAIN_ID_MUMBAI, + CHAIN_ID_FUJI, + CHAIN_ID_GOERLI, + CHAIN_ID_AVAX, + CHAIN_ID_MAINNET, +] + + def s2h(text: str) -> str: - return Web3.toHex(text.encode('ascii')) + return Web3.to_hex(text.encode("ascii")) + def h2s(hex: str) -> str: - return Web3.toText(hex).split('\x00')[-1] + return Web3.to_text(hex).split("\x00")[-1] + def h2sLeft(hex: str) -> str: - return Web3.toText(hex).split('\x00')[0] + return Web3.to_text(hex).split("\x00")[0] + def s2b32(text: str): - return '{:0<66}'.format(Web3.toHex(text.encode('ascii')))[:66] + return "{:0<66}".format(Web3.to_hex(text.encode("ascii")))[:66] + def b322s(b32: bytes): - return b32.decode().split('\x00')[0] + return b32.decode().split("\x00")[0] -def s2b(text:str): + +def s2b(text: str): return s2b32(text) + def b2s(b32: bytes): return b322s(b32) -def keccak256(text:str): - return Web3.solidityKeccak(['string'], [text]).hex() + +def keccak256(text: str): + return Web3.solidity_keccak(["string"], [text]).hex() + def get_account(mnemonic: str, account_offset: int) -> Account: - return accounts.from_mnemonic( - mnemonic, - count=1, - offset=account_offset) + return accounts.from_mnemonic(mnemonic, count=1, offset=account_offset) + + +def wait_for_confirmations(tx): + if web3.chain_id in CHAIN_IDS_REQUIRING_CONFIRMATIONS: + if not is_forked_network(): + print("waiting for confirmations ...") + tx.wait(2) + else: + print("not waiting for confirmations in a forked network...") + + +def is_forked_network(): + return "fork" in network.show_active() + -# source: https://github.com/brownie-mix/upgrades-mix/blob/main/scripts/helpful_scripts.py +# source: https://github.com/brownie-mix/upgrades-mix/blob/main/scripts/helpful_scripts.py def encode_function_data(*args, initializer=None): """Encodes the function call so we can work with an initializer. Args: @@ -51,127 +94,171 @@ def encode_function_data(*args, initializer=None): Returns: [bytes]: Return the encoded bytes. """ - if not len(args): args = b'' + if not len(args): + args = b"" - if initializer: return initializer.encode_input(*args) + if initializer: + return initializer.encode_input(*args) + + return b"" - return b'' # generic upgradable gif module deployment -def deployGifModule( - controllerClass, - storageClass, - registry, - owner, - publishSource -): + + +def deployGifModule(controllerClass, storageClass, registry, owner, publishSource): controller = controllerClass.deploy( - registry.address, - {'from': owner}, - publish_source=publishSource) - + registry.address, {"from": owner}, publish_source=publishSource + ) + storage = storageClass.deploy( - registry.address, - {'from': owner}, - publish_source=publishSource) + registry.address, {"from": owner}, publish_source=publishSource + ) - controller.assignStorage(storage.address, {'from': owner}) - storage.assignController(controller.address, {'from': owner}) + controller.assignStorage(storage.address, {"from": owner}) + storage.assignController(controller.address, {"from": owner}) - registry.register(controller.NAME.call(), controller.address, {'from': owner}) - registry.register(storage.NAME.call(), storage.address, {'from': owner}) + registry.register(controller.NAME.call(), controller.address, {"from": owner}) + registry.register(storage.NAME.call(), storage.address, {"from": owner}) return contractFromAddress(controllerClass, storage.address) + # gif token deployment -def deployGifToken( - tokenName, - tokenClass, - registry, - owner, - publishSource -): - print('token {} deploy'.format(tokenName)) - token = tokenClass.deploy( - {'from': owner}, - publish_source=publishSource) + + +def deployGifToken(tokenName, tokenClass, registry, owner, publishSource): + print("token {} deploy".format(tokenName)) + token = tokenClass.deploy({"from": owner}, publish_source=publishSource) tokenNameB32 = s2b32(tokenName) - print('token {} register'.format(tokenName)) - registry.register(tokenNameB32, token.address, {'from': owner}) + print("token {} register".format(tokenName)) + registry.register(tokenNameB32, token.address, {"from": owner}) return token # generic open zeppelin upgradable gif module deployment -def deployGifModuleV2( - moduleName, - controllerClass, - registry, - owner, - publishSource -): - print('module {} deploy controller'.format(moduleName)) - controller = controllerClass.deploy( - {'from': owner}, - publish_source=publishSource) +def deployGifModuleV2(moduleName, controllerClass, registry, owner, publishSource): + print("module {} deploy controller".format(moduleName)) + controller = controllerClass.deploy({"from": owner}, publish_source=publishSource) encoded_initializer = encode_function_data( - registry.address, - initializer=controller.initialize) + registry.address, initializer=controller.initialize + ) - print('module {} deploy proxy'.format(moduleName)) + print("module {} deploy proxy".format(moduleName)) proxy = CoreProxy.deploy( - controller.address, - encoded_initializer, - {'from': owner}, - publish_source=publishSource) + controller.address, + encoded_initializer, + {"from": owner}, + publish_source=publishSource, + ) moduleNameB32 = s2b32(moduleName) - controllerNameB32 = s2b32('{}Controller'.format(moduleName))[:32] + controllerNameB32 = s2b32("{}Controller".format(moduleName))[:32] - print('module {} ({}) register controller'.format(moduleName, controllerNameB32)) - registry.register(controllerNameB32, controller.address, {'from': owner}) - print('module {} ({}) register proxy'.format(moduleName, moduleNameB32)) - registry.register(moduleNameB32, proxy.address, {'from': owner}) + print("module {} ({}) register controller".format(moduleName, controllerNameB32)) + registry.register(controllerNameB32, controller.address, {"from": owner}) + print("module {} ({}) register proxy".format(moduleName, moduleNameB32)) + registry.register(moduleNameB32, proxy.address, {"from": owner}) return contractFromAddress(controllerClass, proxy.address) # generic upgradable gif service deployment -def deployGifService( - serviceClass, - registry, - owner, - publishSource -): +def deployGifService(serviceClass, registry, owner, publishSource): service = serviceClass.deploy( - registry.address, - {'from': owner}, - publish_source=publishSource) + registry.address, {"from": owner}, publish_source=publishSource + ) - registry.register(service.NAME.call(), service.address, {'from': owner}) + registry.register(service.NAME.call(), service.address, {"from": owner}) return service -def deployGifServiceV2( - serviceName, - serviceClass, - registry, - owner, - publishSource -): + +def deployGifServiceV2(serviceName, serviceClass, registry, owner, publishSource): service = serviceClass.deploy( - registry.address, - {'from': owner}, - publish_source=publishSource) + registry.address, {"from": owner}, publish_source=publishSource + ) - registry.register(s2b32(serviceName), service.address, {'from': owner}) + registry.register(s2b32(serviceName), service.address, {"from": owner}) return service + def contractFromAddress(contractClass, contractAddress): return contract_from_address(contractClass, contractAddress) + def contract_from_address(contractClass, contractAddress): return Contract.from_abi(contractClass._name, contractAddress, contractClass.abi) + + +def save_json(contract_class, file_name=None): + vi = contract_class.get_verification_info() + sji = vi["standard_json_input"] + + if not file_name or len(file_name) == 0: + file_name = "./{}.json".format(contract_class._name) + + print("writing standard json input file {}".format(file_name)) + with open(file_name, "w") as json_file: + json.dump(sji, json_file) + + +enums = { + "ComponentType": ["Oracle", "Product", "Riskpool"], + "ComponentState": [ + "Created", + "Proposed", + "Declined", + "Active", + "Paused", + "Suspended", + "Archived", + ], + "BundleState": ["Active", "Locked", "Closed", "Burned"], + "PolicyFlowState": ["Started", "Active", "Finished"], + "ApplicationState": ["Applied", "Revoked", "Underwritten", "Declined"], + "PolicyState": ["Active", "Expired", "Closed"], + "ClaimState": ["Applied", "Confirmed", "Declined", "Closed"], + "PayoutState": ["Expected", "PaidOut"], +} + + +def decodeEnum(enum_name, value): + if enum_name not in enums: + return "invalid" + return enums[enum_name][value] + + +def getChainName(chainId): + try: + # Fetch the list of chains from ChainList + response = requests.get("https://chainid.network/chains.json") + response.raise_for_status() # Raise an error for bad status codes + chains = response.json() + + # Search for the chain ID in the list + for chain in chains: + if chain["chainId"] == chainId: + return chain["name"] + + return "ChainId not found" + except requests.RequestException as e: + return f"Error fetching chain name for chainId={chainId} {e}" + + +def utcStr(timestamp): + dt_object = datetime.fromtimestamp(timestamp, tz=timezone.utc) + date_string = dt_object.strftime("%Y-%m-%d %H:%M:%S") + return date_string + + +def fromWei(raw, decimals): + return Decimal(raw) / Decimal(10**decimals) + + +def toWei(value, decimals): + return int(Decimal(value) * Decimal(10**decimals)) diff --git a/tests/conftest.py b/tests/conftest.py index 8a77bab..b8f51f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -218,6 +218,7 @@ def gifAyiiDeploy( testCoin, riskpoolWallet: Account ) -> GifAyiiProductComplete: + collateralizationLevel = 10**18 return GifAyiiProductComplete( instance, productOwner, @@ -227,7 +228,8 @@ def gifAyiiDeploy( riskpoolKeeper, investor, testCoin, - riskpoolWallet) + riskpoolWallet, + collateralizationLevel) @pytest.fixture(scope="module") def gifAyiiProduct(gifAyiiDeploy) -> GifAyiiProduct: