├── tests ├── __init__.py ├── test_component_owner_service.py ├── test_deploy_riskpool.py ├── test_instance_from_address.py ├── test_basicriskpool_bundle_allocation_02.py ├── test_treasury_module_validations.py ├── test_coin.py ├── test_deploy_registry.py ├── test_policy_default_flow.py ├── test_product_compromised.py ├── test_instance_operator_service.py ├── test_deploy_oracle.py ├── test_oracle_lifecycle.py ├── test_deploy_product.py ├── test_instance_service.py ├── test_registry.py ├── test_riskpool_active_bundles.py ├── test_registry_upgrade.py ├── test_ayii_product_underwrite.py ├── test_bundle_defund.py ├── test_bundle_token.py ├── test_access_module.py ├── test_basicriskpool_bundle_allocation_01.py └── test_deploy_instance.py ├── scripts ├── __init__.py ├── component.py ├── const.py ├── test_deployment.py ├── setup.py ├── util.py └── product.py ├── .gitattributes ├── .devcontainer ├── starship.toml ├── Dockerfile.ganache ├── scripts │ └── install_solidity.sh ├── docker-compose.yaml ├── devcontainer.json └── Dockerfile ├── .prettierrc ├── .github ├── workflows │ ├── scripts │ │ ├── list_all_errorcodes.sh │ │ ├── validate_errorcodes.sh │ │ ├── validate_events.sh │ │ └── prepare_environment.sh │ ├── no_open_tasks.yml │ ├── build_with_foundry.yml │ ├── errorcodes_events.yml │ ├── runtests_with_coverage.yml │ └── build.yml └── .dependabot.yml ├── .gitignore ├── .solhint.json ├── .solhint.prettier.json ├── contracts ├── tokens │ ├── RiskpoolToken.sol │ └── BundleToken.sol ├── Migrations.sol ├── test │ ├── TestRegistryControllerUpdated.sol │ ├── TestTransferFrom.sol │ ├── TestRegistryCompromisedController.sol │ ├── TestCoin.sol │ ├── TestRiskpool.sol │ ├── TestCoinAlternativeImplementation.sol │ ├── TestOracle.sol │ └── TestCompromisedProduct.sol ├── services │ ├── OracleService.sol │ ├── ProductService.sol │ ├── ComponentOwnerService.sol │ └── InstanceOperatorService.sol ├── examples │ ├── mock │ │ └── ChainlinkToken.sol │ ├── AyiiRiskpool.sol │ ├── AyiiOracle.sol │ └── strings.sol ├── shared │ ├── CoreProxy.sol │ ├── CoreController.sol │ ├── WithRegistry.sol │ └── TransferHelper.sol └── modules │ ├── LicenseController.sol │ ├── AccessController.sol │ ├── QueryModule.sol │ └── RegistryController.sol ├── foundry.toml ├── .gitmodules ├── tests_foundry ├── test │ └── TestCoinTests.t.sol └── modules │ └── RegistryController.t.sol ├── .vscode ├── launch.json.template └── settings.json ├── package.json └── brownie-config.yaml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /.devcontainer/starship.toml: -------------------------------------------------------------------------------- 1 | [nodejs] 2 | disabled = true 3 | 4 | [package] 5 | disabled = true 6 | 7 | [python] 8 | detect_folders = ["scripts","tests"] 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.sol", 5 | "options": { 6 | "endOfLine": "auto" 7 | } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/scripts/list_all_errorcodes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | printf "|Errorcode|File|\n|:--|:--|\n" 4 | egrep -or "(ERROR\:[A-Z0-9_-]+\:[A-Z0-9_-]+)" contracts/* | sed -E "s/([^\:]+)\:(.+)/|\2|\1|/g" | sort 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .env 3 | .history 4 | .hypothesis/ 5 | build/ 6 | build_foundry/ 7 | reports/ 8 | dump_sources/ 9 | node_modules 10 | tmp/ 11 | 12 | **.pyc 13 | 14 | .DS_Store 15 | .vscode/launch.json 16 | 17 | etherisc*.tgz 18 | cache/solidity-files-cache.json 19 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": [], 4 | "rules": { 5 | "compiler-version": ["error", "0.8.2"], 6 | "func-visibility": ["warn", {"ignoreConstructors": true}], 7 | "reason-string": ["warn",{"maxLength": 64}] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.solhint.prettier.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "compiler-version": ["error", "0.8.2"], 6 | "func-visibility": ["warn", {"ignoreConstructors": true}], 7 | "prettier/prettier": "warn" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/no_open_tasks.yml: -------------------------------------------------------------------------------- 1 | name: All tasks in PR checked 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | 9 | jobs: 10 | scan: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: loxygenK/taskonfirm@v1.0.0 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/scripts/validate_errorcodes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DUPES=`egrep -or "(ERROR\:[A-Z0-9_-]+)" contracts/* | sort | uniq -cd` 4 | 5 | if [ -z "$DUPES" ]; then 6 | echo "No duplicate error codes found" 7 | else 8 | echo "Duplicate error codes found:" 9 | echo "$DUPES" 10 | exit 1 11 | fi 12 | 13 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile.ganache: -------------------------------------------------------------------------------- 1 | FROM node:lts-bullseye 2 | 3 | EXPOSE 7545 4 | 5 | RUN npm install -g ganache 6 | RUN mkdir /ganache 7 | 8 | CMD ganache-cli \ 9 | --mnemonic "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" \ 10 | --chain.chainId 1234 \ 11 | --port 7545 \ 12 | --accounts 20 \ 13 | -b 1 \ 14 | -h "0.0.0.0" 15 | -------------------------------------------------------------------------------- /contracts/tokens/RiskpoolToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract RiskpoolToken is 7 | ERC20 8 | { 9 | string public constant NAME = "GIF Riskpool Token"; 10 | string public constant SYMBOL = "RPT"; 11 | 12 | constructor() 13 | ERC20(NAME, SYMBOL) 14 | { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "contracts" 3 | out = "build_foundry" 4 | libs = ["lib"] 5 | test = "tests_foundry" 6 | 7 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 8 | 9 | remappings = [ 10 | "@etherisc/gif-interface/=lib/gif-interface/", 11 | "@openzeppelin/=lib/openzeppelin-contracts/", 12 | "@chainlink/=lib/chainlink/", 13 | "@forgestd/=lib/forge-std/src/", 14 | ] 15 | 16 | -------------------------------------------------------------------------------- /.devcontainer/scripts/install_solidity.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | mkdir -p /home/vscode/.solcx/ 4 | 5 | if [[ $(uname -m) == 'aarch64' ]]; then 6 | wget -O /home/vscode/.solcx/solc-v0.8.2 https://github.com/nikitastupin/solc/raw/main/linux/aarch64/solc-v0.8.2 7 | else 8 | wget -O /home/vscode/.solcx/solc-v0.8.2 https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-v0.8.2+commit.661d1103 9 | fi 10 | 11 | chmod 755 /home/vscode/.solcx/solc* 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/openzeppelin-contracts"] 2 | path = lib/openzeppelin-contracts 3 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 4 | [submodule "lib/gif-interface"] 5 | path = lib/gif-interface 6 | url = https://github.com/etherisc/gif-interface 7 | [submodule "lib/chainlink"] 8 | path = lib/chainlink 9 | url = https://github.com/smartcontractkit/chainlink 10 | [submodule "lib/forge-std"] 11 | path = lib/forge-std 12 | url = https://github.com/foundry-rs/forge-std 13 | -------------------------------------------------------------------------------- /tests_foundry/test/TestCoinTests.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.2; 2 | 3 | import "forge-std/Test.sol"; 4 | import "../../contracts/test/TestCoin.sol"; 5 | 6 | contract TestCoinTest is Test { 7 | TestCoin private testCoin; 8 | 9 | function setUp() public { 10 | testCoin = new TestCoin(); 11 | } 12 | 13 | function testName() public { 14 | assertEq(testCoin.NAME(), "Test Dummy"); 15 | } 16 | 17 | function testSymbol() public { 18 | assertEq(testCoin.SYMBOL(), "TDY"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/.dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # - package-ecosystem: "docker" 9 | # directory: "/.devcontainer" 10 | # schedule: 11 | # interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | 17 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint256 public last_completed_migration; // solhint-disable-line 7 | 8 | constructor() { 9 | owner = msg.sender; 10 | } 11 | 12 | modifier restricted() { 13 | if (msg.sender == owner) _; 14 | } 15 | 16 | function setCompleted(uint256 _completed) public restricted { 17 | last_completed_migration = _completed; 18 | } 19 | 20 | function upgrade(address _newAddress) public restricted { 21 | Migrations upgraded = Migrations(_newAddress); 22 | upgraded.setCompleted(last_completed_migration); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/test/TestRegistryControllerUpdated.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../modules/RegistryController.sol"; 5 | 6 | 7 | contract TestRegistryControllerUpdated is RegistryController { 8 | 9 | string message; 10 | bool upgradeV2; 11 | 12 | function setMessage(string memory _message) public onlyInstanceOperator { message = _message; } 13 | function getMessage() public view returns (string memory) { return message; } 14 | 15 | function upgradeToV2(string memory _message) public { 16 | require(!upgradeV2, "ERROR:REC-102:UPGRADE_ONCE_OMLY"); 17 | upgradeV2 = true; 18 | 19 | setMessage(_message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/build_with_foundry.yml: -------------------------------------------------------------------------------- 1 | name: Build with Foundry 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main, develop ] 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: [ main, develop ] 11 | 12 | jobs: 13 | test: 14 | name: Compile and run tests with foundry 15 | # only run if contracts have changed 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install Foundry 24 | uses: foundry-rs/foundry-toolchain@v1 25 | 26 | - name: Build 27 | run: forge build 28 | 29 | - name: Execute tests 30 | run: forge test -vvvv 31 | 32 | -------------------------------------------------------------------------------- /contracts/services/OracleService.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../shared/CoreController.sol"; 5 | 6 | import "@etherisc/gif-interface/contracts/modules/IQuery.sol"; 7 | import "@etherisc/gif-interface/contracts/services/IOracleService.sol"; 8 | 9 | 10 | contract OracleService is 11 | IOracleService, 12 | CoreController 13 | { 14 | IQuery private _query; 15 | 16 | function _afterInitialize() internal override onlyInitializing { 17 | _query = IQuery(_getContractAddress("Query")); 18 | } 19 | 20 | function respond(uint256 _requestId, bytes calldata _data) external override { 21 | // function below enforces msg.sender to be a registered oracle 22 | _query.respond(_requestId, _msgSender(), _data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | 4 | services: 5 | # ganache: 6 | # build: 7 | # context: .. 8 | # dockerfile: .devcontainer/Dockerfile.ganache 9 | # # ports: 10 | # # - "7545:7545" 11 | brownie: 12 | # See https://aka.ms/vscode-remote/containers/non-root for details. 13 | user: vscode 14 | build: 15 | context: .. 16 | dockerfile: .devcontainer/Dockerfile 17 | args: 18 | VARIANT: 3.10-bookworm 19 | USER_UID: 1000 20 | USER_GID: 1000 21 | INSTALL_NODE: "true" 22 | NODE_VERSION: "lts/*" 23 | volumes: 24 | - ..:/workspace:cached 25 | #- $HOME/.ssh/:/home/vscode/.ssh/ # Mount the ssh folder to authenticate with github 26 | # Overrides default command so things don't shut down after the process ends. 27 | command: sleep infinity 28 | -------------------------------------------------------------------------------- /.github/workflows/scripts/validate_events.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # match any string that starts with "event" and its name does not start with Log 4 | EVT=`grep -orP '$\s+event\s+(?!Log)(\w+)\s*' contracts/*` 5 | 6 | if [ -z "$EVT" ]; then 7 | echo "All event definitions start with 'Log'" 8 | else 9 | echo "Found event definitions not starting with 'Log':" 10 | echo "$EVT" 11 | exit 1 12 | fi 13 | 14 | # check for event defintions with attributes 'policyId', 'applicationId' or 'metadataId' 15 | EVT=`grep -orP '$\s+event\s+\w+\s*\(.*(policyId|applicationId|metadataId).*\)' contracts/*` 16 | 17 | if [ -z "$EVT" ]; then 18 | echo "No event definitions contain attributes 'policyId', 'applicationId' or 'metadataId'" 19 | else 20 | echo "Found event definitions containing attributes 'policyId', 'applicationId' or 'metadataId':" 21 | echo "$EVT" 22 | exit 1 23 | fi 24 | -------------------------------------------------------------------------------- /.vscode/launch.json.template: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Brownie: Compile all", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "/home/vscode/.local/bin/brownie", 12 | "console": "integratedTerminal", 13 | "args": ["compile", "--all"] 14 | }, 15 | { 16 | "name": "run all tests", 17 | "type": "python", 18 | "request": "launch", 19 | "program": "/home/vscode/.local/bin/brownie", 20 | "console": "integratedTerminal", 21 | "args": ["test"] 22 | }, 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /contracts/test/TestTransferFrom.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../shared/TransferHelper.sol"; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | contract TestTransferFrom { 9 | 10 | event LogTransferHelperInputValidation1Failed(bool tokenIsContract, address from, address to); 11 | event LogTransferHelperInputValidation2Failed(uint256 balance, uint256 allowance); 12 | event LogTransferHelperCallFailed(bool callSuccess, uint256 returnDataLength, bytes returnData); 13 | 14 | function unifiedTransferFrom( 15 | IERC20 token, 16 | address from, 17 | address to, 18 | uint256 amount 19 | ) 20 | external 21 | returns(bool) 22 | { 23 | return TransferHelper.unifiedTransferFrom(token, from, to, amount); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/errorcodes_events.yml: -------------------------------------------------------------------------------- 1 | name: Error codes and events 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main, develop ] 7 | tags: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | inspect: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Validate error codes 19 | run: .github/workflows/scripts/validate_errorcodes.sh 20 | 21 | - run: mkdir tmp 22 | 23 | - name: Find all error codes 24 | run: .github/workflows/scripts/list_all_errorcodes.sh > tmp/errorcodes.md 25 | 26 | - name: Archive error codes 27 | uses: actions/upload-artifact@v3 28 | with: 29 | name: error-codes 30 | path: tmp/errorcodes.md 31 | 32 | - name: Validate events 33 | run: .github/workflows/scripts/validate_events.sh 34 | -------------------------------------------------------------------------------- /contracts/test/TestRegistryCompromisedController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | contract TestRegistryCompromisedController { 5 | 6 | bytes32 public constant POLICY = bytes32("Policy"); 7 | bytes32 public constant QUERY = bytes32("Query"); 8 | 9 | mapping(bytes32 => address) public contracts; 10 | 11 | function getContract(bytes32 contractName) 12 | external 13 | view 14 | returns (address moduleAddress) 15 | { 16 | moduleAddress = contracts[contractName]; 17 | } 18 | 19 | function upgradeToV2( 20 | address compromisedPolicyModuleAddress, 21 | address originalQueryModuleAddress 22 | ) 23 | public 24 | { 25 | contracts[POLICY] = compromisedPolicyModuleAddress; 26 | contracts[QUERY] = originalQueryModuleAddress; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@etherisc/gif-contracts", 3 | "version": "2.0.0-rc.1-0", 4 | "description": "This repository holds the GIF core contracts and tools to develop, test and deploy GIF instances.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/etherisc/gif-contracts.git" 8 | }, 9 | "files": [ 10 | "contracts/**/*sol", 11 | "build/**/*" 12 | ], 13 | "keywords": [ 14 | "etherisc", 15 | "dip", 16 | "gif", 17 | "insurance", 18 | "blockchain", 19 | "ethereum", 20 | "solidity" 21 | ], 22 | "license": "Apache-2.0", 23 | "homepage": "https://www.etherisc.com", 24 | "bugs": { 25 | "url": "https://github.com/etherisc/gif-contracts/issues" 26 | }, 27 | "dependencies": { 28 | "@chainlink/contracts": "0.4.1", 29 | "@etherisc/gif-interface": "2.0.0-rc.1-0", 30 | "@openzeppelin/contracts": "4.7.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/test/TestCoin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestCoin is ERC20 { 7 | 8 | string public constant NAME = "Test Dummy"; 9 | string public constant SYMBOL = "TDY"; 10 | 11 | uint256 public constant INITIAL_SUPPLY = 10**24; 12 | 13 | constructor() 14 | ERC20(NAME, SYMBOL) 15 | { 16 | _mint( 17 | _msgSender(), 18 | INITIAL_SUPPLY 19 | ); 20 | } 21 | } 22 | 23 | contract TestCoinX is ERC20 { 24 | 25 | string public constant NAME = "Test Dummy X"; 26 | string public constant SYMBOL = "TDX"; 27 | 28 | uint256 public constant INITIAL_SUPPLY = 10**24; 29 | 30 | constructor() 31 | ERC20(NAME, SYMBOL) 32 | { 33 | _mint( 34 | _msgSender(), 35 | INITIAL_SUPPLY 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests_foundry/modules/RegistryController.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.2; 2 | 3 | import "forge-std/Test.sol"; 4 | import "../../contracts/test/TestCoin.sol"; 5 | import "../../contracts/modules/RegistryController.sol"; 6 | import "../../contracts/shared/CoreProxy.sol"; 7 | 8 | contract RegistryControllerTest is Test { 9 | RegistryController private _registryController; 10 | CoreProxy private _proxy; 11 | 12 | function setUp() public { 13 | _registryController = new RegistryController(); 14 | _proxy = new CoreProxy( 15 | address(_registryController), 16 | abi.encodeWithSignature("initializeRegistry(bytes32)", bytes32("1.0.0")) 17 | ); 18 | } 19 | 20 | function getController() internal view returns (RegistryController) { 21 | return RegistryController(address(_proxy)); 22 | } 23 | 24 | function testRelease() public { 25 | assertEq(getController().getRelease(), "1.0.0"); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /contracts/test/TestRiskpool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@etherisc/gif-interface/contracts/components/BasicRiskpool.sol"; 5 | import "@etherisc/gif-interface/contracts/modules/IBundle.sol"; 6 | import "@etherisc/gif-interface/contracts/modules/IPolicy.sol"; 7 | 8 | contract TestRiskpool is BasicRiskpool { 9 | 10 | uint256 public constant SUM_OF_SUM_INSURED_CAP = 10**24; 11 | 12 | constructor( 13 | bytes32 name, 14 | uint256 collateralization, 15 | address erc20Token, 16 | address wallet, 17 | address registry 18 | ) 19 | BasicRiskpool(name, collateralization, SUM_OF_SUM_INSURED_CAP, erc20Token, wallet, registry) 20 | { } 21 | 22 | // trivial implementation that matches every application 23 | function bundleMatchesApplication( 24 | IBundle.Bundle memory bundle, 25 | IPolicy.Application memory application 26 | ) 27 | public override 28 | pure 29 | returns(bool isMatching) 30 | { 31 | isMatching = true; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /tests/test_component_owner_service.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import brownie 3 | import pytest 4 | 5 | from brownie import ComponentOwnerService 6 | 7 | from scripts.const import ( 8 | COMPONENT_OWNER_SERVICE_NAME 9 | ) 10 | 11 | from scripts.util import ( 12 | h2sLeft, 13 | s2b32, 14 | 15 | ) 16 | 17 | # enforce function isolation for tests below 18 | @pytest.fixture(autouse=True) 19 | def isolation(fn_isolation): 20 | pass 21 | 22 | def test_non_existing_functionality(instance, owner): 23 | componentOwnerService = instance.getComponentOwnerService() 24 | 25 | with pytest.raises(AttributeError): 26 | assert componentOwnerService.foo({'from': owner}) 27 | 28 | def test_component_service_contract_in_registry(instance, owner): 29 | componentOwnerService = instance.getComponentOwnerService() 30 | 31 | registry = instance.getRegistry() 32 | componentOwnerServiceAddress = registry.getContract(s2b32(COMPONENT_OWNER_SERVICE_NAME)) 33 | 34 | assert componentOwnerService.address == componentOwnerServiceAddress 35 | assert componentOwnerService.address != 0x0 36 | -------------------------------------------------------------------------------- /tests/test_deploy_riskpool.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from scripts.instance import GifInstance 5 | from scripts.product import GifTestRiskpool 6 | from scripts.util import s2b32 7 | 8 | # enforce function isolation for tests below 9 | @pytest.fixture(autouse=True) 10 | def isolation(fn_isolation): 11 | pass 12 | 13 | def test_deploy_simple( 14 | instance: GifInstance, 15 | gifTestRiskpool: GifTestRiskpool, 16 | capitalOwner, 17 | testCoin 18 | ): 19 | instanceService = instance.getInstanceService() 20 | 21 | assert instanceService.oracles() == 0 22 | assert instanceService.products() == 0 23 | assert instanceService.riskpools() == 1 24 | 25 | riskpool = gifTestRiskpool.getContract() 26 | assert riskpool.getCollateralizationLevel() == riskpool.getFullCollateralizationLevel() 27 | 28 | assert riskpool.getWallet() == capitalOwner 29 | assert riskpool.getErc20Token() == testCoin 30 | 31 | assert riskpool.bundles() == 0 32 | assert riskpool.getCapital() == 0 33 | assert riskpool.getTotalValueLocked() == 0 34 | assert riskpool.getCapacity() == 0 35 | assert riskpool.getBalance() == 0 36 | assert riskpool.getMaximumNumberOfActiveBundles() == 1 37 | -------------------------------------------------------------------------------- /contracts/examples/mock/ChainlinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | abstract contract ERC677Receiver { 7 | function onTokenTransfer (address _sender, uint _value, bytes calldata _data) public virtual; 8 | } 9 | 10 | contract ChainlinkToken is ERC20 { 11 | constructor(address owner, uint256 supply) ERC20("Chainlink Dummy Token", "CDT"){ 12 | _mint(owner, supply); 13 | } 14 | 15 | function transferAndCall(address _to, uint _value, bytes calldata _data) public returns (bool success){ 16 | super.transfer(_to, _value); 17 | // Transfer(msg.sender, _to, _value, _data); 18 | if (isContract(_to)) { 19 | contractFallback(_to, _value, _data); 20 | } 21 | return true; 22 | } 23 | 24 | function contractFallback(address _to, uint _value, bytes calldata _data) private { 25 | ERC677Receiver receiver = ERC677Receiver(_to); 26 | receiver.onTokenTransfer(msg.sender, _value, _data); 27 | } 28 | 29 | function isContract(address _addr) private view returns (bool hasCode) { 30 | uint length; 31 | assembly { length := extcodesize(_addr) } 32 | return length > 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/shared/CoreProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | 5 | import "@etherisc/gif-interface/contracts/shared/ICoreProxy.sol"; 6 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | contract CoreProxy is 9 | ICoreProxy, 10 | ERC1967Proxy 11 | { 12 | 13 | modifier onlyAdmin() { 14 | require( 15 | msg.sender == _getAdmin(), 16 | "ERROR:CRP-001:NOT_ADMIN"); 17 | _; 18 | } 19 | 20 | constructor(address _controller, bytes memory encoded_initializer) 21 | ERC1967Proxy(_controller, encoded_initializer) 22 | { 23 | _changeAdmin(msg.sender); 24 | } 25 | 26 | function implementation() external view returns (address) { 27 | return _implementation(); 28 | } 29 | 30 | function upgradeToAndCall(address newImplementation, bytes calldata data) 31 | external 32 | payable 33 | onlyAdmin 34 | { 35 | address oldImplementation = _implementation(); 36 | 37 | _upgradeToAndCall(newImplementation, data, true); 38 | 39 | emit LogCoreContractUpgraded( 40 | oldImplementation, 41 | newImplementation); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/component.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | 3 | from brownie.convert import to_bytes 4 | from brownie.network import accounts 5 | from brownie.network.account import Account 6 | 7 | # pylint: disable-msg=E0611 8 | from brownie import ( 9 | Wei, 10 | interface, 11 | Contract, 12 | PolicyController, 13 | OracleService, 14 | ComponentOwnerService, 15 | InstanceOperatorService, 16 | InstanceService, 17 | ) 18 | 19 | from scripts.util import ( 20 | get_account, 21 | contractFromAddress, 22 | s2b32, 23 | ) 24 | 25 | from scripts.instance import GifInstance 26 | 27 | 28 | class GifComponent(object): 29 | 30 | def __init__(self, 31 | componentAddress: Account, 32 | ): 33 | self.component = contractFromAddress(interface.IComponent, componentAddress) 34 | self.instance = GifInstance(registryAddress=self.component.getRegistry()) 35 | 36 | instanceService = self.instance.getInstanceService() 37 | instanceOperatorService = self.instance.getInstanceOperatorService() 38 | componentOwnerService = self.instance.getComponentOwnerService() 39 | riskpoolService = self.instance.getRiskpoolService() 40 | 41 | self.component = contractFromAddress(interface.IComponent, componentAddress) 42 | self.instance = GifInstance(registryAddress=self.component.getRegistry()) -------------------------------------------------------------------------------- /.github/workflows/scripts/prepare_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/env bash 2 | 3 | # Install required solidity compiler version 4 | mkdir -p ~/.solcx/ 5 | wget -O ~/.solcx/solc-v0.8.2 https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-v0.8.2+commit.661d1103 6 | wget -O ~/.solcx/solc-v0.8.17 https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-v0.8.17+commit.8df45f5f 7 | chmod 755 ~/.solcx/solc* 8 | 9 | # Retrieve brownie dependencies 10 | export VERSION_OPEN_ZEPPELIN=4.7.3 11 | export VERSION_CHAINLINK=1.6.0 12 | wget -O /tmp/v${VERSION_OPEN_ZEPPELIN}.tar.gz https://github.com/OpenZeppelin/openzeppelin-contracts/archive/refs/tags/v${VERSION_OPEN_ZEPPELIN}.tar.gz 13 | wget -O /tmp/v${VERSION_CHAINLINK}.tar.gz https://github.com/smartcontractkit/chainlink/archive/refs/tags/v${VERSION_CHAINLINK}.tar.gz 14 | mkdir -p ~/.brownie/packages/OpenZeppelin 15 | cd ~/.brownie/packages/OpenZeppelin 16 | tar xvfz /tmp/v${VERSION_OPEN_ZEPPELIN}.tar.gz 17 | mv openzeppelin-contracts-${VERSION_OPEN_ZEPPELIN} openzeppelin-contracts@${VERSION_OPEN_ZEPPELIN} 18 | mkdir -p ~/.brownie/packages/smartcontractkit 19 | cd ~/.brownie/packages/smartcontractkit 20 | tar xvfz /tmp/v${VERSION_CHAINLINK}.tar.gz 21 | mv chainlink-${VERSION_CHAINLINK} chainlink@${VERSION_CHAINLINK} 22 | 23 | # Install ganache 24 | npm install --global ganache 25 | 26 | # Install brownie 27 | python3 -m pip install --user pipx 28 | python3 -m pipx ensurepath 29 | pipx install eth-brownie 30 | 31 | -------------------------------------------------------------------------------- /contracts/test/TestCoinAlternativeImplementation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestCoinAlternativeImplementation is ERC20 { 7 | 8 | string public constant NAME = "Test Alternative Coin"; 9 | string public constant SYMBOL = "TAC"; 10 | 11 | uint256 public constant INITIAL_SUPPLY = 10**24; 12 | 13 | constructor() 14 | ERC20(NAME, SYMBOL) 15 | { 16 | _mint( 17 | _msgSender(), 18 | INITIAL_SUPPLY 19 | ); 20 | } 21 | 22 | // inspired by ZRX transfer implementation 23 | // see https://soliditydeveloper.com/safe-erc20 24 | function transferFrom(address _from, address _to, uint _value) 25 | public virtual override returns (bool) 26 | { 27 | if (balanceOf(_from) >= _value // check sufficient balance 28 | && allowance(_from, msg.sender) >= _value // check sufficient allowance 29 | && balanceOf(_to) + _value >= balanceOf(_to) // check overflow 30 | && _from != address(0) // sender not zero address 31 | && _to != address(0)) // recipient not zero address 32 | { 33 | return super.transferFrom(_from, _to, _value); // should never fail now 34 | } else { 35 | return false; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/modules/LicenseController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "./ComponentController.sol"; 5 | import "../shared/CoreController.sol"; 6 | 7 | import "@etherisc/gif-interface/contracts/components/IComponent.sol"; 8 | import "@etherisc/gif-interface/contracts/components/IProduct.sol"; 9 | import "@etherisc/gif-interface/contracts/modules/ILicense.sol"; 10 | 11 | 12 | contract LicenseController is 13 | ILicense, 14 | CoreController 15 | { 16 | 17 | ComponentController private _component; 18 | 19 | function _afterInitialize() internal override onlyInitializing { 20 | _component = ComponentController(_getContractAddress("Component")); 21 | } 22 | 23 | // ensures that calling component (productAddress) is a product 24 | function getAuthorizationStatus(address productAddress) 25 | public override 26 | view 27 | returns (uint256 productId, bool isAuthorized, address policyFlow) 28 | { 29 | productId = _component.getComponentId(productAddress); 30 | isAuthorized = _isValidCall(productId); 31 | policyFlow = _component.getPolicyFlow(productId); 32 | } 33 | 34 | function _isValidCall(uint256 productId) internal view returns (bool) { 35 | return _component.getComponentState(productId) == IComponent.ComponentState.Active; 36 | } 37 | 38 | function _getProduct(uint256 id) internal view returns (IProduct product) { 39 | require(_component.isProduct(id), "ERROR:LIC-001:COMPONENT_NOT_PRODUCT"); 40 | IComponent cmp = _component.getComponent(id); 41 | product = IProduct(address(cmp)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.defaultInterpreterPath": "/usr/local/bin/python", 3 | "python.testing.pytestArgs": [ 4 | "tests" 5 | ], 6 | "python.testing.unittestEnabled": false, 7 | "python.testing.pytestEnabled": true, 8 | "terminal.integrated.defaultProfile.linux": "zsh", 9 | "solidity.remappingsUnix": [ 10 | "@openzeppelin/=/home/vscode/.brownie/packages/OpenZeppelin/openzeppelin-contracts@4.7.3", 11 | "@chainlink/=/home/vscode/.brownie/packages/smartcontractkit/chainlink@1.6.0", 12 | "@etherisc/gif-interface/=/home/vscode/.brownie/packages/etherisc/gif-interface@6da625a", 13 | ], 14 | "solidity.compileUsingRemoteVersion": "v0.8.2+commit.661d1103", 15 | "peacock.remoteColor": "1D3C43", 16 | "workbench.colorCustomizations": { 17 | "activityBar.activeBackground": "#2c5c67", 18 | "activityBar.background": "#2c5c67", 19 | "activityBar.foreground": "#e7e7e7", 20 | "activityBar.inactiveForeground": "#e7e7e799", 21 | "activityBarBadge.background": "#2d1328", 22 | "activityBarBadge.foreground": "#e7e7e7", 23 | "commandCenter.border": "#e7e7e799", 24 | "sash.hoverBorder": "#2c5c67", 25 | "statusBar.background": "#1d3c43", 26 | "statusBar.foreground": "#e7e7e7", 27 | "statusBarItem.hoverBackground": "#2c5c67", 28 | "statusBarItem.remoteBackground": "#1d3c43", 29 | "statusBarItem.remoteForeground": "#e7e7e7", 30 | "titleBar.activeBackground": "#1d3c43", 31 | "titleBar.activeForeground": "#e7e7e7", 32 | "titleBar.inactiveBackground": "#1d3c4399", 33 | "titleBar.inactiveForeground": "#e7e7e799" 34 | } 35 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/ruby 3 | { 4 | "name": "gif-contracts", 5 | "dockerComposeFile": "docker-compose.yaml", 6 | "service": "brownie", 7 | "workspaceFolder": "/workspace", 8 | // Set *default* container specific settings.json values on container create. 9 | 10 | // "features": { 11 | // // "github-cli": "latest", 12 | // "docker-from-docker": { 13 | // "version": "latest", 14 | // "moby": true 15 | // } 16 | // }, 17 | 18 | // Add the IDs of extensions you want installed when the container is created. 19 | "customizations": { 20 | "vscode": { 21 | "settings": { 22 | //"terminal.integrated.shell.linux": "/bin/bash" 23 | "editor.fontFamily": "'JetBrainsMono Nerd Font Mono', Menlo, Monaco, 'Courier New', monospace", 24 | "editor.fontSize": 13, 25 | }, 26 | "extensions": [ 27 | "ms-python.python", 28 | "ms-python.vscode-pylance", 29 | "juanblanco.solidity", 30 | "tintinweb.solidity-visual-auditor", 31 | "github.vscode-pull-request-github", 32 | "github.copilot", 33 | "mhutchie.git-graph", 34 | "eamodio.gitlens", 35 | "gruntfuggly.todo-tree", 36 | "oderwat.indent-rainbow", 37 | "johnpapa.vscode-peacock", 38 | "vikas.code-navigation", 39 | ], 40 | } 41 | }, 42 | 43 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 44 | "forwardPorts": [8000], 45 | 46 | // Use 'postCreateCommand' to run commands after the container is created. 47 | "postCreateCommand": "touch .env && brownie compile --all && python --version", 48 | 49 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 50 | "remoteUser": "vscode" 51 | } -------------------------------------------------------------------------------- /tests/test_instance_from_address.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from brownie.network.account import Account 4 | 5 | from scripts.instance import ( 6 | GifRegistry, 7 | GifInstance, 8 | ) 9 | 10 | # enforce function isolation for tests below 11 | @pytest.fixture(autouse=True) 12 | def isolation(fn_isolation): 13 | pass 14 | 15 | 16 | def test_module_addresses(instance: GifInstance): 17 | address = instance.getRegistry().address 18 | addrInst = GifInstance(registryAddress=address) 19 | 20 | assert addrInst.getRegistry().address == address 21 | assert addrInst.getAccess().address == instance.getAccess().address 22 | assert addrInst.getBundle().address == instance.getBundle().address 23 | assert addrInst.getComponent().address == instance.getComponent().address 24 | assert addrInst.getLicense().address == instance.getLicense().address 25 | assert addrInst.getPolicy().address == instance.getPolicy().address 26 | assert addrInst.getPool().address == instance.getPool().address 27 | assert addrInst.getQuery().address == instance.getQuery().address 28 | 29 | 30 | def test_service_addresses(instance: GifInstance): 31 | address = instance.getRegistry().address 32 | addrInst = GifInstance(registryAddress=address) 33 | 34 | assert addrInst.getComponentOwnerService().address == instance.getComponentOwnerService().address 35 | assert addrInst.getInstanceOperatorService().address == instance.getInstanceOperatorService().address 36 | assert addrInst.getProductService().address == instance.getProductService().address 37 | assert addrInst.getOracleService().address == instance.getOracleService().address 38 | assert addrInst.getRiskpoolService().address == instance.getRiskpoolService().address 39 | assert addrInst.getInstanceService().address == instance.getInstanceService().address 40 | -------------------------------------------------------------------------------- /scripts/const.py: -------------------------------------------------------------------------------- 1 | from brownie import accounts 2 | 3 | # === GIF platform ========================================================== # 4 | 5 | # GIF release 6 | GIF_RELEASE = '2.0.0' 7 | 8 | # GIF modules 9 | ACCESS_NAME = 'Access' 10 | BUNDLE_NAME = 'Bundle' 11 | COMPONENT_NAME = 'Component' 12 | 13 | REGISTRY_CONTROLLER_NAME = 'RegistryController' 14 | REGISTRY_NAME = 'Registry' 15 | 16 | ACCESS_CONTROLLER_NAME = 'AccessController' 17 | ACCESS_NAME = 'Access' 18 | 19 | LICENSE_CONTROLLER_NAME = 'LicenseController' 20 | LICENSE_NAME = 'License' 21 | 22 | POLICY_CONTROLLER_NAME = 'PolicyController' 23 | POLICY_NAME = 'Policy' 24 | 25 | POLICY_DEFAULT_FLOW_NAME = 'PolicyDefaultFlow' 26 | POOL_NAME = 'Pool' 27 | 28 | QUERY_NAME = 'Query' 29 | 30 | RISKPOOL_CONTROLLER_NAME = 'RiskpoolController' 31 | RISKPOOL_NAME = 'Riskpool' 32 | TREASURY_NAME = 'Treasury' 33 | 34 | # GIF services 35 | COMPONENT_OWNER_SERVICE_NAME = 'ComponentOwnerService' 36 | PRODUCT_SERVICE_NAME = 'ProductService' 37 | RISKPOOL_SERVICE_NAME = 'RiskpoolService' 38 | ORACLE_SERVICE_NAME = 'OracleService' 39 | INSTANCE_OPERATOR_SERVICE_NAME = 'InstanceOperatorService' 40 | INSTANCE_SERVICE_NAME = 'InstanceService' 41 | 42 | # === GIF testing =========================================================== # 43 | 44 | # ZERO_ADDRESS = accounts.at('0x0000000000000000000000000000000000000000') 45 | ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' 46 | COMPROMISED_ADDRESS = '0x0000000000000000000000000000000000000013' 47 | 48 | # TEST account values 49 | ACCOUNTS_MNEMONIC = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' 50 | 51 | # TEST oracle/rikspool/product values 52 | PRODUCT_NAME = 'Test.Product' 53 | RISKPOOL_NAME = 'Test.Riskpool' 54 | ORACLE_NAME = 'Test.Oracle' 55 | ORACLE_INPUT_FORMAT = '(bytes input)' 56 | ORACLE_OUTPUT_FORMAT = '(bool output)' 57 | -------------------------------------------------------------------------------- /contracts/examples/AyiiRiskpool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | 6 | import "@etherisc/gif-interface/contracts/components/BasicRiskpool.sol"; 7 | import "@etherisc/gif-interface/contracts/modules/IBundle.sol"; 8 | import "@etherisc/gif-interface/contracts/modules/IPolicy.sol"; 9 | 10 | contract AyiiRiskpool is 11 | BasicRiskpool, 12 | AccessControl 13 | { 14 | // 0x5614e11ca6d7673c9c8dcec913465d676494aad1151bb2c1cf40b9d99be4d935 15 | bytes32 public constant INVESTOR_ROLE = keccak256("INVESTOR"); 16 | 17 | // restricts the maximal sum of sum insured that are secured by gthe riskpool 18 | uint256 public constant SUM_OF_SUM_INSURED_CAP = 10**24; 19 | 20 | constructor( 21 | bytes32 name, 22 | uint256 collateralization, 23 | address erc20Token, 24 | address wallet, 25 | address registry 26 | ) 27 | BasicRiskpool(name, collateralization, SUM_OF_SUM_INSURED_CAP, erc20Token, wallet, registry) 28 | { 29 | 30 | _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); 31 | } 32 | 33 | 34 | function grantInvestorRole(address investor) 35 | external 36 | onlyOwner 37 | { 38 | _setupRole(INVESTOR_ROLE, investor); 39 | } 40 | 41 | 42 | function createBundle(bytes memory filter, uint256 initialAmount) 43 | public override 44 | onlyRole(INVESTOR_ROLE) 45 | returns(uint256 bundleId) 46 | { 47 | bundleId = super.createBundle(filter, initialAmount); 48 | } 49 | 50 | 51 | // trivial implementation that matches every application 52 | function bundleMatchesApplication( 53 | IBundle.Bundle memory bundle, 54 | IPolicy.Application memory application 55 | ) 56 | public override 57 | pure 58 | returns(bool isMatching) 59 | { 60 | isMatching = true; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | # secrets management via .env (excluded via .gitignore) 2 | dotenv: .env 3 | 4 | networks: 5 | default: development 6 | development: 7 | gas_price: 1 # (1 wei) 8 | cmd_settings: 9 | # without this explicit setting chainid==1 is returend by block.chainid 10 | accounts: 20 11 | chain_id: 1337 12 | 13 | # brownie default values made explicit 14 | compiler: 15 | evm_version: null 16 | solc: 17 | version: 0.8.2 18 | optimizer: 19 | enabled: true 20 | runs: 200 21 | # https://eth-brownie.readthedocs.io/en/stable/compile.html#compiler-settings 22 | remappings: 23 | - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.7.3" 24 | - "@chainlink=smartcontractkit/chainlink@1.6.0" 25 | - "@etherisc/gif-interface=etherisc/gif-interface@6da625a" 26 | 27 | # packages below will be added to brownie 28 | # you may use 'brownie pm list' after 'brownie compile' 29 | # to list the packages installed via the dependency list below 30 | dependencies: 31 | # **Important**: If you update any version here, please also update them in .vscode/settings.json section 'solidity.remappingsUnix' 32 | # github dependency format: /@ 33 | - OpenZeppelin/openzeppelin-contracts@4.7.3 34 | - smartcontractkit/chainlink@1.6.0 35 | - etherisc/gif-interface@6da625a 36 | 37 | # exclude open zeppeling contracts when calculating test coverage 38 | # https://eth-brownie.readthedocs.io/en/v1.10.3/config.html#exclude_paths 39 | reports: 40 | exclude_contracts: 41 | # chainlink 42 | - ChainlinkClient 43 | - Operator 44 | # openzeppelin 45 | - AccessControl 46 | - AccessControlEnumerable 47 | - Context 48 | - Ownable 49 | - EnumerableMap 50 | - EnumerableSet 51 | - ERC1967Proxy 52 | - ERC20 53 | - ERC721 54 | - IERC20 55 | - IERC721 56 | - Initializable 57 | - SafeERC20 58 | - Strings 59 | -------------------------------------------------------------------------------- /contracts/test/TestOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@etherisc/gif-interface/contracts/components/Oracle.sol"; 5 | 6 | contract TestOracle is Oracle { 7 | 8 | constructor( 9 | bytes32 oracleName, 10 | address registry 11 | ) 12 | Oracle(oracleName, registry) 13 | { } 14 | 15 | function request(uint256 requestId, bytes calldata input) external override onlyQuery { 16 | // decode oracle input data 17 | (uint256 counter, bool immediateResponse) = abi.decode(input, (uint256, bool)); 18 | 19 | if (immediateResponse) { 20 | // obtain data from oracle given the request data (counter) 21 | // for off chain oracles this happens outside the request 22 | // call in a separate asynchronous transaction 23 | bool isLossEvent = _oracleCalculation(counter); 24 | respond(requestId, isLossEvent); 25 | } 26 | } 27 | 28 | function cancel(uint256 requestId) 29 | external override 30 | onlyOwner 31 | { 32 | // TODO mid/low priority 33 | // cancelChainlinkRequest(_requestId, _payment, _callbackFunctionId, _expiration); 34 | } 35 | 36 | // usually called by off-chain oracle (and not internally) 37 | // in which case the function modifier should be changed 38 | // to external 39 | function respond(uint256 requestId, bool isLossEvent) 40 | public 41 | { 42 | // encode data obtained from oracle 43 | bytes memory output = abi.encode(bool(isLossEvent)); 44 | 45 | // trigger inherited response handling 46 | _respond(requestId, output); 47 | } 48 | 49 | // dummy implementation 50 | // "real" oracles will get the output from some off-chain 51 | // component providing the outcome of the business logic 52 | function _oracleCalculation(uint256 counter) internal returns (bool isLossEvent) { 53 | isLossEvent = (counter % 2 == 1); 54 | } 55 | } -------------------------------------------------------------------------------- /contracts/shared/CoreController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@etherisc/gif-interface/contracts/modules/IAccess.sol"; 5 | import "@etherisc/gif-interface/contracts/modules/IRegistry.sol"; 6 | 7 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 8 | import "@openzeppelin/contracts/utils/Context.sol"; 9 | 10 | contract CoreController is 11 | Context, 12 | Initializable 13 | { 14 | IRegistry internal _registry; 15 | IAccess internal _access; 16 | 17 | constructor () { 18 | _disableInitializers(); 19 | } 20 | 21 | modifier onlyInstanceOperator() { 22 | require( 23 | _registry.ensureSender(_msgSender(), "InstanceOperatorService"), 24 | "ERROR:CRC-001:NOT_INSTANCE_OPERATOR"); 25 | _; 26 | } 27 | 28 | modifier onlyPolicyFlow(bytes32 module) { 29 | // Allow only from delegator 30 | require( 31 | address(this) == _getContractAddress(module), 32 | "ERROR:CRC-002:NOT_ON_STORAGE" 33 | ); 34 | 35 | // Allow only ProductService (it delegates to PolicyFlow) 36 | require( 37 | _msgSender() == _getContractAddress("ProductService"), 38 | "ERROR:CRC-003:NOT_PRODUCT_SERVICE" 39 | ); 40 | _; 41 | } 42 | 43 | function initialize(address registry) public initializer { 44 | _registry = IRegistry(registry); 45 | if (_getName() != "Access") { _access = IAccess(_getContractAddress("Access")); } 46 | 47 | _afterInitialize(); 48 | } 49 | 50 | function _getName() internal virtual pure returns(bytes32) { return ""; } 51 | 52 | function _afterInitialize() internal virtual onlyInitializing {} 53 | 54 | function _getContractAddress(bytes32 contractName) internal view returns (address contractAddress) { 55 | contractAddress = _registry.getContract(contractName); 56 | require( 57 | contractAddress != address(0), 58 | "ERROR:CRC-004:CONTRACT_NOT_REGISTERED" 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /scripts/test_deployment.py: -------------------------------------------------------------------------------- 1 | from scripts.deploy_ayii import ( 2 | stakeholders_accounts_ganache, 3 | check_funds, 4 | amend_funds, 5 | deploy, 6 | deploy_setup_including_token, 7 | from_registry, 8 | from_component, 9 | ) 10 | 11 | from scripts.instance import ( 12 | GifInstance, 13 | dump_sources 14 | ) 15 | 16 | from scripts.util import ( 17 | s2b, 18 | b2s, 19 | contract_from_address, 20 | ) 21 | 22 | # pylint: disable-msg=E0611 23 | from brownie import ( 24 | TestCoin 25 | ) 26 | 27 | def main(): 28 | # for ganche the command below may be used 29 | # for other chains, use accounts.add() and record the mnemonics 30 | a = stakeholders_accounts_ganache() 31 | 32 | # deploy TestCoin with instanceOperator 33 | usdc = TestCoin.deploy({'from': a['instanceOperator']}) 34 | 35 | # check_funds checks which stakeholder accounts need funding for the deploy 36 | # also, it checks if the instanceOperator has a balance that allows to provided 37 | # the missing funds for the other accounts 38 | check_funds(a, usdc) 39 | 40 | # amend_funds transfers missing funds to stakeholder addresses using the 41 | # avaulable balance of the instanceOperator 42 | amend_funds(a) 43 | 44 | d = deploy_setup_including_token(a, usdc) 45 | 46 | instance = d['instance'] 47 | registry = instance.getRegistry() 48 | 49 | for i in range(registry.contracts()): 50 | print(b2s(registry.contractName(i))) 51 | 52 | # assert number of contracts and some contract names 53 | assert 32 == registry.contracts() 54 | assert 'InstanceOperatorService' == b2s(registry.contractName(0)) 55 | assert 'Registry' == b2s(registry.contractName(1)) 56 | assert 'RegistryController' == b2s(registry.contractName(2)) 57 | assert 'BundleToken' == b2s(registry.contractName(3)) 58 | assert 'RiskpoolToken' == b2s(registry.contractName(4)) 59 | assert 'AccessController' == b2s(registry.contractName(5)) 60 | assert 'Access' == b2s(registry.contractName(6)) 61 | assert 'PolicyDefaultFlow' == b2s(registry.contractName(21)) 62 | assert 'InstanceOperatorServiceControlle' == b2s(registry.contractName(31)) 63 | 64 | -------------------------------------------------------------------------------- /contracts/shared/WithRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@etherisc/gif-interface/contracts/modules/IRegistry.sol"; 5 | 6 | contract WithRegistry { 7 | 8 | /* 9 | * We can consider the registry address as immutable here as it contains the 10 | * root data structure for the whole GIF Instance. 11 | * We can therefore ensure that a policy flow cannot overwrite the address 12 | * neither by chance nor by intention. 13 | */ 14 | IRegistry public immutable registry; 15 | 16 | modifier onlyInstanceOperator() { 17 | require( 18 | msg.sender == getContractFromRegistry("InstanceOperatorService"), 19 | "ERROR:ACM-001:NOT_INSTANCE_OPERATOR" 20 | ); 21 | _; 22 | } 23 | 24 | modifier onlyOracleService() { 25 | require( 26 | msg.sender == getContractFromRegistry("OracleService"), 27 | "ERROR:ACM-004:NOT_ORACLE_SERVICE" 28 | ); 29 | _; 30 | } 31 | 32 | modifier onlyOracleOwner() { 33 | require( 34 | msg.sender == getContractFromRegistry("OracleOwnerService"), 35 | "ERROR:ACM-005:NOT_ORACLE_OWNER" 36 | ); 37 | _; 38 | } 39 | 40 | modifier onlyProductOwner() { 41 | require( 42 | msg.sender == getContractFromRegistry("ProductOwnerService"), 43 | "ERROR:ACM-006:NOT_PRODUCT_OWNER" 44 | ); 45 | _; 46 | } 47 | 48 | constructor(address _registry) { 49 | registry = IRegistry(_registry); 50 | } 51 | 52 | function getContractFromRegistry(bytes32 _contractName) 53 | public 54 | // override 55 | view 56 | returns (address _addr) 57 | { 58 | _addr = registry.getContract(_contractName); 59 | } 60 | 61 | function getContractInReleaseFromRegistry(bytes32 _release, bytes32 _contractName) 62 | internal 63 | view 64 | returns (address _addr) 65 | { 66 | _addr = registry.getContractInRelease(_release, _contractName); 67 | } 68 | 69 | function getReleaseFromRegistry() internal view returns (bytes32 _release) { 70 | _release = registry.getRelease(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/setup.py: -------------------------------------------------------------------------------- 1 | from brownie.network import accounts 2 | from brownie.network.account import Account 3 | 4 | # pylint: disable-msg=E0611 5 | from brownie import ( 6 | TestCoin, 7 | ) 8 | 9 | from scripts.instance import GifInstance 10 | from scripts.ayii_product import GifAyiiProductComplete 11 | 12 | def fund_riskpool( 13 | instance: GifInstance, 14 | owner: Account, 15 | capitalOwner: Account, 16 | riskpool, 17 | bundleOwner: Account, 18 | coin, 19 | amount: int, 20 | createBundle: bool = True 21 | ): 22 | # transfer funds to riskpool keeper and create allowance 23 | safetyFactor = 2 24 | coin.transfer(bundleOwner, safetyFactor * amount, {'from': owner}) 25 | coin.approve(instance.getTreasury(), safetyFactor * amount, {'from': bundleOwner}) 26 | 27 | # create approval for treasury from capital owner to allow for withdrawls 28 | maxUint256 = 2**256-1 29 | coin.approve(instance.getTreasury(), maxUint256, {'from': capitalOwner}) 30 | 31 | applicationFilter = bytes(0) 32 | 33 | bundleId = None 34 | 35 | if (createBundle): 36 | tx = riskpool.createBundle( 37 | applicationFilter, 38 | amount, 39 | {'from': bundleOwner}) 40 | bundleId = tx.return_value 41 | 42 | return bundleId 43 | 44 | 45 | def fund_customer( 46 | instance: GifInstance, 47 | owner: Account, 48 | account: Account, 49 | coin, 50 | amount: int 51 | ): 52 | coin.transfer(account, amount, {'from': owner}) 53 | coin.approve(instance.getTreasury(), amount, {'from': account}) 54 | 55 | 56 | def apply_for_policy( 57 | instance: GifInstance, 58 | owner: Account, 59 | product, 60 | customer: Account, 61 | coin, 62 | premium: int, 63 | sumInsured: int 64 | ): 65 | # transfer premium funds to customer and create allowance 66 | coin.transfer(customer, premium, {'from': owner}) 67 | coin.approve(instance.getTreasury(), premium, {'from': customer}) 68 | 69 | # create minimal policy application 70 | metaData = bytes(0) 71 | applicationData = bytes(0) 72 | 73 | tx = product.applyForPolicy( 74 | premium, 75 | sumInsured, 76 | metaData, 77 | applicationData, 78 | {'from': customer}) 79 | 80 | # print(tx.events) 81 | 82 | # returns policy id 83 | return tx.return_value 84 | -------------------------------------------------------------------------------- /tests/test_basicriskpool_bundle_allocation_02.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie.network.account import Account 5 | from brownie import ( 6 | interface, 7 | Wei, 8 | TestProduct, 9 | ) 10 | 11 | from scripts.util import ( 12 | s2h, 13 | s2b32, 14 | ) 15 | 16 | from scripts.setup import ( 17 | fund_riskpool, 18 | apply_for_policy, 19 | ) 20 | 21 | from scripts.instance import ( 22 | GifInstance, 23 | ) 24 | 25 | from scripts.product import ( 26 | GifTestProduct, 27 | GifTestRiskpool, 28 | ) 29 | 30 | # enforce function isolation for tests below 31 | @pytest.fixture(autouse=True) 32 | def isolation(fn_isolation): 33 | pass 34 | 35 | # test for distribution of policies between the three bundles as two of them should be full at the end 36 | def test_bundle_allocation_with_three_uneven_bundles( 37 | instance: GifInstance, 38 | testCoin, 39 | gifTestProduct: GifTestProduct, 40 | riskpoolKeeper: Account, 41 | owner: Account, 42 | customer: Account, 43 | feeOwner: Account, 44 | capitalOwner: Account 45 | ): 46 | num_bundles = 3 47 | product = gifTestProduct.getContract() 48 | riskpool = gifTestProduct.getRiskpool().getContract() 49 | riskpool.setMaximumNumberOfActiveBundles(num_bundles, {'from': riskpoolKeeper}) 50 | 51 | initialFunding = [10000, 2500, 1500] 52 | expectedAllocation = [6000, 2000, 1000] 53 | 54 | # fund the riskpools 55 | for i in range(num_bundles): 56 | fund_riskpool(instance, owner, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding[i]) 57 | 58 | assert num_bundles == riskpool.bundles() 59 | 60 | # allocate 3 policies in every bundle 61 | for _ in range(num_bundles * 3): 62 | apply_for_policy(instance, owner, product, customer, testCoin, 100, 1000) 63 | 64 | 65 | # ensure every bundle has same locked capital 66 | for i in range(num_bundles): 67 | # get updates bundle values 68 | bundle = _getBundle(instance, riskpool, i) 69 | ( 70 | _, 71 | _, 72 | _, 73 | _, 74 | _, 75 | _, 76 | lockedCapital, 77 | *_ 78 | ) = bundle 79 | 80 | assert expectedAllocation[i] == lockedCapital 81 | 82 | 83 | def _getBundle(instance, riskpool, bundleIdx): 84 | instanceService = instance.getInstanceService() 85 | bundleId = riskpool.getBundleId(bundleIdx) 86 | return instanceService.getBundle(bundleId) 87 | -------------------------------------------------------------------------------- /contracts/services/ProductService.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../shared/WithRegistry.sol"; 5 | // import "../shared/CoreController.sol"; 6 | import "@etherisc/gif-interface/contracts/modules/ILicense.sol"; 7 | 8 | import "@openzeppelin/contracts/utils/Context.sol"; 9 | 10 | contract ProductService is 11 | WithRegistry, 12 | // CoreController 13 | Context 14 | { 15 | bytes32 public constant NAME = "ProductService"; 16 | 17 | // solhint-disable-next-line no-empty-blocks 18 | constructor(address _registry) WithRegistry(_registry) {} 19 | 20 | fallback() external { 21 | // getAuthorizationStatus enforces msg.sender to be a registered product 22 | (,bool isAuthorized, address policyFlow) = _license().getAuthorizationStatus(_msgSender()); 23 | 24 | require(isAuthorized, "ERROR:PRS-001:NOT_AUTHORIZED"); 25 | require(policyFlow != address(0),"ERROR:PRS-002:POLICY_FLOW_NOT_RESOLVED"); 26 | 27 | _delegate(policyFlow); 28 | } 29 | 30 | 31 | /** 32 | * @dev Delegates the current call to `implementation`. 33 | * 34 | * This function does not return to its internal call site, it will return directly to the external caller. 35 | * This function is a 1:1 copy of _delegate from 36 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.6/contracts/proxy/Proxy.sol 37 | */ 38 | function _delegate(address implementation) internal { 39 | assembly { 40 | // Copy msg.data. We take full control of memory in this inline assembly 41 | // block because it will not return to Solidity code. We overwrite the 42 | // Solidity scratch pad at memory position 0. 43 | calldatacopy(0, 0, calldatasize()) 44 | 45 | // Call the implementation. 46 | // out and outsize are 0 because we don't know the size yet. 47 | let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) 48 | 49 | // Copy the returned data. 50 | returndatacopy(0, 0, returndatasize()) 51 | 52 | switch result 53 | // delegatecall returns 0 on error. 54 | case 0 { 55 | revert(0, returndatasize()) 56 | } 57 | default { 58 | return(0, returndatasize()) 59 | } 60 | } 61 | } 62 | 63 | function _license() internal view returns (ILicense) { 64 | return ILicense(registry.getContract("License")); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /contracts/shared/TransferHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | // inspired/informed by 7 | // https://soliditydeveloper.com/safe-erc20 8 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC20/ERC20.sol 9 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/token/ERC20/utils/SafeERC20.sol 10 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/utils/Address.sol 11 | // https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/TransferHelper.sol 12 | library TransferHelper { 13 | 14 | event LogTransferHelperInputValidation1Failed(bool tokenIsContract, address from, address to); 15 | event LogTransferHelperInputValidation2Failed(uint256 balance, uint256 allowance); 16 | event LogTransferHelperCallFailed(bool callSuccess, uint256 returnDataLength, bytes returnData); 17 | 18 | function unifiedTransferFrom( 19 | IERC20 token, 20 | address from, 21 | address to, 22 | uint256 value 23 | ) 24 | internal 25 | returns(bool success) 26 | { 27 | // input validation step 1 28 | address tokenAddress = address(token); 29 | bool tokenIsContract = (tokenAddress.code.length > 0); 30 | if (from == address(0) || to == address (0) || !tokenIsContract) { 31 | emit LogTransferHelperInputValidation1Failed(tokenIsContract, from, to); 32 | return false; 33 | } 34 | 35 | // input validation step 2 36 | uint256 balance = token.balanceOf(from); 37 | uint256 allowance = token.allowance(from, address(this)); 38 | if (balance < value || allowance < value) { 39 | emit LogTransferHelperInputValidation2Failed(balance, allowance); 40 | return false; 41 | } 42 | 43 | // low-level call to transferFrom 44 | // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); 45 | (bool callSuccess, bytes memory data) = address(token).call( 46 | abi.encodeWithSelector( 47 | 0x23b872dd, 48 | from, 49 | to, 50 | value)); 51 | 52 | success = callSuccess && (false 53 | || data.length == 0 54 | || (data.length == 32 && abi.decode(data, (bool)))); 55 | 56 | if (!success) { 57 | emit LogTransferHelperCallFailed(callSuccess, data.length, data); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /.github/workflows/runtests_with_coverage.yml: -------------------------------------------------------------------------------- 1 | name: Run tests with code coverage 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Set up Python 3.10 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.10' 17 | - name: Setup node environment 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | 22 | - name: Download required solidity compilers 23 | run: | 24 | mkdir -p ~/.solcx/ 25 | wget -O ~/.solcx/solc-v0.8.2 https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-v0.8.2+commit.661d1103 26 | wget -O ~/.solcx/solc-v0.8.15 https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-v0.8.15+commit.e14f2714 27 | chmod 755 ~/.solcx/solc* 28 | 29 | - name: Download brownie dependencies 30 | run: | 31 | export VERSION_OPEN_ZEPPELIN=4.7.3 32 | export VERSION_CHAINLINK=1.6.0 33 | wget -O /tmp/v${VERSION_OPEN_ZEPPELIN}.tar.gz https://github.com/OpenZeppelin/openzeppelin-contracts/archive/refs/tags/v${VERSION_OPEN_ZEPPELIN}.tar.gz 34 | wget -O /tmp/v${VERSION_CHAINLINK}.tar.gz https://github.com/smartcontractkit/chainlink/archive/refs/tags/v${VERSION_CHAINLINK}.tar.gz 35 | mkdir -p ~/.brownie/packages/OpenZeppelin 36 | cd ~/.brownie/packages/OpenZeppelin 37 | tar xvfz /tmp/v${VERSION_OPEN_ZEPPELIN}.tar.gz 38 | mv openzeppelin-contracts-${VERSION_OPEN_ZEPPELIN} openzeppelin-contracts@${VERSION_OPEN_ZEPPELIN} 39 | mkdir -p ~/.brownie/packages/smartcontractkit 40 | cd ~/.brownie/packages/smartcontractkit 41 | tar xvfz /tmp/v${VERSION_CHAINLINK}.tar.gz 42 | mv chainlink-${VERSION_CHAINLINK} chainlink@${VERSION_CHAINLINK} 43 | 44 | - name: Install ganache 45 | run: npm install --global ganache 46 | 47 | - name: Setup brownie 48 | run: wget https://raw.githubusercontent.com/eth-brownie/brownie/master/requirements.txt 49 | - run: pip install -r requirements.txt 50 | - run: pip install eth-brownie 51 | 52 | - name: Compile contracts 53 | run: brownie compile --all 54 | - run: touch .env 55 | - name: Execute tests 56 | run: brownie test -C 57 | 58 | - name: Archive test report artifacts 59 | uses: actions/upload-artifact@v3 60 | with: 61 | name: reports 62 | path: | 63 | reports -------------------------------------------------------------------------------- /tests/test_treasury_module_validations.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie.network.account import Account 5 | 6 | from scripts.const import ZERO_ADDRESS 7 | from scripts.instance import GifInstance 8 | from scripts.product import GifTestOracle, GifTestProduct, GifTestRiskpool 9 | from scripts.util import b2s 10 | 11 | from scripts.setup import ( 12 | apply_for_policy, 13 | ) 14 | 15 | # enforce function isolation for tests below 16 | @pytest.fixture(autouse=True) 17 | def isolation(fn_isolation): 18 | pass 19 | 20 | 21 | def test_fee_spec_for_different_components( 22 | instance, 23 | gifTestProduct: GifTestProduct, 24 | ): 25 | product = gifTestProduct.getContract() 26 | oracle = gifTestProduct.getOracle().getContract() 27 | riskpool = gifTestProduct.getRiskpool().getContract() 28 | instanceService = instance.getInstanceService() 29 | treasury = instance.getTreasury() 30 | 31 | fixedFee = 0 32 | fractionalFee = treasury.getFractionFullUnit() / 10 33 | feeCalculationData = bytes(0) 34 | 35 | feeSpec1 = treasury.createFeeSpecification(product.getId(), fixedFee, fractionalFee, feeCalculationData) 36 | feeSpec2 = treasury.createFeeSpecification(riskpool.getId(), fixedFee, fractionalFee, feeCalculationData) 37 | 38 | with brownie.reverts('ERROR:TRS-020:ID_NOT_PRODUCT_OR_RISKPOOL'): 39 | treasury.createFeeSpecification(oracle.getId(), fixedFee, fractionalFee, feeCalculationData) 40 | 41 | with brownie.reverts('ERROR:TRS-020:ID_NOT_PRODUCT_OR_RISKPOOL'): 42 | treasury.createFeeSpecification(999, fixedFee, fractionalFee, feeCalculationData) 43 | 44 | 45 | 46 | def test_fractional_fee_too_large( 47 | instance, 48 | gifTestProduct: GifTestProduct, 49 | ): 50 | product = gifTestProduct.getContract() 51 | instanceService = instance.getInstanceService() 52 | treasury = instance.getTreasury() 53 | 54 | componentId = product.getId() 55 | fixedFee = 0 56 | fractionalFee = treasury.getFractionFullUnit() / 10 57 | maxFractionalFee = treasury.getFractionFullUnit() / 4 58 | exceedingFractionalFee = treasury.getFractionFullUnit() / 3 59 | feeCalculationData = bytes(0) 60 | 61 | feeSpec1 = treasury.createFeeSpecification(componentId, fixedFee, fractionalFee, feeCalculationData) 62 | feeSpec2 = treasury.createFeeSpecification(componentId, fixedFee, maxFractionalFee, feeCalculationData) 63 | 64 | with brownie.reverts('ERROR:TRS-021:FRACIONAL_FEE_TOO_BIG'): 65 | treasury.createFeeSpecification(componentId, fixedFee, exceedingFractionalFee, feeCalculationData) 66 | -------------------------------------------------------------------------------- /tests/test_coin.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | def _distribute_funds(testCoin, owner, customer): 5 | testCoin.transfer(customer, 10**6, {'from': owner}) 6 | 7 | def test_setup(testCoin, owner): 8 | assert testCoin.symbol() == "TDY" 9 | assert testCoin.totalSupply() == testCoin.balanceOf(owner) 10 | 11 | # enforce function isolation for tests below 12 | @pytest.fixture(autouse=True) 13 | def isolation(fn_isolation): 14 | pass 15 | 16 | def test_test_setup(testCoin, owner, customer): 17 | _distribute_funds(testCoin, owner, customer) 18 | 19 | assert testCoin.symbol() == "TDY" 20 | 21 | assert testCoin.balanceOf(customer) == 10**6 22 | assert testCoin.totalSupply() == testCoin.balanceOf(owner) + testCoin.balanceOf(customer) 23 | 24 | 25 | def test_direct_transfer(testCoin, owner, customer): 26 | transferAmount = 1313 27 | transferBack = 42 28 | transferBackTooMuch = 42000 29 | 30 | # transfer some 31 | assert testCoin.balanceOf(owner) == testCoin.totalSupply() 32 | testCoin.transfer(customer, transferAmount, {'from': owner}) 33 | assert testCoin.balanceOf(owner) == testCoin.totalSupply() - transferAmount 34 | assert testCoin.balanceOf(customer) == transferAmount 35 | 36 | # tansfer back little 37 | testCoin.transfer(owner, transferBack, {'from': customer}) 38 | assert testCoin.balanceOf(owner) == testCoin.totalSupply() - transferAmount + transferBack 39 | assert testCoin.balanceOf(customer) == transferAmount - transferBack 40 | 41 | # transfer back too much 42 | with brownie.reverts('ERC20: transfer amount exceeds balance'): 43 | testCoin.transfer(owner, transferBackTooMuch, {'from': customer}) 44 | 45 | 46 | def test_transfer_with_approval(testCoin, owner, customer): 47 | approvedAmount = 1500 48 | transferAmount = 1313 49 | 50 | # try to transfer without allowance 51 | assert testCoin.allowance(owner, customer) == 0 52 | with brownie.reverts('ERC20: insufficient allowance'): 53 | testCoin.transferFrom(owner, customer, transferAmount, {'from': customer}) 54 | 55 | # owner creates allowance 56 | testCoin.approve(customer, approvedAmount, {'from': owner}) 57 | assert testCoin.allowance(owner, customer) == approvedAmount 58 | 59 | # transfer from with allowance 60 | testCoin.transferFrom(owner, customer, transferAmount, {'from': customer}) 61 | 62 | # check balance after transfer and check updated allowance 63 | assert testCoin.balanceOf(customer) == transferAmount 64 | assert testCoin.allowance(owner, customer) == approvedAmount - transferAmount 65 | -------------------------------------------------------------------------------- /tests/test_deploy_registry.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import brownie 3 | import pytest 4 | 5 | from brownie import ( 6 | CoreProxy, 7 | RegistryController, 8 | ) 9 | 10 | from scripts.const import ( 11 | GIF_RELEASE, 12 | REGISTRY_CONTROLLER_NAME, 13 | REGISTRY_NAME, 14 | ) 15 | 16 | from scripts.util import ( 17 | h2s, 18 | b322s, 19 | s2b32, 20 | encode_function_data, 21 | contractFromAddress, 22 | ) 23 | 24 | # enforce function isolation for tests below 25 | @pytest.fixture(autouse=True) 26 | def isolation(fn_isolation): 27 | pass 28 | 29 | def test_registry_base(owner, customer): 30 | 31 | controller = RegistryController.deploy( 32 | {'from': owner}) 33 | 34 | encoded_initializer = encode_function_data( 35 | s2b32(GIF_RELEASE), 36 | initializer=controller.initializeRegistry) 37 | 38 | proxy = CoreProxy.deploy( 39 | controller.address, 40 | encoded_initializer, 41 | {'from': owner}) 42 | 43 | registry = contractFromAddress(RegistryController, proxy.address) 44 | 45 | doAssertions(registry, proxy, controller, owner, customer) 46 | 47 | 48 | def test_registry(registry, owner, customer): 49 | proxyAddress = registry.getContract(s2b32("Registry"), {'from': customer}) 50 | controllerAddress = registry.getContract(s2b32("RegistryController"), {'from': customer}) 51 | 52 | proxy = contractFromAddress(CoreProxy, proxyAddress) 53 | controller = contractFromAddress(RegistryController, controllerAddress) 54 | 55 | doAssertions(registry, proxy, controller, owner, customer) 56 | 57 | 58 | def doAssertions(registry, proxy, controller, owner, customer): 59 | # ensure release info can be accessed by any account 60 | assert GIF_RELEASE == b322s(registry.getRelease({'from': owner})) 61 | assert GIF_RELEASE == b322s(registry.getRelease({'from': customer})) 62 | 63 | # ensure that registering contracts is disallowed for non-owner account (ie customer) 64 | with brownie.reverts(): 65 | registry.register(s2b32("Registry"), proxy.address, {'from': customer}) 66 | 67 | # ensure owner is allowed to register contracts 68 | registry.register(s2b32("Registry2"), proxy.address, {'from': owner}) 69 | registry.register(s2b32("Registry2Controller"), controller.address, {'from': owner}) 70 | 71 | # ensure getting contract info can be accessed by any account 72 | assert proxy.address == registry.getContract(s2b32("Registry2"), {'from': customer}) 73 | assert controller.address == registry.getContract(s2b32("Registry2Controller"), {'from': customer}) 74 | -------------------------------------------------------------------------------- /contracts/tokens/BundleToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | 7 | import "@etherisc/gif-interface/contracts/tokens/IBundleToken.sol"; 8 | 9 | contract BundleToken is 10 | IBundleToken, 11 | ERC721, 12 | Ownable 13 | { 14 | string public constant NAME = "GIF Bundle Token"; 15 | string public constant SYMBOL = "BTK"; 16 | 17 | mapping(uint256 /** tokenId */ => uint256 /** bundleId */) public bundleIdForTokenId; 18 | address private _bundleModule; 19 | uint256 private _totalSupply; 20 | 21 | modifier onlyBundleModule() { 22 | require(_bundleModule != address(0), "ERROR:BTK-001:NOT_INITIALIZED"); 23 | require(_msgSender() == _bundleModule, "ERROR:BTK-002:NOT_BUNDLE_MODULE"); 24 | _; 25 | } 26 | 27 | constructor() ERC721(NAME, SYMBOL) Ownable() { } 28 | 29 | function setBundleModule(address bundleModule) 30 | external 31 | { 32 | require(_bundleModule == address(0), "ERROR:BTK-003:BUNDLE_MODULE_ALREADY_DEFINED"); 33 | require(bundleModule != address(0), "ERROR:BTK-004:INVALID_BUNDLE_MODULE_ADDRESS"); 34 | _bundleModule = bundleModule; 35 | } 36 | 37 | 38 | function mint(uint256 bundleId, address to) 39 | external 40 | onlyBundleModule 41 | returns(uint256 tokenId) 42 | { 43 | _totalSupply++; 44 | tokenId = _totalSupply; 45 | bundleIdForTokenId[tokenId] = bundleId; 46 | 47 | _safeMint(to, tokenId); 48 | 49 | emit LogBundleTokenMinted(bundleId, tokenId, to); 50 | } 51 | 52 | 53 | function burn(uint256 tokenId) 54 | external 55 | onlyBundleModule 56 | { 57 | require(_exists(tokenId), "ERROR:BTK-005:TOKEN_ID_INVALID"); 58 | _burn(tokenId); 59 | 60 | emit LogBundleTokenBurned(bundleIdForTokenId[tokenId], tokenId); 61 | } 62 | 63 | function burned(uint tokenId) 64 | external override 65 | view 66 | returns(bool isBurned) 67 | { 68 | isBurned = tokenId <= _totalSupply && !_exists(tokenId); 69 | } 70 | 71 | function getBundleId(uint256 tokenId) external override view returns(uint256) { return bundleIdForTokenId[tokenId]; } 72 | function getBundleModuleAddress() external view returns(address) { return _bundleModule; } 73 | 74 | function exists(uint256 tokenId) external override view returns(bool) { return tokenId <= _totalSupply; } 75 | function totalSupply() external override view returns(uint256 tokenCount) { return _totalSupply; } 76 | } 77 | -------------------------------------------------------------------------------- /tests/test_policy_default_flow.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie.network.account import Account 5 | from brownie import ( 6 | Wei, 7 | TestProduct, 8 | ) 9 | 10 | from scripts.const import ( 11 | ZERO_ADDRESS 12 | ) 13 | 14 | from scripts.util import ( 15 | s2h, 16 | s2b32, 17 | ) 18 | 19 | from scripts.setup import ( 20 | fund_riskpool, 21 | apply_for_policy, 22 | ) 23 | 24 | from scripts.instance import ( 25 | GifInstance, 26 | ) 27 | 28 | from scripts.product import ( 29 | GifTestProduct, 30 | GifTestRiskpool, 31 | ) 32 | 33 | # enforce function isolation for tests below 34 | @pytest.fixture(autouse=True) 35 | def isolation(fn_isolation): 36 | pass 37 | 38 | 39 | def test_apply_with_zero_address( 40 | instance: GifInstance, 41 | gifTestProduct: GifTestProduct, 42 | testCoin, 43 | owner: Account, 44 | customer: Account, 45 | riskpoolKeeper: Account, 46 | capitalOwner: Account 47 | ): 48 | instanceService = instance.getInstanceService() 49 | product = gifTestProduct.getContract() 50 | riskpoolWallet = capitalOwner 51 | investor = riskpoolKeeper 52 | 53 | riskpool = gifTestProduct.getRiskpool().getContract() 54 | fund_riskpool(instance, owner, riskpoolWallet, riskpool, investor, testCoin, 1000) 55 | 56 | metaData = bytes(0) 57 | applicationData = bytes(0) 58 | 59 | with brownie.reverts("ERROR:POL-001:INVALID_OWNER"): 60 | product.applyForPolicy( 61 | ZERO_ADDRESS, 62 | 10, 63 | 100, 64 | metaData, 65 | applicationData, 66 | {'from': customer} 67 | ) 68 | 69 | 70 | def test_apply_with_invalid_amounts( 71 | instance: GifInstance, 72 | gifTestProduct: GifTestProduct, 73 | testCoin, 74 | owner: Account, 75 | customer: Account, 76 | riskpoolKeeper: Account, 77 | capitalOwner: Account 78 | ): 79 | instanceService = instance.getInstanceService() 80 | product = gifTestProduct.getContract() 81 | riskpoolWallet = capitalOwner 82 | investor = riskpoolKeeper 83 | 84 | riskpool = gifTestProduct.getRiskpool().getContract() 85 | fund_riskpool(instance, owner, riskpoolWallet, riskpool, investor, testCoin, 1000) 86 | 87 | metaData = bytes(0) 88 | applicationData = bytes(0) 89 | 90 | with brownie.reverts("ERROR:POC-012:PREMIUM_AMOUNT_ZERO"): 91 | product.applyForPolicy( 92 | customer, 93 | 0, 94 | 10, 95 | metaData, 96 | applicationData, 97 | {'from': customer} 98 | ) 99 | 100 | with brownie.reverts("ERROR:POC-013:SUM_INSURED_AMOUNT_TOO_SMALL"): 101 | product.applyForPolicy( 102 | customer, 103 | 10, 104 | 9, 105 | metaData, 106 | applicationData, 107 | {'from': customer} 108 | ) -------------------------------------------------------------------------------- /contracts/services/ComponentOwnerService.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../modules/ComponentController.sol"; 5 | // TODO ComponentOwnerService should not know of the PoolController - if we have a better idea how to build this, it should be changed. 6 | import "../modules/PoolController.sol"; 7 | import "../shared/CoreController.sol"; 8 | 9 | import "@etherisc/gif-interface/contracts/components/IComponent.sol"; 10 | import "@etherisc/gif-interface/contracts/services/IComponentOwnerService.sol"; 11 | 12 | contract ComponentOwnerService is 13 | IComponentOwnerService, 14 | CoreController 15 | { 16 | ComponentController private _component; 17 | 18 | modifier onlyOwnerWithRoleFromComponent(IComponent component) { 19 | address owner = component.getOwner(); 20 | bytes32 requiredRole = _component.getRequiredRole(component.getType()); 21 | require(_msgSender() == owner, "ERROR:COS-001:NOT_OWNER"); 22 | require(_access.hasRole(requiredRole, owner), "ERROR:COS-002:REQUIRED_ROLE_MISSING"); 23 | _; 24 | } 25 | 26 | modifier onlyOwnerWithRole(uint256 id) { 27 | IComponent component = _component.getComponent(id); 28 | require(address(component) != address(0), "ERROR:COS-003:COMPONENT_ID_INVALID"); 29 | 30 | address owner = component.getOwner(); 31 | bytes32 requiredRole = _component.getRequiredRole(_component.getComponentType(id)); 32 | 33 | require(_msgSender() == owner, "ERROR:COS-004:NOT_OWNER"); 34 | require(_access.hasRole(requiredRole, owner), "ERROR:COS-005:REQUIRED_ROLE_MISSING"); 35 | _; 36 | } 37 | 38 | function _afterInitialize() internal override onlyInitializing { 39 | _component = ComponentController(_getContractAddress("Component")); 40 | } 41 | 42 | function propose(IComponent component) 43 | external override 44 | onlyOwnerWithRoleFromComponent(component) 45 | { 46 | _component.propose(component); 47 | } 48 | 49 | function stake(uint256 id) 50 | external override 51 | onlyOwnerWithRole(id) 52 | { 53 | revert("ERROR:COS-006:IMPLEMENATION_MISSING"); 54 | } 55 | 56 | function withdraw(uint256 id) 57 | external override 58 | onlyOwnerWithRole(id) 59 | { 60 | revert("ERROR:COS-007:IMPLEMENATION_MISSING"); 61 | } 62 | 63 | 64 | function pause(uint256 id) 65 | external override 66 | onlyOwnerWithRole(id) 67 | { 68 | _component.pause(id); 69 | } 70 | 71 | function unpause(uint256 id) 72 | external override 73 | onlyOwnerWithRole(id) 74 | { 75 | _component.unpause(id); 76 | } 77 | 78 | function archive(uint256 id) 79 | external override 80 | onlyOwnerWithRole(id) 81 | { 82 | _component.archiveFromComponentOwner(id); 83 | } 84 | } -------------------------------------------------------------------------------- /tests/test_product_compromised.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie import ( 5 | TestOracle, 6 | TestProduct, 7 | TestCompromisedProduct, 8 | ) 9 | 10 | from scripts.instance import GifInstance 11 | 12 | from scripts.product import ( 13 | GifTestOracle, 14 | GifTestProduct, 15 | GifTestRiskpool, 16 | ) 17 | 18 | from scripts.setup import ( 19 | fund_riskpool, 20 | apply_for_policy, 21 | ) 22 | 23 | from scripts.util import s2b32 24 | 25 | 26 | # enforce function isolation for tests below 27 | @pytest.fixture(autouse=True) 28 | def isolation(fn_isolation): 29 | pass 30 | 31 | def test_use_compromised_product( 32 | instance: GifInstance, 33 | owner, 34 | gifTestProduct: GifTestProduct, 35 | testCoin, 36 | productOwner, 37 | capitalOwner, 38 | riskpoolKeeper, 39 | customer 40 | ): 41 | instanceService = instance.getInstanceService() 42 | 43 | assert instanceService.oracles() == 1 44 | assert instanceService.products() == 1 45 | assert instanceService.riskpools() == 1 46 | 47 | product = gifTestProduct.getContract() 48 | assert product.getId() == 3 49 | assert product.getState() == 3 50 | 51 | riskpool = gifTestProduct.getRiskpool().getContract() 52 | assert riskpool.getId() == 2 53 | assert riskpool.getState() == 3 54 | 55 | # asssertions for initialized product 56 | assert instanceService.getComponentToken(product.getId()) == testCoin 57 | assert instanceService.getRiskpoolWallet(riskpool.getId()) == capitalOwner 58 | 59 | # fund riskpool 60 | initialFunding = 10000 61 | fund_riskpool(instance, owner, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding) 62 | 63 | # fund customer and apply for policy 64 | premium = 100 65 | sumInsured = 5000 66 | policyId = apply_for_policy(instance, owner, product, customer, testCoin, premium, sumInsured) 67 | 68 | policy = instanceService.getPolicy(policyId) 69 | print(policy) 70 | 71 | # deploy compromised product 72 | compromisedProduct = TestCompromisedProduct.deploy( 73 | product.getName(), 74 | product.getToken(), 75 | product.getId(), 76 | riskpool.getId(), 77 | instance.getRegistry(), 78 | {'from': customer}) 79 | 80 | # assert name, id and state match with the target product 81 | assert compromisedProduct.getName() == product.getName() 82 | assert compromisedProduct.getId() == product.getId() 83 | assert compromisedProduct.getState() == product.getState() 84 | 85 | # attempt to create a new policy with the compromised product 86 | with brownie.reverts("ERROR:CCR-007:COMPONENT_UNKNOWN"): 87 | apply_for_policy(instance, owner, compromisedProduct, customer, testCoin, premium, sumInsured) 88 | 89 | # attempt to create a new claim for an existing policy with the compromised product 90 | with brownie.reverts("ERROR:CCR-007:COMPONENT_UNKNOWN"): 91 | claimAmount = 5000 92 | compromisedProduct.submitClaim(policyId, claimAmount, {'from': customer}) -------------------------------------------------------------------------------- /tests/test_instance_operator_service.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import brownie 3 | import pytest 4 | 5 | from brownie import ( 6 | AccessController, 7 | InstanceOperatorService, 8 | InstanceService 9 | ) 10 | 11 | from scripts.const import ( 12 | ACCESS_NAME, 13 | INSTANCE_OPERATOR_SERVICE_NAME, 14 | INSTANCE_SERVICE_NAME, 15 | ) 16 | 17 | from scripts.util import ( 18 | h2sLeft, 19 | s2b32, 20 | contractFromAddress 21 | ) 22 | 23 | # enforce function isolation for tests below 24 | @pytest.fixture(autouse=True) 25 | def isolation(fn_isolation): 26 | pass 27 | 28 | def test_non_existing_functionality(instance, owner): 29 | instanceOperatorService = instance.getInstanceOperatorService() 30 | with pytest.raises(AttributeError): 31 | assert instanceOperatorService.foo({'from': owner}) 32 | 33 | 34 | def test_instance_operator_service_contract_in_registry(instance, owner): 35 | instanceOperatorService = instance.getInstanceOperatorService() 36 | registry = instance.getRegistry() 37 | 38 | instanceOperatorServiceAddress = registry.getContract(s2b32(INSTANCE_OPERATOR_SERVICE_NAME)) 39 | 40 | assert instanceOperatorService.address == instanceOperatorServiceAddress 41 | assert instanceOperatorService.address != 0x0 42 | 43 | 44 | def test_role_granting(instance, owner, productOwner, customer): 45 | registry = instance.getRegistry() 46 | 47 | instanceOperatorServiceAddress = registry.getContract(s2b32(INSTANCE_OPERATOR_SERVICE_NAME)) 48 | instanceOperatorService = contractFromAddress(InstanceOperatorService, instanceOperatorServiceAddress) 49 | 50 | instanceServiceAddress = registry.getContract(s2b32(INSTANCE_SERVICE_NAME)) 51 | instanceService = contractFromAddress(InstanceService, instanceServiceAddress) 52 | 53 | # verify that after setup productOwner account does not yet have product owner role 54 | poRole = instanceService.getProductOwnerRole({'from': customer}) 55 | assert not instanceService.hasRole(poRole, productOwner, {'from': customer}) 56 | 57 | print('owner: {}'.format(owner)) 58 | print('instanceOperatorServiceAddress: {}'.format(instanceOperatorServiceAddress)) 59 | print('productOwner: {}'.format(productOwner)) 60 | print('poRole: {}'.format(poRole)) 61 | 62 | # verify that addRoleToAccount is protected and not anybody (ie customer) and grant roles 63 | with brownie.reverts(): 64 | instanceOperatorService.grantRole(poRole, productOwner, {'from': customer}) 65 | 66 | instanceOperatorService.grantRole(poRole, productOwner, {'from': owner}) 67 | 68 | # verify that productOwner account now has product owner role 69 | assert instanceService.hasRole(poRole, productOwner, {'from': customer}) 70 | 71 | 72 | def test_default_admin_role_cannot_be_changed(instance, owner, customer): 73 | registry = instance.getRegistry() 74 | 75 | accessAddress = registry.getContract(s2b32(ACCESS_NAME)) 76 | access = contractFromAddress(AccessController, accessAddress) 77 | 78 | with brownie.reverts(): 79 | access.setDefaultAdminRole(customer, {'from': owner}) 80 | 81 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [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 2 | ARG VARIANT=3-bullseye 3 | FROM mcr.microsoft.com/devcontainers/python:${VARIANT} 4 | 5 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 6 | ARG NODE_VERSION="16" 7 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 8 | 9 | # install mcfly as root 10 | RUN curl -LSfs https://raw.githubusercontent.com/cantino/mcfly/master/ci/install.sh | sh -s -- --git cantino/mcfly 11 | 12 | USER vscode 13 | 14 | # Shell customizations 15 | 16 | # 1) Install and configure starship.rs prompt 17 | RUN curl -fsSL https://starship.rs/install.sh | sh -s -- --yes 18 | RUN echo "eval \"\$(starship init bash)\"" >> ~/.bashrc && echo "eval \"\$(starship init zsh)\"" >> ~/.zshrc 19 | RUN mkdir -p /home/vscode/.config/ 20 | COPY .devcontainer/starship.toml /home/vscode/.config/starship.toml 21 | 22 | # 2) install thefuck 23 | RUN pip3 install thefuck --user \ 24 | && echo 'eval "$(thefuck --alias)"' >> ~/.bashrc \ 25 | && echo 'eval "$(thefuck --alias)"' >> ~/.zshrc 26 | 27 | # 3) install mcfly config 28 | RUN echo 'eval "$(mcfly init zsh)"' >> ~/.zshrc \ 29 | && touch ~/.zsh_history 30 | 31 | RUN pip install eth-brownie 32 | 33 | # [Optional] Uncomment this line to install global node packages. 34 | RUN npm install -g ganache solhint prettier prettier-plugin-solidity solhint-plugin-prettier 35 | 36 | # # download required solidity compiler 37 | COPY .devcontainer/scripts/install_solidity.sh /tmp 38 | RUN /tmp/install_solidity.sh 39 | 40 | # Download openzeppelin and chainlink depedencies (large and slow) 41 | ARG VERSION_OPEN_ZEPPELIN=4.7.3 42 | ARG VERSION_CHAINLINK=1.6.0 43 | RUN wget -O /tmp/v${VERSION_OPEN_ZEPPELIN}.tar.gz https://github.com/OpenZeppelin/openzeppelin-contracts/archive/refs/tags/v${VERSION_OPEN_ZEPPELIN}.tar.gz \ 44 | && wget -O /tmp/v${VERSION_CHAINLINK}.tar.gz https://github.com/smartcontractkit/chainlink/archive/refs/tags/v${VERSION_CHAINLINK}.tar.gz 45 | RUN mkdir -p /home/vscode/.brownie/packages/OpenZeppelin \ 46 | && cd /home/vscode/.brownie/packages/OpenZeppelin \ 47 | && tar xvfz /tmp/v${VERSION_OPEN_ZEPPELIN}.tar.gz \ 48 | && mv openzeppelin-contracts-${VERSION_OPEN_ZEPPELIN} openzeppelin-contracts@${VERSION_OPEN_ZEPPELIN} \ 49 | && mkdir -p /home/vscode/.brownie/packages/smartcontractkit \ 50 | && cd /home/vscode/.brownie/packages/smartcontractkit \ 51 | && tar xvfz /tmp/v${VERSION_CHAINLINK}.tar.gz \ 52 | && mv chainlink-${VERSION_CHAINLINK} chainlink@${VERSION_CHAINLINK} 53 | 54 | # [Optional] Uncomment this section to install additional OS packages. 55 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 56 | # && apt-get -y install --no-install-recommends 57 | 58 | # install foundry 59 | RUN curl -L https://foundry.paradigm.xyz | bash 60 | RUN echo 'export PATH="$PATH:/home/vscode/.foundry/bin"' >> ~/.zshrc 61 | RUN /home/vscode/.foundry/bin/foundryup 62 | 63 | 64 | WORKDIR /workspace 65 | -------------------------------------------------------------------------------- /tests/test_deploy_oracle.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie import TestOracle 5 | 6 | from scripts.instance import GifInstance 7 | from scripts.product import GifTestOracle 8 | from scripts.util import s2b32 9 | 10 | # enforce function isolation for tests below 11 | @pytest.fixture(autouse=True) 12 | def isolation(fn_isolation): 13 | pass 14 | 15 | # def test_deploy(instance: GifInstance, oracleOwner, productOwner): 16 | def test_deploy_simple( 17 | instance: GifInstance, 18 | gifTestOracle: GifTestOracle 19 | ): 20 | instanceService = instance.getInstanceService() 21 | 22 | assert instanceService.oracles() == 1 23 | assert instanceService.products() == 0 24 | assert instanceService.riskpools() == 0 25 | 26 | # TODO add asserts to check inital state of oracle 27 | 28 | 29 | def test_deploy_and_propose(instance: GifInstance, owner, oracleProvider): 30 | instanceService = instance.getInstanceService() 31 | operatorService = instance.getInstanceOperatorService() 32 | componentOwnerService = instance.getComponentOwnerService() 33 | 34 | oracle = TestOracle.deploy( 35 | s2b32("TestOracle"), 36 | instance.getRegistry(), 37 | {'from': oracleProvider}) 38 | 39 | # assert that proposal fails with missing role 40 | providerRole = instanceService.getOracleProviderRole() 41 | operatorService.revokeRole( 42 | providerRole, 43 | oracleProvider, 44 | {'from': instance.getOwner()}) 45 | 46 | assert not instanceService.hasRole(providerRole, oracleProvider) 47 | 48 | with brownie.reverts(): 49 | componentOwnerService.propose( 50 | oracle, 51 | {'from': oracleProvider}) 52 | 53 | # add granting role 54 | operatorService.grantRole( 55 | providerRole, 56 | oracleProvider, 57 | {'from': instance.getOwner()}) 58 | 59 | # try again 60 | componentOwnerService.propose( 61 | oracle, 62 | {'from': oracleProvider}) 63 | 64 | 65 | def test_deploy_and_approve(instance: GifInstance, oracleProvider, productOwner): 66 | instanceService = instance.getInstanceService() 67 | operatorService = instance.getInstanceOperatorService() 68 | componentOwnerService = instance.getComponentOwnerService() 69 | 70 | oracle = TestOracle.deploy( 71 | s2b32("TestOracle"), 72 | instance.getRegistry(), 73 | {'from': oracleProvider}) 74 | 75 | # add granting role and propose 76 | providerRole = instanceService.getOracleProviderRole() 77 | operatorService.grantRole( 78 | providerRole, 79 | oracleProvider, 80 | {'from': instance.getOwner()}) 81 | 82 | componentOwnerService.propose( 83 | oracle, 84 | {'from': oracleProvider}) 85 | 86 | # verify that productOwner cannot approve the oracle 87 | with brownie.reverts(): 88 | operatorService.approve( 89 | oracle.getId(), 90 | {'from': productOwner}) 91 | 92 | # verify that instance operator can approve the propsed oracle 93 | operatorService.approve( 94 | oracle.getId(), 95 | {'from': instance.getOwner()}) 96 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main, develop ] 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: [ main, develop ] 11 | 12 | jobs: 13 | test: 14 | name: Compile and run tests 15 | # only run if contracts have changed 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Python 3.10 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: '3.10' 25 | - name: Setup node environment 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | 30 | - name: Prepare environment 31 | run: .github/workflows/scripts/prepare_environment.sh 32 | 33 | - name: Compile contracts 34 | run: brownie compile --all 35 | - run: touch .env 36 | - name: Execute tests 37 | run: brownie test -n auto 38 | 39 | - name: Install solhint linter 40 | run: npm install --global solhint 41 | - name: Run solhint linter 42 | run: solhint contracts/**/*.sol 43 | 44 | - name: Archive build artifacts 45 | uses: actions/upload-artifact@v3 46 | with: 47 | name: contracts 48 | path: | 49 | build 50 | 51 | 52 | publish: 53 | name: Publish package to npmjs 54 | runs-on: ubuntu-latest 55 | permissions: 56 | contents: read 57 | id-token: write 58 | needs: [test] 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v3 62 | 63 | - name: Download build artifacts 64 | uses: actions/download-artifact@v3 65 | with: 66 | name: contracts 67 | path: build 68 | 69 | - name: Setup node environment 70 | uses: actions/setup-node@v3 71 | with: 72 | node-version: 18 73 | registry-url: 'https://registry.npmjs.org' 74 | # latest npm required for provenance 75 | - run: npm install -g npm 76 | 77 | - run: npm ci 78 | 79 | - name: Set build version identifier 80 | run: npm version "`npm version patch --no-git-tag-version`-`git rev-parse --short HEAD`" --no-git-tag-version 81 | 82 | - run: npm publish --tag next --provenance 83 | env: 84 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }} 85 | 86 | 87 | deployment: 88 | name: Execute test deployment to local ganache chain 89 | runs-on: ubuntu-latest 90 | needs: [test] 91 | steps: 92 | - name: Checkout 93 | uses: actions/checkout@v3 94 | 95 | - name: Set up Python 3.10 96 | uses: actions/setup-python@v4 97 | with: 98 | python-version: '3.10' 99 | 100 | - name: Setup node environment 101 | uses: actions/setup-node@v3 102 | with: 103 | node-version: 16 104 | 105 | - name: Prepare environment 106 | run: .github/workflows/scripts/prepare_environment.sh 107 | 108 | - name: Compile contracts 109 | run: brownie compile --all 110 | - run: touch .env 111 | 112 | # shell does not read .bashrc so nvm must be sourced manually each time 113 | - name: Start ganache 114 | run: | 115 | ganache \ 116 | --mnemonic "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" \ 117 | --chain.chainId 1234 \ 118 | --port 7545 \ 119 | --accounts 20 \ 120 | -q & 121 | 122 | - name: Execute deployment 123 | run: | 124 | brownie networks add Local ganache host=http://127.0.0.1:7545 chainid=1234 125 | brownie run test_deployment.py --network=ganache 126 | -------------------------------------------------------------------------------- /tests/test_oracle_lifecycle.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie.network.account import Account 5 | from brownie import ( 6 | interface, 7 | Wei, 8 | TestProduct, 9 | TestRiskpool, 10 | ) 11 | 12 | from scripts.util import ( 13 | s2h, 14 | s2b32, 15 | ) 16 | 17 | from scripts.setup import ( 18 | fund_riskpool, 19 | apply_for_policy, 20 | ) 21 | 22 | from scripts.instance import ( 23 | GifInstance, 24 | ) 25 | 26 | from scripts.product import ( 27 | GifTestProduct, 28 | GifTestRiskpool, 29 | ) 30 | 31 | # enforce function isolation for tests below 32 | @pytest.fixture(autouse=True) 33 | def isolation(fn_isolation): 34 | pass 35 | 36 | def test_request_for_inactive_oracle( 37 | instance: GifInstance, 38 | testCoin, 39 | gifTestProduct: GifTestProduct, 40 | productOwner, 41 | oracleProvider, 42 | riskpoolKeeper: Account, 43 | owner: Account, 44 | customer: Account, 45 | feeOwner: Account, 46 | capitalOwner: Account 47 | ): 48 | componentOwnerService = instance.getComponentOwnerService() 49 | instanceService = instance.getInstanceService() 50 | 51 | product = gifTestProduct.getContract() 52 | oracle = gifTestProduct.getOracle().getContract() 53 | riskpool = gifTestProduct.getRiskpool().getContract() 54 | 55 | initialFunding = 10000 56 | bundleOwner = riskpoolKeeper 57 | fund_riskpool(instance, owner, capitalOwner, riskpool, bundleOwner, testCoin, initialFunding) 58 | 59 | # transfer premium funds to customer and create allowance 60 | premium = 200 61 | testCoin.transfer(customer, premium, {'from': owner}) 62 | testCoin.approve(instance.getTreasury(), premium, {'from': customer}) 63 | 64 | # create test policy before oracle is paused 65 | sumInsured = 3000 66 | tx = product.applyForPolicy( 67 | premium, 68 | sumInsured, 69 | bytes(0), 70 | bytes(0), 71 | {'from': customer}) 72 | 73 | # returns policy id 74 | policyId = tx.return_value 75 | 76 | # check claim subission work for active oracle 77 | oracleId = oracle.getId() 78 | assert instanceService.getComponentState(oracleId) == 3 79 | 80 | # check normal claim submission works 81 | claimAmount = 50 82 | product.submitClaim( 83 | policyId, 84 | claimAmount, 85 | {'from': customer}) 86 | 87 | # check claim submission with deferred response works 88 | tx = product.submitClaimWithDeferredResponse( 89 | policyId, 90 | claimAmount, 91 | {'from': customer}) 92 | 93 | print(tx.info()) 94 | (claimId, requestId) = tx.return_value 95 | 96 | # pause oracle (oracle no longer active) 97 | componentOwnerService.pause( 98 | oracleId, 99 | {'from': oracleProvider}) 100 | 101 | # assert oracle is paused 102 | assert instanceService.getComponentState(oracleId) == 4 103 | 104 | # check creating new claim no longer works 105 | with brownie.reverts("ERROR:QUC-042:ORACLE_NOT_ACTIVE"): 106 | product.submitClaim( 107 | policyId, 108 | claimAmount, 109 | {'from': customer}) 110 | 111 | # check oracle no longer accepts response 112 | with brownie.reverts("ERROR:QUC-042:ORACLE_NOT_ACTIVE"): 113 | isLossEvent = True 114 | oracle.respond(requestId, isLossEvent) 115 | 116 | # unpause oracle (oracle active again) 117 | componentOwnerService.unpause( 118 | oracleId, 119 | {'from': oracleProvider}) 120 | 121 | # assert oracle is active agains 122 | assert instanceService.getComponentState(oracleId) == 3 123 | 124 | # check oracle accepts response in active state 125 | tx = oracle.respond(requestId, isLossEvent) 126 | print(tx.info()) 127 | 128 | -------------------------------------------------------------------------------- /tests/test_deploy_product.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie import ( 5 | TestOracle, 6 | TestProduct, 7 | ) 8 | 9 | from scripts.instance import GifInstance 10 | 11 | from scripts.product import ( 12 | GifTestOracle, 13 | GifTestProduct, 14 | GifTestRiskpool, 15 | ) 16 | 17 | from scripts.util import s2b32 18 | 19 | 20 | # enforce function isolation for tests below 21 | @pytest.fixture(autouse=True) 22 | def isolation(fn_isolation): 23 | pass 24 | 25 | def test_deploy_simple( 26 | instance: GifInstance, 27 | testCoin, 28 | capitalOwner, 29 | productOwner, 30 | gifTestOracle: GifTestOracle, 31 | gifTestRiskpool: GifTestRiskpool, 32 | ): 33 | instanceService = instance.getInstanceService() 34 | 35 | assert instanceService.oracles() == 1 36 | assert instanceService.products() == 0 37 | assert instanceService.riskpools() == 1 38 | 39 | product = GifTestProduct( 40 | instance, 41 | testCoin, 42 | capitalOwner, 43 | productOwner, 44 | gifTestOracle, 45 | gifTestRiskpool) 46 | 47 | assert instanceService.oracles() == 1 48 | assert instanceService.products() == 1 49 | assert instanceService.riskpools() == 1 50 | 51 | # asssertions for initialized product 52 | assert instanceService.getComponentToken(product.getId()) == testCoin 53 | assert instanceService.getRiskpoolWallet(gifTestRiskpool.getId()) == capitalOwner 54 | 55 | # check capitalization for riskpool 56 | riskpool = gifTestRiskpool.getContract() 57 | assert riskpool.getCollateralizationLevel() == riskpool.getFullCollateralizationLevel() 58 | 59 | 60 | 61 | def test_deploy_approve_product( 62 | instance: GifInstance, 63 | testCoin, 64 | capitalOwner, 65 | productOwner, 66 | oracleProvider, 67 | riskpoolKeeper, 68 | ): 69 | instanceService = instance.getInstanceService() 70 | operatorService = instance.getInstanceOperatorService() 71 | componentOwnerService = instance.getComponentOwnerService() 72 | registry = instance.getRegistry() 73 | 74 | oracle = GifTestOracle( 75 | instance, 76 | oracleProvider, 77 | name='TestOracle2') 78 | 79 | collateralization = 10000 80 | riskpool = GifTestRiskpool( 81 | instance, 82 | riskpoolKeeper, 83 | capitalOwner, 84 | testCoin, 85 | collateralization, 86 | name='TestRiskpool2') 87 | 88 | product = TestProduct.deploy( 89 | s2b32("TestProduct"), 90 | testCoin.address, 91 | capitalOwner, 92 | oracle.getId(), 93 | riskpool.getId(), 94 | registry, 95 | {'from': productOwner}) 96 | 97 | # add granting role and propose 98 | ownerRole = instanceService.getProductOwnerRole() 99 | operatorService.grantRole( 100 | ownerRole, 101 | productOwner, 102 | {'from': instance.getOwner()}) 103 | 104 | # check that product owner may not propose compoent 105 | # without being owner 106 | with brownie.reverts(): 107 | componentOwnerService.propose( 108 | oracle.getContract(), 109 | {'from': productOwner}) 110 | 111 | # check that product owner may proposes his/her product 112 | componentOwnerService.propose( 113 | product, 114 | {'from': productOwner}) 115 | 116 | productId = product.getId() 117 | assert instanceService.getComponentState(productId) == 1 118 | assert product.getState() == 1 119 | 120 | # verify that oracleOwner or productOwner cannot approve the product 121 | with brownie.reverts(): 122 | operatorService.approve( 123 | product.getId(), 124 | {'from': oracleProvider}) 125 | 126 | with brownie.reverts(): 127 | operatorService.approve( 128 | product.getId(), 129 | {'from': productOwner}) 130 | 131 | # verify that instance operator can approve the propsed product 132 | operatorService.approve( 133 | product.getId(), 134 | {'from': instance.getOwner()}) 135 | 136 | assert instanceService.getComponentState(productId) == 3 137 | assert product.getState() == 3 138 | -------------------------------------------------------------------------------- /contracts/modules/AccessController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../shared/CoreController.sol"; 5 | 6 | import "@etherisc/gif-interface/contracts/modules/IAccess.sol"; 7 | 8 | import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; 9 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 10 | 11 | 12 | contract AccessController is 13 | IAccess, 14 | CoreController, 15 | AccessControlEnumerable 16 | { 17 | 18 | // 0xe984cfd1d1fa34f80e24ddb2a60c8300359d79eee44555bc35c106eb020394cd 19 | bytes32 public constant PRODUCT_OWNER_ROLE = keccak256("PRODUCT_OWNER_ROLE"); 20 | 21 | // 0xd26b4cd59ffa91e4599f3d18b02fcd5ffb06e03216f3ee5f25f68dc75cbbbaa2 22 | bytes32 public constant ORACLE_PROVIDER_ROLE = keccak256("ORACLE_PROVIDER_ROLE"); 23 | 24 | // 0x3c4cdb47519f2f89924ebeb1ee7a8a43b8b00120826915726460bb24576012fd 25 | bytes32 public constant RISKPOOL_KEEPER_ROLE = keccak256("RISKPOOL_KEEPER_ROLE"); 26 | 27 | mapping(bytes32 => bool) public validRole; 28 | 29 | bool private _defaultAdminSet; 30 | 31 | function _afterInitialize() internal override { 32 | // add product owner, oracle provider and riskpool keeper roles 33 | _populateValidRoles(); 34 | } 35 | 36 | function _getName() internal override pure returns(bytes32) { return "Access"; } 37 | 38 | // IMPORTANT check the setting of the default admin role 39 | // after the deployment of a GIF instance. 40 | // this method is called in the deployment of 41 | // the instance operator proxy/controller 42 | function setDefaultAdminRole(address defaultAdmin) 43 | external 44 | { 45 | require(!_defaultAdminSet, "ERROR:ACL-001:ADMIN_ROLE_ALREADY_SET"); 46 | _defaultAdminSet = true; 47 | 48 | _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); 49 | } 50 | 51 | //--- manage role ownership ---------------------------------------------// 52 | function grantRole(bytes32 role, address principal) 53 | public 54 | override(IAccessControl, IAccess) 55 | onlyInstanceOperator 56 | { 57 | require(validRole[role], "ERROR:ACL-002:ROLE_UNKNOWN_OR_INVALID"); 58 | AccessControl.grantRole(role, principal); 59 | } 60 | 61 | function revokeRole(bytes32 role, address principal) 62 | public 63 | override(IAccessControl, IAccess) 64 | onlyInstanceOperator 65 | { 66 | AccessControl.revokeRole(role, principal); 67 | } 68 | 69 | function renounceRole(bytes32 role, address principal) 70 | public 71 | override(IAccessControl, IAccess) 72 | { 73 | AccessControl.renounceRole(role, principal); 74 | } 75 | 76 | //--- manage roles ------------------------------------------------------// 77 | function addRole(bytes32 role) 78 | public override 79 | onlyInstanceOperator 80 | { 81 | require(!validRole[role], "ERROR:ACL-003:ROLE_EXISTING_AND_VALID"); 82 | validRole[role] = true; 83 | } 84 | 85 | function invalidateRole(bytes32 role) 86 | public override 87 | onlyInstanceOperator 88 | { 89 | require(validRole[role], "ERROR:ACL-004:ROLE_UNKNOWN_OR_INVALID"); 90 | validRole[role] = false; 91 | } 92 | 93 | function hasRole(bytes32 role, address principal) 94 | public view 95 | override(IAccessControl, IAccess) 96 | returns(bool) 97 | { 98 | return super.hasRole(role, principal); 99 | } 100 | 101 | function getDefaultAdminRole() public pure override returns(bytes32) { 102 | return DEFAULT_ADMIN_ROLE; 103 | } 104 | 105 | function getProductOwnerRole() public pure override returns(bytes32) { 106 | return PRODUCT_OWNER_ROLE; 107 | } 108 | 109 | function getOracleProviderRole() public pure override returns(bytes32) { 110 | return ORACLE_PROVIDER_ROLE; 111 | } 112 | 113 | function getRiskpoolKeeperRole() public pure override returns(bytes32) { 114 | return RISKPOOL_KEEPER_ROLE; 115 | } 116 | 117 | function _populateValidRoles() private { 118 | validRole[PRODUCT_OWNER_ROLE] = true; 119 | validRole[ORACLE_PROVIDER_ROLE] = true; 120 | validRole[RISKPOOL_KEEPER_ROLE] = true; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/test_instance_service.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import brownie 3 | import pytest 4 | 5 | from web3 import Web3 6 | 7 | from brownie import ( 8 | InstanceService, 9 | interface 10 | ) 11 | 12 | from scripts.const import ( 13 | COMPONENT_OWNER_SERVICE_NAME, 14 | INSTANCE_OPERATOR_SERVICE_NAME, 15 | INSTANCE_SERVICE_NAME, 16 | ORACLE_SERVICE_NAME, 17 | PRODUCT_SERVICE_NAME, 18 | RISKPOOL_SERVICE_NAME, 19 | ZERO_ADDRESS 20 | ) 21 | 22 | from scripts.util import ( 23 | contractFromAddress, 24 | s2b32, 25 | ) 26 | 27 | # enforce function isolation for tests below 28 | @pytest.fixture(autouse=True) 29 | def isolation(fn_isolation): 30 | pass 31 | 32 | 33 | def test_get_chain_id(instance): 34 | w3 = Web3() 35 | instanceService = instance.getInstanceService() 36 | assert instanceService.getChainId() == w3.eth.chain_id 37 | 38 | 39 | def test_get_instance_id(instance): 40 | instanceService = instance.getInstanceService() 41 | registryAddress = instanceService.getRegistry() 42 | 43 | gifInstanceId = instanceService.getInstanceId() 44 | web3keccak = Web3.solidityKeccak( 45 | ['uint256', 'address'], 46 | [instanceService.getChainId(), registryAddress]).hex() 47 | 48 | assert gifInstanceId == web3keccak 49 | 50 | 51 | def test_services_against_registry(instance, owner): 52 | instanceService = instance.getInstanceService() 53 | registryAddress = instanceService.getRegistry() 54 | registry = contractFromAddress(interface.IRegistry, registryAddress) 55 | isAddressFromRegistry = _addressFrom(registry, INSTANCE_SERVICE_NAME) 56 | 57 | assert instanceService.address == isAddressFromRegistry 58 | assert instanceService.address != 0x0 59 | 60 | psAddress = _addressFrom(registry, PRODUCT_SERVICE_NAME) 61 | assert psAddress != ZERO_ADDRESS 62 | assert instanceService.getProductService() == psAddress 63 | 64 | osAddress = _addressFrom(registry, ORACLE_SERVICE_NAME) 65 | assert osAddress != ZERO_ADDRESS 66 | assert instanceService.getOracleService() == osAddress 67 | 68 | rsAddress = _addressFrom(registry, RISKPOOL_SERVICE_NAME) 69 | assert rsAddress != ZERO_ADDRESS 70 | assert instanceService.getRiskpoolService() == rsAddress 71 | 72 | cosAddress = _addressFrom(registry, COMPONENT_OWNER_SERVICE_NAME) 73 | assert cosAddress != ZERO_ADDRESS 74 | assert instanceService.getComponentOwnerService() == cosAddress 75 | 76 | iosAddress = _addressFrom(registry, INSTANCE_OPERATOR_SERVICE_NAME) 77 | assert iosAddress != ZERO_ADDRESS 78 | assert instanceService.getInstanceOperatorService() == iosAddress 79 | 80 | def test_component_access(instance, owner, gifTestProduct): 81 | instanceService = instance.getInstanceService() 82 | registryAddress = instanceService.getRegistry() 83 | 84 | product = gifTestProduct.getContract() 85 | oracle = gifTestProduct.getOracle().getContract() 86 | riskpool = gifTestProduct.getRiskpool().getContract() 87 | 88 | assert registryAddress == product.getRegistry() 89 | assert registryAddress == oracle.getRegistry() 90 | assert registryAddress == riskpool.getRegistry() 91 | 92 | assert instanceService.getComponentId(product.address) == product.getId() 93 | assert instanceService.getComponentId(oracle.address) == oracle.getId() 94 | assert instanceService.getComponentId(riskpool.address) == riskpool.getId() 95 | 96 | assert instanceService.getComponentType(product.getId()) == product.getType() 97 | assert instanceService.getComponentType(oracle.getId()) == oracle.getType() 98 | assert instanceService.getComponentType(riskpool.getId()) == riskpool.getType() 99 | 100 | assert instanceService.getComponentState(product.getId()) == product.getState() 101 | assert instanceService.getComponentState(oracle.getId()) == oracle.getState() 102 | assert instanceService.getComponentState(riskpool.getId()) == riskpool.getState() 103 | 104 | pfis = contractFromAddress( 105 | interface.IComponent, 106 | instanceService.getComponent( 107 | product.getId())) 108 | 109 | assert pfis.getId() == product.getId() 110 | assert pfis.getName() == product.getName() 111 | assert pfis.getType() == product.getType() 112 | assert pfis.getState() == product.getState() 113 | 114 | def _addressFrom(registry, contractName): 115 | return registry.getContract(s2b32(contractName)) 116 | -------------------------------------------------------------------------------- /tests/test_registry.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from pprint import pp 3 | import brownie 4 | import pytest 5 | 6 | from brownie import ( 7 | AccessController, 8 | InstanceOperatorService, 9 | TestCoin 10 | ) 11 | 12 | from scripts.const import ( 13 | GIF_RELEASE, 14 | INSTANCE_OPERATOR_SERVICE_NAME, 15 | REGISTRY_CONTROLLER_NAME, 16 | REGISTRY_NAME, 17 | ZERO_ADDRESS, 18 | ) 19 | 20 | from scripts.util import ( 21 | b322s, 22 | s2b32, 23 | deployGifModuleV2, 24 | contract_from_address, 25 | ) 26 | 27 | # enforce function isolation for tests below 28 | @pytest.fixture(autouse=True) 29 | def isolation(fn_isolation): 30 | pass 31 | 32 | def test_registry_release(registry, owner): 33 | assert GIF_RELEASE == b322s(registry.getRelease({'from': owner})) 34 | 35 | def test_registry_release_any_account(registry, accounts): 36 | assert GIF_RELEASE == b322s(registry.getRelease({'from': accounts[0]})) 37 | 38 | def test_registry_controller(registry, registryController, owner, accounts): 39 | release = registry.getRelease({'from': owner}) 40 | controllerAddress = registry.getContract(s2b32(REGISTRY_CONTROLLER_NAME)) 41 | registryAddress = registry.getContract(s2b32(REGISTRY_NAME)) 42 | 43 | assert registryController.address == controllerAddress 44 | assert registry.address == registryAddress 45 | 46 | 47 | def test_registry_max_coponents(registry, owner): 48 | assert GIF_RELEASE == b322s(registry.getRelease({'from': owner})) 49 | # assert registry has three contracts (instance operator service, registry, registry proxy) 50 | assert 3 == registry.contracts() 51 | 52 | expectedNames = [] 53 | 54 | # register another 97 contracts 55 | for i in range(97): 56 | name = "TestCoin%s" % i 57 | tx = TestCoin.deploy({'from': owner}) 58 | tx = registry.register(s2b32(name), tx, {'from': owner}) 59 | expectedNames.append(name) 60 | 61 | assert 100 == registry.contracts() 62 | 63 | namesFromRegistry = get_contract_names(registry) 64 | 65 | # ignore first three elements 66 | for i in range(len(namesFromRegistry[3:])): 67 | assert b322s(namesFromRegistry[i + 3]) == expectedNames[i] 68 | 69 | # ensure that contract #101 cannot be registered 70 | with brownie.reverts("ERROR:REC-010:MAX_CONTRACTS_LIMIT"): 71 | tx = TestCoin.deploy({'from': owner}) 72 | registry.register(s2b32("OneTooMany"), tx, {'from': owner}) 73 | 74 | 75 | def test_registry_deregister(registry, owner): 76 | assert GIF_RELEASE == b322s(registry.getRelease({'from': owner})) 77 | 78 | name1 = s2b32("TestCoin1") 79 | name2 = s2b32("TestCoin2") 80 | name3 = s2b32("TestCoin3") 81 | 82 | tx1 = TestCoin.deploy({'from': owner}) 83 | tx = registry.register(name1, tx1, {'from': owner}) 84 | 85 | tx2 = TestCoin.deploy({'from': owner}) 86 | tx = registry.register(name2, tx2, {'from': owner}) 87 | 88 | assert tx1 == registry.getContract(name1) 89 | assert tx2 == registry.getContract(name2) 90 | 91 | with brownie.reverts("ERROR:REC-020:CONTRACT_UNKNOWN"): 92 | tx = registry.deregister(name3, {'from': owner}) 93 | 94 | assert tx1 == registry.getContract(name1) 95 | assert tx2 == registry.getContract(name2) 96 | 97 | tx = registry.deregister(name1, {'from': owner}) 98 | print(tx.info()) 99 | 100 | assert ZERO_ADDRESS == registry.getContract(name1) 101 | assert tx2 == registry.getContract(name2) 102 | 103 | 104 | def test_register_edgecases(registry, owner): 105 | name1 = s2b32("TestCoin1") 106 | name2 = s2b32("TestCoin2") 107 | 108 | tx1 = TestCoin.deploy({'from': owner}) 109 | tx2 = TestCoin.deploy({'from': owner}) 110 | 111 | with brownie.reverts("ERROR:REC-011:RELEASE_UNKNOWN"): 112 | registry.registerInRelease(s2b32("unknown release"), name1, tx1, {'from': owner}) 113 | 114 | with brownie.reverts("ERROR:REC-012:CONTRACT_NAME_EMPTY"): 115 | registry.register("", tx1, {'from': owner}) 116 | 117 | registry.register(name1, tx1, {'from': owner}) 118 | with brownie.reverts("ERROR:REC-013:CONTRACT_NAME_EXISTS"): 119 | registry.register(name1, tx2, {'from': owner}) 120 | 121 | with brownie.reverts("ERROR:REC-014:CONTRACT_ADDRESS_ZERO"): 122 | registry.register(name2, ZERO_ADDRESS, {'from': owner}) 123 | 124 | 125 | 126 | def get_contract_names(registry): 127 | contract_names = [] 128 | for i in range(registry.contracts()): 129 | contract_names.append(registry.contractName(i)) 130 | return contract_names 131 | -------------------------------------------------------------------------------- /tests/test_riskpool_active_bundles.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie.network.account import Account 5 | 6 | from scripts.setup import ( 7 | fund_riskpool, 8 | ) 9 | 10 | from scripts.instance import ( 11 | GifInstance, 12 | ) 13 | 14 | from scripts.product import ( 15 | GifTestProduct, 16 | ) 17 | 18 | # enforce function isolation for tests below 19 | @pytest.fixture(autouse=True) 20 | def isolation(fn_isolation): 21 | pass 22 | 23 | 24 | def test_create_bundle_max_active( 25 | instance: GifInstance, 26 | testCoin, 27 | gifTestProduct: GifTestProduct, 28 | riskpoolKeeper: Account, 29 | owner: Account, 30 | riskpoolWallet: Account 31 | ): 32 | riskpool = gifTestProduct.getRiskpool().getContract() 33 | riskpoolId = riskpool.getId() 34 | 35 | instanceService = instance.getInstanceService() 36 | assert instanceService.getComponentState(riskpoolId) == 3 37 | 38 | initialFunding = 10000 39 | fund_riskpool(instance, owner, riskpoolWallet, riskpool, riskpoolKeeper, testCoin, initialFunding) 40 | 41 | testCoin.transfer(riskpoolKeeper, 10 * initialFunding, {'from': owner}) 42 | testCoin.approve(instance.getTreasury(), 10 * initialFunding, {'from': riskpoolKeeper}) 43 | 44 | assert riskpool.bundles() == 1 45 | assert riskpool.activeBundles() == 1 46 | assert instanceService.activeBundles(riskpoolId) == 1 47 | 48 | bundle1 = _getBundle(instance, riskpool, 0) 49 | 50 | # ensure creation of another bundle is not allowed (max active bundles is 1 by default) 51 | with brownie.reverts("ERROR:POL-043:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): 52 | riskpool.createBundle( 53 | bytes(0), 54 | initialFunding, 55 | {'from': riskpoolKeeper}) 56 | 57 | riskpool.closeBundle(bundle1[0], {'from': riskpoolKeeper}) 58 | 59 | assert riskpool.bundles() == 1 60 | assert riskpool.activeBundles() == 0 61 | assert instanceService.activeBundles(riskpoolId) == 0 62 | 63 | riskpool.createBundle( 64 | bytes(0), 65 | initialFunding, 66 | {'from': riskpoolKeeper}) 67 | 68 | assert riskpool.bundles() == 2 69 | assert riskpool.activeBundles() == 1 70 | assert instanceService.activeBundles(riskpoolId) == 1 71 | 72 | # ensure a seconds bundle can be added when setting max active bundles to 2 73 | riskpool.setMaximumNumberOfActiveBundles(2, {'from': riskpoolKeeper}) 74 | riskpool.createBundle( 75 | bytes(0), 76 | initialFunding, 77 | {'from': riskpoolKeeper}) 78 | 79 | assert riskpool.bundles() == 3 80 | assert riskpool.activeBundles() == 2 81 | assert instanceService.activeBundles(riskpoolId) == 2 82 | 83 | bundle2 = _getBundle(instance, riskpool, 1) 84 | bundle3 = _getBundle(instance, riskpool, 2) 85 | 86 | with brownie.reverts("ERROR:POL-043:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): 87 | riskpool.createBundle( 88 | bytes(0), 89 | initialFunding, 90 | {'from': riskpoolKeeper}) 91 | 92 | # ensure another bundle can be created only after locking one bundle 93 | riskpool.lockBundle(bundle2[0], {'from': riskpoolKeeper}) 94 | 95 | assert riskpool.bundles() == 3 96 | assert riskpool.activeBundles() == 1 97 | assert instanceService.activeBundles(riskpoolId) == 1 98 | 99 | riskpool.createBundle( 100 | bytes(0), 101 | initialFunding, 102 | {'from': riskpoolKeeper}) 103 | 104 | assert riskpool.bundles() == 4 105 | assert riskpool.activeBundles() == 2 106 | assert instanceService.activeBundles(riskpoolId) == 2 107 | 108 | # ensure locked bundle cannot be unlocked while max active bundles are in use 109 | with brownie.reverts("ERROR:POL-043:MAXIMUM_NUMBER_OF_ACTIVE_BUNDLES_REACHED"): 110 | riskpool.unlockBundle(bundle2[0], {'from': riskpoolKeeper}) 111 | 112 | riskpool.closeBundle(bundle3[0], {'from': riskpoolKeeper}) 113 | 114 | assert riskpool.bundles() == 4 115 | assert riskpool.activeBundles() == 1 116 | assert instanceService.activeBundles(riskpoolId) == 1 117 | 118 | # ensure bundles can be created after closing one more bundle 119 | riskpool.createBundle( 120 | bytes(0), 121 | initialFunding, 122 | {'from': riskpoolKeeper}) 123 | 124 | assert riskpool.bundles() == 5 125 | assert riskpool.activeBundles() == 2 126 | assert instanceService.activeBundles(riskpoolId) == 2 127 | 128 | 129 | def _getBundle(instance, riskpool, bundleIdx): 130 | instanceService = instance.getInstanceService() 131 | bundleId = riskpool.getBundleId(bundleIdx) 132 | return instanceService.getBundle(bundleId) 133 | -------------------------------------------------------------------------------- /contracts/examples/AyiiOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.2; 3 | 4 | import "./strings.sol"; 5 | 6 | import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; 7 | import "@etherisc/gif-interface/contracts/components/Oracle.sol"; 8 | 9 | contract AyiiOracle is 10 | Oracle, ChainlinkClient 11 | { 12 | using strings for bytes32; 13 | using Chainlink for Chainlink.Request; 14 | 15 | mapping(bytes32 /* Chainlink request ID */ => uint256 /* GIF request ID */) public gifRequests; 16 | bytes32 public jobId; 17 | uint256 public payment; 18 | 19 | event LogAyiiRequest(uint256 requestId, bytes32 chainlinkRequestId); 20 | 21 | event LogAyiiFulfill( 22 | uint256 requestId, 23 | bytes32 chainlinkRequestId, 24 | bytes32 projectId, 25 | bytes32 uaiId, 26 | bytes32 cropId, 27 | uint256 aaay 28 | ); 29 | 30 | constructor( 31 | bytes32 _name, 32 | address _registry, 33 | address _chainLinkToken, 34 | address _chainLinkOperator, 35 | bytes32 _jobId, 36 | uint256 _payment 37 | ) 38 | Oracle(_name, _registry) 39 | { 40 | updateRequestDetails( 41 | _chainLinkToken, 42 | _chainLinkOperator, 43 | _jobId, 44 | _payment); 45 | } 46 | 47 | function updateRequestDetails( 48 | address _chainLinkToken, 49 | address _chainLinkOperator, 50 | bytes32 _jobId, 51 | uint256 _payment 52 | ) 53 | public 54 | onlyOwner 55 | { 56 | if (_chainLinkToken != address(0)) { setChainlinkToken(_chainLinkToken); } 57 | if (_chainLinkOperator != address(0)) { setChainlinkOracle(_chainLinkOperator); } 58 | 59 | jobId = _jobId; 60 | payment = _payment; 61 | } 62 | 63 | function request(uint256 gifRequestId, bytes calldata input) 64 | external override 65 | onlyQuery 66 | { 67 | Chainlink.Request memory request_ = buildChainlinkRequest( 68 | jobId, 69 | address(this), 70 | this.fulfill.selector 71 | ); 72 | 73 | ( 74 | bytes32 projectId, 75 | bytes32 uaiId, 76 | bytes32 cropId 77 | ) = abi.decode(input, (bytes32, bytes32, bytes32)); 78 | 79 | request_.add("projectId", projectId.toB32String()); 80 | request_.add("uaiId", uaiId.toB32String()); 81 | request_.add("cropId", cropId.toB32String()); 82 | 83 | bytes32 chainlinkRequestId = sendChainlinkRequest(request_, payment); 84 | 85 | gifRequests[chainlinkRequestId] = gifRequestId; 86 | emit LogAyiiRequest(gifRequestId, chainlinkRequestId); 87 | } 88 | 89 | function fulfill( 90 | bytes32 chainlinkRequestId, 91 | bytes32 projectId, 92 | bytes32 uaiId, 93 | bytes32 cropId, 94 | uint256 aaay 95 | ) 96 | public recordChainlinkFulfillment(chainlinkRequestId) 97 | { 98 | uint256 gifRequest = gifRequests[chainlinkRequestId]; 99 | bytes memory data = abi.encode(projectId, uaiId, cropId, aaay); 100 | _respond(gifRequest, data); 101 | 102 | delete gifRequests[chainlinkRequestId]; 103 | emit LogAyiiFulfill(gifRequest, chainlinkRequestId, projectId, uaiId, cropId, aaay); 104 | } 105 | 106 | function cancel(uint256 requestId) 107 | external override 108 | onlyOwner 109 | { 110 | // TODO mid/low priority 111 | // cancelChainlinkRequest(_requestId, _payment, _callbackFunctionId, _expiration); 112 | } 113 | 114 | // only used for testing of chainlink operator 115 | function encodeFulfillParameters( 116 | bytes32 chainlinkRequestId, 117 | bytes32 projectId, 118 | bytes32 uaiId, 119 | bytes32 cropId, 120 | uint256 aaay 121 | ) 122 | external 123 | pure 124 | returns(bytes memory parameterData) 125 | { 126 | return abi.encode( 127 | chainlinkRequestId, 128 | projectId, 129 | uaiId, 130 | cropId, 131 | aaay 132 | ); 133 | } 134 | 135 | function getChainlinkJobId() external view returns(bytes32 chainlinkJobId) { 136 | return jobId; 137 | } 138 | 139 | function getChainlinkPayment() external view returns(uint256 paymentAmount) { 140 | return payment; 141 | } 142 | 143 | function getChainlinkToken() external view returns(address linkTokenAddress) { 144 | return chainlinkTokenAddress(); 145 | } 146 | 147 | function getChainlinkOperator() external view returns(address operator) { 148 | return chainlinkOracleAddress(); 149 | } 150 | } 151 | 152 | -------------------------------------------------------------------------------- /contracts/examples/strings.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache2 2 | 3 | // source: https://github.com/Arachnid/solidity-stringutils 4 | /* 5 | * @title String & slice utility library for Solidity contracts. 6 | * @author Nick Johnson 7 | * 8 | * @dev Functionality in this library is largely implemented using an 9 | * abstraction called a 'slice'. A slice represents a part of a string - 10 | * anything from the entire string to a single character, or even no 11 | * characters at all (a 0-length slice). Since a slice only has to specify 12 | * an offset and a length, copying and manipulating slices is a lot less 13 | * expensive than copying and manipulating the strings they reference. 14 | * 15 | * To further reduce gas costs, most functions on slice that need to return 16 | * a slice modify the original one instead of allocating a new one; for 17 | * instance, `s.split(".")` will return the text up to the first '.', 18 | * modifying s to only contain the remainder of the string after the '.'. 19 | * In situations where you do not want to modify the original slice, you 20 | * can make a copy first with `.copy()`, for example: 21 | * `s.copy().split(".")`. Try and avoid using this idiom in loops; since 22 | * Solidity has no memory management, it will result in allocating many 23 | * short-lived slices that are later discarded. 24 | * 25 | * Functions that return two slices come in two versions: a non-allocating 26 | * version that takes the second slice as an argument, modifying it in 27 | * place, and an allocating version that allocates and returns the second 28 | * slice; see `nextRune` for example. 29 | * 30 | * Functions that have to copy string data will return strings rather than 31 | * slices; these can be cast back to slices for further processing if 32 | * required. 33 | * 34 | * For convenience, some functions are provided with non-modifying 35 | * variants that create a new slice and return both; for instance, 36 | * `s.splitNew('.')` leaves s unmodified, and returns two values 37 | * corresponding to the left and right parts of the string. 38 | */ 39 | pragma solidity 0.8.2; 40 | 41 | library strings { 42 | 43 | struct slice { 44 | uint _len; 45 | uint _ptr; 46 | } 47 | 48 | function memcpy(uint dest, uint src, uint len_) private pure { 49 | // Copy word-length chunks while possible 50 | for(; len_ >= 32; len_ -= 32) { 51 | assembly { 52 | mstore(dest, mload(src)) 53 | } 54 | dest += 32; 55 | src += 32; 56 | } 57 | 58 | // Copy remaining bytes 59 | uint mask = type(uint).max; 60 | if (len_ > 0) { 61 | mask = 256 ** (32 - len_) - 1; 62 | } 63 | assembly { 64 | let srcpart := and(mload(src), not(mask)) 65 | let destpart := and(mload(dest), mask) 66 | mstore(dest, or(destpart, srcpart)) 67 | } 68 | } 69 | 70 | /* 71 | * @dev Returns the length of a null-terminated bytes32 string. 72 | * @param self The value to find the length of. 73 | * @return The length of the string, from 0 to 32. 74 | */ 75 | function len(bytes32 self) internal pure returns (uint) { 76 | uint ret; 77 | if (self == 0) 78 | return 0; 79 | if (uint(self) & type(uint128).max == 0) { 80 | ret += 16; 81 | self = bytes32(uint(self) / 0x100000000000000000000000000000000); 82 | } 83 | if (uint(self) & type(uint64).max == 0) { 84 | ret += 8; 85 | self = bytes32(uint(self) / 0x10000000000000000); 86 | } 87 | if (uint(self) & type(uint32).max == 0) { 88 | ret += 4; 89 | self = bytes32(uint(self) / 0x100000000); 90 | } 91 | if (uint(self) & type(uint16).max == 0) { 92 | ret += 2; 93 | self = bytes32(uint(self) / 0x10000); 94 | } 95 | if (uint(self) & type(uint8).max == 0) { 96 | ret += 1; 97 | } 98 | return 32 - ret; 99 | } 100 | 101 | // merge of toSliceB32 and toString of strings library 102 | function toB32String(bytes32 self) internal pure returns (string memory) { 103 | slice memory slc; 104 | assembly { 105 | let ptr := mload(0x40) 106 | mstore(0x40, add(ptr, 0x20)) 107 | mstore(ptr, self) 108 | mstore(add(slc, 0x20), ptr) 109 | } 110 | slc._len = len(self); 111 | 112 | string memory ret = new string(slc._len); 113 | uint retptr; 114 | assembly { retptr := add(ret, 32) } 115 | memcpy(retptr, slc._ptr, slc._len); 116 | return ret; 117 | } 118 | } -------------------------------------------------------------------------------- /tests/test_registry_upgrade.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import brownie 3 | import pytest 4 | 5 | from scripts.const import ( 6 | GIF_RELEASE, 7 | REGISTRY_CONTROLLER_NAME, 8 | REGISTRY_NAME, 9 | ZERO_ADDRESS, 10 | COMPROMISED_ADDRESS, 11 | ) 12 | 13 | from scripts.util import ( 14 | b322s, 15 | s2b32, 16 | encode_function_data, 17 | contractFromAddress, 18 | ) 19 | 20 | # enforce function isolation for tests below 21 | @pytest.fixture(autouse=True) 22 | def isolation(fn_isolation): 23 | pass 24 | 25 | def test_registry_release(registry, owner): 26 | assert GIF_RELEASE == b322s(registry.getRelease()) 27 | 28 | def test_registry_update(registry, registryController, registryControllerV2Test, owner): 29 | 30 | # verify implementations 31 | proxy = contractFromAddress(brownie.CoreProxy, registry.address) 32 | controllerAddress = proxy.implementation() 33 | 34 | assert registryController.address == controllerAddress 35 | assert registryController.address != registryControllerV2Test.address 36 | 37 | # prepare registry upgrade 38 | newMessage = "hey" 39 | encodedInitializer = encode_function_data( 40 | newMessage, 41 | initializer=registryControllerV2Test.upgradeToV2) 42 | 43 | # upgrade registry 44 | proxy.upgradeToAndCall( 45 | registryControllerV2Test.address, 46 | encodedInitializer, 47 | {'from': owner}) 48 | 49 | # verify updated implementations 50 | assert registryControllerV2Test.address == proxy.implementation() 51 | 52 | # verify existing and new functionality 53 | registryV2 = contractFromAddress(brownie.TestRegistryControllerUpdated, registry.address) 54 | assert GIF_RELEASE == b322s(registryV2.getRelease()) 55 | assert newMessage == registryV2.getMessage() 56 | 57 | # ensure calling upgrade a second time fails 58 | with brownie.reverts(): 59 | proxy.upgradeToAndCall( 60 | registryControllerV2Test.address, 61 | encodedInitializer, 62 | {'from': owner}) 63 | 64 | 65 | # test upgrade with well behaved upgraded module that inherits from CoreController 66 | def test_upgrade_by_anybody_fails(registry, registryController, registryControllerV2Test, customer): 67 | 68 | # verify implementations 69 | proxy = contractFromAddress(brownie.CoreProxy, registry.address) 70 | controllerAddress = proxy.implementation() 71 | 72 | assert registryController.address == controllerAddress 73 | assert registryController.address != registryControllerV2Test.address 74 | 75 | # prepare registry upgrade 76 | newMessage = "hey (by customer)" 77 | encodedInitializer = encode_function_data( 78 | newMessage, 79 | initializer=registryControllerV2Test.upgradeToV2) 80 | 81 | # verify upgrade cannot be done by non instance operator accounts 82 | # assumption: well behaved upgraded module that inherits from CoreController 83 | with brownie.reverts("ERROR:CRP-001:NOT_ADMIN"): 84 | proxy.upgradeToAndCall( 85 | registryControllerV2Test.address, 86 | encodedInitializer, 87 | {'from': customer}) 88 | 89 | 90 | # test upgrade with compromised module that does not inherit from CoreController 91 | def test_compromised_upgrade_by_anybody_fails(instance, registryCompromisedControllerV2Test, customer): 92 | 93 | registry = instance.getRegistry() 94 | 95 | # verify implementations 96 | proxy = contractFromAddress(brownie.CoreProxy, registry.address) 97 | controllerAddress = proxy.implementation() 98 | 99 | # prepare registry upgrade 100 | queryOriginalQueryAddress = registry.getContract(s2b32("Query")) 101 | queryOriginalPolicyAddress = registry.getContract(s2b32("Policy")) 102 | 103 | assert COMPROMISED_ADDRESS != queryOriginalPolicyAddress 104 | 105 | encodedInitializer = encode_function_data( 106 | COMPROMISED_ADDRESS, 107 | queryOriginalQueryAddress, 108 | initializer=registryCompromisedControllerV2Test.upgradeToV2) 109 | 110 | # upgrade registry, ensure customer may not upgrade contract 111 | with brownie.reverts("ERROR:CRP-001:NOT_ADMIN"): 112 | proxy.upgradeToAndCall( 113 | registryCompromisedControllerV2Test.address, 114 | encodedInitializer, 115 | {'from': customer}) 116 | 117 | # verify updated implementations 118 | assert registryCompromisedControllerV2Test.address == proxy.implementation() 119 | 120 | # verify upgraded registry retuns compromised policy address 121 | registryV2 = contractFromAddress(brownie.TestRegistryCompromisedController, registry.address) 122 | assert registryV2.getContract(s2b32("Query")) == queryOriginalQueryAddress 123 | assert registryV2.getContract(s2b32("Policy")) == COMPROMISED_ADDRESS 124 | assert registryV2.getContract(s2b32("Pool")) == ZERO_ADDRESS 125 | 126 | # if we landed here this means that compromised upgrade was successful 127 | assert False 128 | -------------------------------------------------------------------------------- /scripts/util.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | 3 | # pylint: disable-msg=E0611 4 | from brownie import ( 5 | Contract, 6 | CoreProxy, 7 | ) 8 | 9 | from brownie.convert import to_bytes 10 | from brownie.network import accounts 11 | from brownie.network.account import Account 12 | 13 | def s2h(text: str) -> str: 14 | return Web3.toHex(text.encode('ascii')) 15 | 16 | def h2s(hex: str) -> str: 17 | return Web3.toText(hex).split('\x00')[-1] 18 | 19 | def h2sLeft(hex: str) -> str: 20 | return Web3.toText(hex).split('\x00')[0] 21 | 22 | def s2b32(text: str): 23 | return '{:0<66}'.format(Web3.toHex(text.encode('ascii')))[:66] 24 | 25 | def b322s(b32: bytes): 26 | return b32.decode().split('\x00')[0] 27 | 28 | def s2b(text:str): 29 | return s2b32(text) 30 | 31 | def b2s(b32: bytes): 32 | return b322s(b32) 33 | 34 | def keccak256(text:str): 35 | return Web3.solidityKeccak(['string'], [text]).hex() 36 | 37 | def get_account(mnemonic: str, account_offset: int) -> Account: 38 | return accounts.from_mnemonic( 39 | mnemonic, 40 | count=1, 41 | offset=account_offset) 42 | 43 | # source: https://github.com/brownie-mix/upgrades-mix/blob/main/scripts/helpful_scripts.py 44 | def encode_function_data(*args, initializer=None): 45 | """Encodes the function call so we can work with an initializer. 46 | Args: 47 | initializer ([brownie.network.contract.ContractTx], optional): 48 | The initializer function we want to call. Example: `box.store`. 49 | Defaults to None. 50 | args (Any, optional): 51 | The arguments to pass to the initializer function 52 | Returns: 53 | [bytes]: Return the encoded bytes. 54 | """ 55 | if not len(args): args = b'' 56 | 57 | if initializer: return initializer.encode_input(*args) 58 | 59 | return b'' 60 | 61 | # generic upgradable gif module deployment 62 | def deployGifModule( 63 | controllerClass, 64 | storageClass, 65 | registry, 66 | owner, 67 | publishSource 68 | ): 69 | controller = controllerClass.deploy( 70 | registry.address, 71 | {'from': owner}, 72 | publish_source=publishSource) 73 | 74 | storage = storageClass.deploy( 75 | registry.address, 76 | {'from': owner}, 77 | publish_source=publishSource) 78 | 79 | controller.assignStorage(storage.address, {'from': owner}) 80 | storage.assignController(controller.address, {'from': owner}) 81 | 82 | registry.register(controller.NAME.call(), controller.address, {'from': owner}) 83 | registry.register(storage.NAME.call(), storage.address, {'from': owner}) 84 | 85 | return contractFromAddress(controllerClass, storage.address) 86 | 87 | # gif token deployment 88 | def deployGifToken( 89 | tokenName, 90 | tokenClass, 91 | registry, 92 | owner, 93 | publishSource 94 | ): 95 | print('token {} deploy'.format(tokenName)) 96 | token = tokenClass.deploy( 97 | {'from': owner}, 98 | publish_source=publishSource) 99 | 100 | tokenNameB32 = s2b32(tokenName) 101 | print('token {} register'.format(tokenName)) 102 | registry.register(tokenNameB32, token.address, {'from': owner}) 103 | 104 | return token 105 | 106 | 107 | # generic open zeppelin upgradable gif module deployment 108 | def deployGifModuleV2( 109 | moduleName, 110 | controllerClass, 111 | registry, 112 | owner, 113 | publishSource 114 | ): 115 | print('module {} deploy controller'.format(moduleName)) 116 | controller = controllerClass.deploy( 117 | {'from': owner}, 118 | publish_source=publishSource) 119 | 120 | encoded_initializer = encode_function_data( 121 | registry.address, 122 | initializer=controller.initialize) 123 | 124 | print('module {} deploy proxy'.format(moduleName)) 125 | proxy = CoreProxy.deploy( 126 | controller.address, 127 | encoded_initializer, 128 | {'from': owner}, 129 | publish_source=publishSource) 130 | 131 | moduleNameB32 = s2b32(moduleName) 132 | controllerNameB32 = s2b32('{}Controller'.format(moduleName)[:32]) 133 | 134 | print('module {} ({}) register controller'.format(moduleName, controllerNameB32)) 135 | registry.register(controllerNameB32, controller.address, {'from': owner}) 136 | print('module {} ({}) register proxy'.format(moduleName, moduleNameB32)) 137 | registry.register(moduleNameB32, proxy.address, {'from': owner}) 138 | 139 | return contractFromAddress(controllerClass, proxy.address) 140 | 141 | 142 | # generic upgradable gif service deployment 143 | def deployGifService( 144 | serviceClass, 145 | registry, 146 | owner, 147 | publishSource 148 | ): 149 | service = serviceClass.deploy( 150 | registry.address, 151 | {'from': owner}, 152 | publish_source=publishSource) 153 | 154 | registry.register(service.NAME.call(), service.address, {'from': owner}) 155 | 156 | return service 157 | 158 | def deployGifServiceV2( 159 | serviceName, 160 | serviceClass, 161 | registry, 162 | owner, 163 | publishSource 164 | ): 165 | service = serviceClass.deploy( 166 | registry.address, 167 | {'from': owner}, 168 | publish_source=publishSource) 169 | 170 | registry.register(s2b32(serviceName), service.address, {'from': owner}) 171 | 172 | return service 173 | 174 | def contractFromAddress(contractClass, contractAddress): 175 | return contract_from_address(contractClass, contractAddress) 176 | 177 | def contract_from_address(contractClass, contractAddress): 178 | return Contract.from_abi(contractClass._name, contractAddress, contractClass.abi) 179 | -------------------------------------------------------------------------------- /tests/test_ayii_product_underwrite.py: -------------------------------------------------------------------------------- 1 | from re import A 2 | import brownie 3 | import pytest 4 | 5 | from brownie.network.account import Account 6 | 7 | from brownie import ( 8 | interface, 9 | AyiiProduct, 10 | BundleToken 11 | ) 12 | 13 | from scripts.ayii_product import ( 14 | GifAyiiProduct 15 | ) 16 | 17 | from scripts.setup import ( 18 | fund_riskpool, 19 | fund_customer, 20 | ) 21 | 22 | from scripts.instance import GifInstance 23 | from scripts.util import s2b32, contractFromAddress 24 | 25 | # enforce function isolation for tests below 26 | @pytest.fixture(autouse=True) 27 | def isolation(fn_isolation): 28 | pass 29 | 30 | 31 | # underwrite the policy after the apply_for_policy has failed due to low riskpool balance 32 | def test_underwrite_after_apply_with_riskpool_empty( 33 | instance: GifInstance, 34 | instanceOperator, 35 | gifAyiiProduct: GifAyiiProduct, 36 | riskpoolWallet, 37 | riskpoolKeeper: Account, 38 | investor, 39 | insurer, 40 | customer, 41 | ): 42 | instanceService = instance.getInstanceService() 43 | 44 | product = gifAyiiProduct.getContract() 45 | oracle = gifAyiiProduct.getOracle().getContract() 46 | riskpool = gifAyiiProduct.getRiskpool().getContract() 47 | 48 | clOperator = gifAyiiProduct.getOracle().getClOperator() 49 | 50 | print('--- test setup underfunded riskpool --------------------------') 51 | 52 | token = gifAyiiProduct.getToken() 53 | assert token.balanceOf(riskpoolWallet) == 0 54 | 55 | 56 | riskpoolBalanceBeforeFunding = token.balanceOf(riskpoolWallet) 57 | assert 0 == riskpoolBalanceBeforeFunding 58 | 59 | riskId = prepare_risk(product, insurer) 60 | 61 | premium = 300 62 | sumInsured = 2000 63 | 64 | # ensure the riskpool is funded, but too low for insurance 65 | riskpoolFunding = 1000 66 | fund_riskpool( 67 | instance, 68 | instanceOperator, 69 | riskpoolWallet, 70 | riskpool, 71 | investor, 72 | token, 73 | riskpoolFunding) 74 | riskpoolBalanceAfterFunding = token.balanceOf(riskpoolWallet) 75 | assert riskpoolBalanceAfterFunding > 0 76 | 77 | print('--- test setup customer --------------------------') 78 | 79 | customerFunding = 5000 80 | fund_customer(instance, instanceOperator, customer, token, customerFunding) 81 | 82 | print('--- apply for policy on underfunded riskpool --------------------------') 83 | # ensure application works for policy with underfunded riskpool 84 | tx = product.applyForPolicy(customer, premium, sumInsured, riskId, {'from': insurer}) 85 | process_id = tx.return_value 86 | events = tx.events 87 | print(events) 88 | 89 | assert 'LogAyiiPolicyApplicationCreated' in events 90 | assert 'LogRiskpoolCollateralizationFailed' in events 91 | 92 | assert 'LogAyiiPolicyCreated' not in events 93 | 94 | # ensure application exists and has state Applied 95 | application = instanceService.getApplication(process_id) 96 | assert 0 == application[0] # ApplicationState.Applied 97 | 98 | assert 1 == product.applications() 99 | assert 0 == product.policies(riskId) 100 | 101 | assert process_id == product.getApplicationId(0) 102 | 103 | # ensure that explicity underwriting still fails 104 | tx = product.underwrite(process_id, {'from': insurer}) 105 | assert False == tx.return_value 106 | 107 | events = tx.events 108 | print(events) 109 | assert 'LogRiskpoolCollateralizationFailed' in events 110 | 111 | print('--- fully fund riskpool --------------------------') 112 | # ensure the riskpool is fully funded 113 | riskpool.setMaximumNumberOfActiveBundles(2, {'from': riskpoolKeeper}) 114 | riskpoolFunding = 20000 115 | fund_riskpool( 116 | instance, 117 | instanceOperator, 118 | riskpoolWallet, 119 | riskpool, 120 | investor, 121 | token, 122 | riskpoolFunding) 123 | 124 | # check riskpool funds and book keeping after funding 125 | riskpoolBalanceAfter2ndFunding = token.balanceOf(riskpoolWallet) 126 | assert riskpoolBalanceAfter2ndFunding > riskpoolBalanceAfterFunding 127 | assert riskpool.bundles() == 2 128 | 129 | print('--- underwrite application --------------------------') 130 | # now underwrite the policy as the riskpool is now funded 131 | tx = product.underwrite(process_id, {'from': insurer}) 132 | assert True == tx.return_value 133 | 134 | events = tx.events 135 | print(events) 136 | assert 'LogAyiiPolicyCreated' in events 137 | 138 | # ensure application exists and has state Applied 139 | application = instanceService.getApplication(process_id) 140 | assert 2 == application[0] # ApplicationState.Underwritten 141 | 142 | def test_underwrite_invalid_policy_id( 143 | gifAyiiProduct: GifAyiiProduct, 144 | insurer, 145 | ): 146 | product = gifAyiiProduct.getContract() 147 | 148 | with brownie.reverts("ERROR:POC-101:APPLICATION_DOES_NOT_EXIST"): 149 | tx = product.underwrite(s2b32('does_not_exist'), {'from': insurer}) 150 | 151 | 152 | 153 | def prepare_risk(product, insurer): 154 | print('--- test setup risks -------------------------------------') 155 | 156 | projectId = s2b32('2022.kenya.wfp.ayii') 157 | uaiId = [s2b32('1234'), s2b32('2345')] 158 | cropId = s2b32('mixed') 159 | 160 | triggerFloat = 0.75 161 | exitFloat = 0.1 162 | tsiFloat = 0.9 163 | aphFloat = [2.0, 1.8] 164 | 165 | multiplier = product.getPercentageMultiplier() 166 | trigger = multiplier * triggerFloat 167 | exit = multiplier * exitFloat 168 | tsi = multiplier * tsiFloat 169 | aph = [multiplier * aphFloat[0], multiplier * aphFloat[1]] 170 | 171 | tx = product.createRisk(projectId, uaiId[0], cropId, trigger, exit, tsi, aph[0], {'from': insurer}) 172 | return tx.return_value 173 | -------------------------------------------------------------------------------- /contracts/modules/QueryModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "./ComponentController.sol"; 5 | import "../shared/CoreController.sol"; 6 | 7 | import "@etherisc/gif-interface/contracts/components/IComponent.sol"; 8 | import "@etherisc/gif-interface/contracts/components/IOracle.sol"; 9 | import "@etherisc/gif-interface/contracts/modules/IQuery.sol"; 10 | import "@etherisc/gif-interface/contracts/services/IInstanceService.sol"; 11 | 12 | 13 | contract QueryModule is 14 | IQuery, 15 | CoreController 16 | { 17 | ComponentController private _component; 18 | OracleRequest[] private _oracleRequests; 19 | 20 | modifier onlyOracleService() { 21 | require( 22 | _msgSender() == _getContractAddress("OracleService"), 23 | "ERROR:CRC-001:NOT_ORACLE_SERVICE" 24 | ); 25 | _; 26 | } 27 | 28 | modifier onlyResponsibleOracle(uint256 requestId, address responder) { 29 | OracleRequest memory oracleRequest = _oracleRequests[requestId]; 30 | 31 | require( 32 | oracleRequest.createdAt > 0, 33 | "ERROR:QUC-002:REQUEST_ID_INVALID" 34 | ); 35 | 36 | uint256 oracleId = oracleRequest.responsibleOracleId; 37 | address oracleAddress = address(_getOracle(oracleId)); 38 | require( 39 | oracleAddress == responder, 40 | "ERROR:QUC-003:ORACLE_NOT_RESPONSIBLE" 41 | ); 42 | _; 43 | } 44 | 45 | function _afterInitialize() internal override onlyInitializing { 46 | _component = ComponentController(_getContractAddress("Component")); 47 | } 48 | 49 | /* Oracle Request */ 50 | // request only works for active oracles 51 | // function call _getOracle reverts if oracle is not active 52 | // as a result all request call on oracles that are not active will revert 53 | function request( 54 | bytes32 processId, 55 | bytes calldata input, 56 | string calldata callbackMethodName, 57 | address callbackContractAddress, 58 | uint256 responsibleOracleId 59 | ) 60 | external 61 | override 62 | onlyPolicyFlow("Query") 63 | returns (uint256 requestId) 64 | { 65 | uint256 componentId = _component.getComponentId(callbackContractAddress); 66 | require( 67 | _component.isProduct(componentId), 68 | "ERROR:QUC-010:CALLBACK_ADDRESS_IS_NOT_PRODUCT" 69 | ); 70 | 71 | requestId = _oracleRequests.length; 72 | _oracleRequests.push(); 73 | 74 | // TODO: get token from product 75 | 76 | OracleRequest storage req = _oracleRequests[requestId]; 77 | req.processId = processId; 78 | req.data = input; 79 | req.callbackMethodName = callbackMethodName; 80 | req.callbackContractAddress = callbackContractAddress; 81 | req.responsibleOracleId = responsibleOracleId; 82 | req.createdAt = block.timestamp; // solhint-disable-line 83 | 84 | _getOracle(responsibleOracleId).request( 85 | requestId, 86 | input 87 | ); 88 | 89 | emit LogOracleRequested(processId, requestId, responsibleOracleId); 90 | } 91 | 92 | /* Oracle Response */ 93 | // respond only works for active oracles 94 | // modifier onlyResponsibleOracle contains a function call to _getOracle 95 | // which reverts if oracle is not active 96 | // as a result, all response calls by oracles that are not active will revert 97 | function respond( 98 | uint256 requestId, 99 | address responder, 100 | bytes calldata data 101 | ) 102 | external override 103 | onlyOracleService 104 | onlyResponsibleOracle(requestId, responder) 105 | { 106 | OracleRequest storage req = _oracleRequests[requestId]; 107 | string memory functionSignature = string( 108 | abi.encodePacked( 109 | req.callbackMethodName, 110 | "(uint256,bytes32,bytes)" 111 | )); 112 | bytes32 processId = req.processId; 113 | 114 | (bool success, ) = 115 | req.callbackContractAddress.call( 116 | abi.encodeWithSignature( 117 | functionSignature, 118 | requestId, 119 | processId, 120 | data 121 | ) 122 | ); 123 | 124 | require(success, "ERROR:QUC-020:PRODUCT_CALLBACK_UNSUCCESSFUL"); 125 | delete _oracleRequests[requestId]; 126 | 127 | // TODO implement reward payment 128 | 129 | emit LogOracleResponded(processId, requestId, responder, success); 130 | } 131 | 132 | function cancel(uint256 requestId) 133 | external override 134 | onlyPolicyFlow("Query") 135 | { 136 | OracleRequest storage oracleRequest = _oracleRequests[requestId]; 137 | require(oracleRequest.createdAt > 0, "ERROR:QUC-030:REQUEST_ID_INVALID"); 138 | delete _oracleRequests[requestId]; 139 | emit LogOracleCanceled(requestId); 140 | } 141 | 142 | 143 | function getProcessId(uint256 requestId) 144 | external 145 | view 146 | returns(bytes32 processId) 147 | { 148 | OracleRequest memory oracleRequest = _oracleRequests[requestId]; 149 | require(oracleRequest.createdAt > 0, "ERROR:QUC-040:REQUEST_ID_INVALID"); 150 | return oracleRequest.processId; 151 | } 152 | 153 | 154 | function getOracleRequestCount() public view returns (uint256 _count) { 155 | return _oracleRequests.length; 156 | } 157 | 158 | function _getOracle(uint256 id) internal view returns (IOracle oracle) { 159 | IComponent cmp = _component.getComponent(id); 160 | oracle = IOracle(address(cmp)); 161 | 162 | require( 163 | _component.getComponentType(id) == IComponent.ComponentType.Oracle, 164 | "ERROR:QUC-041:COMPONENT_NOT_ORACLE" 165 | ); 166 | 167 | require( 168 | _component.getComponentState(id) == IComponent.ComponentState.Active, 169 | "ERROR:QUC-042:ORACLE_NOT_ACTIVE" 170 | ); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /tests/test_bundle_defund.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie.network.account import Account 5 | from brownie import ( 6 | interface, 7 | Wei, 8 | TestProduct, 9 | ) 10 | 11 | from scripts.util import ( 12 | s2h, 13 | s2b32, 14 | ) 15 | 16 | from scripts.setup import ( 17 | fund_riskpool, 18 | apply_for_policy, 19 | ) 20 | 21 | from scripts.instance import ( 22 | GifInstance, 23 | ) 24 | 25 | from scripts.product import ( 26 | GifTestProduct, 27 | GifTestRiskpool, 28 | ) 29 | 30 | # enforce function isolation for tests below 31 | @pytest.fixture(autouse=True) 32 | def isolation(fn_isolation): 33 | pass 34 | 35 | def test_fund_defund_simple( 36 | instance: GifInstance, 37 | testCoin, 38 | gifTestProduct: GifTestProduct, 39 | riskpoolKeeper: Account, 40 | owner: Account, 41 | customer: Account, 42 | feeOwner: Account, 43 | capitalOwner: Account 44 | ): 45 | riskpool = gifTestProduct.getRiskpool().getContract() 46 | initialFunding = 10000 47 | 48 | riskpoolWallet = capitalOwner 49 | investor = riskpoolKeeper 50 | fund_riskpool(instance, owner, riskpoolWallet, riskpool, investor, testCoin, initialFunding) 51 | 52 | assert riskpool.bundles() == 1 53 | bundle = _getBundle(instance, riskpool, 0) 54 | ( 55 | bundleId, 56 | riskpoolId, 57 | tokenId, 58 | state, 59 | filter, 60 | capital, 61 | lockedCapital, 62 | balance, 63 | createdAt, 64 | updatedAt 65 | ) = bundle 66 | 67 | print(bundle) 68 | 69 | riskpoolWalletBalanceBeforeDefunding = testCoin.balanceOf(riskpoolWallet) 70 | investorBalanceBeforeDefunding = testCoin.balanceOf(investor) 71 | bundleBalanceBeforeDefunding = balance 72 | 73 | assert capital == balance 74 | assert bundleBalanceBeforeDefunding == riskpoolWalletBalanceBeforeDefunding 75 | 76 | riskpool.defundBundle(bundleId, bundleBalanceBeforeDefunding, {'from':investor}) 77 | 78 | ( 79 | bundleId, 80 | riskpoolId, 81 | tokenId, 82 | state, 83 | filter, 84 | capital, 85 | lockedCapital, 86 | balance, 87 | createdAt, 88 | updatedAt 89 | ) = _getBundle(instance, riskpool, 0) 90 | 91 | assert capital == 0 92 | assert balance == 0 93 | 94 | assert testCoin.balanceOf(riskpoolWallet) == 0 95 | assert testCoin.balanceOf(investor) == investorBalanceBeforeDefunding + bundleBalanceBeforeDefunding 96 | 97 | 98 | def test_fund_defund_with_policy( 99 | instance: GifInstance, 100 | testCoin, 101 | gifTestProduct: GifTestProduct, 102 | riskpoolKeeper: Account, 103 | owner: Account, 104 | customer: Account, 105 | feeOwner: Account, 106 | capitalOwner: Account 107 | ): 108 | product = gifTestProduct.getContract() 109 | riskpool = gifTestProduct.getRiskpool().getContract() 110 | initialFunding = 10000 111 | 112 | riskpoolWallet = capitalOwner 113 | investor = riskpoolKeeper 114 | fund_riskpool(instance, owner, riskpoolWallet, riskpool, investor, testCoin, initialFunding) 115 | 116 | assert riskpool.bundles() == 1 117 | bundle = _getBundle(instance, riskpool, 0) 118 | print('bundle after funding: {}'.format(bundle)) 119 | 120 | ( 121 | bundleId, 122 | riskpoolId, 123 | tokenId, 124 | state, 125 | filter, 126 | capital, 127 | lockedCapital, 128 | balance, 129 | createdAt, 130 | updatedAt 131 | ) = bundle 132 | 133 | # application spec 134 | premium = 100 135 | sumInsured = 5000 136 | metaData = s2b32('meta') 137 | applicationData = s2b32('application') 138 | 139 | # transfer funds to customer and create allowance 140 | testCoin.transfer(customer, premium, {'from': owner}) 141 | testCoin.approve(instance.getTreasury(), premium, {'from': customer}) 142 | 143 | # create policy 144 | policy_tx = product.applyForPolicy( 145 | premium, 146 | sumInsured, 147 | metaData, 148 | applicationData, 149 | {'from': customer}) 150 | 151 | policyId = policy_tx.return_value 152 | 153 | print('bundle after premium: {}'.format(_getBundle(instance, riskpool, 0))) 154 | 155 | # expire and close policy to free locked capital in bundle 156 | product.expire(policyId) 157 | product.close(policyId) 158 | 159 | # record state before defunding 160 | bundleBeforeDefunding = _getBundle(instance, riskpool, 0) 161 | ( 162 | bundleId, 163 | riskpoolId, 164 | tokenId, 165 | state, 166 | filter, 167 | capital, 168 | lockedCapital, 169 | balance, 170 | createdAt, 171 | updatedAt 172 | ) = bundleBeforeDefunding 173 | print('bundle after closing policy: {}'.format(bundleBeforeDefunding)) 174 | 175 | riskpoolWalletBalanceBeforeDefunding = testCoin.balanceOf(riskpoolWallet) 176 | investorBalanceBeforeDefunding = testCoin.balanceOf(investor) 177 | bundleBalanceBeforeDefunding = balance 178 | 179 | # defund amount larger than capital 180 | defundAmount = balance 181 | assert defundAmount > capital # balance includes net premium, capital doesn't 182 | assert bundleBalanceBeforeDefunding == riskpoolWalletBalanceBeforeDefunding 183 | 184 | riskpool.defundBundle(bundleId, defundAmount, {'from':investor}) 185 | 186 | bundleAfterDefunding = _getBundle(instance, riskpool, 0) 187 | print('bundle after defunding by {}: {}'.format(defundAmount, bundleAfterDefunding)) 188 | 189 | ( 190 | bundleId, 191 | riskpoolId, 192 | tokenId, 193 | state, 194 | filter, 195 | capital, 196 | lockedCapital, 197 | balance, 198 | createdAt, 199 | updatedAt 200 | ) = bundleAfterDefunding 201 | 202 | assert capital == 0 203 | assert balance == 0 204 | 205 | assert testCoin.balanceOf(riskpoolWallet) == 0 206 | assert testCoin.balanceOf(investor) == investorBalanceBeforeDefunding + defundAmount 207 | 208 | 209 | def _getBundle(instance, riskpool, bundleIdx): 210 | instanceService = instance.getInstanceService() 211 | bundleId = riskpool.getBundleId(bundleIdx) 212 | return instanceService.getBundle(bundleId) 213 | -------------------------------------------------------------------------------- /tests/test_bundle_token.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from scripts.const import ZERO_ADDRESS 5 | 6 | def test_setup(bundleToken, owner): 7 | assert bundleToken.symbol() == "BTK" 8 | assert bundleToken.totalSupply() == 0 9 | 10 | # enforce function isolation for tests below 11 | @pytest.fixture(autouse=True) 12 | def isolation(fn_isolation): 13 | pass 14 | 15 | def test_setup_with_instance(instance, owner): 16 | bundleToken = instance.bundleToken 17 | bundleModule = instance.bundle 18 | 19 | # check after intance deployment bundle module is already set 20 | with brownie.reverts('ERROR:BTK-003:BUNDLE_MODULE_ALREADY_DEFINED'): 21 | bundleToken.setBundleModule(owner) 22 | 23 | # check that initial wiring corresponds to expectation 24 | assert bundleToken.getBundleModuleAddress() == bundleModule.address 25 | 26 | 27 | # check initialization process then happy path with mint/burn 28 | def test_initialize(bundleToken, owner, riskpoolKeeper, customer): 29 | 30 | # check uninitialized case 31 | bundleId = 3 32 | with brownie.reverts('ERROR:BTK-001:NOT_INITIALIZED'): 33 | bundleToken.mint( 34 | bundleId, 35 | customer, 36 | {'from': owner}) 37 | 38 | # check setting of minter 39 | with brownie.reverts('ERROR:BTK-004:INVALID_BUNDLE_MODULE_ADDRESS'): 40 | bundleToken.setBundleModule(ZERO_ADDRESS) 41 | 42 | # use riskpool keeper as surrogate bundle module 43 | bundleToken.setBundleModule(riskpoolKeeper) 44 | 45 | # check that minter can only be set once 46 | with brownie.reverts('ERROR:BTK-003:BUNDLE_MODULE_ALREADY_DEFINED'): 47 | bundleToken.setBundleModule(customer) 48 | 49 | # check that minting/burning now works 50 | bundleId = 3 51 | tokenId = 1 52 | assert bundleToken.totalSupply() == 0 53 | assert bundleToken.exists(tokenId) == False 54 | assert bundleToken.burned(tokenId) == False 55 | 56 | # check minting works for 'bundle module' 57 | tx = bundleToken.mint( 58 | bundleId, 59 | customer, 60 | {'from': riskpoolKeeper}) 61 | 62 | tokenId = tx.return_value 63 | 64 | assert bundleToken.totalSupply() == 1 65 | assert tokenId == 1 66 | assert bundleToken.exists(tokenId) == True 67 | assert bundleToken.burned(tokenId) == False 68 | 69 | # check that burning does work for non-minter (eg customer) 70 | bundleToken.burn( 71 | tokenId, 72 | {'from': riskpoolKeeper}) 73 | 74 | assert bundleToken.totalSupply() == 1 75 | assert bundleToken.exists(tokenId) == True 76 | assert bundleToken.burned(tokenId) == True 77 | 78 | 79 | def test_mint(bundleToken, owner, riskpoolKeeper, customer): 80 | 81 | # use riskpool keeper as surrogate bundle module 82 | bundleToken.setBundleModule(riskpoolKeeper) 83 | 84 | assert bundleToken.totalSupply() == 0 85 | assert bundleToken.balanceOf(customer) == 0 86 | 87 | bundleId = 3 88 | tx = bundleToken.mint( 89 | bundleId, 90 | customer, 91 | {'from': riskpoolKeeper}) 92 | 93 | assert bundleToken.totalSupply() == 1 94 | assert bundleToken.balanceOf(customer) == 1 95 | 96 | tokenId = tx.return_value 97 | 98 | assert tokenId == 1 99 | assert bundleToken.exists(tokenId) == True 100 | assert bundleToken.burned(tokenId) == False 101 | assert bundleToken.ownerOf(tokenId) == customer 102 | assert bundleToken.getBundleId(tokenId) == bundleId 103 | 104 | 105 | def test_burn(bundleToken, owner, riskpoolKeeper, customer): 106 | 107 | # use riskpool keeper as surrogate bundle module 108 | bundleToken.setBundleModule(riskpoolKeeper) 109 | bundleId = 3 110 | tx = bundleToken.mint( 111 | bundleId, 112 | customer, 113 | {'from': riskpoolKeeper}) 114 | 115 | tokenId = tx.return_value 116 | 117 | assert bundleToken.exists(tokenId) == True 118 | assert bundleToken.burned(tokenId) == False 119 | 120 | nonExistingId = 13 121 | with brownie.reverts('ERROR:BTK-005:TOKEN_ID_INVALID'): 122 | bundleToken.burn( 123 | nonExistingId, 124 | {'from': riskpoolKeeper}) 125 | 126 | bundleToken.burn( 127 | tokenId, 128 | {'from': riskpoolKeeper}) 129 | 130 | assert bundleToken.totalSupply() == 1 131 | assert bundleToken.exists(tokenId) == True 132 | assert bundleToken.burned(tokenId) == True 133 | 134 | 135 | def test_mint_only_bundle_module(bundleToken, owner, riskpoolKeeper, customer): 136 | 137 | # use riskpool keeper as surrogate bundle module 138 | bundleToken.setBundleModule(riskpoolKeeper) 139 | 140 | # customer must not be able to mint herself a nft 141 | bundleId = 3 142 | with brownie.reverts('ERROR:BTK-002:NOT_BUNDLE_MODULE'): 143 | bundleToken.mint( 144 | bundleId, 145 | customer, 146 | {'from': customer}) 147 | 148 | assert bundleToken.totalSupply() == 0 149 | 150 | # owner must be able to mint customer a nft 151 | tx = bundleToken.mint( 152 | bundleId, 153 | customer, 154 | {'from': riskpoolKeeper}) 155 | 156 | assert bundleToken.totalSupply() == 1 157 | 158 | 159 | def test_burn_only_bundle_module(bundleToken, owner, riskpoolKeeper, customer): 160 | 161 | # use riskpool keeper as surrogate bundle module 162 | bundleToken.setBundleModule(riskpoolKeeper) 163 | 164 | # customer must not be able to mint herself a nft 165 | bundleId = 3 166 | 167 | tx = bundleToken.mint( 168 | bundleId, 169 | customer, 170 | {'from': riskpoolKeeper}) 171 | 172 | tokenId = tx.return_value 173 | 174 | # check that burning does not work for non-minter (eg customer) 175 | with brownie.reverts('ERROR:BTK-002:NOT_BUNDLE_MODULE'): 176 | bundleToken.burn( 177 | tokenId, 178 | {'from': customer}) 179 | 180 | assert bundleToken.totalSupply() == 1 181 | assert bundleToken.exists(tokenId) == True 182 | assert bundleToken.burned(tokenId) == False 183 | 184 | # check that burning does work for non-minter (eg customer) 185 | bundleToken.burn( 186 | tokenId, 187 | {'from': riskpoolKeeper}) 188 | 189 | assert bundleToken.totalSupply() == 1 190 | assert bundleToken.exists(tokenId) == True 191 | assert bundleToken.burned(tokenId) == True 192 | -------------------------------------------------------------------------------- /tests/test_access_module.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie import AccessController 5 | 6 | from scripts.util import ( 7 | keccak256, 8 | s2b32, 9 | contractFromAddress 10 | ) 11 | 12 | # enforce function isolation for tests below 13 | @pytest.fixture(autouse=True) 14 | def isolation(fn_isolation): 15 | pass 16 | 17 | def test_initial_setup( 18 | instance, 19 | owner, 20 | productOwner, 21 | oracleProvider, 22 | riskpoolKeeper, 23 | ): 24 | instanceService = instance.getInstanceService() 25 | instanceOperatorService = instance.getInstanceOperatorService() 26 | (poRole, opRole, rkRole, daRole) = getRoles(instanceService) 27 | 28 | assert poRole != opRole 29 | assert poRole != rkRole 30 | assert poRole != daRole 31 | assert opRole != rkRole 32 | assert opRole != daRole 33 | assert rkRole != daRole 34 | 35 | assert poRole == keccak256('PRODUCT_OWNER_ROLE') 36 | assert opRole == keccak256('ORACLE_PROVIDER_ROLE') 37 | assert rkRole == keccak256('RISKPOOL_KEEPER_ROLE') 38 | 39 | # check component owners against their roles 40 | assert not instanceService.hasRole(poRole, productOwner) 41 | assert not instanceService.hasRole(opRole, oracleProvider) 42 | assert not instanceService.hasRole(rkRole, riskpoolKeeper) 43 | 44 | # check default role assignemnt 45 | assert instanceService.hasRole(daRole, instanceOperatorService.address) 46 | 47 | 48 | def test_default_admin_role( 49 | instance, 50 | owner, 51 | productOwner, 52 | oracleProvider, 53 | riskpoolKeeper, 54 | ): 55 | instanceOperatorService = instance.getInstanceOperatorService() 56 | instanceService = instance.getInstanceService() 57 | ioDict = {'from': owner} 58 | 59 | (poRole, opRole, rkRole, daRole) = getRoles(instanceService) 60 | 61 | # check default admin role assignemnt to instance operator service 62 | assert instanceService.hasRole(daRole, instanceOperatorService.address) 63 | 64 | registry = instance.getRegistry() 65 | access = contractFromAddress(AccessController, registry.getContract(s2b32('Access'))) 66 | 67 | # check that 'random' accaounts can't re-assign the admin role 68 | with brownie.reverts('ERROR:ACL-001:ADMIN_ROLE_ALREADY_SET'): 69 | access.setDefaultAdminRole(productOwner, {'from': productOwner}) 70 | 71 | # check that not even the instance operator can change the role assignment 72 | with brownie.reverts('ERROR:ACL-001:ADMIN_ROLE_ALREADY_SET'): 73 | access.setDefaultAdminRole(productOwner, ioDict) 74 | 75 | 76 | def test_role_assignment( 77 | instance, 78 | owner, 79 | productOwner, 80 | oracleProvider, 81 | riskpoolKeeper, 82 | ): 83 | instanceOperatorService = instance.getInstanceOperatorService() 84 | instanceService = instance.getInstanceService() 85 | ioDict = {'from': owner} 86 | 87 | (poRole, opRole, rkRole, daRole) = getRoles(instanceService) 88 | 89 | instanceOperatorService.grantRole(poRole, productOwner, ioDict) 90 | instanceOperatorService.grantRole(opRole, oracleProvider, ioDict) 91 | instanceOperatorService.grantRole(rkRole, riskpoolKeeper, ioDict) 92 | 93 | assert instanceService.hasRole(poRole, productOwner) 94 | assert instanceService.hasRole(opRole, oracleProvider) 95 | assert instanceService.hasRole(rkRole, riskpoolKeeper) 96 | 97 | instanceOperatorService.revokeRole(poRole, productOwner, ioDict) 98 | instanceOperatorService.revokeRole(opRole, oracleProvider, ioDict) 99 | instanceOperatorService.revokeRole(rkRole, riskpoolKeeper, ioDict) 100 | 101 | assert not instanceService.hasRole(poRole, productOwner) 102 | assert not instanceService.hasRole(opRole, oracleProvider) 103 | assert not instanceService.hasRole(rkRole, riskpoolKeeper) 104 | 105 | 106 | def test_role_creation( 107 | instance, 108 | owner, 109 | productOwner, 110 | oracleProvider, 111 | riskpoolKeeper, 112 | ): 113 | instanceOperatorService = instance.getInstanceOperatorService() 114 | instanceService = instance.getInstanceService() 115 | ioDict = {'from': owner} 116 | 117 | NEW_ROLE = keccak256('NEW_ROLE') 118 | 119 | # check that unknown roles cannot be granted 120 | with brownie.reverts('ERROR:ACL-002:ROLE_UNKNOWN_OR_INVALID'): 121 | instanceOperatorService.grantRole(NEW_ROLE, productOwner, ioDict) 122 | 123 | # check that a non instance operator cannot create new role 124 | with brownie.reverts('ERROR:IOS-001:NOT_INSTANCE_OPERATOR'): 125 | instanceOperatorService.createRole(NEW_ROLE, {'from': productOwner}) 126 | 127 | # role creation 128 | instanceOperatorService.createRole(NEW_ROLE, ioDict) 129 | 130 | assert not instanceService.hasRole(NEW_ROLE, productOwner) 131 | 132 | # grant newly created role 133 | instanceOperatorService.grantRole(NEW_ROLE, productOwner, ioDict) 134 | 135 | # check granting 136 | assert instanceService.hasRole(NEW_ROLE, productOwner) 137 | 138 | 139 | def test_role_invalidation( 140 | instance, 141 | owner, 142 | productOwner, 143 | oracleProvider, 144 | riskpoolKeeper, 145 | ): 146 | instanceOperatorService = instance.getInstanceOperatorService() 147 | instanceService = instance.getInstanceService() 148 | ioDict = {'from': owner} 149 | 150 | (poRole, opRole, rkRole, daRole) = getRoles(instanceService) 151 | 152 | instanceOperatorService.grantRole(poRole, productOwner, ioDict) 153 | 154 | # check that a non instance operator cannot invalidate a role 155 | with brownie.reverts('ERROR:IOS-001:NOT_INSTANCE_OPERATOR'): 156 | instanceOperatorService.invalidateRole(poRole, {'from': productOwner}) 157 | 158 | NEW_ROLE = keccak256('NEW_ROLE') 159 | 160 | with brownie.reverts('ERROR:ACL-004:ROLE_UNKNOWN_OR_INVALID'): 161 | instanceOperatorService.invalidateRole(NEW_ROLE, ioDict) 162 | 163 | instanceOperatorService.invalidateRole(poRole, ioDict) 164 | 165 | with brownie.reverts('ERROR:ACL-002:ROLE_UNKNOWN_OR_INVALID'): 166 | instanceOperatorService.grantRole(poRole, oracleProvider, ioDict) 167 | 168 | 169 | def getRoles(instanceService): 170 | poRole = instanceService.getProductOwnerRole() 171 | opRole = instanceService.getOracleProviderRole() 172 | rkRole = instanceService.getRiskpoolKeeperRole() 173 | daRole = instanceService.getDefaultAdminRole() 174 | 175 | return (poRole, opRole, rkRole, daRole) 176 | -------------------------------------------------------------------------------- /tests/test_basicriskpool_bundle_allocation_01.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie.network.account import Account 5 | from brownie import ( 6 | interface, 7 | Wei, 8 | TestProduct, 9 | ) 10 | 11 | from scripts.util import ( 12 | s2h, 13 | s2b32, 14 | ) 15 | 16 | from scripts.setup import ( 17 | fund_riskpool, 18 | apply_for_policy, 19 | ) 20 | 21 | from scripts.instance import ( 22 | GifInstance, 23 | ) 24 | 25 | from scripts.product import ( 26 | GifTestProduct, 27 | GifTestRiskpool, 28 | ) 29 | 30 | # enforce function isolation for tests below 31 | @pytest.fixture(autouse=True) 32 | def isolation(fn_isolation): 33 | pass 34 | 35 | 36 | def test_bundle_allocation_with_one_bundle( 37 | instance: GifInstance, 38 | testCoin, 39 | gifTestProduct: GifTestProduct, 40 | riskpoolKeeper: Account, 41 | owner: Account, 42 | customer: Account, 43 | feeOwner: Account, 44 | capitalOwner: Account 45 | ): 46 | product = gifTestProduct.getContract() 47 | riskpool = gifTestProduct.getRiskpool().getContract() 48 | 49 | initialFunding = 10000 50 | 51 | # fund the riskpool with two bundles 52 | bid1 = fund_riskpool(instance, owner, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding) 53 | 54 | assert 1 == riskpool.bundles() 55 | 56 | # ensure the bundles are configured 57 | bundle = _getBundle(instance, riskpool, 0) 58 | ( 59 | bundleId, 60 | riskpoolId, 61 | _, 62 | state, 63 | _, 64 | _, 65 | lockedCapital, 66 | *_ 67 | ) = bundle 68 | 69 | assert bundleId == 1 70 | assert riskpoolId == riskpool.getId() 71 | assert lockedCapital == 0 72 | 73 | # apply for the first policy 74 | policyId = apply_for_policy(instance, owner, product, customer, testCoin, 100, 3000) 75 | 76 | # ensure is correctly configured 77 | policyController = instance.getPolicy() 78 | application = policyController.getApplication(policyId).dict() 79 | policy = policyController.getPolicy(policyId).dict() 80 | 81 | assert policy is not None 82 | # PolicyState {Active, Expired} 83 | assert application['state'] == 2 84 | assert policy['state'] == 0 85 | 86 | # apply for the second policy 87 | policyId2 = apply_for_policy(instance, owner, product, customer, testCoin, 100, 4000) 88 | 89 | # ensure is correctly configured 90 | metadata2 = policyController.getMetadata(policyId2).dict() 91 | application2 = policyController.getApplication(policyId2).dict() 92 | policy2 = policyController.getPolicy(policyId2).dict() 93 | 94 | assert policy2 is not None 95 | # PolicyState {Active, Expired} 96 | assert application2['state'] == 2 97 | assert policy2['state'] == 0 98 | 99 | 100 | # get updates bundle values 101 | bundle = _getBundle(instance, riskpool, 0) 102 | ( 103 | bundleId, 104 | riskpoolId, 105 | _, 106 | state, 107 | _, 108 | _, 109 | lockedCapital, 110 | *_ 111 | ) = bundle 112 | 113 | # ensure the policies were allocated to different bundles 114 | assert lockedCapital == 7000 115 | 116 | def test_bundle_allocation_with_three_equal_bundles( 117 | instance: GifInstance, 118 | testCoin, 119 | gifTestProduct: GifTestProduct, 120 | riskpoolKeeper: Account, 121 | owner: Account, 122 | customer: Account, 123 | feeOwner: Account, 124 | capitalOwner: Account 125 | ): 126 | num_bundles = 3 127 | product = gifTestProduct.getContract() 128 | riskpool = gifTestProduct.getRiskpool().getContract() 129 | riskpool.setMaximumNumberOfActiveBundles(num_bundles, {'from': riskpoolKeeper}) 130 | 131 | initialFunding = 10000 132 | 133 | # fund the riskpools 134 | for _ in range(num_bundles): 135 | fund_riskpool(instance, owner, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding) 136 | 137 | assert num_bundles == riskpool.bundles() 138 | 139 | # allocate 3 policies in every bundle 140 | for _ in range(num_bundles * 3): 141 | apply_for_policy(instance, owner, product, customer, testCoin, 100, 1000) 142 | 143 | 144 | # ensure every bundle has same locked capital 145 | for i in range(num_bundles): 146 | # get updates bundle values 147 | bundle = _getBundle(instance, riskpool, i) 148 | ( 149 | _, 150 | _, 151 | _, 152 | _, 153 | _, 154 | _, 155 | lockedCapital, 156 | *_ 157 | ) = bundle 158 | 159 | assert 3000 == lockedCapital 160 | 161 | 162 | def test_failing_bundle_allocation( 163 | instance: GifInstance, 164 | testCoin, 165 | gifTestProduct: GifTestProduct, 166 | riskpoolKeeper: Account, 167 | owner: Account, 168 | customer: Account, 169 | feeOwner: Account, 170 | capitalOwner: Account 171 | ): 172 | num_bundles = 2 173 | product = gifTestProduct.getContract() 174 | riskpool = gifTestProduct.getRiskpool().getContract() 175 | riskpool.setMaximumNumberOfActiveBundles(num_bundles, {'from': riskpoolKeeper}) 176 | instanceService = instance.getInstanceService() 177 | 178 | initialFunding = 1000 179 | 180 | # fund the riskpools 181 | for _ in range(num_bundles): 182 | fund_riskpool(instance, owner, capitalOwner, riskpool, riskpoolKeeper, testCoin, initialFunding) 183 | 184 | # create minimal policy application 185 | premium = 100 186 | sumInsured = 1200 187 | metaData = bytes(0) 188 | applicationData = bytes(0) 189 | 190 | testCoin.transfer(customer, premium, {'from': owner}) 191 | testCoin.approve(instance.getTreasury(), premium, {'from': customer}) 192 | 193 | tx = product.applyForPolicy( 194 | premium, 195 | sumInsured, 196 | metaData, 197 | applicationData, 198 | {'from': customer}) 199 | 200 | processId = tx.return_value 201 | 202 | print('processId {}'.format(processId)) 203 | print(tx.info()) 204 | 205 | assert 'LogRiskpoolCollateralizationFailed' in tx.events 206 | assert len(tx.events['LogRiskpoolCollateralizationFailed']) == 1 207 | 208 | failedEvent = tx.events['LogRiskpoolCollateralizationFailed'][0] 209 | assert failedEvent['processId'] == processId 210 | assert failedEvent['amount'] == sumInsured 211 | 212 | application = instanceService.getApplication(processId) 213 | applicationState = application[0] 214 | assert applicationState == 0 # enum ApplicationState {Applied, Revoked, Underwritten, Declined} 215 | 216 | 217 | def _getBundle(instance, riskpool, bundleIdx): 218 | instanceService = instance.getInstanceService() 219 | bundleId = riskpool.getBundleId(bundleIdx) 220 | return instanceService.getBundle(bundleId) 221 | -------------------------------------------------------------------------------- /tests/test_deploy_instance.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from scripts.const import ( 5 | GIF_RELEASE, 6 | REGISTRY_NAME, 7 | ACCESS_NAME, 8 | BUNDLE_NAME, 9 | COMPONENT_NAME, 10 | LICENSE_NAME, 11 | POLICY_NAME, 12 | POLICY_DEFAULT_FLOW_NAME, 13 | POOL_NAME, 14 | TREASURY_NAME, 15 | COMPONENT_OWNER_SERVICE_NAME, 16 | INSTANCE_OPERATOR_SERVICE_NAME, 17 | INSTANCE_SERVICE_NAME, 18 | ORACLE_SERVICE_NAME, 19 | PRODUCT_SERVICE_NAME, 20 | RISKPOOL_SERVICE_NAME, 21 | ) 22 | 23 | from scripts.instance import GifInstance 24 | from scripts.util import s2b32 25 | 26 | # enforce function isolation for tests below 27 | @pytest.fixture(autouse=True) 28 | def isolation(fn_isolation): 29 | pass 30 | 31 | 32 | def test_Registry(instance: GifInstance, owner): 33 | registry = instance.getRegistry() 34 | 35 | assert registry.address == registry.getContract(s2b32(REGISTRY_NAME)) 36 | assert registry.getRelease() == s2b32(GIF_RELEASE) 37 | 38 | with pytest.raises(AttributeError): 39 | assert registry.foo({'from': owner}) 40 | 41 | 42 | def test_Access(instance: GifInstance, owner): 43 | registry = instance.getRegistry() 44 | access = instance.getAccess() 45 | 46 | assert access.address == registry.getContract(s2b32(ACCESS_NAME)) 47 | assert access.address != 0x0 48 | 49 | assert access.getProductOwnerRole() != access.getOracleProviderRole() 50 | assert access.getOracleProviderRole() != access.getRiskpoolKeeperRole() 51 | assert access.getRiskpoolKeeperRole() != access.getProductOwnerRole() 52 | 53 | with pytest.raises(AttributeError): 54 | assert access.foo({'from': owner}) 55 | 56 | 57 | def test_Bundle(instance: GifInstance, owner): 58 | registry = instance.getRegistry() 59 | bundle = instance.getBundle() 60 | 61 | assert bundle.address == registry.getContract(s2b32(BUNDLE_NAME)) 62 | assert bundle.address != 0x0 63 | 64 | assert bundle.bundles() == 0 65 | 66 | with brownie.reverts('ERROR:BUC-060:BUNDLE_DOES_NOT_EXIST'): 67 | bundle.getBundle(0) 68 | 69 | with pytest.raises(AttributeError): 70 | assert bundle.foo({'from': owner}) 71 | 72 | 73 | def test_Component(instance: GifInstance, owner): 74 | registry = instance.getRegistry() 75 | component = instance.getComponent() 76 | 77 | assert component.address == registry.getContract(s2b32(COMPONENT_NAME)) 78 | assert component.address != 0x0 79 | 80 | assert component.exists(1) == False 81 | assert component.components() == 0 82 | assert component.products() == 0 83 | assert component.oracles() == 0 84 | assert component.riskpools() == 0 85 | 86 | with pytest.raises(AttributeError): 87 | assert component.foo({'from': owner}) 88 | 89 | 90 | def test_License(instance: GifInstance, owner): 91 | registry = instance.getRegistry() 92 | license = instance.getLicense() 93 | 94 | assert license.address == registry.getContract(s2b32(LICENSE_NAME)) 95 | assert license.address != 0x0 96 | 97 | with brownie.reverts("ERROR:CCR-007:COMPONENT_UNKNOWN"): 98 | license.getAuthorizationStatus(registry.address) 99 | 100 | with pytest.raises(AttributeError): 101 | assert license.foo({'from': owner}) 102 | 103 | 104 | def test_Policy(instance: GifInstance, owner): 105 | registry = instance.getRegistry() 106 | policy = instance.getPolicy() 107 | 108 | assert policy.address == registry.getContract(s2b32(POLICY_NAME)) 109 | assert policy.address != 0x0 110 | 111 | assert policy.processIds() == 0 112 | with brownie.reverts('ERROR:POC-102:POLICY_DOES_NOT_EXIST'): 113 | policy.getPolicy(s2b32('')) 114 | 115 | with pytest.raises(AttributeError): 116 | assert policy.foo({'from': owner}) 117 | 118 | 119 | def test_PolicyDefaultFlow(instance: GifInstance, owner): 120 | registry = instance.getRegistry() 121 | policyDefaultFlow = instance.getPolicyDefaultFlow() 122 | 123 | policyDefaultFlowAddress = registry.getContract(s2b32(POLICY_DEFAULT_FLOW_NAME)) 124 | 125 | assert policyDefaultFlow.address == policyDefaultFlowAddress 126 | assert policyDefaultFlow.address != 0x0 127 | 128 | with brownie.reverts('ERROR:POC-101:APPLICATION_DOES_NOT_EXIST'): 129 | policyDefaultFlow.getApplicationData(s2b32('')) 130 | 131 | with pytest.raises(AttributeError): 132 | assert policyDefaultFlow.foo({'from': owner}) 133 | 134 | 135 | def test_Pool(instance: GifInstance, owner): 136 | registry = instance.getRegistry() 137 | pool = instance.getPool() 138 | 139 | assert pool.address == registry.getContract(s2b32(POOL_NAME)) 140 | assert pool.address != 0x0 141 | 142 | assert pool.riskpools() == 0 143 | 144 | with pytest.raises(AttributeError): 145 | assert pool.foo({'from': owner}) 146 | 147 | 148 | def test_Treasury(instance: GifInstance, owner, feeOwner): 149 | registry = instance.getRegistry() 150 | treasury = instance.getTreasury() 151 | 152 | assert treasury.address == registry.getContract(s2b32(TREASURY_NAME)) 153 | assert treasury.address != 0x0 154 | 155 | assert treasury.getFractionFullUnit() == 10**18 156 | assert treasury.getInstanceWallet() == feeOwner 157 | 158 | with pytest.raises(AttributeError): 159 | assert treasury.foo({'from': owner}) 160 | 161 | 162 | def test_InstanceService(instance, registry, owner): 163 | registry = instance.getRegistry() 164 | instanceService = instance.getInstanceService() 165 | 166 | assert instanceService.address == registry.getContract(s2b32(INSTANCE_SERVICE_NAME)) 167 | assert instanceService.getInstanceOperator() == owner 168 | assert owner != 0x0 169 | 170 | with pytest.raises(AttributeError): 171 | assert instanceService.foo({'from': owner}) 172 | 173 | 174 | def test_OracleService(instance, registry, owner): 175 | registry = instance.getRegistry() 176 | oracleService = instance.getOracleService() 177 | 178 | assert oracleService.address == registry.getContract(s2b32(ORACLE_SERVICE_NAME)) 179 | assert oracleService.address != 0x0 180 | 181 | with pytest.raises(AttributeError): 182 | assert oracleService.foo({'from': owner}) 183 | 184 | 185 | def test_ProductService(instance, registry, owner): 186 | registry = instance.getRegistry() 187 | productService = instance.getProductService() 188 | 189 | assert productService.address == registry.getContract(s2b32(PRODUCT_SERVICE_NAME)) 190 | assert productService.address != 0x0 191 | 192 | with pytest.raises(AttributeError): 193 | assert productService.foo({'from': owner}) 194 | 195 | 196 | def test_RiskpoolService(instance, registry, owner): 197 | registry = instance.getRegistry() 198 | riskpoolService = instance.getRiskpoolService() 199 | 200 | assert riskpoolService.address == registry.getContract(s2b32(RISKPOOL_SERVICE_NAME)) 201 | assert riskpoolService.address != 0x0 202 | 203 | with pytest.raises(AttributeError): 204 | assert riskpoolService.foo({'from': owner}) 205 | -------------------------------------------------------------------------------- /contracts/test/TestCompromisedProduct.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "@etherisc/gif-interface/contracts/components/IComponent.sol"; 5 | import "@etherisc/gif-interface/contracts/components/IProduct.sol"; 6 | 7 | import "@etherisc/gif-interface/contracts/modules/IAccess.sol"; 8 | import "@etherisc/gif-interface/contracts/modules/IPolicy.sol"; 9 | import "@etherisc/gif-interface/contracts/modules/IRegistry.sol"; 10 | 11 | import "@etherisc/gif-interface/contracts/services/IComponentOwnerService.sol"; 12 | import "@etherisc/gif-interface/contracts/services/IProductService.sol"; 13 | import "@etherisc/gif-interface/contracts/services/IInstanceService.sol"; 14 | 15 | import "@openzeppelin/contracts/access/Ownable.sol"; 16 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 17 | 18 | /* 19 | the TestCompromisedProduct claims to be an existing product that connects to an existing 20 | riskpool with the goal to create fraud claims that lead to fraud payouts whith the intention 21 | to drain the riskpool. 22 | 23 | for this the compromised product claims 24 | - to be a product 25 | - to be in state active (independent of an approval step by the instance operator) 26 | */ 27 | contract TestCompromisedProduct is 28 | IProduct, 29 | Ownable 30 | { 31 | IComponent.ComponentState public constant FAKE_STATE = IComponent.ComponentState.Active; 32 | 33 | bytes32 public constant POLICY_FLOW = "PolicyDefaultFlow"; 34 | 35 | bytes32 private _componentName; 36 | address private _tokenAddress; 37 | uint256 private _componentId; 38 | uint256 private _riskpoolId; 39 | 40 | IRegistry private _registry; 41 | IAccess private _access; 42 | IComponentOwnerService private _componentOwnerService; 43 | IInstanceService private _instanceService; 44 | address private _policyFlow; 45 | IProductService private _productService; 46 | 47 | uint256 private _policies; 48 | uint256 private _claims; 49 | 50 | modifier onlyPolicyHolder(bytes32 policyId) { 51 | address policyHolder = _instanceService.getMetadata(policyId).owner; 52 | require( 53 | _msgSender() == policyHolder, 54 | "ERROR:TCP-1:INVALID_POLICY_OR_HOLDER" 55 | ); 56 | _; 57 | } 58 | 59 | constructor( 60 | bytes32 fakeProductName, 61 | address tokenAddress, 62 | uint256 fakeComponentId, 63 | uint256 fakeRiskpoolId, 64 | address registryAddress 65 | ) 66 | Ownable() 67 | { 68 | _componentName = fakeProductName; 69 | _tokenAddress = tokenAddress; 70 | _componentId = fakeComponentId; 71 | _riskpoolId = fakeRiskpoolId; 72 | 73 | _registry = IRegistry(registryAddress); 74 | _access = _getAccess(); 75 | _componentOwnerService = _getComponentOwnerService(); 76 | _instanceService = _getInstanceService(); 77 | _policyFlow = _getContractAddress(POLICY_FLOW); 78 | _productService = _getProductService(); 79 | } 80 | 81 | function applyForPolicy( 82 | uint256 premium, 83 | uint256 sumInsured, 84 | bytes calldata metaData, 85 | bytes calldata applicationData 86 | ) 87 | external 88 | payable 89 | returns (bytes32 processId) 90 | { 91 | address payable policyHolder = payable(_msgSender()); 92 | 93 | // Create and underwrite new application 94 | processId = _productService.newApplication( 95 | policyHolder, 96 | premium, 97 | sumInsured, 98 | metaData, 99 | applicationData); 100 | 101 | _productService.underwrite(processId); 102 | } 103 | 104 | function collectPremium(bytes32 policyId) 105 | external 106 | { 107 | IPolicy.Policy memory policy = _instanceService.getPolicy(policyId); 108 | _productService.collectPremium(policyId, policy.premiumExpectedAmount); 109 | } 110 | 111 | function submitClaim(bytes32 policyId, uint256 claimAmount) 112 | external 113 | onlyPolicyHolder(policyId) 114 | { 115 | // increase claims counter 116 | _claims += 1; 117 | 118 | // create claim and confirm it 119 | uint256 claimId = _productService.newClaim(policyId, claimAmount, abi.encode(0)); 120 | _productService.confirmClaim(policyId, claimId, claimAmount); 121 | 122 | // create payout record 123 | uint256 payoutId = _productService.newPayout(policyId, claimId, claimAmount, abi.encode(0)); 124 | _productService.processPayout(policyId, payoutId); 125 | } 126 | 127 | //--- product service access --------------------------------------------// 128 | 129 | //--- iproduct ----------------------------------------------------------// 130 | function getToken() external override view returns(address token) { return _tokenAddress; } 131 | function getPolicyFlow() external override view returns(address policyFlow) { return _getContractAddress(POLICY_FLOW); } 132 | function getRiskpoolId() external override view returns(uint256 riskpoolId) { return _riskpoolId; } 133 | 134 | function getApplicationDataStructure() external override view returns(string memory dataStructure) { return ""; } 135 | function getClaimDataStructure() external override view returns(string memory dataStructure) { return ""; } 136 | function getPayoutDataStructure() external override view returns(string memory dataStructure) { return ""; } 137 | 138 | function riskPoolCapacityCallback(uint256 capacity) external override {} 139 | 140 | //--- icomponent --------------------------------------------------------// 141 | function setId(uint256 id) external override {} // does not care about id 142 | 143 | function getName() external override view returns(bytes32) { return _componentName; } 144 | function getId() external override view returns(uint256) { return _componentId; } 145 | function getType() external override view returns(ComponentType) { return IComponent.ComponentType.Product; } 146 | function getState() external override view returns(ComponentState) { return IComponent.ComponentState.Active; } 147 | function getOwner() external override view returns(address) { return owner(); } 148 | function getRegistry() external override view returns(IRegistry) { return _registry; } 149 | 150 | function isProduct() public override view returns(bool) { return true; } 151 | function isOracle() public override view returns(bool) { return false; } 152 | function isRiskpool() public override view returns(bool) { return false; } 153 | 154 | function proposalCallback() external override {} 155 | function approvalCallback() external override {} 156 | function declineCallback() external override {} 157 | function suspendCallback() external override {} 158 | function resumeCallback() external override {} 159 | function pauseCallback() external override {} 160 | function unpauseCallback() external override {} 161 | function archiveCallback() external override {} 162 | 163 | function _getAccess() private view returns (IAccess) { 164 | return IAccess(_getContractAddress("Access")); 165 | } 166 | 167 | function _getInstanceService() private view returns (IInstanceService) { 168 | return IInstanceService(_getContractAddress("InstanceService")); 169 | } 170 | 171 | function _getComponentOwnerService() private view returns (IComponentOwnerService) { 172 | return IComponentOwnerService(_getContractAddress("ComponentOwnerService")); 173 | } 174 | 175 | function _getProductService() private view returns (IProductService) { 176 | return IProductService(_getContractAddress("ProductService")); 177 | } 178 | 179 | function _getContractAddress(bytes32 contractName) private view returns (address) { 180 | return _registry.getContract(contractName); 181 | } 182 | 183 | } -------------------------------------------------------------------------------- /scripts/product.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | 3 | from brownie.convert import to_bytes 4 | from brownie.network import accounts 5 | from brownie.network.account import Account 6 | 7 | # pylint: disable-msg=E0611 8 | from brownie import ( 9 | Wei, 10 | Contract, 11 | PolicyController, 12 | OracleService, 13 | ComponentOwnerService, 14 | InstanceOperatorService, 15 | TestRiskpool, 16 | TestOracle, 17 | TestProduct, 18 | ) 19 | 20 | from scripts.const import ( 21 | RISKPOOL_NAME, 22 | ORACLE_INPUT_FORMAT, 23 | ORACLE_OUTPUT_FORMAT, 24 | ORACLE_NAME, 25 | PRODUCT_NAME, 26 | ) 27 | 28 | from scripts.util import ( 29 | get_account, 30 | encode_function_data, 31 | # s2h, 32 | s2b32, 33 | deployGifModule, 34 | deployGifService, 35 | ) 36 | 37 | from scripts.instance import ( 38 | GifInstance, 39 | ) 40 | 41 | 42 | class GifTestRiskpool(object): 43 | 44 | def __init__(self, 45 | instance: GifInstance, 46 | riskpoolKeeper: Account, 47 | erc20Token: Account, 48 | riskpoolWallet: Account, 49 | collateralization:int, 50 | name=RISKPOOL_NAME, 51 | publishSource=False, 52 | setRiskpoolWallet=True 53 | ): 54 | instanceService = instance.getInstanceService() 55 | operatorService = instance.getInstanceOperatorService() 56 | componentOwnerService = instance.getComponentOwnerService() 57 | riskpoolService = instance.getRiskpoolService() 58 | 59 | # 1) add role to keeper 60 | keeperRole = instanceService.getRiskpoolKeeperRole() 61 | operatorService.grantRole( 62 | keeperRole, 63 | riskpoolKeeper, 64 | {'from': instance.getOwner()}) 65 | 66 | # 2) keeper deploys riskpool 67 | if not setRiskpoolWallet: 68 | name += '_NO_WALLET' 69 | 70 | self.riskpool = TestRiskpool.deploy( 71 | s2b32(name), 72 | collateralization, 73 | erc20Token, 74 | riskpoolWallet, 75 | instance.getRegistry(), 76 | {'from': riskpoolKeeper}, 77 | publish_source=publishSource) 78 | 79 | # 3) riskpool keeperproposes oracle to instance 80 | componentOwnerService.propose( 81 | self.riskpool, 82 | {'from': riskpoolKeeper}) 83 | 84 | # 4) instance operator approves riskpool 85 | operatorService.approve( 86 | self.riskpool.getId(), 87 | {'from': instance.getOwner()}) 88 | 89 | # 5) instance operator assigns riskpool wallet 90 | if setRiskpoolWallet: 91 | operatorService.setRiskpoolWallet( 92 | self.riskpool.getId(), 93 | riskpoolWallet, 94 | {'from': instance.getOwner()}) 95 | 96 | # 6) setup capital fees 97 | fixedFee = 42 98 | fractionalFee = instanceService.getFeeFractionFullUnit() / 20 # corresponds to 5% 99 | feeSpec = operatorService.createFeeSpecification( 100 | self.riskpool.getId(), 101 | fixedFee, 102 | fractionalFee, 103 | b'', 104 | {'from': instance.getOwner()}) 105 | 106 | operatorService.setCapitalFees( 107 | feeSpec, 108 | {'from': instance.getOwner()}) 109 | 110 | def getId(self) -> int: 111 | return self.riskpool.getId() 112 | 113 | def getContract(self) -> TestRiskpool: 114 | return self.riskpool 115 | 116 | 117 | class GifTestOracle(object): 118 | 119 | def __init__(self, 120 | instance: GifInstance, 121 | oracleOwner: Account, 122 | name=ORACLE_NAME, 123 | publishSource=False 124 | ): 125 | instanceService = instance.getInstanceService() 126 | operatorService = instance.getInstanceOperatorService() 127 | componentOwnerService = instance.getComponentOwnerService() 128 | oracleService = instance.getOracleService() 129 | 130 | # 1) add oracle provider role to owner 131 | providerRole = instanceService.getOracleProviderRole() 132 | operatorService.grantRole( 133 | providerRole, 134 | oracleOwner, 135 | {'from': instance.getOwner()}) 136 | 137 | # 2) oracle provider creates oracle 138 | self.oracle = TestOracle.deploy( 139 | s2b32(name), 140 | instance.getRegistry(), 141 | {'from': oracleOwner}, 142 | publish_source=publishSource) 143 | 144 | # 3) oracle owner proposes oracle to instance 145 | componentOwnerService.propose( 146 | self.oracle, 147 | {'from': oracleOwner}) 148 | 149 | # 4) instance operator approves oracle 150 | operatorService.approve( 151 | self.oracle.getId(), 152 | {'from': instance.getOwner()}) 153 | 154 | def getId(self) -> int: 155 | return self.oracle.getId() 156 | 157 | def getContract(self) -> TestOracle: 158 | return self.oracle 159 | 160 | 161 | class GifTestProduct(object): 162 | 163 | def __init__(self, 164 | instance: GifInstance, 165 | token: Account, 166 | capitalOwner: Account, 167 | productOwner: Account, 168 | oracle: GifTestOracle, 169 | riskpool: GifTestRiskpool, 170 | name=PRODUCT_NAME, 171 | publishSource=False 172 | ): 173 | self.policy = instance.getPolicy() 174 | self.oracle = oracle 175 | self.riskpool = riskpool 176 | 177 | instanceService = instance.getInstanceService() 178 | operatorService = instance.getInstanceOperatorService() 179 | componentOwnerService = instance.getComponentOwnerService() 180 | registry = instance.getRegistry() 181 | 182 | # 1) add oracle provider role to owner 183 | ownerRole = instanceService.getProductOwnerRole() 184 | operatorService.grantRole( 185 | ownerRole, 186 | productOwner, 187 | {'from': instance.getOwner()}) 188 | 189 | # 2) product owner creates product 190 | self.product = TestProduct.deploy( 191 | s2b32(name), 192 | token.address, 193 | capitalOwner, 194 | oracle.getId(), 195 | riskpool.getId(), 196 | instance.getRegistry(), 197 | {'from': productOwner}, 198 | publish_source=publishSource) 199 | 200 | # 3) product owner proposes product to instance 201 | componentOwnerService.propose( 202 | self.product, 203 | {'from': productOwner}) 204 | 205 | # 4) instance operator approves product 206 | operatorService.approve( 207 | self.product.getId(), 208 | {'from': instance.getOwner()}) 209 | 210 | # 5) instance owner sets token in treasury 211 | operatorService.setProductToken( 212 | self.product.getId(), 213 | token, 214 | {'from': instance.getOwner()}) 215 | 216 | # 5) instance owner creates and sets product fee spec 217 | fixedFee = 3 218 | fractionalFee = instanceService.getFeeFractionFullUnit() / 10 # corresponds to 10% 219 | feeSpec = operatorService.createFeeSpecification( 220 | self.product.getId(), 221 | fixedFee, 222 | fractionalFee, 223 | b'', 224 | {'from': instance.getOwner()}) 225 | 226 | operatorService.setPremiumFees( 227 | feeSpec, 228 | {'from': instance.getOwner()}) 229 | 230 | 231 | def getId(self) -> int: 232 | return self.product.getId() 233 | 234 | def getOracle(self) -> GifTestOracle: 235 | return self.oracle 236 | 237 | def getRiskpool(self) -> GifTestRiskpool: 238 | return self.riskpool 239 | 240 | def getContract(self) -> TestProduct: 241 | return self.product 242 | 243 | def getPolicy(self, policyId: str): 244 | return self.policy.getPolicy(policyId) 245 | -------------------------------------------------------------------------------- /contracts/services/InstanceOperatorService.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../modules/AccessController.sol"; 5 | import "../modules/BundleController.sol"; 6 | import "../modules/ComponentController.sol"; 7 | import "../modules/PoolController.sol"; 8 | import "../modules/TreasuryModule.sol"; 9 | import "../shared/CoreController.sol"; 10 | import "../test/TestProduct.sol"; 11 | import "../tokens/BundleToken.sol"; 12 | 13 | import "@etherisc/gif-interface/contracts/components/IComponent.sol"; 14 | import "@etherisc/gif-interface/contracts/components/IProduct.sol"; 15 | import "@etherisc/gif-interface/contracts/modules/IQuery.sol"; 16 | import "@etherisc/gif-interface/contracts/modules/ITreasury.sol"; 17 | import "@etherisc/gif-interface/contracts/services/IInstanceOperatorService.sol"; 18 | 19 | import "@openzeppelin/contracts/access/Ownable.sol"; 20 | 21 | contract InstanceOperatorService is 22 | IInstanceOperatorService, 23 | CoreController, 24 | Ownable 25 | { 26 | ComponentController private _component; 27 | PoolController private _pool; 28 | TreasuryModule private _treasury; 29 | 30 | modifier onlyInstanceOperatorAddress() { 31 | require(owner() == _msgSender(), "ERROR:IOS-001:NOT_INSTANCE_OPERATOR"); 32 | _; 33 | } 34 | 35 | function _afterInitialize() internal override onlyInitializing { 36 | _component = ComponentController(_getContractAddress("Component")); 37 | _pool = PoolController(_getContractAddress("Pool")); 38 | _treasury = TreasuryModule(_getContractAddress("Treasury")); 39 | 40 | _transferOwnership(_msgSender()); 41 | _linkBundleModuleToBundleToken(); 42 | _setDefaultAdminRole(); 43 | } 44 | 45 | function _setDefaultAdminRole() private { 46 | AccessController access = AccessController(_getContractAddress("Access")); 47 | access.setDefaultAdminRole(address(this)); 48 | } 49 | 50 | function _linkBundleModuleToBundleToken() private { 51 | BundleToken token = BundleToken(_getContractAddress("BundleToken")); 52 | address bundleAddress = _getContractAddress("Bundle"); 53 | token.setBundleModule(bundleAddress); 54 | } 55 | 56 | /* registry */ 57 | function prepareRelease(bytes32 _newRelease) 58 | external override 59 | onlyInstanceOperatorAddress 60 | { 61 | _registry.prepareRelease(_newRelease); 62 | } 63 | 64 | function register(bytes32 _contractName, address _contractAddress) 65 | external override 66 | onlyInstanceOperatorAddress 67 | { 68 | _registry.register(_contractName, _contractAddress); 69 | } 70 | 71 | function deregister(bytes32 _contractName) 72 | external override 73 | onlyInstanceOperatorAddress 74 | { 75 | _registry.deregister(_contractName); 76 | } 77 | 78 | function registerInRelease( 79 | bytes32 _release, 80 | bytes32 _contractName, 81 | address _contractAddress 82 | ) 83 | external override 84 | onlyInstanceOperatorAddress 85 | { 86 | _registry.registerInRelease(_release, _contractName, _contractAddress); 87 | } 88 | 89 | function deregisterInRelease(bytes32 _release, bytes32 _contractName) 90 | external override 91 | onlyInstanceOperatorAddress 92 | { 93 | _registry.deregisterInRelease(_release, _contractName); 94 | } 95 | 96 | /* access */ 97 | function createRole(bytes32 _role) 98 | external override 99 | onlyInstanceOperatorAddress 100 | { 101 | _access.addRole(_role); 102 | } 103 | 104 | function invalidateRole(bytes32 _role) 105 | external override 106 | onlyInstanceOperatorAddress 107 | { 108 | _access.invalidateRole(_role); 109 | } 110 | 111 | function grantRole(bytes32 role, address principal) 112 | external override 113 | onlyInstanceOperatorAddress 114 | { 115 | _access.grantRole(role, principal); 116 | } 117 | 118 | function revokeRole(bytes32 role, address principal) 119 | external override 120 | onlyInstanceOperatorAddress 121 | { 122 | _access.revokeRole(role, principal); 123 | } 124 | 125 | /* component */ 126 | function approve(uint256 id) 127 | external override 128 | onlyInstanceOperatorAddress 129 | { 130 | _component.approve(id); 131 | 132 | if (_component.isProduct(id)) { 133 | IComponent component = _component.getComponent(id); 134 | IProduct product = IProduct(address(component)); 135 | 136 | _pool.setRiskpoolForProduct( 137 | id, 138 | product.getRiskpoolId()); 139 | } 140 | } 141 | 142 | function decline(uint256 id) 143 | external override 144 | onlyInstanceOperatorAddress 145 | { 146 | _component.decline(id); 147 | } 148 | 149 | function suspend(uint256 id) 150 | external override 151 | onlyInstanceOperatorAddress 152 | { 153 | _component.suspend(id); 154 | } 155 | 156 | function resume(uint256 id) 157 | external override 158 | onlyInstanceOperatorAddress 159 | { 160 | _component.resume(id); 161 | } 162 | 163 | function archive(uint256 id) 164 | external override 165 | onlyInstanceOperatorAddress 166 | { 167 | _component.archiveFromInstanceOperator(id); 168 | } 169 | 170 | // service staking 171 | // TODO implement setDefaultStaking staking 172 | function setDefaultStaking( 173 | uint16 componentType, 174 | bytes calldata data 175 | ) 176 | external override 177 | onlyInstanceOperatorAddress 178 | { 179 | revert("ERROR:IOS-010:IMPLEMENATION_MISSING"); 180 | } 181 | 182 | // TODO implement adjustStakingRequirements staking 183 | function adjustStakingRequirements( 184 | uint256 id, 185 | bytes calldata data 186 | ) 187 | external override 188 | onlyInstanceOperatorAddress 189 | { 190 | revert("ERROR:IOS-011:IMPLEMENATION_MISSING"); 191 | } 192 | 193 | /* treasury */ 194 | function suspendTreasury() 195 | external override 196 | onlyInstanceOperatorAddress 197 | { 198 | _treasury.suspend(); 199 | } 200 | 201 | function resumeTreasury() 202 | external override 203 | onlyInstanceOperatorAddress 204 | { 205 | _treasury.resume(); 206 | } 207 | 208 | function setInstanceWallet(address walletAddress) 209 | external override 210 | onlyInstanceOperatorAddress 211 | { 212 | _treasury.setInstanceWallet(walletAddress); 213 | } 214 | 215 | function setRiskpoolWallet(uint256 riskpoolId, address riskpoolWalletAddress) 216 | external override 217 | onlyInstanceOperatorAddress 218 | { 219 | _treasury.setRiskpoolWallet(riskpoolId, riskpoolWalletAddress); 220 | } 221 | 222 | function setProductToken(uint256 productId, address erc20Address) 223 | external override 224 | onlyInstanceOperatorAddress 225 | { 226 | _treasury.setProductToken(productId, erc20Address); 227 | } 228 | 229 | function createFeeSpecification( 230 | uint256 componentId, 231 | uint256 fixedFee, 232 | uint256 fractionalFee, 233 | bytes calldata feeCalculationData 234 | ) 235 | external override 236 | view 237 | returns(ITreasury.FeeSpecification memory) 238 | { 239 | return _treasury.createFeeSpecification( 240 | componentId, 241 | fixedFee, 242 | fractionalFee, 243 | feeCalculationData 244 | ); 245 | } 246 | 247 | function setPremiumFees(ITreasury.FeeSpecification calldata feeSpec) 248 | external override 249 | onlyInstanceOperatorAddress 250 | { 251 | _treasury.setPremiumFees(feeSpec); 252 | } 253 | 254 | function setCapitalFees(ITreasury.FeeSpecification calldata feeSpec) 255 | external override 256 | onlyInstanceOperatorAddress 257 | { 258 | _treasury.setCapitalFees(feeSpec); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /contracts/modules/RegistryController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity 0.8.2; 3 | 4 | import "../shared/CoreController.sol"; 5 | 6 | import "@etherisc/gif-interface/contracts/modules/IRegistry.sol"; 7 | 8 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 9 | import "@openzeppelin/contracts/utils/Strings.sol"; 10 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 11 | 12 | 13 | contract RegistryController is 14 | IRegistry, 15 | CoreController 16 | { 17 | using EnumerableSet for EnumerableSet.Bytes32Set; 18 | 19 | /** 20 | * @dev Save number of items to iterate through 21 | * Currently we have < 20 contracts. 22 | */ 23 | uint256 public constant MAX_CONTRACTS = 100; 24 | 25 | /** 26 | * @dev Current release 27 | * We use semantic versioning. 28 | */ 29 | bytes32 public release; 30 | 31 | uint256 public startBlock; 32 | 33 | mapping(bytes32 /* release */ => mapping(bytes32 /* contract name */ => address /* contract address */)) public _contracts; 34 | mapping(bytes32 /* release */ => uint256 /* number of contracts in release */) public _contractsInRelease; 35 | mapping(bytes32 /* release */ => EnumerableSet.Bytes32Set /* contract names */) private _contractNames; 36 | 37 | function initializeRegistry(bytes32 _initialRelease) public initializer { 38 | // _setupRegistry(address(this)); 39 | _registry = this; 40 | 41 | // this is a temporary assignment and must only be used 42 | // during the intial setup of a gif instance 43 | // at execution time _msgSender is the address of the 44 | // registry proxy. 45 | release = _initialRelease; 46 | _contracts[release]["InstanceOperatorService"] = _msgSender(); 47 | EnumerableSet.add(_contractNames[release], "InstanceOperatorService"); 48 | _contractsInRelease[release] = 1; 49 | 50 | 51 | // register the deployment block for reading logs 52 | startBlock = block.number; 53 | } 54 | 55 | function ensureSender(address sender, bytes32 _contractName) 56 | external view override 57 | returns(bool _senderMatches) 58 | { 59 | _senderMatches = (sender == _getContractInRelease(release, _contractName)); 60 | } 61 | 62 | /** 63 | * @dev get current release 64 | */ 65 | function getRelease() 66 | external override view 67 | returns (bytes32 _release) 68 | { 69 | _release = release; 70 | } 71 | 72 | /** 73 | * @dev Get contract's address in the current release 74 | */ 75 | function getContract(bytes32 _contractName) 76 | public override view 77 | returns (address _addr) 78 | { 79 | _addr = _getContractInRelease(release, _contractName); 80 | } 81 | 82 | /** 83 | * @dev Register contract in the current release 84 | */ 85 | function register(bytes32 _contractName, address _contractAddress) 86 | external override 87 | onlyInstanceOperator 88 | { 89 | _registerInRelease(release, false, _contractName, _contractAddress); 90 | } 91 | 92 | /** 93 | * @dev Deregister contract in the current release 94 | */ 95 | function deregister(bytes32 _contractName) 96 | external override 97 | onlyInstanceOperator 98 | { 99 | _deregisterInRelease(release, _contractName); 100 | } 101 | 102 | /** 103 | * @dev Get contract's address in certain release 104 | */ 105 | function getContractInRelease(bytes32 _release, bytes32 _contractName) 106 | external override view 107 | returns (address _addr) 108 | { 109 | _addr = _getContractInRelease(_release, _contractName); 110 | } 111 | 112 | /** 113 | * @dev Register contract in certain release 114 | */ 115 | function registerInRelease(bytes32 _release, bytes32 _contractName, address _contractAddress) 116 | external override 117 | onlyInstanceOperator 118 | { 119 | _registerInRelease(_release, false, _contractName, _contractAddress); 120 | } 121 | 122 | function deregisterInRelease(bytes32 _release, bytes32 _contractName) 123 | external override 124 | onlyInstanceOperator 125 | { 126 | _deregisterInRelease(_release, _contractName); 127 | } 128 | 129 | /** 130 | * @dev Create new release, copy contracts from previous release 131 | */ 132 | function prepareRelease(bytes32 _newRelease) 133 | external override 134 | onlyInstanceOperator 135 | { 136 | uint256 countContracts = _contractsInRelease[release]; 137 | 138 | require(countContracts > 0, "ERROR:REC-001:EMPTY_RELEASE"); 139 | require( 140 | _contractsInRelease[_newRelease] == 0, 141 | "ERROR:REC-002:NEW_RELEASE_NOT_EMPTY" 142 | ); 143 | 144 | // TODO think about how to avoid this loop 145 | for (uint256 i = 0; i < countContracts; i += 1) { 146 | bytes32 name = EnumerableSet.at(_contractNames[release], i); 147 | _registerInRelease( 148 | _newRelease, 149 | true, 150 | name, 151 | _contracts[release][name] 152 | ); 153 | } 154 | 155 | release = _newRelease; 156 | 157 | emit LogReleasePrepared(release); 158 | } 159 | 160 | function contracts() external override view returns (uint256 _numberOfContracts) { 161 | _numberOfContracts = EnumerableSet.length(_contractNames[release]); 162 | } 163 | 164 | function contractName(uint256 idx) external override view returns (bytes32 _contractName) { 165 | _contractName = EnumerableSet.at(_contractNames[release], idx); 166 | } 167 | 168 | /** 169 | * @dev Get contract's address in certain release 170 | */ 171 | function _getContractInRelease(bytes32 _release, bytes32 _contractName) 172 | internal view 173 | returns (address _addr) 174 | { 175 | _addr = _contracts[_release][_contractName]; 176 | } 177 | 178 | /** 179 | * @dev Register contract in certain release 180 | */ 181 | function _registerInRelease( 182 | bytes32 _release, 183 | bool isNewRelease, 184 | bytes32 _contractName, 185 | address _contractAddress 186 | ) 187 | internal 188 | { 189 | bool isNew = false; 190 | 191 | require( 192 | EnumerableSet.length(_contractNames[_release]) < MAX_CONTRACTS, 193 | "ERROR:REC-010:MAX_CONTRACTS_LIMIT" 194 | ); 195 | 196 | // during `prepareRelease` the _release is not yet known, so check should not fail in this case 197 | require(_contractsInRelease[_release] > 0 || isNewRelease, "ERROR:REC-011:RELEASE_UNKNOWN"); 198 | require(_contractName != 0x00, "ERROR:REC-012:CONTRACT_NAME_EMPTY"); 199 | require( 200 | (! EnumerableSet.contains(_contractNames[_release], _contractName) ) 201 | // the contract 'InstanceOperatorService' is initially registered with the owner address (see method initializeRegistry()); 202 | // due to this this special check is required 203 | || (_contractName == "InstanceOperatorService" && _contracts[_release][_contractName] == _msgSender()), 204 | "ERROR:REC-013:CONTRACT_NAME_EXISTS"); 205 | require(_contractAddress != address(0), "ERROR:REC-014:CONTRACT_ADDRESS_ZERO"); 206 | 207 | if (_contracts[_release][_contractName] == address(0)) { 208 | EnumerableSet.add(_contractNames[_release], _contractName); 209 | _contractsInRelease[_release]++; 210 | isNew = true; 211 | } 212 | 213 | _contracts[_release][_contractName] = _contractAddress; 214 | require( 215 | _contractsInRelease[_release] == EnumerableSet.length(_contractNames[_release]), 216 | "ERROR:REC-015:CONTRACT_NUMBER_MISMATCH" 217 | ); 218 | 219 | emit LogContractRegistered( 220 | _release, 221 | _contractName, 222 | _contractAddress, 223 | isNew 224 | ); 225 | } 226 | 227 | 228 | /** 229 | * @dev Deregister contract in certain release 230 | */ 231 | function _deregisterInRelease(bytes32 _release, bytes32 _contractName) 232 | internal 233 | onlyInstanceOperator 234 | { 235 | require(EnumerableSet.contains(_contractNames[_release], _contractName), "ERROR:REC-020:CONTRACT_UNKNOWN"); 236 | 237 | EnumerableSet.remove(_contractNames[_release], _contractName); 238 | 239 | _contractsInRelease[_release] -= 1; 240 | delete _contracts[_release][_contractName]; 241 | 242 | require( 243 | _contractsInRelease[_release] == EnumerableSet.length(_contractNames[_release]), 244 | "ERROR:REC-021:CONTRACT_NUMBER_MISMATCH"); 245 | emit LogContractDeregistered(_release, _contractName); 246 | } 247 | } 248 | --------------------------------------------------------------------------------