├── .nvm ├── tests ├── __init__.py ├── test_xchain.py ├── lib │ ├── test_bls.py │ ├── test_snapshot.py │ └── test_umbral_deserializer.py ├── conftest.py └── application │ └── conftest.py ├── deployment ├── __init__.py ├── networks.py ├── constructor_params │ ├── lynx │ │ ├── free-fee-model.yml │ │ ├── reimbursement-pool.yml │ │ ├── upgrade-coordinator.yml │ │ ├── upgrades-child.yml │ │ ├── child.yml │ │ ├── upgrades-root.yml │ │ ├── root.yml │ │ └── bqeth.yml │ ├── mainnet │ │ ├── free-fee-model.yml │ │ ├── redeploy-taco-child.yml │ │ ├── redeploy-coordinator.yml │ │ ├── reimbursement-pool.yml │ │ ├── legacy.yml │ │ ├── redeploy-taco-app.yml │ │ ├── repair-child.yml │ │ ├── redeploy-marlin.yml │ │ ├── redeploy-bqeth.yml │ │ ├── root.yml │ │ ├── child.yml │ │ ├── bqeth.yml │ │ └── new-subscription.yml │ ├── tapir │ │ ├── free-fee-model.yml │ │ ├── upgrade-mock-polygon-root.yml │ │ ├── upgrade-taco-child-app.yml │ │ ├── upgrade-coordinator.yml │ │ ├── upgrade-child.yml │ │ ├── upgrade-root.yml │ │ ├── child.yml │ │ ├── root.yml │ │ └── bqeth.yml │ ├── open_access_authorizer.yml │ ├── subscription_manager.yml │ ├── ci │ │ └── child.yml │ └── dashboard │ │ └── root.yml ├── types.py ├── options.py ├── confirm.py ├── constants.py └── legacy.py ├── .npmignore ├── setup.py ├── .gitattributes ├── tsconfig.build.json ├── .prettierrc.json ├── .github ├── .secrets.act.template ├── .env.lynx.act ├── scripts │ ├── eval_dkg.sh │ ├── import_account.py │ └── initiate_dkg.sh ├── workflows │ ├── npm.yml │ ├── evaluate.yml │ ├── heartbeat.yml │ └── main.yaml ├── README.md └── PULL_REQUEST_TEMPLATE.md ├── .solhintignore ├── .prettierignore ├── tsconfig.json ├── .gitignore ├── contracts ├── test │ ├── Dummy.sol │ ├── TestToken.sol │ ├── ReceiveApprovalMethodMock.sol │ ├── EncryptionAuthorizerTestSet.sol │ ├── CoordinatorTestSet.sol │ ├── StandardSubscriptionTestSet.sol │ ├── AdjudicatorTestSet.sol │ └── TACoChildApplicationTestSet.sol ├── contracts │ ├── coordination │ │ ├── IReimbursementPool.sol │ │ ├── IEncryptionAuthorizer.sol │ │ ├── ITACoChildToRoot.sol │ │ ├── ITACoRootToChild.sol │ │ ├── IFeeModel.sol │ │ ├── FreeFeeModel.sol │ │ └── subscription │ │ │ ├── AbstractSubscription.sol │ │ │ └── EncryptorSlotsSubscription.sol │ ├── testnet │ │ ├── OpenAccessAuthorizer.sol │ │ └── TapirSet.sol │ ├── lib │ │ ├── LookupKey.sol │ │ ├── Bits.sol │ │ └── BLS12381.sol │ └── TestnetThresholdStaking.sol ├── aragon │ └── interfaces │ │ └── IERC900History.sol ├── threshold │ ├── ITACoChildApplication.sol │ ├── IApplicationWithOperator.sol │ └── IApplicationWithDecreaseDelay.sol └── xchain │ ├── PolygonRoot.sol │ └── PolygonChild.sol ├── pyproject.toml ├── scripts ├── normalize_registry.py ├── mainnet │ ├── convert_artifacts.py │ ├── deploy_legacy.py │ ├── deploy_free_fee_model.py │ ├── redeploy_subscription.py │ ├── redeploy_coordinator.py │ ├── redeploy_taco_app.py │ ├── redeploy_taco_child.py │ ├── deploy_standard_subscription.py │ ├── deploy_reimbursement_pool.py │ ├── repair_child.py │ ├── deploy_root.py │ └── deploy_child.py ├── dashboard │ ├── convert_artifacts.py │ └── deploy_root.py ├── lynx │ ├── deploy_free_fee_model.py │ ├── upgrade_root.py │ ├── coordinator_approve_free_fee_model.py │ ├── coordinator_sets_reimbursement_pool.py │ ├── confirm_operator_addresses.py │ ├── convert_registries.py │ ├── deploy_reimbursement_pool.py │ ├── deploy_standard_subscription.py │ ├── upgrade_child.py │ ├── coordinator_approve_standard_subscription.py │ ├── upgrade_coordinator.py │ ├── deploy_root.py │ ├── deploy_child.py │ └── configure_staking.py ├── tapir │ ├── deploy_standard_subscription.py │ ├── coordinator_approve_free_fee_model.py │ ├── confirm_operator_addresses.py │ ├── upgrade_taco_child_app.py │ ├── deploy_free_fee_model.py │ ├── upgrade_mock_polygon_root.py │ ├── coordinator_approve_standard_subscription.py │ ├── upgrade_root.py │ ├── upgrade_child.py │ ├── upgrade_coordinator.py │ ├── deploy_root.py │ ├── deploy_child.py │ └── configure_staking.py ├── deploy_subscription_manager.py ├── deploy_open_access_authorizer.py ├── merge_registries.py ├── derive_accounts.py ├── simulate_coordinator_upgrade.py ├── cancel_handover.py ├── request_handover.py ├── list_contracts.py ├── ritual_membership.py ├── verify.py └── ci │ └── deploy_child.py ├── .eslintrc.json ├── Pipfile ├── tox.ini ├── setup.cfg ├── .pre-commit-config.yaml ├── package.json ├── ape-config.yaml ├── .solhint.json ├── test └── registry.spec.ts └── src └── index.ts /.nvm: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /deployment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_xchain.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude the "test" directory 2 | /test -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-solidity"], 3 | "printWidth": 100 4 | } 5 | -------------------------------------------------------------------------------- /deployment/networks.py: -------------------------------------------------------------------------------- 1 | from ape import networks 2 | 3 | 4 | def is_local_network(): 5 | return networks.network.name in ["local"] 6 | -------------------------------------------------------------------------------- /.github/.secrets.act.template: -------------------------------------------------------------------------------- 1 | DKG_INITIATOR_PRIVATE_KEY= 2 | DKG_INITIATOR_PASSPHRASE= 3 | RPC_PROVIDER= 4 | WEB3_INFURA_API_KEY= 5 | POLYGONSCAN_API_KEY= 6 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | **/contracts/contracts/proxy/ 2 | **/contracts/contracts/StakingEscrow.sol 3 | **/contracts/contracts/NuCypherToken.sol 4 | **/contracts/test/proxy/ 5 | **/contracts/test/StakingEscrowTestSet.sol 6 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/free-fee-model.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: lynx-free-fee-model 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: free-fee-model.json 8 | 9 | contracts: 10 | - FreeFeeModel 11 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/free-fee-model.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: mainnet-free-fee-model 3 | chain_id: 137 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: free-fee-model.json 8 | 9 | contracts: 10 | - FreeFeeModel 11 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/free-fee-model.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-free-fee-model 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir-free-fee-model.json 8 | 9 | contracts: 10 | - FreeFeeModel 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/contracts/contracts/proxy/ 2 | **/contracts/contracts/StakingEscrow.sol 3 | **/contracts/contracts/NuCypherToken.sol 4 | **/contracts/test/proxy/ 5 | **/contracts/test/StakingEscrowTestSet.sol 6 | **/.cache 7 | **/deployment/artifacts/*.json 8 | Pipfile.lock 9 | -------------------------------------------------------------------------------- /deployment/constructor_params/open_access_authorizer.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: open-access-authorizer 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: open_access_authorizer.json 8 | 9 | contracts: 10 | - OpenAccessAuthorizer 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "resolveJsonModule": true, 8 | "esModuleInterop": true 9 | }, 10 | "include": ["src", "test"] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .env 3 | .history 4 | .hypothesis/ 5 | .tox/ 6 | build/ 7 | reports/ 8 | venv/ 9 | .python-version 10 | bin/ 11 | .vscode/ 12 | .idea/ 13 | node_modules/ 14 | .build 15 | contracts/.cache 16 | env 17 | .cache/ 18 | dist/ 19 | .cosine/ 20 | .github/.secrets.act 21 | -------------------------------------------------------------------------------- /contracts/test/Dummy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract Dummy { 6 | // solhint-disable-next-line no-empty-blocks 7 | receive() external payable {} 8 | 9 | // solhint-disable-next-line no-empty-blocks 10 | fallback() external payable {} 11 | } 12 | -------------------------------------------------------------------------------- /.github/.env.lynx.act: -------------------------------------------------------------------------------- 1 | ACTIONS_RUNTIME_TOKEN=dummy 2 | ACTIONS_RUNTIME_URL=dummy ACT=true act 3 | DKG_AUTHORITY_ADDRESS=0x3B42d26E19FF860bC4dEbB920DD8caA53F93c600 4 | ECOSYSTEM=polygon 5 | NETWORK=amoy 6 | DOMAIN=lynx 7 | DURATION=86400 8 | ACCESS_CONTROLLER=GlobalAllowList 9 | FEE_MODEL=0x14EB9BB700E45D2Ee9233056b8cc341276c688Ba 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py36', 'py37', 'py38'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.eggs 8 | | \.git 9 | | \.hg 10 | | \.mypy_cache 11 | | \.tox 12 | | \.venv 13 | | _build 14 | | buck-out 15 | | build 16 | | dist 17 | | env 18 | | venv 19 | )/ 20 | ''' 21 | -------------------------------------------------------------------------------- /.github/scripts/eval_dkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Heartbeat: Evaluate Ritual" 4 | 5 | echo "ECOSYSTEM: ${ECOSYSTEM}" 6 | echo "NETWORK: ${NETWORK}" 7 | echo "DOMAIN: ${DOMAIN}" 8 | 9 | ape run evaluate_heartbeat \ 10 | --domain ${DOMAIN} \ 11 | --network ${ECOSYSTEM}:${NETWORK}:${RPC_PROVIDER} 12 | 13 | echo "All Heartbeat Rituals Evaluated" 14 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/IReimbursementPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @title IReimbursementPool 7 | * @notice IReimbursementPool 8 | */ 9 | interface IReimbursementPool { 10 | function isAuthorized(address caller) external view returns (bool); 11 | 12 | function refund(uint256 gasSpent, address receiver) external; 13 | } 14 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/IEncryptionAuthorizer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IEncryptionAuthorizer { 6 | function isAuthorized( 7 | uint32 ritualId, 8 | bytes memory evidence, // supporting evidence for authorization 9 | bytes memory ciphertextHeader // data to be signed by authorized 10 | ) external view returns (bool); 11 | } 12 | -------------------------------------------------------------------------------- /deployment/constructor_params/subscription_manager.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: lynx-subscription-manager 3 | chain_id: 80002 # amoy 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: subscription-manager.json 8 | 9 | constants: 10 | FEE_RATE_PER_SECOND: 1000000000 11 | 12 | contracts: 13 | - SubscriptionManager: 14 | proxy: 15 | constructor: 16 | _data: $encode:initialize,$FEE_RATE_PER_SECOND 17 | -------------------------------------------------------------------------------- /contracts/contracts/testnet/OpenAccessAuthorizer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../coordination/IEncryptionAuthorizer.sol"; 6 | 7 | contract OpenAccessAuthorizer is IEncryptionAuthorizer { 8 | function isAuthorized( 9 | uint32, 10 | bytes memory, 11 | bytes memory 12 | ) external pure override returns (bool) { 13 | return true; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scripts/normalize_registry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from pathlib import Path 3 | 4 | import click 5 | from deployment.registry import normalize_registry 6 | 7 | 8 | @click.command() 9 | @click.option( 10 | "--registry", 11 | help="Filepath to registry file", 12 | type=click.Path(dir_okay=False, exists=True, path_type=Path), 13 | required=True, 14 | ) 15 | def cli(registry): 16 | """Normalize registry file""" 17 | normalize_registry(registry) 18 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/upgrade-mock-polygon-root.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-mock-polygon-root-upgrade 3 | chain_id: 11155111 # sepolia 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir-mock-polygon-root-upgrade.json 8 | 9 | constants: 10 | TACO_APPLICATION: "0xCcFf527698E78a536d80695D9Af4F4f3265ADA05" 11 | 12 | 13 | contracts: 14 | - MockPolygonRoot: 15 | constructor: 16 | _rootApplication: $TACO_APPLICATION 17 | -------------------------------------------------------------------------------- /contracts/aragon/interfaces/IERC900History.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | // Minimum interface to interact with Aragon's Aggregator 6 | interface IERC900History { 7 | function totalStakedForAt(address addr, uint256 blockNumber) external view returns (uint256); 8 | 9 | function totalStakedAt(uint256 blockNumber) external view returns (uint256); 10 | 11 | function supportsHistory() external pure returns (bool); 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to NPM 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: "18.x" 14 | registry-url: "https://registry.npmjs.org" 15 | - run: npm install ; npm publish 16 | env: 17 | NODE_AUTH_TOKEN: ${{ secrets.NPM_API_KEY }} 18 | -------------------------------------------------------------------------------- /contracts/contracts/lib/LookupKey.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library LookupKey { 5 | /** 6 | * @notice Returns the key used to lookup authorizations 7 | * @param ritualId The ID of the ritual 8 | * @param encryptor The address of the encryptor 9 | * @return The key used to lookup authorizations 10 | */ 11 | function lookupKey(uint32 ritualId, address encryptor) internal pure returns (bytes32) { 12 | return keccak256(abi.encodePacked(ritualId, encryptor)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/reimbursement-pool.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: reimbursement-lynx 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: reimbursement.json 8 | 9 | constants: 10 | STATIC_GAS: 21000 # tx base cost + refund function gas. For the moment let's just include tx base cost 11 | MAX_GAS_PRICE: 1234000000000 # 1234 gwei (dummy value for now) 12 | 13 | contracts: 14 | - ReimbursementPool: 15 | constructor: 16 | _staticGas: $STATIC_GAS 17 | _maxGasPrice: $MAX_GAS_PRICE 18 | -------------------------------------------------------------------------------- /contracts/contracts/testnet/TapirSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract TapirRitualToken is ERC20("TapirRitualToken", "TRT") { 8 | constructor(uint256 _totalSupplyOfTokens) { 9 | _mint(msg.sender, _totalSupplyOfTokens); 10 | } 11 | } 12 | 13 | contract TapirStakingToken is ERC20("TapirStakingToken", "TST") { 14 | constructor(uint256 _totalSupplyOfTokens) { 15 | _mint(msg.sender, _totalSupplyOfTokens); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/threshold/ITACoChildApplication.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../contracts/coordination/ITACoChildToRoot.sol"; 6 | 7 | interface ITACoChildApplication is ITACoChildToRoot { 8 | function operatorToStakingProvider(address _operator) external view returns (address); 9 | 10 | function authorizedStake(address _stakingProvider) external view returns (uint96); 11 | 12 | function minimumAuthorization() external view returns (uint96); 13 | //TODO: Function to get locked stake duration? 14 | } 15 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/upgrade-taco-child-app.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-taco-child-app-upgrade 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir-taco-child-app-upgrade.json 8 | 9 | constants: 10 | MOCK_POLYGON_CHILD: "0x469fBc4737f4d502d46B393E9a625EF754672644" 11 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 12 | 13 | contracts: 14 | - TACoChildApplication: 15 | constructor: 16 | _rootApplication: $MOCK_POLYGON_CHILD 17 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 18 | -------------------------------------------------------------------------------- /scripts/mainnet/convert_artifacts.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from deployment.confirm import _continue 4 | from deployment.constants import ARTIFACTS_DIR 5 | from deployment.legacy import convert_legacy_npm_artifacts 6 | 7 | INPUT_DIR = Path(__file__).parent.parent.parent / "artifacts" 8 | OUTPUT_FILEPATH = ARTIFACTS_DIR / "mainnet.json" 9 | 10 | 11 | def main(): 12 | print(f"input_dir: {INPUT_DIR.absolute()}") 13 | _continue() 14 | 15 | convert_legacy_npm_artifacts( 16 | directory=INPUT_DIR, 17 | chain_id=1, 18 | output_filepath=OUTPUT_FILEPATH 19 | ) 20 | -------------------------------------------------------------------------------- /scripts/mainnet/deploy_legacy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "legacy.yml" 10 | 11 | 12 | def main(): 13 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 14 | 15 | staking_escrow = deployer.deploy(project.StakingEscrow) 16 | 17 | deployments = [ 18 | staking_escrow, 19 | ] 20 | 21 | deployer.finalize(deployments=deployments) 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "prettier", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "overrides": [ 13 | { 14 | "env": { 15 | "node": true 16 | }, 17 | "files": [ 18 | ".eslintrc.{js,cjs}" 19 | ], 20 | "parserOptions": { 21 | "sourceType": "script" 22 | } 23 | } 24 | ], 25 | "parserOptions": { 26 | "ecmaVersion": "latest", 27 | "sourceType": "module" 28 | }, 29 | "rules": {} 30 | } 31 | -------------------------------------------------------------------------------- /scripts/mainnet/deploy_free_fee_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | 10 | VERIFY = False 11 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "free-fee-model.yml" 12 | 13 | 14 | def main(): 15 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 16 | 17 | free_fee_model = deployer.deploy(project.FreeFeeModel) 18 | 19 | deployments = [free_fee_model] 20 | 21 | deployer.finalize(deployments=deployments) 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | black = "*" 8 | coincurve = "*" 9 | cryptography = "*" 10 | eth-ape = "*" 11 | ape-solidity = ">=0.8.4" 12 | ape-etherscan = {git = "https://github.com/derekpierre/ape-etherscan.git", ref = "flimsy-verification"} 13 | ape-polygon = "*" 14 | ape-infura = "*" 15 | ape-foundry = "*" 16 | flake8 = "*" 17 | isort = "*" 18 | nucypher-core = "==0.15.0" 19 | pre-commit = "*" 20 | tox = "*" 21 | pyyaml = "*" 22 | bump2version = "*" 23 | web3 = "<7" 24 | 25 | [dev-packages] 26 | 27 | [requires] 28 | python_version = "3" 29 | -------------------------------------------------------------------------------- /contracts/test/TestToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | /** 8 | * @notice Token for testing 9 | */ 10 | contract TestToken is ERC20("Test", "TT") { 11 | constructor(uint256 _totalSupplyOfTokens) { 12 | _mint(msg.sender, _totalSupplyOfTokens); 13 | } 14 | 15 | function mint(address account, uint256 value) external { 16 | _mint(account, value); 17 | } 18 | 19 | function burn(address account, uint256 value) external { 20 | _burn(account, value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/upgrade-coordinator.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: lynx-upgrade-coordinator 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: lynx-upgrade-coordinator.json 8 | 9 | constants: 10 | TACO_CHILD_APPLICATION: "0x42F30AEc1A36995eEFaf9536Eb62BD751F982D32" 11 | DKG_TIMEOUT_SECONDS: 3600 # 1 hour 12 | HANDOVER_TIMEOUT_SECONDS: 900 # 15 minutes 13 | 14 | contracts: 15 | - Coordinator: 16 | constructor: 17 | _application: $TACO_CHILD_APPLICATION 18 | _dkgTimeout: $DKG_TIMEOUT_SECONDS 19 | _handoverTimeout: $HANDOVER_TIMEOUT_SECONDS 20 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/upgrade-coordinator.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-upgrade-coordinator 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir-upgrade-coordinator.json 8 | 9 | constants: 10 | TACO_CHILD_APPLICATION: "0x489287Ed5BdF7a35fEE411FBdCc47331093D0769" 11 | DKG_TIMEOUT_SECONDS: 3600 # 1 hour 12 | HANDOVER_TIMEOUT_SECONDS: 900 # 15 minutes 13 | 14 | contracts: 15 | - Coordinator: 16 | constructor: 17 | _application: $TACO_CHILD_APPLICATION 18 | _dkgTimeout: $DKG_TIMEOUT_SECONDS 19 | _handoverTimeout: $HANDOVER_TIMEOUT_SECONDS 20 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/redeploy-taco-child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: mainnet-redeploy-taco-child 3 | chain_id: 137 # Polygon Mainnet 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: mainnet-redeploy-taco-child.json 8 | 9 | constants: 10 | # See deployment/artifacts/mainnet.json 11 | POLYGON_CHILD: "0x1f5C5fd6A66723fA22a778CC53263dd3FA6851E5" 12 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 13 | 14 | contracts: 15 | - TACoChildApplication: 16 | constructor: 17 | _rootApplication: $POLYGON_CHILD 18 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 19 | -------------------------------------------------------------------------------- /scripts/dashboard/convert_artifacts.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from deployment.confirm import _continue 4 | from deployment.constants import ARTIFACTS_DIR 5 | from deployment.legacy import convert_legacy_npm_artifacts 6 | 7 | INPUT_DIR = Path(__file__).parent.parent.parent / "deployment" / "artifacts" / "old" / "dashboard" 8 | OUTPUT_FILEPATH = ARTIFACTS_DIR / "dashboard.json" 9 | 10 | 11 | def main(): 12 | print(f"input_dir: {INPUT_DIR.absolute()}") 13 | _continue() 14 | 15 | convert_legacy_npm_artifacts( 16 | directory=INPUT_DIR, 17 | chain_id=5, 18 | output_filepath=OUTPUT_FILEPATH 19 | ) 20 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # How to run the DKG workflow locally with act 2 | 3 | To run the DKG workflow locally, you need to have act installed. 4 | 5 | https://github.com/nektos/act 6 | 7 | Verify the values in `.env.lynx.act` and `.secrets` file completed with the 8 | necessary environment variables (see the template files in the same directory `.secrets.act.template`). 9 | 10 | Then you can run the following command: 11 | 12 | ```bash 13 | act workflow_dispatch -j initiate_dkg \ 14 | --var-file .github/.env.lynx.act \ 15 | --secret-file .github/.secrets.act \ 16 | --container-architecture linux/amd64 \ 17 | --artifact-server-path /tmp/artifacts 18 | ``` 19 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | lint 4 | tests 5 | skipsdist=True 6 | 7 | [flake8] 8 | max-line-length = 100 9 | ignore = E203,W503,F403,F405 10 | 11 | [testenv] 12 | passenv = 13 | GITHUB_TOKEN 14 | WEB3_INFURA_PROJECT_ID 15 | deps = -r{toxinidir}/requirements.txt 16 | basepython=python3` 17 | 18 | [testenv:lint] 19 | extras=linter 20 | commands = 21 | black --check {toxinidir}/scripts {toxinidir}/tests {toxinidir}/utils 22 | flake8 {toxinidir}/scripts {toxinidir}/tests {toxinidir}/utils 23 | isort --check-only --diff --recursive {toxinidir}/scripts {toxinidir}/tests {toxinidir}/utils 24 | 25 | [testenv:tests] 26 | commands = 27 | python -m pytest tests/ 28 | -------------------------------------------------------------------------------- /contracts/test/ReceiveApprovalMethodMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @notice Contract for using in token tests 7 | */ 8 | contract ReceiveApprovalMethodMock { 9 | address public sender; 10 | uint256 public value; 11 | address public tokenContract; 12 | bytes public extraData; 13 | 14 | function receiveApproval( 15 | address _from, 16 | uint256 _value, 17 | address _tokenContract, 18 | bytes calldata _extraData 19 | ) external { 20 | sender = _from; 21 | value = _value; 22 | tokenContract = _tokenContract; 23 | extraData = _extraData; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/redeploy-coordinator.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: redeploy-coordinator 3 | chain_id: 137 # Polygon Mainnet 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: mainnet-redeploy-coordinator.json 8 | 9 | constants: 10 | # See deployment/artifacts/mainnet.json 11 | TACO_CHILD_APPLICATION: "0xFa07aaB78062Fac4C36995bF28F6D677667973F5" 12 | DKG_TIMEOUT_SECONDS: 10800 # 3 hours (existing value) 13 | HANDOVER_TIMEOUT_SECONDS: 3600 # 1 hour 14 | 15 | contracts: 16 | - Coordinator: 17 | constructor: 18 | _application: $TACO_CHILD_APPLICATION 19 | _dkgTimeout: $DKG_TIMEOUT_SECONDS 20 | _handoverTimeout: $HANDOVER_TIMEOUT_SECONDS 21 | -------------------------------------------------------------------------------- /scripts/lynx/deploy_free_fee_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, ARTIFACTS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | from deployment.registry import merge_registries 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "free-fee-model.yml" 13 | LYNX_REGISTRY = ARTIFACTS_DIR / "lynx.json" 14 | 15 | 16 | def main(): 17 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 18 | 19 | free_fee_model = deployer.deploy(project.FreeFeeModel) 20 | 21 | deployments = [free_fee_model] 22 | 23 | deployer.finalize(deployments=deployments) 24 | -------------------------------------------------------------------------------- /.github/scripts/import_account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | from ape_accounts import import_account_from_private_key 6 | 7 | 8 | def main(): 9 | try: 10 | passphrase = os.environ["DKG_INITIATOR_PASSPHRASE"] 11 | private_key = os.environ["DKG_INITIATOR_PRIVATE_KEY"] 12 | except KeyError: 13 | raise Exception( 14 | "There are missing environment variables." 15 | "Please set DKG_INITIATOR_PASSPHRASE and DKG_INITIATOR_PRIVATE_KEY." 16 | ) 17 | account = import_account_from_private_key( 18 | 'automation', 19 | passphrase, 20 | private_key 21 | ) 22 | print(f"Account imported: {account.address}") 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.25.0 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P[^.]*)\.(?P\d+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{stage}.{devnum} 8 | {major}.{minor}.{patch}-{stage} 9 | {major}.{minor}.{patch} 10 | 11 | [bumpversion:part:stage] 12 | first_value = dev 13 | values = 14 | dev 15 | rc 16 | 17 | [bumpversion:part:devnum] 18 | first_value = 0 19 | 20 | [bumpversion:file:package.json] 21 | 22 | [flake8] 23 | max-line-length = 100 24 | ignore = E203,W503 25 | 26 | [tool:isort] 27 | force_grid_wrap = 0 28 | include_trailing_comma = True 29 | line_length = 100 30 | multi_line_output = 3 31 | use_parentheses = True 32 | 33 | [options] 34 | packages = find: 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Type of PR:** 2 | - [ ] Bugfix 3 | - [ ] Feature 4 | - [ ] Documentation 5 | - [ ] Other 6 | 7 | **Required reviews:** 8 | - [ ] 1 9 | - [ ] 2 10 | - [X] 3 11 | 12 | **What this does:** 13 | > High-level idea of the changes introduced in this PR. 14 | > List relevant API changes (if any), as well as related PRs and issues. 15 | 16 | **Issues fixed/closed:** 17 | > - Fixes #... 18 | 19 | **Why it's needed:** 20 | > Explain how this PR fits in the greater context of the NuCypher Network. 21 | > E.g., if this PR address a `nucypher/productdev` issue, let reviewers know! 22 | 23 | **Notes for reviewers:** 24 | > What should reviewers focus on? 25 | > Is there a particular commit/function/section of your PR that requires more attention from reviewers? 26 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/ITACoChildToRoot.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @title ITACoChildToRoot 7 | * @notice Interface for x-chain interactions from coordinator to application 8 | */ 9 | interface ITACoChildToRoot { 10 | /** 11 | * @notice Signals that an operator address is confirmed 12 | * @param stakingProvider Staking provider address 13 | * @param operator Operator address 14 | */ 15 | event OperatorConfirmed(address indexed stakingProvider, address indexed operator); 16 | 17 | function confirmOperatorAddress(address operator) external; 18 | 19 | function penalize(address stakingProvider) external; 20 | 21 | function release(address stakingProvider) external; 22 | } 23 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/upgrades-child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: lynx-child-upgrade 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: lynx-upgrade.json 8 | 9 | constants: 10 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 11 | MOCK_POLYGON_CHILD: "0x4FD23FAB4A09F85872bf240ABBd484cb4F9a5F79" 12 | LYNX_RITUAL_TOKEN: "0x064Be2a9740e565729BC0d47bC616c5bb8Cc87B9" 13 | 14 | contracts: 15 | - TACoChildApplication: 16 | constructor: 17 | _rootApplication: $MOCK_POLYGON_CHILD 18 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 19 | - Coordinator: 20 | constructor: 21 | _application: "0x42F30AEc1A36995eEFaf9536Eb62BD751F982D32" 22 | _currency: $LYNX_RITUAL_TOKEN 23 | _feeRatePerSecond: 1 24 | -------------------------------------------------------------------------------- /scripts/lynx/upgrade_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "upgrades-root.yml" 10 | 11 | 12 | def main(): 13 | """ 14 | This script upgrades TACoApplication in Lynx Sepolia. 15 | """ 16 | 17 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 18 | 19 | taco_app_proxy_address = "0x329bc9Df0e45f360583374726ccaFF003264a136" 20 | new_taco_app_implementation = deployer.upgrade(project.TACoApplication, taco_app_proxy_address) 21 | 22 | deployments = [ 23 | new_taco_app_implementation, 24 | ] 25 | 26 | deployer.finalize(deployments=deployments) 27 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/reimbursement-pool.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: reimbursement-mainnet 3 | chain_id: 137 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: reimbursement-mainnet.json 8 | 9 | constants: 10 | # Approx avg cost according to Keep tests (see e.g. https://github.com/keep-network/keep-core/actions/runs/10783327230/job/29905024826). 11 | # Also current value used for tBTC ReimbursementPool on Ethereum Mainnet (see https://etherscan.io/address/0x8adF3f35dBE4026112bCFc078872bcb967732Ea8#readContract#F4) 12 | STATIC_GAS: 40800 13 | # 110 gwei, so we cap refunds for each 30-size ritual to 10 POL (0.33 POL per node) 14 | MAX_GAS_PRICE: 110000000000 15 | 16 | contracts: 17 | - ReimbursementPool: 18 | constructor: 19 | _staticGas: $STATIC_GAS 20 | _maxGasPrice: $MAX_GAS_PRICE 21 | -------------------------------------------------------------------------------- /scripts/mainnet/redeploy_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "redeploy-marlin.yml" 10 | 11 | 12 | def main(): 13 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 14 | 15 | # NuCo Multisig owns contract so it must do the proxy upgrade 16 | new_implementation = deployer.deploy(project.StandardSubscription) 17 | 18 | # TODO Careful with contract registry since address should be the proxy address, 19 | # not the implementation address - basically only update abi in contract registry 20 | deployments = [ 21 | new_implementation, 22 | ] 23 | 24 | deployer.finalize(deployments=deployments) 25 | -------------------------------------------------------------------------------- /scripts/mainnet/redeploy_coordinator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "redeploy-coordinator.yml" 10 | 11 | 12 | def main(): 13 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 14 | 15 | # NuCo Multisig owns contract so it must do the proxy upgrade 16 | coordinator_implementation = deployer.deploy(project.Coordinator) 17 | 18 | # TODO Careful with contract registry since address should be the proxy address, 19 | # not the implementation address - basically only update abi in contract registry 20 | deployments = [ 21 | coordinator_implementation, 22 | ] 23 | 24 | deployer.finalize(deployments=deployments) 25 | -------------------------------------------------------------------------------- /scripts/mainnet/redeploy_taco_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "redeploy-taco-app.yml" 10 | 11 | 12 | def main(): 13 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 14 | 15 | # NuCo Multisig owns contract so it must do the proxy upgrade 16 | taco_application_implementation = deployer.deploy(project.TACoApplication) 17 | 18 | # TODO Careful with contract registry since address should be the proxy address, 19 | # not the implementation address - basically only update abi in contract registry 20 | deployments = [ 21 | taco_application_implementation, 22 | ] 23 | 24 | deployer.finalize(deployments=deployments) 25 | -------------------------------------------------------------------------------- /scripts/dashboard/deploy_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 5 | from deployment.params import Deployer 6 | 7 | VERIFY = False 8 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "dashboard" / "root.yml" 9 | 10 | 11 | def main(): 12 | """ 13 | This script deploys only the TACo Root Application for Dashboard development. 14 | """ 15 | 16 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 17 | 18 | taco_application = deployer.deploy(project.TACoApplication) 19 | 20 | mock_polygon_root = deployer.deploy(project.MockPolygonRoot) 21 | deployer.transact(taco_application.setChildApplication, mock_polygon_root.address) 22 | 23 | deployments = [ 24 | taco_application, 25 | mock_polygon_root, 26 | ] 27 | 28 | deployer.finalize(deployments=deployments) 29 | -------------------------------------------------------------------------------- /scripts/tapir/deploy_standard_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, ARTIFACTS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | from deployment.registry import merge_registries 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "new-subscription.yml" 13 | TAPIR_REGISTRY = ARTIFACTS_DIR / "tapir.json" 14 | 15 | 16 | def main(): 17 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 18 | std_subscription = deployer.deploy(project.StandardSubscription) 19 | deployments = [std_subscription] 20 | deployer.finalize(deployments=deployments) 21 | merge_registries( 22 | registry_1_filepath=TAPIR_REGISTRY, 23 | registry_2_filepath=deployer.registry_filepath, 24 | output_filepath=TAPIR_REGISTRY, 25 | ) 26 | -------------------------------------------------------------------------------- /scripts/mainnet/redeploy_taco_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "redeploy-taco-child.yml" 10 | 11 | 12 | def main(): 13 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 14 | 15 | # NuCo Multisig owns contract so it must do the proxy upgrade 16 | taco_child_application_implementation = deployer.deploy(project.TACoChildApplication) 17 | 18 | # TODO Careful with contract registry since address should be the proxy address, 19 | # not the implementation address - basically only update abi in contract registry 20 | deployments = [ 21 | taco_child_application_implementation, 22 | ] 23 | 24 | deployer.finalize(deployments=deployments) 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.11.0 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://github.com/pycqa/flake8 8 | rev: 6.1.0 9 | hooks: 10 | - id: flake8 11 | 12 | - repo: https://github.com/pre-commit/mirrors-isort 13 | rev: v5.10.1 14 | hooks: 15 | - id: isort 16 | 17 | - repo: local 18 | hooks: 19 | - id: lint-sol 20 | name: "lint solidity" 21 | description: "Checks Solidity code according to the package's linter configuration" 22 | language: node 23 | entry: solhint 24 | files: '\.sol$' 25 | args: 26 | - --config=./.solhint.json 27 | - --ignore-path=./.solhintignore 28 | - ./contracts/**/*.sol 29 | additional_dependencies: 30 | - solhint 31 | - solhint-plugin-prettier 32 | - prettier 33 | - prettier-plugin-solidity 34 | -------------------------------------------------------------------------------- /scripts/lynx/coordinator_approve_free_fee_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ARTIFACTS_DIR 6 | from deployment.params import Transactor 7 | from deployment.registry import contracts_from_registry 8 | 9 | LYNX_REGISTRY_FILEPATH = ARTIFACTS_DIR / "lynx.json" 10 | 11 | def main(): 12 | """ 13 | Coordinator approves the fee model for Free Fee Model 14 | 15 | ape run lynx coordinator_approve_free_fee_model --network polygon:amoy:infura 16 | """ 17 | 18 | transactor = Transactor() 19 | deployments = contracts_from_registry( 20 | filepath=LYNX_REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 21 | ) 22 | coordinator = deployments[project.Coordinator.contract_type.name] 23 | free_fee_model = deployments[project.FreeFeeModel.contract_type.name] 24 | 25 | transactor.transact(coordinator.approveFeeModel, free_fee_model.address) 26 | -------------------------------------------------------------------------------- /scripts/lynx/coordinator_sets_reimbursement_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ARTIFACTS_DIR 6 | from deployment.params import Transactor 7 | from deployment.registry import contracts_from_registry 8 | 9 | LYNX_REGISTRY_FILEPATH = ARTIFACTS_DIR / "lynx.json" 10 | 11 | def main(): 12 | """ 13 | Coordinator sets the ReimbursementPool 14 | 15 | ape run lynx coordinator_sets_reimbursement_pool --network polygon:amoy:infura 16 | """ 17 | 18 | transactor = Transactor() 19 | deployments = contracts_from_registry( 20 | filepath=LYNX_REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 21 | ) 22 | coordinator = deployments[project.Coordinator.contract_type.name] 23 | reimbursement_pool = deployments[project.ReimbursementPool.contract_type.name] 24 | 25 | transactor.transact(coordinator.setReimbursementPool, reimbursement_pool.address) 26 | -------------------------------------------------------------------------------- /scripts/tapir/coordinator_approve_free_fee_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ARTIFACTS_DIR 6 | from deployment.params import Transactor 7 | from deployment.registry import contracts_from_registry 8 | 9 | TAPIR_REGISTRY_FILEPATH = ARTIFACTS_DIR / "tapir.json" 10 | 11 | def main(): 12 | """ 13 | Coordinator approves the fee model for Free Fee Model 14 | 15 | ape run tapir coordinator_approve_free_fee_model --network polygon:amoy:infura 16 | """ 17 | 18 | transactor = Transactor() 19 | deployments = contracts_from_registry( 20 | filepath=TAPIR_REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 21 | ) 22 | coordinator = deployments[project.Coordinator.contract_type.name] 23 | free_fee_model = deployments[project.FreeFeeModel.contract_type.name] 24 | 25 | transactor.transact(coordinator.approveFeeModel, free_fee_model.address) 26 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/upgrade-child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-child-upgrade 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir-child-upgrade.json 8 | 9 | constants: 10 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 11 | TAPIR_RITUAL_TOKEN: "0xf91afFE7cf1d9c367Cb56eDd70C0941a4E8570d9" 12 | TACO_CHILD_APPLICATION_PROXY: "0x489287Ed5BdF7a35fEE411FBdCc47331093D0769" 13 | DKG_TIMEOUT_SECONDS: 3600 # 1 hour 14 | HANDOVER_TIMEOUT_SECONDS: 900 # 15 minutes 15 | 16 | contracts: 17 | - MockPolygonChild 18 | - TACoChildApplication: 19 | constructor: 20 | _rootApplication: $MockPolygonChild 21 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 22 | - Coordinator: 23 | constructor: 24 | _application: $TACO_CHILD_APPLICATION_PROXY 25 | _dkgTimeout: $DKG_TIMEOUT_SECONDS 26 | _handoverTimeout: $HANDOVER_TIMEOUT_SECONDS 27 | -------------------------------------------------------------------------------- /scripts/deploy_subscription_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import merge_registries 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "subscription_manager.yml" 11 | LYNX = ARTIFACTS_DIR / "lynx.json" 12 | TAPIR = ARTIFACTS_DIR / "tapir.json" 13 | 14 | 15 | def main(): 16 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 17 | subscription_manager = deployer.deploy(project.SubscriptionManager) 18 | deployments = [subscription_manager] 19 | deployer.finalize(deployments=deployments) 20 | 21 | for domain in (LYNX, TAPIR): 22 | merge_registries( 23 | registry_1_filepath=domain, 24 | registry_2_filepath=deployer.registry_filepath, 25 | output_filepath=domain, 26 | ) 27 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/legacy.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: mainnet-legacy-upgrade 3 | chain_id: 1 # Ethereum Mainnet 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: mainnet-legacy-upgrade.json 8 | 9 | constants: 10 | # Threshold Network - References: 11 | # - https://docs.threshold.network/resources/contract-addresses/mainnet/threshold-dao 12 | T_TOKEN_ETH_MAINNET: "0xCdF7028ceAB81fA0C6971208e83fa7872994beE5" 13 | T_STAKING_CONTRACT: "0x01B67b1194C75264d06F808A921228a95C765dd7" 14 | NU_VENDING_MACHINE: "0x1CCA7E410eE41739792eA0A24e00349Dd247680e" 15 | 16 | NU_CYPHER_TOKEN: "0x4fE83213D56308330EC302a8BD641f1d0113A4Cc" 17 | WORKLOCK: "0xe9778E69a961e64d3cdBB34CF6778281d34667c2" 18 | 19 | contracts: 20 | - StakingEscrow: 21 | constructor: 22 | _token: $NU_CYPHER_TOKEN 23 | _workLock: $WORKLOCK 24 | _tStaking: $T_STAKING_CONTRACT 25 | _tToken: $T_TOKEN_ETH_MAINNET 26 | _vendingMachine: $NU_VENDING_MACHINE 27 | -------------------------------------------------------------------------------- /deployment/types.py: -------------------------------------------------------------------------------- 1 | import click 2 | from eth_utils import to_checksum_address 3 | 4 | 5 | class MinInt(click.ParamType): 6 | name = "minint" 7 | 8 | def __init__(self, min_value): 9 | self.min_value = min_value 10 | 11 | def convert(self, value, param, ctx): 12 | try: 13 | ivalue = int(value) 14 | except ValueError: 15 | self.fail(f"{value} is not a valid integer", param, ctx) 16 | if ivalue < self.min_value: 17 | self.fail( 18 | f"{value} is less than the minimum allowed value of {self.min_value}", param, ctx 19 | ) 20 | return ivalue 21 | 22 | 23 | class ChecksumAddress(click.ParamType): 24 | name = "checksum_address" 25 | 26 | def convert(self, value, param, ctx): 27 | try: 28 | value = to_checksum_address(value=value) 29 | except ValueError: 30 | self.fail("Invalid ethereum address") 31 | else: 32 | return value 33 | -------------------------------------------------------------------------------- /scripts/deploy_open_access_authorizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, ARTIFACTS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | from deployment.registry import merge_registries 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "open_access_authorizer.yml" 13 | LYNX = ARTIFACTS_DIR / "lynx.json" 14 | TAPIR = ARTIFACTS_DIR / "tapir.json" 15 | 16 | 17 | def main(): 18 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 19 | open_access_authorizer = deployer.deploy(project.OpenAccessAuthorizer) 20 | deployments = [open_access_authorizer] 21 | deployer.finalize(deployments=deployments) 22 | 23 | for domain in (LYNX, TAPIR): 24 | merge_registries( 25 | registry_1_filepath=domain, 26 | registry_2_filepath=deployer.registry_filepath, 27 | output_filepath=domain, 28 | ) 29 | -------------------------------------------------------------------------------- /scripts/tapir/confirm_operator_addresses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import networks, project 4 | from deployment.constants import ARTIFACTS_DIR, TAPIR_NODES 5 | from deployment.params import Transactor 6 | from deployment.registry import contracts_from_registry 7 | from deployment.utils import check_plugins 8 | 9 | REGISTRY_FILEPATH = ARTIFACTS_DIR / "tapir.json" 10 | 11 | 12 | def main(): 13 | """ 14 | Confirm tapir operator addresses on the Sepolia side of the bridge 15 | 16 | ape run tapir confirm_operator_addresses --network ethereum:sepolia:infura 17 | """ 18 | check_plugins() 19 | transactor = Transactor() 20 | deployments = contracts_from_registry( 21 | filepath=REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 22 | ) 23 | mock_polygon_root = deployments[project.MockPolygonRoot.contract_type.name] 24 | for _, operator in TAPIR_NODES.items(): 25 | transactor.transact(mock_polygon_root.confirmOperatorAddress, operator) 26 | -------------------------------------------------------------------------------- /scripts/lynx/confirm_operator_addresses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | 4 | from ape import networks, project 5 | 6 | from deployment.constants import ARTIFACTS_DIR, LYNX_NODES 7 | from deployment.params import Transactor 8 | from deployment.registry import contracts_from_registry 9 | from deployment.utils import check_plugins 10 | 11 | LYNX_REGISTRY_FILEPATH = ARTIFACTS_DIR / "lynx.json" 12 | 13 | 14 | def main(): 15 | """ 16 | Confirm lynx operator addresses on the Sepolia side of the bridge 17 | 18 | ape run lynx confirm_operator_addresses --network ethereum:sepolia:infura 19 | """ 20 | check_plugins() 21 | transactor = Transactor() 22 | deployments = contracts_from_registry( 23 | filepath=LYNX_REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 24 | ) 25 | mock_polygon_root = deployments[project.MockPolygonRoot.contract_type.name] 26 | for _, operator in LYNX_NODES.items(): 27 | transactor.transact(mock_polygon_root.confirmOperatorAddress, operator) 28 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/ITACoRootToChild.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @title ITACoRootToChild 7 | * @notice Interface for x-chain interactions from application to coordinator 8 | */ 9 | interface ITACoRootToChild { 10 | event OperatorUpdated(address indexed stakingProvider, address indexed operator); 11 | event AuthorizationUpdated( 12 | address indexed stakingProvider, 13 | uint96 authorized, 14 | uint96 deauthorizing, 15 | uint64 endDeauthorization 16 | ); 17 | 18 | function updateOperator(address stakingProvider, address operator) external; 19 | 20 | function updateAuthorization(address stakingProvider, uint96 authorized) external; 21 | 22 | function updateAuthorization( 23 | address stakingProvider, 24 | uint96 authorized, 25 | uint96 deauthorizing, 26 | uint64 endDeauthorization 27 | ) external; 28 | 29 | function release(address stakingProvider) external; 30 | } 31 | -------------------------------------------------------------------------------- /scripts/tapir/upgrade_taco_child_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import contracts_from_registry 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "upgrade-taco-child-app.yml" 11 | 12 | 13 | def main(): 14 | """ 15 | This script upgrades TACoChildApplication contract for Tapir on Polygon Amoy. 16 | """ 17 | 18 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 19 | instances = contracts_from_registry(filepath=ARTIFACTS_DIR / "tapir.json", chain_id=80002) 20 | 21 | taco_child_application = deployer.upgrade( 22 | project.TACoChildApplication, 23 | instances[project.TACoChildApplication.contract_type.name].address, 24 | ) 25 | 26 | deployments = [ 27 | taco_child_application, 28 | ] 29 | 30 | deployer.finalize(deployments=deployments) 31 | -------------------------------------------------------------------------------- /scripts/mainnet/deploy_standard_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, ARTIFACTS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | from deployment.registry import merge_registries 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "new-subscription.yml" 13 | MAINNET_REGISTRY = ARTIFACTS_DIR / "mainnet.json" 14 | 15 | 16 | def main(): 17 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 18 | 19 | global_allow_list = deployer.deploy(project.GlobalAllowList) 20 | 21 | std_subscription = deployer.deploy(project.StandardSubscription) 22 | 23 | deployments = [global_allow_list, std_subscription] 24 | 25 | deployer.finalize(deployments=deployments) 26 | merge_registries( 27 | registry_1_filepath=MAINNET_REGISTRY, 28 | registry_2_filepath=deployer.registry_filepath, 29 | output_filepath=MAINNET_REGISTRY, 30 | ) 31 | -------------------------------------------------------------------------------- /.github/scripts/initiate_dkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Heartbeat: Initiate Ritual" 4 | 5 | echo "Network ${ECOSYSTEM}:${NETWORK}:${RPC_PROVIDER}" 6 | echo "Authority: ${DKG_AUTHORITY_ADDRESS}" 7 | echo "Access Controller: ${ACCESS_CONTROLLER}" 8 | echo "Fee Model: ${FEE_MODEL}" 9 | echo "Duration: ${DURATION}" 10 | 11 | ape run initiate_ritual \ 12 | --heartbeat \ 13 | --auto \ 14 | --account automation \ 15 | --network ${ECOSYSTEM}:${NETWORK}:${RPC_PROVIDER} \ 16 | --domain ${DOMAIN} \ 17 | --access-controller ${ACCESS_CONTROLLER} \ 18 | --authority ${DKG_AUTHORITY_ADDRESS} \ 19 | --fee-model ${FEE_MODEL} \ 20 | --duration ${DURATION} \ 21 | --excluded-nodes 22 | 23 | if [ $? -ne 0 ]; then 24 | echo "❗️ DKG heartbeat round failed" 25 | exit 1 26 | fi 27 | 28 | echo "✅ All Heartbeat Rituals Initiated" 29 | 30 | -------------------------------------------------------------------------------- /scripts/lynx/convert_registries.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | 4 | from eth_typing import ChecksumAddress, HexAddress, HexStr 5 | 6 | from deployment.constants import ARTIFACTS_DIR 7 | from deployment.legacy import convert_legacy_registry 8 | 9 | DEPLOYER = ChecksumAddress(HexAddress(HexStr("0x3B42d26E19FF860bC4dEbB920DD8caA53F93c600"))) 10 | LEGACY_ROOT_REGISTRY_FILEPATH = ARTIFACTS_DIR / "lynx" / "lynx-alpha-13-root-registry.json" 11 | LEGACY_CHILD_REGISTRY_FILEPATH = ARTIFACTS_DIR / "lynx" / "lynx-alpha-13-child-registry.json" 12 | OUTPUT_FILEPATH = ARTIFACTS_DIR / "lynx.json" 13 | 14 | 15 | def main(): 16 | if OUTPUT_FILEPATH.exists(): 17 | raise FileExistsError(f"Output filepath already exists at {OUTPUT_FILEPATH}") 18 | for chain_id, filepath in ( 19 | (80002, LEGACY_CHILD_REGISTRY_FILEPATH), 20 | (5, LEGACY_ROOT_REGISTRY_FILEPATH), 21 | ): 22 | convert_legacy_registry( 23 | legacy_filepath=filepath, 24 | output_filepath=OUTPUT_FILEPATH, 25 | chain_id=chain_id, 26 | ) 27 | -------------------------------------------------------------------------------- /scripts/lynx/deploy_reimbursement_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, ARTIFACTS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | from deployment.registry import contracts_from_registry 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "reimbursement-pool.yml" 13 | LYNX_REGISTRY = ARTIFACTS_DIR / "lynx.json" 14 | 15 | 16 | def main(): 17 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 18 | 19 | deployments = contracts_from_registry( 20 | filepath=LYNX_REGISTRY, chain_id=networks.active_provider.chain_id 21 | ) 22 | coordinator = deployments[project.Coordinator.contract_type.name] 23 | 24 | reimbursement_pool = deployer.deploy(project.ReimbursementPool) 25 | 26 | deployments = [reimbursement_pool] 27 | 28 | deployer.finalize(deployments=deployments) 29 | 30 | deployer.transact(reimbursement_pool.authorize, coordinator.address) 31 | -------------------------------------------------------------------------------- /scripts/mainnet/deploy_reimbursement_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, ARTIFACTS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | from deployment.registry import contracts_from_registry 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "reimbursement-pool.yml" 13 | MAINNET_REGISTRY = ARTIFACTS_DIR / "mainnet.json" 14 | 15 | 16 | def main(): 17 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 18 | 19 | deployments = contracts_from_registry( 20 | filepath=MAINNET_REGISTRY, chain_id=networks.active_provider.chain_id 21 | ) 22 | coordinator = deployments[project.Coordinator.contract_type.name] 23 | 24 | reimbursement_pool = deployer.deploy(project.ReimbursementPool) 25 | 26 | deployments = [reimbursement_pool] 27 | 28 | deployer.finalize(deployments=deployments) 29 | 30 | deployer.transact(reimbursement_pool.authorize, coordinator.address) 31 | -------------------------------------------------------------------------------- /scripts/lynx/deploy_standard_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ( 6 | CONSTRUCTOR_PARAMS_DIR, ARTIFACTS_DIR, 7 | ) 8 | from deployment.params import Deployer 9 | from deployment.registry import merge_registries 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "new-subscription.yml" 13 | LYNX_REGISTRY = ARTIFACTS_DIR / "lynx.json" 14 | 15 | 16 | def main(): 17 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 18 | 19 | global_allow_list = deployer.deploy(project.GlobalAllowList) 20 | 21 | std_subscription = deployer.deploy(project.StandardSubscription) 22 | 23 | deployments = [global_allow_list, std_subscription] 24 | 25 | deployer.finalize(deployments=deployments) 26 | 27 | deployer.transact(std_subscription.setAdopter, deployer.get_account().address) 28 | 29 | merge_registries( 30 | registry_1_filepath=LYNX_REGISTRY, 31 | registry_2_filepath=deployer.registry_filepath, 32 | output_filepath=LYNX_REGISTRY, 33 | ) 34 | -------------------------------------------------------------------------------- /scripts/tapir/deploy_free_fee_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ( 6 | ARTIFACTS_DIR, 7 | CONSTRUCTOR_PARAMS_DIR, 8 | ) 9 | from deployment.params import Deployer 10 | from deployment.registry import contracts_from_registry, merge_registries 11 | 12 | VERIFY = False 13 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "free-fee-model.yml" 14 | TAPIR_REGISTRY_FILEPATH = ARTIFACTS_DIR / "tapir.json" 15 | 16 | 17 | def main(): 18 | deployments = contracts_from_registry( 19 | filepath=TAPIR_REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 20 | ) 21 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 22 | 23 | free_fee_model = deployer.deploy(project.FreeFeeModel) 24 | deployments = [free_fee_model] 25 | 26 | deployer.finalize(deployments=deployments) 27 | merge_registries( 28 | registry_1_filepath=TAPIR_REGISTRY_FILEPATH, 29 | registry_2_filepath=deployer.registry_filepath, 30 | output_filepath=TAPIR_REGISTRY_FILEPATH, 31 | ) 32 | -------------------------------------------------------------------------------- /scripts/tapir/upgrade_mock_polygon_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import contracts_from_registry 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "upgrade-mock-polygon-root.yml" 11 | 12 | 13 | def main(): 14 | """ 15 | This script upgrades MockPolygonRoot contract for Tapir on Eth Sepolia. 16 | """ 17 | 18 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 19 | instances = contracts_from_registry(filepath=ARTIFACTS_DIR / "tapir.json", chain_id=11155111) 20 | 21 | taco_application = project.TACoApplication.at( 22 | instances[project.TACoApplication.contract_type.name].address 23 | ) 24 | 25 | mock_polygon_root = deployer.deploy(project.MockPolygonRoot) 26 | deployer.transact(taco_application.setChildApplication, mock_polygon_root.address) 27 | 28 | deployments = [ 29 | mock_polygon_root, 30 | ] 31 | 32 | deployer.finalize(deployments=deployments) 33 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/upgrade-root.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-root-upgrade 3 | chain_id: 11155111 # sepolia 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir-root-upgrade.json 8 | 9 | constants: 10 | TAPIR_STAKING_TOKEN: "0x28C35644F713c7Ee5C6A105e7AB0Fc144889a1Af" 11 | IN_SECONDS_1_HOUR: 3600 12 | IN_SECONDS_1_DAY: 86400 13 | IN_SECONDS_7_DAYS: 604800 14 | IN_SECONDS_60_DAYS: 5184000 15 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 16 | PENALTY_DEFAULT: 1000 # 10% 17 | PENALTY_INCREMENT: 500 # 5% increment 18 | 19 | contracts: 20 | - TestnetThresholdStaking 21 | - TACoApplication: 22 | constructor: 23 | _token: $TAPIR_STAKING_TOKEN 24 | _tStaking: $TestnetThresholdStaking 25 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 26 | _minOperatorSeconds: $IN_SECONDS_1_HOUR 27 | _rewardDuration: $IN_SECONDS_7_DAYS 28 | _deauthorizationDuration: $IN_SECONDS_60_DAYS 29 | _penaltyDefault: $PENALTY_DEFAULT 30 | _penaltyDuration: $IN_SECONDS_1_DAY # <= _rewardDuration 31 | _penaltyIncrement: $PENALTY_INCREMENT 32 | -------------------------------------------------------------------------------- /scripts/lynx/upgrade_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import contracts_from_registry 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "upgrades-child.yml" 11 | 12 | 13 | def main(): 14 | """ 15 | This script upgrades TACoChildApplication and Coordinator on Lynx/Amoy. 16 | """ 17 | 18 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 19 | instances = contracts_from_registry(filepath=ARTIFACTS_DIR / "lynx.json", chain_id=80002) 20 | 21 | child_application = deployer.upgrade( 22 | project.TACoChildApplication, 23 | instances[project.TACoChildApplication.contract_type.name].address, 24 | ) 25 | 26 | coordinator = deployer.upgrade( 27 | project.Coordinator, instances[project.Coordinator.contract_type.name].address 28 | ) 29 | 30 | deployments = [ 31 | child_application, 32 | coordinator, 33 | ] 34 | 35 | deployer.finalize(deployments=deployments) 36 | -------------------------------------------------------------------------------- /deployment/constructor_params/ci/child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: ci-child 3 | chain_id: 1337 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: ci.json 8 | 9 | constants: 10 | ONE_HOUR_IN_SECONDS: 3600 11 | ONE_DAY_IN_SECONDS: 86400 12 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 13 | TEN_MILLION_TOKENS_IN_WEI_UNITS: 10000000000000000000000000 # https://www.youtube.com/watch?v=EJR1H5tf5wE 14 | MAX_DKG_SIZE: 4 15 | 16 | contracts: 17 | - MockPolygonChild 18 | - TACoChildApplication: 19 | proxy: 20 | constructor: 21 | _rootApplication: $MockPolygonChild 22 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 23 | - LynxRitualToken: 24 | constructor: 25 | _totalSupplyOfTokens: $TEN_MILLION_TOKENS_IN_WEI_UNITS 26 | - Coordinator: 27 | proxy: 28 | constructor: 29 | _data: $encode:initialize,$MAX_DKG_SIZE,$deployer 30 | constructor: 31 | _application: $TACoChildApplication 32 | _dkgTimeout: $ONE_HOUR_IN_SECONDS 33 | _handoverTimeout: $ONE_DAY_IN_SECONDS 34 | - GlobalAllowList: 35 | constructor: 36 | _coordinator: $Coordinator 37 | -------------------------------------------------------------------------------- /scripts/lynx/coordinator_approve_standard_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ARTIFACTS_DIR 6 | from deployment.params import Transactor 7 | from deployment.registry import contracts_from_registry 8 | 9 | LYNX_REGISTRY_FILEPATH = ARTIFACTS_DIR / "lynx.json" 10 | 11 | def main(): 12 | """ 13 | Coordinator approves the fee model for StandardSubscription 14 | 15 | ape run lynx coordinator_approve_fee_model --network polygon:amoy:infura 16 | """ 17 | 18 | transactor = Transactor() 19 | deployments = contracts_from_registry( 20 | filepath=LYNX_REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 21 | ) 22 | coordinator = deployments[project.Coordinator.contract_type.name] 23 | std_subscription = deployments[project.StandardSubscription.contract_type.name] 24 | 25 | # Grant TREASURY_ROLE 26 | TREASURY_ROLE = coordinator.TREASURY_ROLE() 27 | transactor.transact( 28 | coordinator.grantRole, 29 | TREASURY_ROLE, 30 | transactor.get_account().address 31 | ) 32 | transactor.transact(coordinator.approveFeeModel, std_subscription.address) 33 | -------------------------------------------------------------------------------- /scripts/tapir/coordinator_approve_standard_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ARTIFACTS_DIR 6 | from deployment.params import Transactor 7 | from deployment.registry import contracts_from_registry 8 | 9 | TAPIR_REGISTRY_FILEPATH = ARTIFACTS_DIR / "tapir.json" 10 | 11 | def main(): 12 | """ 13 | Coordinator approves the fee model for StandardSubscription 14 | 15 | ape run tapir coordinator_approve_fee_model --network polygon:amoy:infura 16 | """ 17 | 18 | transactor = Transactor() 19 | deployments = contracts_from_registry( 20 | filepath=TAPIR_REGISTRY_FILEPATH, chain_id=networks.active_provider.chain_id 21 | ) 22 | coordinator = deployments[project.Coordinator.contract_type.name] 23 | std_subscription = deployments[project.StandardSubscription.contract_type.name] 24 | 25 | # Grant TREASURY_ROLE 26 | TREASURY_ROLE = coordinator.TREASURY_ROLE() 27 | transactor.transact( 28 | coordinator.grantRole, 29 | TREASURY_ROLE, 30 | transactor.get_account().address 31 | ) 32 | transactor.transact(coordinator.approveFeeModel, std_subscription.address) 33 | -------------------------------------------------------------------------------- /scripts/tapir/upgrade_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import contracts_from_registry 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "upgrade-root.yml" 11 | 12 | 13 | def main(): 14 | """ 15 | This script upgrades root contracts for Tapir on Eth Sepolia. 16 | """ 17 | 18 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 19 | instances = contracts_from_registry(filepath=ARTIFACTS_DIR / "tapir.json", chain_id=11155111) 20 | 21 | mock_threshold_staking = deployer.deploy(project.TestnetThresholdStaking) 22 | 23 | taco_application = deployer.upgrade( 24 | project.TACoApplication, 25 | instances[project.TACoApplication.contract_type.name].address, 26 | ) 27 | 28 | deployer.transact(mock_threshold_staking.setApplication, taco_application.address) 29 | 30 | deployments = [ 31 | mock_threshold_staking, 32 | taco_application, 33 | ] 34 | 35 | deployer.finalize(deployments=deployments) 36 | -------------------------------------------------------------------------------- /contracts/xchain/PolygonRoot.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@fx-portal/contracts/tunnel/FxBaseRootTunnel.sol"; 6 | 7 | contract PolygonRoot is FxBaseRootTunnel { 8 | address public immutable rootApplication; 9 | 10 | constructor( 11 | address _checkpointManager, 12 | address _fxRoot, 13 | address _rootApplication, 14 | address _fxChildTunnel 15 | ) FxBaseRootTunnel(_checkpointManager, _fxRoot) { 16 | require( 17 | _rootApplication != address(0) && _fxChildTunnel != address(0), 18 | "Wrong input parameters" 19 | ); 20 | rootApplication = _rootApplication; 21 | fxChildTunnel = _fxChildTunnel; 22 | } 23 | 24 | function _processMessageFromChild(bytes memory data) internal override { 25 | // solhint-disable-next-line avoid-low-level-calls 26 | (bool success, ) = rootApplication.call(data); 27 | require(success, "Root tx failed"); 28 | } 29 | 30 | fallback() external payable { 31 | require(msg.sender == rootApplication, "Caller must be the root app"); 32 | _sendMessageToChild(msg.data); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/test/EncryptionAuthorizerTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract CoordinatorForEncryptionAuthorizerMock { 6 | uint32 public numberOfRituals; 7 | mapping(uint32 => address) public getAuthority; 8 | mapping(uint32 => bool) public isRitualActive; 9 | 10 | function mockNewRitual(address authority) external { 11 | getAuthority[numberOfRituals] = authority; 12 | isRitualActive[numberOfRituals] = true; 13 | numberOfRituals += 1; 14 | } 15 | 16 | function mockEndRitual(uint32 ritualId) external { 17 | isRitualActive[ritualId] = false; 18 | } 19 | } 20 | 21 | contract SubscriptionForManagedAllowListMock { 22 | uint32 public numberOfRituals; 23 | mapping(uint32 => address) public getAuthority; 24 | mapping(uint32 => bool) public isRitualActive; 25 | 26 | function mockNewRitual(address authority) external { 27 | getAuthority[numberOfRituals] = authority; 28 | isRitualActive[numberOfRituals] = true; 29 | numberOfRituals += 1; 30 | } 31 | 32 | function mockEndRitual(uint32 ritualId) external { 33 | isRitualActive[ritualId] = false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-child 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir.json 8 | 9 | constants: 10 | ONE_HOUR_IN_SECONDS: 3600 11 | ONE_DAY_IN_SECONDS: 86400 12 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 13 | TEN_MILLION_TOKENS_IN_WEI_UNITS: 10000000000000000000000000 14 | MAX_DKG_SIZE: 32 15 | 16 | contracts: 17 | - MockPolygonChild 18 | - TACoChildApplication: 19 | proxy: 20 | constructor: 21 | _rootApplication: $MockPolygonChild 22 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 23 | - TapirRitualToken: 24 | constructor: 25 | _totalSupplyOfTokens: $TEN_MILLION_TOKENS_IN_WEI_UNITS 26 | - Coordinator: 27 | proxy: 28 | constructor: 29 | _data: $encode:initialize,$MAX_DKG_SIZE,$deployer 30 | constructor: 31 | _application: $TACoChildApplication 32 | _dkgTimeout: $ONE_HOUR_IN_SECONDS 33 | _handoverTimeout: $ONE_DAY_IN_SECONDS 34 | _currency: $TapirRitualToken 35 | _feeRatePerSecond: 1 36 | - GlobalAllowList: 37 | constructor: 38 | _coordinator: $Coordinator 39 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/IFeeModel.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @title IFeeModel 7 | * @notice IFeeModel 8 | */ 9 | interface IFeeModel { 10 | function processRitualPayment( 11 | address initiator, 12 | uint32 ritualId, 13 | uint256 numberOfProviders, 14 | uint32 duration 15 | ) external; 16 | 17 | function processRitualExtending( 18 | address initiator, 19 | uint32 ritualId, 20 | uint256 numberOfProviders, 21 | uint32 duration 22 | ) external; 23 | 24 | /** 25 | * @dev This function is called before the setAuthorizations function 26 | * @param ritualId The ID of the ritual 27 | * @param addresses The addresses to be authorized 28 | * @param value The authorization status 29 | */ 30 | function beforeSetAuthorization( 31 | uint32 ritualId, 32 | address[] calldata addresses, 33 | bool value 34 | ) external; 35 | 36 | /** 37 | * @dev This function is called before the isAuthorized function 38 | * @param ritualId The ID of the ritual 39 | */ 40 | function beforeIsAuthorized(uint32 ritualId) external view; 41 | } 42 | -------------------------------------------------------------------------------- /contracts/contracts/lib/Bits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Taken from https://github.com/ethereum/solidity-examples/blob/master/src/bits/Bits.sol 7 | */ 8 | library Bits { 9 | uint256 internal constant ONE = uint256(1); 10 | 11 | /** 12 | * @notice Sets the bit at the given 'index' in 'self' to: 13 | * '1' - if the bit is '0' 14 | * '0' - if the bit is '1' 15 | * @return The modified value 16 | */ 17 | function toggleBit(uint256 self, uint8 index) internal pure returns (uint256) { 18 | return self ^ (ONE << index); 19 | } 20 | 21 | /** 22 | * @notice Get the value of the bit at the given 'index' in 'self'. 23 | */ 24 | function bit(uint256 self, uint8 index) internal pure returns (uint8) { 25 | return uint8((self >> index) & 1); 26 | } 27 | 28 | /** 29 | * @notice Check if the bit at the given 'index' in 'self' is set. 30 | * @return 'true' - if the value of the bit is '1', 31 | * 'false' - if the value of the bit is '0' 32 | */ 33 | function bitSet(uint256 self, uint8 index) internal pure returns (bool) { 34 | return (self >> index) & 1 == 1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/redeploy-taco-app.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: mainnet-redeploy-taco-app.json 3 | chain_id: 1 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: mainnet-redeploy-taco-app.json 8 | 9 | constants: 10 | # Threshold Network - References: 11 | # - https://docs.threshold.network/resources/contract-addresses/mainnet/threshold-dao 12 | T_TOKEN_ETH_MAINNET: "0xCdF7028ceAB81fA0C6971208e83fa7872994beE5" 13 | T_STAKING_CONTRACT: "0x01B67b1194C75264d06F808A921228a95C765dd7" 14 | 15 | # TACo specific constants: 16 | IN_SECONDS_1_DAY: 86400 17 | IN_SECONDS_182_DAYS: 15724800 18 | IN_SECONDS_AVERAGE_MONTH_DURATION: 2628000 # 365*24*60*60/12 19 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 20 | 21 | contracts: 22 | - TACoApplication: 23 | constructor: 24 | _token: $T_TOKEN_ETH_MAINNET 25 | _tStaking: $T_STAKING_CONTRACT 26 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 27 | _minOperatorSeconds: $IN_SECONDS_1_DAY 28 | _rewardDuration: $IN_SECONDS_AVERAGE_MONTH_DURATION 29 | _deauthorizationDuration: $IN_SECONDS_182_DAYS 30 | _penaltyDefault: 1 31 | _penaltyDuration: 1 32 | _penaltyIncrement: 1 33 | 34 | -------------------------------------------------------------------------------- /scripts/mainnet/repair_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "repair-child.yml" 10 | 11 | 12 | def main(): 13 | """ 14 | This script deploys latest TACoApplication and PolygonChild contracts on Polygon/Mainnet, setting 15 | the PolygonChild's child application to the existing TACoApplication proxy. 16 | """ 17 | 18 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 19 | 20 | polygon_child = deployer.deploy(project.PolygonChild) 21 | 22 | taco_child_application_implementation = deployer.deploy(project.TACoChildApplication) 23 | 24 | taco_child_application_proxy = '0xFa07aaB78062Fac4C36995bF28F6D677667973F5' 25 | deployer.transact(polygon_child.setChildApplication, taco_child_application_proxy) 26 | # PolygonChild must set the root tunnel on the repair root script after deployment and upgrade 27 | 28 | deployments = [ 29 | polygon_child, 30 | taco_child_application_implementation, 31 | ] 32 | 33 | deployer.finalize(deployments=deployments) 34 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: lynx-child 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: lynx.json 8 | 9 | constants: 10 | ONE_HOUR_IN_SECONDS: 3600 11 | ONE_DAY_IN_SECONDS: 86400 12 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 13 | TEN_MILLION_TOKENS_IN_WEI_UNITS: 10000000000000000000000000 # https://www.youtube.com/watch?v=EJR1H5tf5wE 14 | MAX_DKG_SIZE: 4 15 | 16 | contracts: 17 | - MockPolygonChild 18 | - TACoChildApplication: 19 | proxy: 20 | constructor: 21 | _rootApplication: $MockPolygonChild 22 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 23 | - LynxRitualToken: 24 | constructor: 25 | _totalSupplyOfTokens: $TEN_MILLION_TOKENS_IN_WEI_UNITS 26 | - Coordinator: 27 | proxy: 28 | constructor: 29 | _data: $encode:initialize,$MAX_DKG_SIZE,$deployer 30 | constructor: 31 | _application: $TACoChildApplication 32 | _dkgTimeout: $ONE_HOUR_IN_SECONDS 33 | _handoverTimeout: $ONE_DAY_IN_SECONDS 34 | _currency: $LynxRitualToken 35 | _feeRatePerSecond: 1 36 | - GlobalAllowList: 37 | constructor: 38 | _coordinator: $Coordinator 39 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/upgrades-root.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: lynx-root 3 | chain_id: 11155111 # sepolia 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: lynx-upgrade.json 8 | 9 | constants: 10 | IN_SECONDS_1_HOUR: 3600 11 | IN_SECONDS_1_DAY: 86400 12 | IN_SECONDS_7_DAYS: 604800 13 | IN_SECONDS_60_DAYS: 5184000 14 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 15 | TEN_MILLION_TOKENS_IN_WEI_UNITS: 10000000000000000000000000 16 | PENALTY_DEFAULT: 1000 # 10% 17 | PENALTY_INCREMENT: 500 # 5% increment 18 | 19 | contracts: 20 | - TACoApplication: 21 | constructor: 22 | _token: "0x347370278531Db455Aec3BFD0F30d57e41422353" 23 | _tStaking: "0x18006f9A84C0bAD4CD96Aa69C7cE17aD760cDaD2" 24 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 25 | _minOperatorSeconds: $IN_SECONDS_1_HOUR 26 | _rewardDuration: $IN_SECONDS_1_DAY 27 | _deauthorizationDuration: $IN_SECONDS_60_DAYS 28 | _commitmentDurationOptions: 29 | [$IN_SECONDS_91_DAYS, $IN_SECONDS_182_DAYS, $IN_SECONDS_364_DAYS, $IN_SECONDS_546_DAYS] 30 | _penaltyDefault: $PENALTY_DEFAULT 31 | _penaltyDuration: $IN_SECONDS_1_DAY # <= _rewardDuration 32 | _penaltyIncrement: $PENALTY_INCREMENT 33 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/FreeFeeModel.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | /** 8 | * @title FreeFeeModel 9 | * @notice Free FeeModel 10 | */ 11 | contract FreeFeeModel is Ownable { 12 | mapping(address initiator => bool approved) public initiatorWhiteList; 13 | 14 | constructor() Ownable(msg.sender) {} 15 | 16 | function approveInitiator(address initiator) external onlyOwner { 17 | initiatorWhiteList[initiator] = true; 18 | } 19 | 20 | function processRitualPayment(address initiator, uint32, uint256, uint32) external { 21 | require(initiatorWhiteList[initiator], "Initiator not approved"); 22 | } 23 | 24 | function processRitualExtending(address initiator, uint32, uint256, uint32) external { 25 | require(initiatorWhiteList[initiator], "Initiator not approved"); 26 | } 27 | 28 | function beforeSetAuthorization( 29 | uint32 ritualId, 30 | address[] calldata addresses, 31 | bool value 32 | ) external { 33 | // solhint-disable-previous-line no-empty-blocks 34 | } 35 | 36 | function beforeIsAuthorized(uint32 ritualId) external view { 37 | // solhint-disable-previous-line no-empty-blocks 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /deployment/options.py: -------------------------------------------------------------------------------- 1 | import click 2 | from eth_typing import ChecksumAddress 3 | 4 | from deployment.constants import ACCESS_CONTROLLERS, SUPPORTED_TACO_DOMAINS 5 | 6 | access_controller_option = click.option( 7 | "--access-controller", 8 | "-a", 9 | help="global allow list or open access authorizer.", 10 | type=click.Choice(ACCESS_CONTROLLERS), 11 | required=True, 12 | ) 13 | 14 | domain_option = click.option( 15 | "--domain", 16 | "-d", 17 | help="TACo domain", 18 | type=click.Choice(SUPPORTED_TACO_DOMAINS), 19 | required=True, 20 | ) 21 | 22 | ritual_id_option = click.option( 23 | "--ritual-id", "-r", help="ID of the ritual", required=True, type=int 24 | ) 25 | 26 | subscription_contract_option = click.option( 27 | "--subscription-contract", 28 | "-s", 29 | help="Address of a subscription contract", 30 | type=ChecksumAddress, 31 | required=True, 32 | ) 33 | 34 | encryptor_slots_option = click.option( 35 | "--encryptor-slots", 36 | "-es", 37 | help="Number of encryptor slots to pay for.", 38 | required=True, 39 | type=int, 40 | ) 41 | 42 | encryptors_option = click.option( 43 | "--encryptors", 44 | "-e", 45 | help="List of encryptor addresses to remove.", 46 | multiple=True, 47 | required=True, 48 | type=ChecksumAddress, 49 | ) 50 | -------------------------------------------------------------------------------- /scripts/merge_registries.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from pathlib import Path 3 | 4 | import click 5 | from deployment.registry import merge_registries 6 | 7 | 8 | @click.command() 9 | @click.option( 10 | "--registry-1", 11 | help="Filepath to registry file 1", 12 | type=click.Path(dir_okay=False, exists=True, path_type=Path), 13 | required=True, 14 | ) 15 | @click.option( 16 | "--registry-2", 17 | help="Filepath to registry file 2", 18 | type=click.Path(dir_okay=False, exists=True, path_type=Path), 19 | required=True, 20 | ) 21 | @click.option( 22 | "--output-registry", 23 | "-o", 24 | help="Filepath of output registry file", 25 | type=click.Path(dir_okay=False, exists=False, path_type=Path), 26 | required=True, 27 | ) 28 | @click.option( 29 | "--deprecated-contract", 30 | "-d", 31 | "deprecated_contracts", 32 | help="Names of any deprecated contracts to exclude from the merge", 33 | required=False, 34 | multiple=True, 35 | ) 36 | def cli(registry_1, registry_2, output_registry, deprecated_contracts): 37 | """Merge two registry entries into one.""" 38 | merge_registries( 39 | registry_1_filepath=registry_1, 40 | registry_2_filepath=registry_2, 41 | output_filepath=output_registry, 42 | deprecated_contracts=deprecated_contracts, 43 | ) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nucypher/nucypher-contracts", 3 | "version": "0.25.0", 4 | "license": "AGPL-3.0-or-later", 5 | "description": "Threshold Access Control (TACo) Smart Contracts", 6 | "author": "NuCypher", 7 | "files": [ 8 | "/contracts/**/*.sol", 9 | "/deployment/artifacts/*.json", 10 | "!/contracts/test/**/*", 11 | "dist" 12 | ], 13 | "main": "dist/src/index.js", 14 | "types": "dist/src/index.d.ts", 15 | "scripts": { 16 | "build": "tsc -p tsconfig.build.json", 17 | "prepublishOnly": "npm run build", 18 | "test": "vitest run", 19 | "solhint": "solhint 'contracts/**/*.sol'", 20 | "solhint:fix": "solhint 'contracts/**/*.sol' --fix", 21 | "lint": "eslint src test --ext .ts", 22 | "lint:fix": "eslint src test --ext .ts --fix", 23 | "prettier:fix": "prettier --write contracts" 24 | }, 25 | "devDependencies": { 26 | "@typescript-eslint/eslint-plugin": "^6.9.0", 27 | "eslint": "^8.52.0", 28 | "eslint-config-prettier": "^9.0.0", 29 | "eslint-plugin-import": "^2.29.0", 30 | "eslint-plugin-n": "^16.2.0", 31 | "eslint-plugin-promise": "^6.1.1", 32 | "ganache": "^7.9.1", 33 | "prettier": "^2.8.8", 34 | "prettier-plugin-solidity": "^1.1.3", 35 | "solhint": "^5.0.1", 36 | "solhint-plugin-prettier": "^0.0.5", 37 | "typescript": "^5.2.2", 38 | "vitest": "^0.34.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/tapir/upgrade_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import contracts_from_registry 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "upgrade-child.yml" 11 | 12 | 13 | def main(): 14 | """ 15 | This script upgrades child contracts for Tapir on Polygon Amoy. 16 | """ 17 | 18 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 19 | instances = contracts_from_registry(filepath=ARTIFACTS_DIR / "tapir.json", chain_id=80002) 20 | 21 | mock_polygon_child = deployer.deploy(project.MockPolygonChild) 22 | 23 | taco_child_application = deployer.upgrade( 24 | project.TACoChildApplication, 25 | instances[project.TACoChildApplication.contract_type.name].address, 26 | ) 27 | 28 | deployer.transact(mock_polygon_child.setChildApplication, taco_child_application.address) 29 | 30 | coordinator = deployer.upgrade( 31 | project.Coordinator, instances[project.Coordinator.contract_type.name].address 32 | ) 33 | 34 | deployments = [ 35 | mock_polygon_child, 36 | taco_child_application, 37 | coordinator, 38 | ] 39 | 40 | deployer.finalize(deployments=deployments) 41 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/root.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: lynx-root 3 | chain_id: 11155111 # sepolia 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: lynx.json 8 | 9 | constants: 10 | IN_SECONDS_1_HOUR: 3600 11 | IN_SECONDS_1_DAY: 86400 12 | IN_SECONDS_7_DAYS: 604800 13 | IN_SECONDS_60_DAYS: 5184000 14 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 15 | TEN_MILLION_TOKENS_IN_WEI_UNITS: 10000000000000000000000000 16 | PENALTY_DEFAULT: 1000 # 10% 17 | PENALTY_INCREMENT: 500 # 5% increment 18 | 19 | contracts: 20 | - LynxStakingToken: 21 | constructor: 22 | _totalSupplyOfTokens: $TEN_MILLION_TOKENS_IN_WEI_UNITS 23 | - TestnetThresholdStaking 24 | - TACoApplication: 25 | proxy: 26 | constructor: 27 | _data: $encode:initialize 28 | constructor: 29 | _token: $LynxStakingToken 30 | _tStaking: $TestnetThresholdStaking 31 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 32 | _minOperatorSeconds: $IN_SECONDS_1_HOUR 33 | _rewardDuration: $IN_SECONDS_1_DAY 34 | _deauthorizationDuration: $IN_SECONDS_60_DAYS 35 | _penaltyDefault: $PENALTY_DEFAULT 36 | _penaltyDuration: $IN_SECONDS_1_DAY # <= _rewardDuration 37 | _penaltyIncrement: $PENALTY_INCREMENT 38 | - MockPolygonRoot: 39 | constructor: 40 | _rootApplication: $TACoApplication 41 | -------------------------------------------------------------------------------- /.github/workflows/evaluate.yml: -------------------------------------------------------------------------------- 1 | name: Evaluate DKG 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 5' # Every Monday at 05:00 7 | 8 | jobs: 9 | evaluate_dkg: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Install latest Rust stable 17 | uses: dtolnay/rust-toolchain@stable 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.12.4' 23 | 24 | - name: Install dependencies 25 | run: pip3 install -e . -r requirements.txt 26 | 27 | - name: Download Artifact 28 | uses: dawidd6/action-download-artifact@v9 29 | with: 30 | workflow: heartbeat.yml 31 | name: heartbeat-rituals 32 | 33 | - name: Evaluate Ritual 34 | run: .github/scripts/eval_dkg.sh 35 | env: 36 | # Secret environment variables (secrets) 37 | WEB3_INFURA_API_KEY: ${{ secrets.WEB3_INFURA_API_KEY }} 38 | 39 | # Non-secret environment variables (config) 40 | DOMAIN: ${{ vars.DOMAIN }} 41 | NETWORK: ${{ vars.NETWORK }} 42 | ECOSYSTEM: ${{ vars.ECOSYSTEM }} 43 | RPC_PROVIDER: ${{ vars.RPC_PROVIDER }} 44 | 45 | - name: Upload Artifact 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: offenders 49 | path: offenders.json 50 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/root.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: tapir-root 3 | chain_id: 11155111 # sepolia 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: tapir.json 8 | 9 | constants: 10 | IN_SECONDS_1_HOUR: 3600 11 | IN_SECONDS_7_DAYS: 604800 12 | IN_SECONDS_60_DAYS: 5184000 13 | IN_SECONDS_91_DAYS: 7862400 14 | IN_SECONDS_182_DAYS: 15724800 15 | IN_SECONDS_364_DAYS: 31449600 16 | IN_SECONDS_546_DAYS: 47174400 17 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 18 | TEN_MILLION_TOKENS_IN_WEI_UNITS: 10000000000000000000000000 19 | YEAR_2025: 1735689600 20 | 21 | contracts: 22 | - TapirStakingToken: 23 | constructor: 24 | _totalSupplyOfTokens: $TEN_MILLION_TOKENS_IN_WEI_UNITS 25 | - TestnetThresholdStaking 26 | - TACoApplication: 27 | proxy: 28 | constructor: 29 | _data: $encode:initialize 30 | constructor: 31 | _token: $TapirStakingToken 32 | _tStaking: $TestnetThresholdStaking 33 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 34 | _minOperatorSeconds: $IN_SECONDS_1_HOUR 35 | _rewardDuration: $IN_SECONDS_7_DAYS 36 | _deauthorizationDuration: $IN_SECONDS_60_DAYS 37 | _commitmentDurationOptions: [$IN_SECONDS_91_DAYS, $IN_SECONDS_182_DAYS, $IN_SECONDS_364_DAYS, $IN_SECONDS_546_DAYS] 38 | _commitmentDeadline: $YEAR_2025 39 | - MockPolygonRoot: 40 | constructor: 41 | _rootApplication: $TACoApplication 42 | -------------------------------------------------------------------------------- /contracts/xchain/PolygonChild.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@fx-portal/contracts/tunnel/FxBaseChildTunnel.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | contract PolygonChild is FxBaseChildTunnel, Ownable { 9 | address public childApplication; 10 | 11 | constructor(address _fxChild) FxBaseChildTunnel(_fxChild) Ownable(msg.sender) {} 12 | 13 | function _processMessageFromRoot( 14 | uint256 /* stateId */, 15 | address sender, 16 | bytes memory data 17 | ) internal override validateSender(sender) { 18 | // solhint-disable-next-line avoid-low-level-calls 19 | (bool success, ) = childApplication.call(data); 20 | require(success, "Child tx failed"); 21 | } 22 | 23 | function setFxRootTunnel(address _fxRootTunnel) external override onlyOwner { 24 | require(fxRootTunnel == address(0x0), "FxBaseChildTunnel: ROOT_TUNNEL_ALREADY_SET"); 25 | fxRootTunnel = _fxRootTunnel; 26 | if (childApplication != address(0)) { 27 | renounceOwnership(); 28 | } 29 | } 30 | 31 | function setChildApplication(address _childApplication) public onlyOwner { 32 | childApplication = _childApplication; 33 | if (fxRootTunnel != address(0)) { 34 | renounceOwnership(); 35 | } 36 | } 37 | 38 | fallback() external { 39 | require(msg.sender == childApplication, "Only child app can call this method"); 40 | _sendMessageToRoot(msg.data); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scripts/derive_accounts.py: -------------------------------------------------------------------------------- 1 | import click 2 | from ape.cli import ConnectedProviderCommand 3 | from eth_account import Account 4 | 5 | BASE_PATH = "m/44'/1'/0'/0/{}" # testnet derivation path for accounts 6 | BIP39_PASSPHRASE = "" # optional; set if you used one in your wallet 7 | COUNT = 5 # how many to derive 8 | 9 | 10 | Account.enable_unaudited_hdwallet_features() 11 | 12 | 13 | @click.command(cls=ConnectedProviderCommand) 14 | @click.option( 15 | "--mnemonic-file", 16 | "-m", 17 | help="File with mnemonic to use for account derivation", 18 | type=click.Path(exists=True), 19 | required=True, 20 | ) 21 | @click.option( 22 | "--passphrase", 23 | "-p", 24 | help="Optional passphrase for the mnemonic", 25 | type=click.STRING, 26 | default=BIP39_PASSPHRASE, 27 | required=False, 28 | ) 29 | @click.option( 30 | "--count", "-c", help="Number of accounts to derive", type=int, default=COUNT, required=False 31 | ) 32 | def cli(mnemonic_file, passphrase, count): 33 | """Derive testnet accounts from a mnemonic.""" 34 | with open(mnemonic_file, "r") as f: 35 | # only the first line is used 36 | mnemonic = f.readline().strip() 37 | 38 | for i in range(count): 39 | path = BASE_PATH.format(i) 40 | account = Account.from_mnemonic(mnemonic, passphrase=passphrase, account_path=path) 41 | print(f"Account {i} (Path - {path}):") 42 | print(f"\tAddress: {account.address} ") 43 | print(f"\tPrivate Key: {account.key.hex()}") 44 | print("----------------------------------") 45 | 46 | 47 | if __name__ == "__main__": 48 | cli() 49 | -------------------------------------------------------------------------------- /deployment/confirm.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from ape.utils import ZERO_ADDRESS 4 | 5 | 6 | def _confirm_deployment(contract_name: str) -> None: 7 | """Asks the user to confirm the deployment of a single contract.""" 8 | answer = input(f"Deploy {contract_name} Y/N? ") 9 | if answer.lower().strip() == "n": 10 | print("Aborting deployment!") 11 | exit(-1) 12 | 13 | 14 | def _continue() -> None: 15 | """Asks the user to continue.""" 16 | answer = input(f"Continue Y/N? ") 17 | if answer.lower().strip() == "n": 18 | print("Aborting deployment!") 19 | exit(-1) 20 | 21 | 22 | def _confirm_zero_address() -> None: 23 | answer = input("Zero Address detected for deployment parameter; Continue? Y/N? ") 24 | if answer.lower().strip() == "n": 25 | print("Aborting deployment!") 26 | exit(-1) 27 | 28 | 29 | def _confirm_resolution(resolved_params: OrderedDict, contract_name: str) -> None: 30 | """Asks the user to confirm the resolved constructor parameters for a single contract.""" 31 | if len(resolved_params) == 0: 32 | print(f"\n(i) No constructor parameters for {contract_name}") 33 | _confirm_deployment(contract_name) 34 | return 35 | 36 | print(f"\nConstructor parameters for {contract_name}") 37 | contains_zero_address = False 38 | for name, resolved_value in resolved_params.items(): 39 | print(f"\t{name}={resolved_value}") 40 | if not contains_zero_address: 41 | contains_zero_address = resolved_value == ZERO_ADDRESS 42 | _confirm_deployment(contract_name) 43 | if contains_zero_address: 44 | _confirm_zero_address() 45 | -------------------------------------------------------------------------------- /tests/lib/test_bls.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from hexbytes import HexBytes 4 | 5 | G1_SIZE = 48 6 | G2_SIZE = 48 * 2 7 | 8 | @pytest.fixture(scope="module") 9 | def bls(accounts, project): 10 | return project.BLSLibraryMock.deploy(sender=accounts[0]) 11 | 12 | def test_constants(bls): 13 | assert bls.G1_POINT_SIZE() == G1_SIZE 14 | assert bls.G2_POINT_SIZE() == G2_SIZE 15 | 16 | def test_bytes_to_g1(bls): 17 | point_bytes = os.urandom(G1_SIZE) 18 | word0 = HexBytes(point_bytes[:32]) 19 | word1 = HexBytes(point_bytes[32:]) 20 | assert tuple(bls.bytesToG1Point(point_bytes)) == (word0, word1) 21 | 22 | def test_bytes_to_g2(bls): 23 | point_bytes = os.urandom(G2_SIZE) 24 | word0 = HexBytes(point_bytes[:32]) 25 | word1 = HexBytes(point_bytes[32:64]) 26 | word2 = HexBytes(point_bytes[64:]) 27 | assert bls.bytesToG2Point(point_bytes) == (word0, word1, word2) 28 | 29 | def test_g1_to_bytes(bls): 30 | g1_point = (os.urandom(32), os.urandom(16)) 31 | assert bls.g1PointToBytes(g1_point) == HexBytes(b''.join(g1_point)) 32 | 33 | def test_g2_to_bytes(bls): 34 | g2_point = (os.urandom(32), os.urandom(32), os.urandom(32)) 35 | assert bls.g2PointToBytes(g2_point) == HexBytes(b''.join(g2_point)) 36 | 37 | def test_eq_g1(bls): 38 | g1_point = (os.urandom(32), os.urandom(16)) 39 | assert bls.eqG1Point(g1_point, g1_point) 40 | assert not bls.eqG1Point(g1_point, (os.urandom(32), os.urandom(16))) 41 | 42 | def test_eq_g2(bls): 43 | g2_point = (os.urandom(32), os.urandom(32), os.urandom(32)) 44 | assert bls.eqG2Point(g2_point, g2_point) 45 | assert not bls.eqG2Point(g2_point, (os.urandom(32), os.urandom(32), os.urandom(32))) -------------------------------------------------------------------------------- /deployment/constructor_params/dashboard/root.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: dashboard-root 3 | chain_id: 11155111 # Sepolia Testnet 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: dashboard.json 8 | 9 | constants: 10 | IN_SECONDS_1_DAY: 86400 11 | IN_SECONDS_182_DAYS: 15724800 12 | IN_SECONDS_364_DAYS: 31449600 13 | IN_SECONDS_AVERAGE_MONTH_DURATION: 2628000 # 365*24*60*60/12 14 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 15 | TIMESTAMP_FOR_2023_12_30_2359_UTC: 1703980799 16 | # https://app.safe.global/home?safe=sep:0xED152d8E2ba12C9E51D4170047877966bCcD5190 17 | SEPOLIA_SAFE: "0xED152d8E2ba12C9E51D4170047877966bCcD5190" 18 | # Sepolia Threshold addresses - see https://docs.threshold.network/resources/contract-addresses/sepolia-testnet 19 | SEPOLIA_T_TOKEN: "0x46abDF5aD1726ba700794539C3dB8fE591854729" 20 | SEPOLIA_T_STAKING: "0x3d4cb85c0e3c5bd1667B7E30f3E86B3FAB878Ff8" 21 | 22 | contracts: 23 | - TACoApplication: 24 | proxy: 25 | constructor: 26 | initialOwner: $SEPOLIA_SAFE # Upgrades owner 27 | _data: $encode:initialize 28 | constructor: 29 | _token: $SEPOLIA_T_TOKEN 30 | _tStaking: $SEPOLIA_T_STAKING 31 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 32 | _minOperatorSeconds: $IN_SECONDS_1_DAY 33 | _rewardDuration: $IN_SECONDS_AVERAGE_MONTH_DURATION 34 | _deauthorizationDuration: $IN_SECONDS_182_DAYS 35 | _commitmentDurationOptions: [$IN_SECONDS_182_DAYS, $IN_SECONDS_364_DAYS] 36 | _commitmentDeadline: $TIMESTAMP_FOR_2023_12_30_2359_UTC 37 | - MockPolygonRoot: 38 | constructor: 39 | _rootApplication: $TACoApplication -------------------------------------------------------------------------------- /ape-config.yaml: -------------------------------------------------------------------------------- 1 | name: nucypher-contracts 2 | contracts_folder: contracts 3 | 4 | plugins: 5 | - name: solidity 6 | - name: polygon 7 | - name: ape-etherscan 8 | - name: foundry 9 | - name: infura 10 | 11 | dependencies: 12 | - name: openzeppelin 13 | github: OpenZeppelin/openzeppelin-contracts 14 | version: 5.0.0 15 | config_override: 16 | solidity: 17 | version: 0.8.23 18 | evm_version: paris 19 | - name: openzeppelin-upgradeable 20 | github: OpenZeppelin/openzeppelin-contracts-upgradeable 21 | version: 5.0.0 22 | - name: fx-portal 23 | github: 0xPolygon/fx-portal 24 | version: 1.0.5 25 | - name: threshold 26 | github: threshold-network/solidity-contracts 27 | version: 1.2.1 28 | 29 | solidity: 30 | version: 0.8.23 31 | evm_version: paris 32 | 33 | ethereum: 34 | mainnet: 35 | transaction_acceptance_timeout: 600 # 10 minutes 36 | 37 | polygon: 38 | mainnet: 39 | transaction_acceptance_timeout: 600 # 10 minutes 40 | amoy: 41 | gas_limit: auto # because we are cutting it close with Coordinator 42 | 43 | foundry: 44 | host: auto 45 | fork: 46 | ethereum: 47 | mainnet: 48 | upstream_provider: infura 49 | cache: false 50 | sepolia: 51 | upstream_provider: infura 52 | cache: false 53 | polygon: 54 | mainnet: 55 | upstream_provider: infura 56 | cache: false 57 | amoy: 58 | upstream_provider: infura 59 | cache: false 60 | 61 | etherscan: 62 | ethereum: 63 | rate_limit: 5 64 | polygon: 65 | rate_limit: 5 66 | 67 | test: 68 | mnemonic: test test test test test test test test test test test junk 69 | number_of_accounts: 40 70 | -------------------------------------------------------------------------------- /scripts/tapir/upgrade_coordinator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import contracts_from_registry, merge_registries 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "upgrade-coordinator.yml" 11 | TAPIR_REGISTRY_FILEPATH = ARTIFACTS_DIR / "tapir.json" 12 | 13 | 14 | def main(): 15 | """ 16 | This script upgrades Coordinator on Tapir/Amoy. 17 | """ 18 | 19 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 20 | instances = contracts_from_registry(filepath=ARTIFACTS_DIR / "tapir.json", chain_id=80002) 21 | 22 | # implementation = deployer.deploy(project.Coordinator) 23 | # latest reinitializer function used for most recent upgrade - reinitializer(2) 24 | # encoded_initializer_function = implementation.initializeNumberOfRituals.encode_input() 25 | # encoded_initializer_function = b"" 26 | # coordinator = deployer.upgradeTo( 27 | # implementation, 28 | # instances[project.Coordinator.contract_type.name].address, 29 | # encoded_initializer_function, 30 | # ) 31 | coordinator = deployer.upgrade( 32 | project.Coordinator, instances[project.Coordinator.contract_type.name].address 33 | ) 34 | 35 | deployments = [ 36 | coordinator, 37 | ] 38 | 39 | deployer.finalize(deployments=deployments) 40 | merge_registries( 41 | registry_1_filepath=TAPIR_REGISTRY_FILEPATH, 42 | registry_2_filepath=deployer.registry_filepath, 43 | output_filepath=TAPIR_REGISTRY_FILEPATH, 44 | ) 45 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "explicit-types": ["error", "explicit"], 6 | "no-console": "error", 7 | "no-empty-blocks": "error", 8 | "compiler-version": ["error", "^0.8.0"], 9 | "max-states-count": ["warn", 20], 10 | "no-unused-vars": "error", 11 | "reason-string": ["error", { "maxLength": 250 }], 12 | "constructor-syntax": "error", 13 | "quotes": ["error", "double"], 14 | "const-name-snakecase": "error", 15 | "contract-name-camelcase": "error", 16 | "event-name-camelcase": "error", 17 | "func-name-mixedcase": "error", 18 | "immutable-vars-naming": ["error", { "immutablesAsConstants": false }], 19 | "modifier-name-mixedcase": "error", 20 | "var-name-mixedcase": "error", 21 | "named-parameters-mapping": "warn", 22 | "use-forbidden-name": "error", 23 | "imports-on-top": "error", 24 | "ordering": "warn", 25 | "visibility-modifier-order": "error", 26 | "avoid-call-value": "error", 27 | "avoid-low-level-calls": "error", 28 | "avoid-sha3": "error", 29 | "avoid-suicide": "error", 30 | "avoid-throw": "error", 31 | "avoid-tx-origin": "error", 32 | "check-send-result": "error", 33 | "func-visibility": ["error", { "ignoreConstructors": true }], 34 | "multiple-sends": "error", 35 | "no-inline-assembly": "off", 36 | "no-unused-import": "error", 37 | "not-rely-on-block-hash": "error", 38 | "not-rely-on-time": "off", 39 | "reentrancy": "error", 40 | "state-visibility": "error", 41 | "no-global-import": "off", 42 | "func-named-parameters": "off", 43 | "prettier/prettier": "error", 44 | "no-complex-fallback": "warn" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scripts/lynx/upgrade_coordinator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import contracts_from_registry, merge_registries 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "upgrade-coordinator.yml" 11 | LYNX_REGISTRY = ARTIFACTS_DIR / "lynx.json" 12 | 13 | 14 | def main(): 15 | """ 16 | This script upgrades Coordinator on Lynx/Amoy. 17 | """ 18 | 19 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 20 | instances = contracts_from_registry(filepath=ARTIFACTS_DIR / "lynx.json", chain_id=80002) 21 | 22 | # `initializeNumberOfRituals` was used for the original upgrade, but should NOT be 23 | # used for subsequent upgrades of Coordinator 24 | # implementation = deployer.deploy(project.Coordinator) 25 | # encoded_initializer_function = implementation.initializeNumberOfRituals.encode_input() 26 | # encoded_initializer_function = b"" 27 | # coordinator = deployer.upgradeTo( 28 | # implementation, 29 | # instances[project.Coordinator.contract_type.name].address, 30 | # encoded_initializer_function, 31 | # ) 32 | coordinator = deployer.upgrade( 33 | project.Coordinator, instances[project.Coordinator.contract_type.name].address 34 | ) 35 | 36 | deployments = [ 37 | coordinator, 38 | ] 39 | 40 | deployer.finalize(deployments=deployments) 41 | merge_registries( 42 | registry_1_filepath=LYNX_REGISTRY, 43 | registry_2_filepath=deployer.registry_filepath, 44 | output_filepath=LYNX_REGISTRY, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import IntEnum 3 | 4 | import pytest 5 | from ape import project 6 | 7 | # Common constants 8 | G1_SIZE = 48 9 | G2_SIZE = 48 * 2 10 | ONE_DAY = 24 * 60 * 60 11 | 12 | RitualState = IntEnum( 13 | "RitualState", 14 | [ 15 | "NON_INITIATED", 16 | "DKG_AWAITING_TRANSCRIPTS", 17 | "DKG_AWAITING_AGGREGATIONS", 18 | "DKG_TIMEOUT", 19 | "DKG_INVALID", 20 | "ACTIVE", 21 | "EXPIRED", 22 | ], 23 | start=0, 24 | ) 25 | 26 | HandoverState = IntEnum( 27 | "HandoverState", 28 | [ 29 | "NON_INITIATED", 30 | "HANDOVER_AWAITING_TRANSCRIPT", 31 | "HANDOVER_AWAITING_BLINDED_SHARE", 32 | "HANDOVER_AWAITING_FINALIZATION", 33 | "HANDOVER_TIMEOUT", 34 | ], 35 | start=0, 36 | ) 37 | 38 | 39 | # Utility functions 40 | def transcript_size(shares, threshold): 41 | return 40 + (1 + shares) * G2_SIZE + threshold * G1_SIZE 42 | 43 | 44 | def generate_transcript(shares, threshold): 45 | return os.urandom(transcript_size(shares, threshold)) 46 | 47 | 48 | def gen_public_key(): 49 | return (os.urandom(32), os.urandom(32), os.urandom(32)) 50 | 51 | 52 | def access_control_error_message(address, role=None): 53 | role = role or b"\x00" * 32 54 | return f"account={address}, neededRole={role}" 55 | 56 | 57 | # Fixtures 58 | @pytest.fixture(scope="session") 59 | def oz_dependency(): 60 | return project.dependencies["openzeppelin"]["5.0.0"] 61 | 62 | 63 | @pytest.fixture 64 | def creator(accounts): 65 | return accounts[0] 66 | 67 | 68 | @pytest.fixture 69 | def account1(accounts): 70 | return accounts[1] 71 | 72 | 73 | @pytest.fixture 74 | def account2(accounts): 75 | return accounts[2] 76 | -------------------------------------------------------------------------------- /contracts/test/CoordinatorTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../threshold/ITACoChildApplication.sol"; 6 | import "../contracts/coordination/Coordinator.sol"; 7 | 8 | /** 9 | * @notice Contract for testing Coordinator contract 10 | */ 11 | contract ChildApplicationForCoordinatorMock is ITACoChildApplication { 12 | uint96 public minimumAuthorization = 0; 13 | 14 | mapping(address => uint96) public authorizedStake; 15 | mapping(address => address) public stakingProviderToOperator; 16 | mapping(address => address) public operatorToStakingProvider; 17 | mapping(address => bool) public confirmations; 18 | 19 | mapping(address => bool) public stakingProviderReleased; 20 | 21 | function updateOperator(address _stakingProvider, address _operator) external { 22 | address oldOperator = stakingProviderToOperator[_stakingProvider]; 23 | operatorToStakingProvider[oldOperator] = address(0); 24 | stakingProviderToOperator[_stakingProvider] = _operator; 25 | operatorToStakingProvider[_operator] = _stakingProvider; 26 | } 27 | 28 | function updateAuthorization(address _stakingProvider, uint96 _amount) external { 29 | authorizedStake[_stakingProvider] = _amount; 30 | } 31 | 32 | function confirmOperatorAddress(address _operator) external { 33 | confirmations[_operator] = true; 34 | } 35 | 36 | // solhint-disable-next-line no-empty-blocks 37 | function penalize(address _stakingProvider) external {} 38 | 39 | // solhint-disable-next-line no-empty-blocks 40 | function release(address _stakingProvider) external override { 41 | stakingProviderReleased[_stakingProvider] = true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /deployment/constructor_params/tapir/bqeth.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: bqeth-tapir 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: bqeth-tapir.json 8 | 9 | constants: 10 | # See deployment/artifacts/tapir.json 11 | COORDINATOR_PROXY: "0xE690b6bCC0616Dc5294fF84ff4e00335cA52C388" 12 | TAPIR_RITUAL_TOKEN: "0xf91afFE7cf1d9c367Cb56eDd70C0941a4E8570d9" 13 | TESTNET_DEPLOYER: "0x3B42d26E19FF860bC4dEbB920DD8caA53F93c600" 14 | GLOBAL_ALLOW_LIST: "0xcc537b292d142dABe2424277596d8FFCC3e6A12D" 15 | 16 | MAX_NODES: 5 17 | 18 | # - Fee parameters: 19 | INITIAL_BASE_FEE_RATE: 4050925925925 # $0.35 per day, in DAI units per second (in Python: 35*10**16 // 86400) 20 | ENCRYPTOR_FEE_RATE: 63419583967 # $2 per year, in DAI units per second (in Python: 2 * 10**18 // 86400 // 365) 21 | BASE_FEE_RATE_INCREASE: 500 # 5%/year ~ 2.47%/semester, expressed in basis points (0.01%) 22 | 23 | PERIOD: 172800 # 2 days 24 | YELLOW_PERIOD: 86400 # 1 day 25 | RED_PERIOD: 86400 # 1 day 26 | 27 | contracts: 28 | - StandardSubscription: 29 | proxy: 30 | constructor: 31 | initialOwner: $TESTNET_DEPLOYER 32 | _data: $encode:initialize,$TESTNET_DEPLOYER 33 | constructor: 34 | _coordinator: $COORDINATOR_PROXY 35 | _accessController: $GLOBAL_ALLOW_LIST 36 | _feeToken: $TAPIR_RITUAL_TOKEN 37 | _adopterSetter: $TESTNET_DEPLOYER 38 | _initialBaseFeeRate: $INITIAL_BASE_FEE_RATE 39 | _baseFeeRateIncrease: $BASE_FEE_RATE_INCREASE 40 | _encryptorFeeRate: $ENCRYPTOR_FEE_RATE 41 | _maxNodes: $MAX_NODES 42 | _subscriptionPeriodDuration: $PERIOD 43 | _yellowPeriodDuration: $YELLOW_PERIOD 44 | _redPeriodDuration: $RED_PERIOD 45 | -------------------------------------------------------------------------------- /scripts/tapir/deploy_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "root.yml" 10 | 11 | 12 | def main(): 13 | """ 14 | This script deploys only the Proxied Tapir TACo Root Application. 15 | 16 | October 6th, 2023, Deployment: 17 | ape-run deploy_root --network etherscan:sepolia:infura 18 | ape-etherscan 0.6.10 19 | ape-infura 0.6.4 20 | ape-polygon 0.6.6 21 | ape-solidity 0.6.9 22 | eth-ape 0.6.20 23 | 24 | 25 | January 8th, 2024 26 | ape run tapir deploy_root --network ethereum:sepolia:infura 27 | ape-etherscan 0.6.10 28 | ape-infura 0.6.4 29 | ape-polygon 0.6.6 30 | ape-solidity 0.6.9 31 | eth-ape 0.6.19 32 | """ 33 | 34 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 35 | 36 | reward_token = deployer.deploy(project.TapirStakingToken) 37 | 38 | mock_threshold_staking = deployer.deploy(project.TestnetThresholdStaking) 39 | 40 | taco_application = deployer.deploy(project.TACoApplication) 41 | 42 | deployer.transact(mock_threshold_staking.setApplication, taco_application.address) 43 | 44 | mock_polygon_root = deployer.deploy(project.MockPolygonRoot) 45 | deployer.transact(taco_application.setChildApplication, mock_polygon_root.address) 46 | 47 | deployments = [ 48 | reward_token, 49 | mock_threshold_staking, 50 | taco_application, 51 | mock_polygon_root, 52 | ] 53 | 54 | deployer.finalize(deployments=deployments) 55 | -------------------------------------------------------------------------------- /test/registry.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { ChainId, type ContractName, contractNames, type Domain, getContract } from "../src"; 4 | 5 | const testCases: Array<[string, number, ContractName]> = contractNames.flatMap((contract) => [ 6 | ["lynx", 80002, contract], 7 | ["tapir", 80002, contract], 8 | ["mainnet", 137, contract], 9 | ]); 10 | 11 | describe("registry", () => { 12 | it.each(testCases)( 13 | `should work for domain %s, chainId %i, contract %s`, 14 | (domain, chainId, contract) => { 15 | const contractAddress = getContract(domain as Domain, chainId as ChainId, contract); 16 | expect(contractAddress).toBeDefined(); 17 | }, 18 | ); 19 | 20 | it("should throw for invalid domain", () => { 21 | expect(() => getContract("invalid-domain", 80002, "Coordinator")).toThrow(); 22 | }); 23 | 24 | it("should throw for invalid chainId", () => { 25 | expect(() => getContract("lynx", 0, "Coordinator")).toThrow(); 26 | }); 27 | 28 | it("should throw for invalid contract", () => { 29 | expect(() => getContract("lynx", 80002, "InvalidContract")).toThrow(); 30 | }); 31 | 32 | it("should return the same contract address for the same domain, chainId, and contract", () => { 33 | const contractAddress1 = getContract("lynx", 80002, "Coordinator"); 34 | const contractAddress2 = getContract("lynx", 80002, "Coordinator"); 35 | expect(contractAddress1).toEqual(contractAddress2); 36 | }); 37 | 38 | it("should return different contract addresses for different domains", () => { 39 | const contractAddress1 = getContract("lynx", 80002, "Coordinator"); 40 | const contractAddress2 = getContract("tapir", 80002, "Coordinator"); 41 | expect(contractAddress1).not.toEqual(contractAddress2); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/repair-child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: mainnet-child-repair 3 | chain_id: 137 # Polygon Mainnet 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: mainnet-child-repair.json 8 | 9 | constants: 10 | # DAI Token on Polygon PoS - References: 11 | # - https://polygonscan.com/token/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 12 | # - https://github.com/search?q=0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063&type=code 13 | DAI_ON_POLYGON: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" 14 | 15 | # TACO specific constants: 16 | PRIVATE_BETA_FEE_RATE: 4050925925925 # $0.35 per day, expressed in DAI units per seconds (in Python: 35*10**16 // 86400) 17 | MAX_DKG_SIZE: 32 18 | DKG_TIMEOUT_IN_SECONDS: 3600 # One hour in seconds 19 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 20 | 21 | # Threshold Network - References: 22 | # - https://docs.threshold.network/resources/contract-addresses/mainnet/threshold-dao 23 | # - https://github.com/keep-network/tbtc-v2/issues/594 24 | TREASURY_GUILD_ON_POLYGON: "0xc3Bf49eBA094AF346830dF4dbB42a07dE378EeB6" 25 | INTEGRATIONS_GUILD_ON_POLYGON: "0x5bD70E385414C8dCC25305AeB7E542d8FC70e667" 26 | THRESHOLD_COUNCIL_ON_POLYGON: "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f" 27 | 28 | # FxPortal addresses - References: 29 | # - https://github.com/0xPolygon/fx-portal#deployment-addresses 30 | # - https://github.com/0xPolygon/fx-portal/blob/main/config/config.json 31 | # - https://wiki.polygon.technology/docs/pos/design/bridge/l1-l2-communication/state-transfer/#prerequisites 32 | FXPORTAL_FXCHILD: "0x8397259c983751DAf40400790063935a11afa28a" 33 | 34 | contracts: 35 | - PolygonChild: 36 | constructor: 37 | _fxChild: $FXPORTAL_FXCHILD 38 | - TACoChildApplication: 39 | constructor: 40 | _rootApplication: $PolygonChild 41 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 42 | -------------------------------------------------------------------------------- /scripts/simulate_coordinator_upgrade.py: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # > ape run --interactive simulate_coordinator_upgrade --network polygon:mainnet-fork:foundry 3 | 4 | from ape import accounts, chain, networks, project 5 | 6 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 7 | from deployment.params import Deployer 8 | 9 | 10 | OZ_DEPENDENCY = project.dependencies["openzeppelin"]["5.0.0"] 11 | PROXY_ADMIN_ADDRESS = "0xeE711368eabA106A0cf7a07B33B84cD930331fFd" 12 | COORDINATOR_PROXY_ADDRESS = "0xE74259e3dafe30bAA8700238e324b47aC98FE755" 13 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "redeploy-coordinator.yml" 14 | NUCO_MULTISIG_ADDRESS = "0x861aa915C785dEe04684444560fC7A2AB43a1543" 15 | 16 | 17 | def main(): 18 | if networks.active_provider.network.name != "mainnet-fork": 19 | raise RuntimeError("This script must be run on mainnet-fork only!") 20 | 21 | # Impersonate NuCo Multisig 22 | nuco_multisig = accounts.test_accounts.impersonate_account(NUCO_MULTISIG_ADDRESS) 23 | chain.set_balance(nuco_multisig.address, "5 ether") 24 | 25 | # Ensure ProxyAdmin is as expected on mainnet 26 | proxy_admin = OZ_DEPENDENCY.ProxyAdmin.at(PROXY_ADMIN_ADDRESS) 27 | assert proxy_admin.owner() == nuco_multisig.address 28 | 29 | # Deploy new Coordinator implementation 30 | deployer = Deployer.from_yaml( 31 | filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=False, account=nuco_multisig, autosign=True 32 | ) 33 | new_coordinator = deployer.deploy(project.Coordinator) 34 | 35 | # Upgrade Coordinator proxy to new implementation 36 | coordinator = project.Coordinator.at(COORDINATOR_PROXY_ADDRESS) 37 | tx = proxy_admin.upgradeAndCall(coordinator.address, new_coordinator, b"", sender=nuco_multisig) 38 | 39 | print(f"Upgraded Coordinator to {new_coordinator.address} via ProxyAdmin {proxy_admin.address}") 40 | 41 | assert False, "Stop here" # Force activation of interactive mode to inspect state after upgrade 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import lynxRegistryJson from "../deployment/artifacts/lynx.json"; 2 | import mainnetRegistryJson from "../deployment/artifacts/mainnet.json"; 3 | import tapirRegistryJson from "../deployment/artifacts/tapir.json"; 4 | 5 | export type Abi = unknown; 6 | 7 | export interface DeployedContract { 8 | address: string; 9 | abi: Abi; 10 | } 11 | 12 | // Only expose contracts that are used in the SDK 13 | export const contractNames = ["Coordinator", "GlobalAllowList", "SubscriptionManager"] as const; 14 | 15 | export type ContractName = (typeof contractNames)[number]; 16 | 17 | export interface Contract { 18 | name: ContractName; 19 | abi: Abi; 20 | } 21 | 22 | export type ContractRegistry = Record>; 23 | 24 | export const domainRegistry: Record = { 25 | lynx: lynxRegistryJson, 26 | tapir: tapirRegistryJson, 27 | mainnet: mainnetRegistryJson, 28 | }; 29 | 30 | export type Domain = "mainnet" | "oryx" | "tapir" | "lynx"; 31 | 32 | export type ChainId = 1 | 5 | 137 | 80002; 33 | 34 | export type ChecksumAddress = `0x${string}`; 35 | 36 | export const getContract = ( 37 | domain: Domain | string, 38 | chainId: ChainId | number, 39 | contract: ContractName | string, 40 | ): ChecksumAddress => { 41 | if (!contractNames.includes(contract as ContractName)) { 42 | throw new Error(`Invalid contract name: ${contract}`); 43 | } 44 | 45 | const registry = domainRegistry[domain]; 46 | if (!registry) { 47 | throw new Error(`No contract registry found for domain: ${domain}`); 48 | } 49 | 50 | const contracts = registry[chainId as ChainId]; 51 | if (!contracts) { 52 | throw new Error(`No contracts found for chainId: ${chainId}`); 53 | } 54 | 55 | const deployedContract = contracts[contract]; 56 | if (!deployedContract) { 57 | throw new Error(`No contract found for name: ${contract}`); 58 | } 59 | 60 | return deployedContract.address as ChecksumAddress; 61 | }; 62 | -------------------------------------------------------------------------------- /scripts/cancel_handover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import click 4 | from ape.cli import ConnectedProviderCommand, account_option, network_option 5 | 6 | from deployment import registry 7 | from deployment.constants import SUPPORTED_TACO_DOMAINS 8 | from deployment.params import Transactor 9 | from deployment.types import ChecksumAddress 10 | from deployment.utils import check_plugins 11 | 12 | 13 | @click.command(cls=ConnectedProviderCommand, name="cancel-handover") 14 | @account_option() 15 | @network_option(required=True) 16 | @click.option( 17 | "--domain", 18 | "-d", 19 | help="TACo domain", 20 | type=click.Choice(SUPPORTED_TACO_DOMAINS), 21 | required=True, 22 | ) 23 | @click.option("--ritual-id", "-r", help="Ritual ID to check", type=int, required=True) 24 | @click.option( 25 | "--departing-provider", 26 | "-dp", 27 | help="The ethereum address of the departing staking provider.", 28 | required=True, 29 | type=ChecksumAddress(), 30 | ) 31 | @click.option( 32 | "--auto", 33 | help="Automatically sign transactions.", 34 | is_flag=True, 35 | ) 36 | def cli( 37 | domain, 38 | account, 39 | network, 40 | ritual_id, 41 | departing_provider, 42 | auto, 43 | ): 44 | """Cancel the handover.""" 45 | 46 | # Setup 47 | check_plugins() 48 | click.echo(f"Connected to {network.name} network.") 49 | 50 | # Get the contracts from the registry 51 | coordinator_contract = registry.get_contract(domain=domain, contract_name="Coordinator") 52 | 53 | # cancel the handover 54 | click.echo( 55 | f"Cancelling handover for ritual ID {ritual_id} " 56 | f"and departing provider {departing_provider}..." 57 | ) 58 | 59 | transactor = Transactor(account=account, autosign=auto) 60 | transactor.transact( 61 | coordinator_contract.cancelHandover, 62 | ritual_id, 63 | departing_provider, 64 | ) 65 | 66 | 67 | if __name__ == "__main__": 68 | cli() 69 | -------------------------------------------------------------------------------- /scripts/lynx/deploy_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "root.yml" 10 | 11 | 12 | def main(): 13 | """ 14 | This script deploys only the Proxied Lynx TACo Root Application. 15 | 16 | September 25, 2023, Deployment: 17 | ape-run deploy_lynx_root --network etherscan:sepolia:infura 18 | ape-etherscan 0.6.10 19 | ape-infura 0.6.3 20 | ape-polygon 0.6.5 21 | ape-solidity 0.6.9 22 | eth-ape 0.6.20 23 | 24 | November 16, 2023, Update: 25 | ape-etherscan 0.6.10 26 | ape-infura 0.6.4 27 | ape-polygon 0.6.6 28 | ape-solidity 0.6.9 29 | eth-ape 0.6.20 30 | 31 | December 14, 2023, Switch to Sepolia: 32 | ape-etherscan 0.6.10 33 | ape-polygon 0.6.6 34 | ape-solidity 0.6.9 35 | ape-infura 0.6.4 36 | eth-ape 0.6.19 37 | 38 | """ 39 | 40 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 41 | 42 | reward_token = deployer.deploy(project.LynxStakingToken) 43 | 44 | mock_threshold_staking = deployer.deploy(project.TestnetThresholdStaking) 45 | 46 | taco_application = deployer.deploy(project.TACoApplication) 47 | 48 | deployer.transact(mock_threshold_staking.setApplication, taco_application.address) 49 | 50 | mock_polygon_root = deployer.deploy(project.MockPolygonRoot) 51 | deployer.transact(taco_application.setChildApplication, mock_polygon_root.address) 52 | 53 | deployments = [ 54 | reward_token, 55 | mock_threshold_staking, 56 | taco_application, 57 | mock_polygon_root, 58 | ] 59 | 60 | deployer.finalize(deployments=deployments) 61 | -------------------------------------------------------------------------------- /deployment/constructor_params/lynx/bqeth.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: bqeth-lynx 3 | chain_id: 80002 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: bqeth-lynx.json 8 | 9 | constants: 10 | # See deployment/artifacts/lynx.json 11 | COORDINATOR_PROXY: "0xE9e94499bB0f67b9DBD75506ec1735486DE57770" 12 | 13 | # LynxRitualToken, see deployment/artifacts/lynx.json 14 | LYNX_RITUAL_TOKEN: "0x064Be2a9740e565729BC0d47bC616c5bb8Cc87B9" 15 | 16 | # lynx deployer account 17 | LYNX_DEPLOYER: "0x3B42d26E19FF860bC4dEbB920DD8caA53F93c600" 18 | 19 | MAX_NODES: 4 20 | 21 | # Let's use proposed values for mainnet. See https://github.com/nucypher/tdec/issues/169 22 | INITIAL_BASE_FEE_RATE: 4050925925925 # $0.35 per day, in DAI units per second (in Python: 35*10**16 // 86400) 23 | ENCRYPTOR_FEE_RATE: 63419583967 # $2 per year, in DAI units per second (in Python: 2 * 10**18 // 86400 // 365) 24 | BASE_FEE_RATE_INCREASE: 500 # 5%, expressed as parts on 10,000 25 | 26 | # TODO: Are these good period durations for testing in Lynx? 27 | PERIOD: 172800 # 2 days 28 | YELLOW_PERIOD: 86400 # 1 day 29 | RED_PERIOD: 86400 # 1 day 30 | 31 | contracts: 32 | - GlobalAllowList: 33 | constructor: 34 | _coordinator: $COORDINATOR_PROXY 35 | - StandardSubscription: 36 | proxy: 37 | constructor: 38 | initialOwner: $LYNX_DEPLOYER # Upgrades owner 39 | _data: $encode:initialize,$LYNX_DEPLOYER 40 | constructor: 41 | _coordinator: $COORDINATOR_PROXY 42 | _accessController: $GlobalAllowList 43 | _feeToken: $LYNX_RITUAL_TOKEN 44 | _adopterSetter: $LYNX_DEPLOYER 45 | _initialBaseFeeRate: $INITIAL_BASE_FEE_RATE 46 | _baseFeeRateIncrease: $BASE_FEE_RATE_INCREASE 47 | _encryptorFeeRate: $ENCRYPTOR_FEE_RATE 48 | _maxNodes: $MAX_NODES 49 | _subscriptionPeriodDuration: $PERIOD 50 | _yellowPeriodDuration: $YELLOW_PERIOD 51 | _redPeriodDuration: $RED_PERIOD -------------------------------------------------------------------------------- /.github/workflows/heartbeat.yml: -------------------------------------------------------------------------------- 1 | name: Heartbeat DKG 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 1' # Every Monday at 00:00 7 | 8 | jobs: 9 | initiate_dkg: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Install latest Rust stable 17 | uses: dtolnay/rust-toolchain@stable 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.12.4' 23 | 24 | - name: Install dependencies 25 | run: pip3 install -e . -r requirements.txt 26 | 27 | - name: Import Ape Account 28 | run: .github/scripts/import_account.py 29 | env: 30 | DKG_INITIATOR_PRIVATE_KEY: ${{ secrets.DKG_INITIATOR_PRIVATE_KEY }} 31 | DKG_INITIATOR_PASSPHRASE: ${{ secrets.DKG_INITIATOR_PASSPHRASE }} 32 | 33 | - name: Initiate Ritual 34 | run: .github/scripts/initiate_dkg.sh 35 | env: 36 | # Secret environment variables (secrets) 37 | APE_ACCOUNTS_automation_PASSPHRASE: ${{ secrets.DKG_INITIATOR_PASSPHRASE }} 38 | WEB3_INFURA_API_KEY: ${{ secrets.WEB3_INFURA_API_KEY }} 39 | WEB3_INFURA_API_SECRET: ${{ secrets.WEB3_INFURA_API_SECRET }} 40 | ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} 41 | POLYGONSCAN_API_KEY: ${{ secrets.POLYGONSCAN_API_KEY }} 42 | 43 | # Non-secret environment variables (config) 44 | DKG_AUTHORITY_ADDRESS: ${{ vars.DKG_AUTHORITY_ADDRESS }} 45 | DOMAIN: ${{ vars.DOMAIN }} 46 | NETWORK: ${{ vars.NETWORK }} 47 | ECOSYSTEM: ${{ vars.ECOSYSTEM }} 48 | RPC_PROVIDER: ${{ vars.RPC_PROVIDER }} 49 | ACCESS_CONTROLLER: ${{ vars.ACCESS_CONTROLLER }} 50 | FEE_MODEL: ${{ vars.FEE_MODEL }} 51 | DURATION: ${{ vars.DURATION }} 52 | EXCLUDED_NODES: ${{ vars.EXCLUDED_NODES }} 53 | 54 | - name: Upload Artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: heartbeat-rituals 58 | path: heartbeat-rituals.json 59 | -------------------------------------------------------------------------------- /scripts/mainnet/deploy_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project, networks 4 | 5 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | from deployment.registry import read_registry 8 | 9 | VERIFY = False 10 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "root.yml" 11 | 12 | 13 | def main(): 14 | current_mainnet_registry = read_registry(ARTIFACTS_DIR / "mainnet-child.json") 15 | polygon_chain_id = 137 16 | polygon_child_name = "PolygonChild" 17 | polygon_childs = [ 18 | entry 19 | for entry in current_mainnet_registry 20 | if entry.chain_id == polygon_chain_id and entry.name == polygon_child_name 21 | ] 22 | if len(polygon_childs) != 1: 23 | raise ValueError("Mainnet root deployment requires valid child deployment first") 24 | 25 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 26 | 27 | # TODO: Workaround to provide PolygonChild address from existing registry 28 | polygon_child = polygon_childs.pop() 29 | deployer.constructor_parameters.parameters["PolygonRoot"][ 30 | "_fxChildTunnel" 31 | ] = polygon_child.address 32 | 33 | taco_application = deployer.deploy(project.TACoApplication) 34 | 35 | polygon_root = deployer.deploy(project.PolygonRoot) 36 | 37 | # This line was missing from the original script 38 | # with networks.polygon.mainnet.use_provider("infura"): 39 | # polygon_child = project.PolygonChild.at('invalid') 40 | # deployer.transact(polygon_child.setFxRootTunnel, polygon_root.address) 41 | 42 | # Need to set child application before transferring ownership 43 | deployer.transact(taco_application.setChildApplication, polygon_root.address) 44 | deployer.transact( 45 | taco_application.transferOwnership, deployer.constants.THRESHOLD_COUNCIL_ETH_MAINNET 46 | ) 47 | 48 | deployments = [ 49 | taco_application, 50 | polygon_root, 51 | ] 52 | 53 | deployer.finalize(deployments=deployments) 54 | -------------------------------------------------------------------------------- /scripts/mainnet/deploy_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "mainnet" / "child.yml" 10 | 11 | 12 | def main(): 13 | 14 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 15 | 16 | polygon_child = deployer.deploy(project.PolygonChild) 17 | 18 | taco_child_application = deployer.deploy(project.TACoChildApplication) 19 | 20 | deployer.transact(polygon_child.setChildApplication, taco_child_application.address) 21 | 22 | coordinator = deployer.deploy(project.Coordinator) 23 | 24 | deployer.transact(taco_child_application.initialize, coordinator.address) 25 | 26 | # Grant TREASURY_ROLE to Treasury Guild Multisig on Polygon (0xc3Bf49eBA094AF346830dF4dbB42a07dE378EeB6) 27 | TREASURY_ROLE = coordinator.TREASURY_ROLE() 28 | deployer.transact( 29 | coordinator.grantRole, 30 | TREASURY_ROLE, 31 | deployer.constants.TREASURY_GUILD_ON_POLYGON 32 | ) 33 | 34 | # Grant INITIATOR_ROLE to Integrations Guild 35 | INITIATOR_ROLE = coordinator.INITIATOR_ROLE() 36 | deployer.transact( 37 | coordinator.grantRole, 38 | INITIATOR_ROLE, 39 | deployer.constants.INTEGRATIONS_GUILD_ON_POLYGON 40 | ) 41 | # TODO: BetaProgramInitiator will be deployed separately, so council will grant the role later 42 | 43 | # Change Coordinator admin to Council on Polygon 44 | deployer.transact( 45 | coordinator.beginDefaultAdminTransfer, 46 | deployer.constants.THRESHOLD_COUNCIL_ON_POLYGON 47 | ) 48 | # This requires the Council accepting the transfer by calling acceptDefaultAdminTransfer() 49 | 50 | global_allow_list = deployer.deploy(project.GlobalAllowList) 51 | 52 | deployments = [ 53 | polygon_child, 54 | taco_child_application, 55 | coordinator, 56 | global_allow_list, 57 | ] 58 | 59 | deployer.finalize(deployments=deployments) 60 | -------------------------------------------------------------------------------- /contracts/threshold/IApplicationWithOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity ^0.8.9; 17 | 18 | import "@threshold/contracts/staking/IApplication.sol"; 19 | 20 | /// @title Interface for Threshold Network applications with operator role 21 | interface IApplicationWithOperator is IApplication { 22 | /// @notice Returns operator registered for the given staking provider. 23 | function stakingProviderToOperator(address stakingProvider) external view returns (address); 24 | 25 | /// @notice Returns staking provider of the given operator. 26 | function operatorToStakingProvider(address operator) external view returns (address); 27 | 28 | /// @notice Used by staking provider to set operator address that will 29 | /// operate a node. The operator addressmust be unique. 30 | /// Reverts if the operator is already set for the staking provider 31 | /// or if the operator address is already in use. 32 | /// @dev Depending on application the given staking provider can set operator 33 | /// address only one or multiple times. Besides that application can decide 34 | /// if function reverts if there is a pending authorization decrease for 35 | /// the staking provider. 36 | function registerOperator(address operator) external; 37 | 38 | // TODO consider that? 39 | // /// @notice Used by additional role (owner for example) to set operator address that will 40 | // /// operate a node for the specified staking provider. 41 | // function registerOperator(address stakingProvider, address operator) external; 42 | } 43 | -------------------------------------------------------------------------------- /contracts/contracts/lib/BLS12381.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @notice Library for dealing with BLS12-381 entities 7 | */ 8 | library BLS12381 { 9 | uint256 public constant G1_POINT_SIZE = 48; 10 | uint256 public constant G2_POINT_SIZE = 96; 11 | 12 | // G1 points require 48 bytes, so we need 2 32-byte words 13 | // (or more exactly, a 32-byte word and a 16-byte word) 14 | struct G1Point { 15 | bytes32 word0; 16 | bytes16 word1; 17 | } 18 | 19 | // G2 points require 96 bytes, so we can use exactly 3 32-byte words 20 | struct G2Point { 21 | bytes32 word0; 22 | bytes32 word1; 23 | bytes32 word2; 24 | } 25 | 26 | function bytesToG1Point( 27 | bytes calldata pointBytes 28 | ) internal pure returns (G1Point memory point) { 29 | require(pointBytes.length == G1_POINT_SIZE, "Wrong G1 point size"); 30 | point.word0 = bytes32(pointBytes[:32]); 31 | point.word1 = bytes16(pointBytes[32:]); 32 | } 33 | 34 | function bytesToG2Point( 35 | bytes calldata pointBytes 36 | ) internal pure returns (G2Point memory point) { 37 | require(pointBytes.length == G2_POINT_SIZE, "Wrong G2 point size"); 38 | point.word0 = bytes32(pointBytes[:32]); 39 | point.word1 = bytes32(pointBytes[32:64]); 40 | point.word2 = bytes32(pointBytes[64:]); 41 | } 42 | 43 | function g1PointToBytes(G1Point memory point) internal pure returns (bytes memory) { 44 | return bytes.concat(point.word0, point.word1); 45 | } 46 | 47 | function g2PointToBytes(G2Point memory point) internal pure returns (bytes memory) { 48 | return bytes.concat(point.word0, point.word1, point.word2); 49 | } 50 | 51 | function eqG1Point(G1Point memory p0, G1Point memory p1) internal pure returns (bool) { 52 | return p0.word0 == p1.word0 && p0.word1 == p1.word1; 53 | } 54 | 55 | function eqG2Point(G2Point memory p0, G2Point memory p1) internal pure returns (bool) { 56 | return p0.word0 == p1.word0 && p0.word1 == p1.word1 && p0.word2 == p1.word2; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/tapir/deploy_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "tapir" / "child.yml" 10 | 11 | 12 | def main(): 13 | """ 14 | This script deploys the Mock Tapir TACo Root Application, 15 | Proxied Tapir TACo Child Application, Tapir Ritual Token, and Tapir Coordinator. 16 | 17 | October 6th, 2023, Deployment: 18 | ape-run deploy_child --network polygon:mumbai:infura 19 | ape-etherscan 0.6.10 20 | ape-infura 0.6.4 21 | ape-polygon 0.6.6 22 | ape-solidity 0.6.9 23 | eth-ape 0.6.20 24 | 25 | January 8th, 2024 26 | ape run tapir deploy_child --network polygon:mumbai:infura 27 | ape-etherscan 0.6.10 28 | ape-infura 0.6.4 29 | ape-polygon 0.6.6 30 | ape-solidity 0.6.9 31 | eth-ape 0.6.19 32 | 33 | April 2nd, 2024 34 | ape run tapir deploy_child --network polygon:amoy:infura 35 | ape-etherscan 0.7.0 36 | ape-infura 0.7.2 37 | ape-polygon 0.7.2 38 | ape-solidity 0.7.1 39 | eth-ape 0.7.7 40 | """ 41 | 42 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 43 | 44 | mock_polygon_child = deployer.deploy(project.MockPolygonChild) 45 | 46 | taco_child_application = deployer.deploy(project.TACoChildApplication) 47 | 48 | deployer.transact(mock_polygon_child.setChildApplication, taco_child_application.address) 49 | 50 | ritual_token = deployer.deploy(project.TapirRitualToken) 51 | coordinator = deployer.deploy(project.Coordinator) 52 | 53 | deployer.transact(taco_child_application.initialize, coordinator.address) 54 | 55 | global_allow_list = deployer.deploy(project.GlobalAllowList) 56 | 57 | deployments = [ 58 | mock_polygon_child, 59 | taco_child_application, 60 | ritual_token, 61 | coordinator, 62 | global_allow_list, 63 | ] 64 | 65 | deployer.finalize(deployments=deployments) 66 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/redeploy-marlin.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: marlin-upgrade 3 | chain_id: 137 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: marlin-upgrade.json 8 | 9 | constants: 10 | # See deployment/artifacts/mainnet.json 11 | COORDINATOR_PROXY: "0xE74259e3dafe30bAA8700238e324b47aC98FE755" 12 | GLOBAL_ALLOWLIST: "0x3E37C7A9a83B326a0d156DE3Ee6B18fd8079f698" 13 | 14 | # See https://github.com/nucypher/tdec/issues/137#issuecomment-1881525878 15 | # and https://app.safe.global/home?safe=matic:0x861aa915C785dEe04684444560fC7A2AB43a1543 16 | NUCO_MULTISIG: "0x861aa915C785dEe04684444560fC7A2AB43a1543" 17 | 18 | # DAI Token on Polygon PoS - References: 19 | # - https://polygonscan.com/token/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 20 | # - https://github.com/search?q=0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063&type=code 21 | DAI_ON_POLYGON: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" 22 | 23 | # Subscription Parameters 24 | # See https://github.com/nucypher/tdec/issues/169 25 | 26 | MAX_NODES: 30 27 | 28 | # - Fee parameters: 29 | INITIAL_BASE_FEE_RATE: 5787037037037 # $0.5 per day, in DAI units per second (in Python: 50*10**16 // 86400) 30 | ENCRYPTOR_FEE_RATE: 79274479959 # $2.5 per year, in DAI units per second (in Python: 250 * 10**16 // 86400 // 365) 31 | BASE_FEE_RATE_INCREASE_PER_PERIOD: 500 # 5%/year, expressed in basis points (0.01%) 32 | # - Duration parameters --> 1 period = 365 days 33 | ONE_YEAR_IN_SECONDS: 31536000 # 365 days (~1 year) 34 | THREE_MONTHS_IN_SECONDS: 7776000 # 90 day (~3 months) 35 | 36 | contracts: 37 | - StandardSubscription: 38 | constructor: 39 | _coordinator: $COORDINATOR_PROXY 40 | _accessController: $GLOBAL_ALLOWLIST 41 | _feeToken: $DAI_ON_POLYGON 42 | _adopterSetter: $NUCO_MULTISIG 43 | _initialBaseFeeRate: $INITIAL_BASE_FEE_RATE 44 | _baseFeeRateIncrease: $BASE_FEE_RATE_INCREASE_PER_PERIOD 45 | _encryptorFeeRate: $ENCRYPTOR_FEE_RATE 46 | _maxNodes: $MAX_NODES 47 | _subscriptionPeriodDuration: $ONE_YEAR_IN_SECONDS 48 | _yellowPeriodDuration: $THREE_MONTHS_IN_SECONDS 49 | _redPeriodDuration: $THREE_MONTHS_IN_SECONDS 50 | -------------------------------------------------------------------------------- /scripts/request_handover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import click 4 | from ape.cli import ConnectedProviderCommand, account_option, network_option 5 | 6 | from deployment import registry 7 | from deployment.constants import SUPPORTED_TACO_DOMAINS 8 | from deployment.params import Transactor 9 | from deployment.types import ChecksumAddress 10 | from deployment.utils import check_plugins 11 | 12 | 13 | @click.command(cls=ConnectedProviderCommand, name="request-handover") 14 | @account_option() 15 | @network_option(required=True) 16 | @click.option( 17 | "--domain", 18 | "-d", 19 | help="TACo domain", 20 | type=click.Choice(SUPPORTED_TACO_DOMAINS), 21 | required=True, 22 | ) 23 | @click.option("--ritual-id", "-r", help="Ritual ID to check", type=int, required=True) 24 | @click.option( 25 | "--departing-provider", 26 | "-dp", 27 | help="The ethereum address of the departing staking provider.", 28 | required=True, 29 | type=ChecksumAddress(), 30 | ) 31 | @click.option( 32 | "--incoming-provider", 33 | "-ip", 34 | help="The ethereum address of the incoming staking provider.", 35 | required=True, 36 | type=ChecksumAddress(), 37 | ) 38 | @click.option( 39 | "--auto", 40 | help="Automatically sign transactions.", 41 | is_flag=True, 42 | ) 43 | def cli( 44 | domain, 45 | account, 46 | network, 47 | ritual_id, 48 | departing_provider, 49 | incoming_provider, 50 | auto, 51 | ): 52 | """Request a handover.""" 53 | 54 | # Setup 55 | check_plugins() 56 | click.echo(f"Connected to {network.name} network.") 57 | 58 | # Get the contracts from the registry 59 | coordinator_contract = registry.get_contract(domain=domain, contract_name="Coordinator") 60 | 61 | # Issue handover request 62 | click.echo( 63 | f"Requesting handover for ritual {ritual_id} from " 64 | f"{departing_provider} to {incoming_provider}..." 65 | ) 66 | transactor = Transactor(account=account, autosign=auto) 67 | transactor.transact( 68 | coordinator_contract.handoverRequest, 69 | ritual_id, 70 | departing_provider, 71 | incoming_provider, 72 | ) 73 | 74 | 75 | if __name__ == "__main__": 76 | cli() 77 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/redeploy-bqeth.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: bqeth-upgrade 3 | chain_id: 137 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: bqeth-upgrade.json 8 | 9 | constants: 10 | # See deployment/artifacts/mainnet.json 11 | COORDINATOR_PROXY: "0xE74259e3dafe30bAA8700238e324b47aC98FE755" 12 | BQETH_GLOBAL_ALLOWLIST: "0x6D25F454D9FDE0d9b71f34965cb53316e6549c94" 13 | 14 | # See https://github.com/nucypher/tdec/issues/137#issuecomment-1881525878 15 | # and https://app.safe.global/home?safe=matic:0x861aa915C785dEe04684444560fC7A2AB43a1543 16 | NUCO_MULTISIG: "0x861aa915C785dEe04684444560fC7A2AB43a1543" 17 | 18 | # DAI Token on Polygon PoS - References: 19 | # - https://polygonscan.com/token/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 20 | # - https://github.com/search?q=0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063&type=code 21 | DAI_ON_POLYGON: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" 22 | 23 | # Subscription Parameters 24 | # See https://github.com/nucypher/tdec/issues/169 25 | 26 | MAX_NODES: 30 27 | 28 | # - Fee parameters: 29 | INITIAL_BASE_FEE_RATE: 4050925925925 # $0.35 per day, in DAI units per second (in Python: 35*10**16 // 86400) 30 | ENCRYPTOR_FEE_RATE: 63419583967 # $2 per year, in DAI units per second (in Python: 2 * 10**18 // 86400 // 365) 31 | BASE_FEE_RATE_INCREASE_PER_PERIOD: 247 # 5%/year ~ 2.47%/semester, expressed in basis points (0.01%) 32 | # - Duration parameters --> 1 period = 6 months 33 | SIX_MONTHS_IN_SECONDS: 15811200 # 183 days (~6 months) 34 | THREE_MONTHS_IN_SECONDS: 7776000 # 90 day (~3 months) 35 | 36 | contracts: 37 | - StandardSubscription: 38 | constructor: 39 | _coordinator: $COORDINATOR_PROXY 40 | _accessController: $BQETH_GLOBAL_ALLOWLIST 41 | _feeToken: $DAI_ON_POLYGON 42 | _adopterSetter: $NUCO_MULTISIG 43 | _initialBaseFeeRate: $INITIAL_BASE_FEE_RATE 44 | _baseFeeRateIncrease: $BASE_FEE_RATE_INCREASE_PER_PERIOD 45 | _encryptorFeeRate: $ENCRYPTOR_FEE_RATE 46 | _maxNodes: $MAX_NODES 47 | _subscriptionPeriodDuration: $SIX_MONTHS_IN_SECONDS 48 | _yellowPeriodDuration: $THREE_MONTHS_IN_SECONDS 49 | _redPeriodDuration: $THREE_MONTHS_IN_SECONDS 50 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: ["push", "pull_request"] 2 | 3 | name: main workflow 4 | 5 | env: 6 | # ETHERSCAN_TOKEN: 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | # WEB3_INFURA_PROJECT_ID: 9 | 10 | # increasing available memory for node reduces issues with ganache crashing 11 | # https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes 12 | NODE_OPTIONS: --max_old_space_size=4096 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v3 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 18.x 33 | cache: "npm" 34 | - run: npm install 35 | 36 | - name: Setup Python 3.11 37 | uses: actions/setup-python@v4 38 | with: 39 | cache: "pip" 40 | python-version: "3.11" 41 | - run: pip install -e . -r requirements.txt 42 | 43 | - name: Run Ape Tests 44 | run: ape test 45 | 46 | - name: Run Deployment Test 47 | run: ape run ci deploy_child 48 | 49 | - name: Run Registry Scripts to list contracts 50 | run: | 51 | ape run list_contracts --domain lynx 52 | ape run list_contracts --domain tapir 53 | ape run list_contracts --domain mainnet 54 | 55 | - name: Build npm package 56 | run: npm run build 57 | 58 | - name: Run TS Tests 59 | run: npm run test 60 | 61 | linting: 62 | runs-on: ubuntu-latest 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | 67 | - name: Setup Node.js 68 | uses: actions/setup-node@v4 69 | with: 70 | node-version: 18.x 71 | cache: "npm" 72 | - run: npm install 73 | 74 | - name: Solidity Lint 75 | run: npm run solhint 76 | 77 | - name: TypeScript Lint 78 | run: npm run lint 79 | 80 | - name: Python Lint 81 | uses: cclauss/GitHub-Action-for-pylint@0.7.0 82 | continue-on-error: true 83 | with: 84 | args: "pylint **/*.py" 85 | -------------------------------------------------------------------------------- /scripts/lynx/deploy_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import project 4 | 5 | from deployment.constants import CONSTRUCTOR_PARAMS_DIR 6 | from deployment.params import Deployer 7 | 8 | VERIFY = False 9 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "lynx" / "child.yml" 10 | 11 | 12 | def main(): 13 | """ 14 | This script deploys the Mock Lynx TACo Root Application, 15 | Proxied Lynx TACo Child Application, Lynx Ritual Token, and Lynx Coordinator. 16 | 17 | September 25, 2023, Deployment: 18 | ape-run deploy_lynx --network polygon:mumbai:infura 19 | ape-etherscan 0.6.10 20 | ape-infura 0.6.3 21 | ape-polygon 0.6.5 22 | ape-solidity 0.6.9 23 | eth-ape 0.6.20 24 | 25 | November 16, 2023, Update: 26 | ape-etherscan 0.6.10 27 | ape-infura 0.6.4 28 | ape-polygon 0.6.6 29 | ape-solidity 0.6.9 30 | eth-ape 0.6.20 31 | 32 | March 28, 2024. Deployment on amoy (mumbai deprecated) 33 | ape-run deploy_child --network polygon:amoy:infura 34 | ape-etherscan 0.7.0 35 | ape-infura 0.7.2.dev2+g5215faf (customized local version for amoy support - 36 | https://github.com/ApeWorX/ape-infura/pull/76) 37 | ape-polygon 0.7.2 38 | ape-solidity 0.7.1 39 | eth-ape 0.7.7 40 | 41 | """ 42 | 43 | deployer = Deployer.from_yaml(filepath=CONSTRUCTOR_PARAMS_FILEPATH, verify=VERIFY) 44 | 45 | mock_polygon_child = deployer.deploy(project.MockPolygonChild) 46 | 47 | taco_child_application = deployer.deploy(project.TACoChildApplication) 48 | 49 | deployer.transact(mock_polygon_child.setChildApplication, taco_child_application.address) 50 | 51 | ritual_token = deployer.deploy(project.LynxRitualToken) 52 | 53 | coordinator = deployer.deploy(project.Coordinator) 54 | deployer.transact(taco_child_application.initialize, coordinator.address) 55 | 56 | global_allow_list = deployer.deploy(project.GlobalAllowList) 57 | 58 | deployments = [ 59 | mock_polygon_child, 60 | taco_child_application, 61 | ritual_token, 62 | coordinator, 63 | global_allow_list, 64 | ] 65 | 66 | deployer.finalize(deployments=deployments) 67 | -------------------------------------------------------------------------------- /scripts/list_contracts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | 4 | from itertools import groupby 5 | from typing import Optional, List, Tuple 6 | 7 | import click 8 | from ape.cli import ConnectedProviderCommand 9 | 10 | from deployment.constants import SUPPORTED_TACO_DOMAINS 11 | from deployment.registry import read_registry, RegistryEntry 12 | from deployment.utils import get_chain_name, registry_filepath_from_domain 13 | 14 | 15 | def _format_chain_name(chain_name: str) -> str: 16 | """Format the chain name to capitalize each word and join with slashes.""" 17 | return "/".join(word.capitalize() for word in chain_name.split()) 18 | 19 | 20 | def _get_registry_entries(domain: Optional[str] = None) -> List[Tuple[str, List[RegistryEntry]]]: 21 | """Parse the registry files for the given domain or all supported domains.""" 22 | registry_entries = list() 23 | for taco_domain in SUPPORTED_TACO_DOMAINS: 24 | if domain and domain != taco_domain: 25 | continue 26 | registry_filepath = registry_filepath_from_domain(domain=taco_domain) 27 | entries = read_registry(filepath=registry_filepath) 28 | registry_entries.append((taco_domain, entries)) 29 | return registry_entries 30 | 31 | 32 | def _display_registry_entries(registry_entries: List[Tuple[str, List[RegistryEntry]]]) -> None: 33 | """Display registry entries grouped by chain ID.""" 34 | for domain, entries in registry_entries: 35 | grouped_entries = groupby(entries, key=lambda e: e.chain_id) 36 | click.secho(f"\n{domain.capitalize()} Domain", fg="green") 37 | 38 | for chain_id, chain_entries in grouped_entries: 39 | chain_name = _format_chain_name(get_chain_name(chain_id)) 40 | click.secho(f" {chain_name}", fg="yellow") 41 | 42 | for index, entry in enumerate(chain_entries, start=1): 43 | click.secho(f" {index}. {entry.name} {entry.address}", fg="cyan") 44 | 45 | 46 | @click.command(cls=ConnectedProviderCommand, name="list-contracts") 47 | @click.option( 48 | "--domain", 49 | "-d", 50 | help="TACo domain", 51 | type=click.Choice(SUPPORTED_TACO_DOMAINS), 52 | ) 53 | def cli(domain): 54 | """List all contracts in the registry. Optionally filter by domain.""" 55 | registry_entries = _get_registry_entries(domain) 56 | _display_registry_entries(registry_entries) 57 | 58 | 59 | if __name__ == "__main__": 60 | cli() 61 | -------------------------------------------------------------------------------- /scripts/ritual_membership.py: -------------------------------------------------------------------------------- 1 | import click 2 | from ape import networks, project 3 | from ape.cli import ConnectedProviderCommand, network_option 4 | from eth_typing import ChecksumAddress 5 | from eth_utils import to_checksum_address 6 | 7 | from deployment.constants import SUPPORTED_TACO_DOMAINS 8 | from deployment.registry import contracts_from_registry 9 | from deployment.utils import registry_filepath_from_domain 10 | 11 | 12 | @click.command(cls=ConnectedProviderCommand) 13 | @network_option(required=True) 14 | @click.option( 15 | "--domain", 16 | "-d", 17 | help="TACo domain", 18 | type=click.Choice(SUPPORTED_TACO_DOMAINS), 19 | required=True, 20 | ) 21 | @click.option( 22 | "--staking-provider-address", 23 | "-p", 24 | help="Staking provider address to check", 25 | type=ChecksumAddress, 26 | required=True, 27 | ) 28 | def cli(network, domain, staking_provider_address): 29 | """Lists all the active rituals that a staking provider is participating in.""" 30 | registry_filepath = registry_filepath_from_domain(domain=domain) 31 | contracts = contracts_from_registry( 32 | registry_filepath, chain_id=networks.active_provider.chain_id 33 | ) 34 | 35 | provider_checksum_address = to_checksum_address(staking_provider_address) 36 | # lower used for comparing against sorted list 37 | provider_checksum_address_lower = provider_checksum_address.lower() 38 | 39 | coordinator = project.Coordinator.at(contracts["Coordinator"].address) 40 | num_rituals = coordinator.numberOfRituals() 41 | 42 | ritual_memberships = [] 43 | for ritual_id in range(0, num_rituals): 44 | if not coordinator.isRitualActive(ritual_id): 45 | continue 46 | 47 | participants = coordinator.getParticipants(ritual_id) 48 | for participant in participants: 49 | provider = participant.provider 50 | if provider == provider_checksum_address: 51 | ritual_memberships.append(ritual_id) 52 | break 53 | if provider_checksum_address_lower < provider: 54 | # list of participants is sorted so stop early if already passed 55 | break 56 | 57 | if not ritual_memberships: 58 | print(f"\nStaking provider {provider_checksum_address} is not part of any rituals") 59 | return 60 | 61 | print("\nActive Ritual Memberships:") 62 | for ritual_id in ritual_memberships: 63 | print(f"\t- ID: #{ritual_id}") 64 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/root.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: mainnet-root 3 | chain_id: 1 # Ethereum Mainnet 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: mainnet-root.json 8 | 9 | constants: 10 | # Threshold Network - References: 11 | # - https://docs.threshold.network/resources/contract-addresses/mainnet/threshold-dao 12 | T_TOKEN_ETH_MAINNET: "0xCdF7028ceAB81fA0C6971208e83fa7872994beE5" 13 | T_STAKING_CONTRACT: "0x01B67b1194C75264d06F808A921228a95C765dd7" 14 | THRESHOLD_COUNCIL_ETH_MAINNET: "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f" 15 | 16 | # FxPortal addresses - References: 17 | # - https://github.com/0xPolygon/fx-portal#deployment-addresses 18 | # - https://github.com/0xPolygon/fx-portal/blob/main/config/config.json 19 | # - https://wiki.polygon.technology/docs/pos/design/bridge/l1-l2-communication/state-transfer/#prerequisites 20 | FXPORTAL_CHECKPOINT_MANAGER: "0x86e4dc95c7fbdbf52e33d563bbdb00823894c287" 21 | FXPORTAL_FXROOT: "0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2" 22 | POLYGON_CHILD: "invalid" # Temporary invalid value that will be overwritten by workaround in deployment script 23 | 24 | # TACo specific constants: 25 | IN_SECONDS_1_DAY: 86400 26 | IN_SECONDS_91_DAYS: 7862400 27 | IN_SECONDS_182_DAYS: 15724800 28 | IN_SECONDS_364_DAYS: 31449600 29 | IN_SECONDS_546_DAYS: 47174400 30 | IN_SECONDS_AVERAGE_MONTH_DURATION: 2628000 # 365*24*60*60/12 31 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 32 | TIMESTAMP_FOR_2023_12_30_2359_UTC: 1703980799 33 | 34 | contracts: 35 | - TACoApplication: 36 | proxy: 37 | constructor: 38 | initialOwner: $THRESHOLD_COUNCIL_ETH_MAINNET # Upgrades owner 39 | _data: $encode:initialize 40 | constructor: 41 | _token: $T_TOKEN_ETH_MAINNET 42 | _tStaking: $T_STAKING_CONTRACT 43 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 44 | _minOperatorSeconds: $IN_SECONDS_1_DAY 45 | _rewardDuration: $IN_SECONDS_AVERAGE_MONTH_DURATION 46 | _deauthorizationDuration: $IN_SECONDS_182_DAYS 47 | _commitmentDurationOptions: [$IN_SECONDS_91_DAYS, $IN_SECONDS_182_DAYS, $IN_SECONDS_364_DAYS, $IN_SECONDS_546_DAYS] 48 | _commitmentDeadline: $TIMESTAMP_FOR_2023_12_30_2359_UTC 49 | - PolygonRoot: 50 | constructor: 51 | _checkpointManager: $FXPORTAL_CHECKPOINT_MANAGER 52 | _fxRoot: $FXPORTAL_FXROOT 53 | _rootApplication: $TACoApplication 54 | _fxChildTunnel: $POLYGON_CHILD # This is a Polygon address 55 | -------------------------------------------------------------------------------- /scripts/verify.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import click 4 | from ape import networks 5 | from ape.cli import ConnectedProviderCommand, network_option 6 | 7 | from deployment.constants import SUPPORTED_TACO_DOMAINS 8 | from deployment.registry import contracts_from_registry 9 | from deployment.utils import get_contract_container, registry_filepath_from_domain, verify_contracts 10 | 11 | 12 | @click.command(cls=ConnectedProviderCommand) 13 | @network_option(required=True) 14 | @click.option("--contract-name", "-c", help="Contract to verify", type=click.STRING, required=True) 15 | @click.option( 16 | "--domain", 17 | "-d", 18 | help="TACo domain; used for obtaining contract registry", 19 | type=click.Choice(SUPPORTED_TACO_DOMAINS), 20 | required=False, 21 | ) 22 | @click.option( 23 | "--registry-filepath", 24 | "-f", 25 | type=click.Path(dir_okay=False, exists=True, path_type=Path), 26 | help="Registry filepath if the contract is not part of a common domain registry", 27 | required=False, 28 | ) 29 | def cli(network, domain, contract_name, registry_filepath): 30 | """Verify a deployed contract.""" 31 | if not (bool(registry_filepath) ^ bool(domain)): 32 | raise click.BadOptionUsage( 33 | option_name="--domain", 34 | message=( 35 | f"Provide either 'domain' or 'registry_filepath'; " 36 | f"got {domain}, {registry_filepath}" 37 | ), 38 | ) 39 | 40 | registry_filepath = registry_filepath or registry_filepath_from_domain(domain=domain) 41 | chain_id = networks.active_provider.chain_id 42 | contracts = contracts_from_registry(registry_filepath, chain_id=chain_id) 43 | 44 | try: 45 | contract_instance = contracts[contract_name] 46 | except KeyError: 47 | raise ValueError( 48 | f"Contract '{contract_name}' not found in registry, '{registry_filepath}', " 49 | f"for chain {chain_id}" 50 | ) 51 | 52 | # check whether contract is a proxy 53 | proxy_info = networks.provider.network.ecosystem.get_proxy_info(contract_instance.address) 54 | if proxy_info: 55 | # we have an instance of a proxy contract, but need the underlying implementation 56 | print(f"Proxy contract detected; verifying implementation contract at {proxy_info.target}") 57 | contract_container = get_contract_container(contract_instance.contract_type.name) 58 | contract_instance = contract_container.at(proxy_info.target) 59 | 60 | verify_contracts([contract_instance]) 61 | 62 | 63 | if __name__ == "__main__": 64 | cli() 65 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/child.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: mainnet-child 3 | chain_id: 137 # Polygon Mainnet 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: mainnet-child.json 8 | 9 | constants: 10 | # DAI Token on Polygon PoS - References: 11 | # - https://polygonscan.com/token/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 12 | # - https://github.com/search?q=0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063&type=code 13 | DAI_ON_POLYGON: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" 14 | 15 | # TACO specific constants: 16 | PRIVATE_BETA_FEE_RATE: 4050925925925 # $0.35 per day, expressed in DAI units per seconds (in Python: 35*10**16 // 86400) 17 | MAX_DKG_SIZE: 32 18 | DKG_TIMEOUT_IN_SECONDS: 3600 # One hour in seconds 19 | HANDOVER_TIMEOUT_IN_SECONDS: 86400 # One day in seconds 20 | FORTY_THOUSAND_TOKENS_IN_WEI_UNITS: 40000000000000000000000 21 | 22 | # Threshold Network - References: 23 | # - https://docs.threshold.network/resources/contract-addresses/mainnet/threshold-dao 24 | # - https://github.com/keep-network/tbtc-v2/issues/594 25 | TREASURY_GUILD_ON_POLYGON: "0xc3Bf49eBA094AF346830dF4dbB42a07dE378EeB6" 26 | INTEGRATIONS_GUILD_ON_POLYGON: "0x5bD70E385414C8dCC25305AeB7E542d8FC70e667" 27 | THRESHOLD_COUNCIL_ON_POLYGON: "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f" 28 | 29 | # FxPortal addresses - References: 30 | # - https://github.com/0xPolygon/fx-portal#deployment-addresses 31 | # - https://github.com/0xPolygon/fx-portal/blob/main/config/config.json 32 | # - https://wiki.polygon.technology/docs/pos/design/bridge/l1-l2-communication/state-transfer/#prerequisites 33 | FXPORTAL_FXCHILD: "0x8397259c983751DAf40400790063935a11afa28a" 34 | 35 | contracts: 36 | - PolygonChild: 37 | constructor: 38 | _fxChild: $FXPORTAL_FXCHILD 39 | - TACoChildApplication: 40 | proxy: 41 | constructor: 42 | initialOwner: $THRESHOLD_COUNCIL_ON_POLYGON # Upgrades owner 43 | constructor: 44 | _rootApplication: $PolygonChild 45 | _minimumAuthorization: $FORTY_THOUSAND_TOKENS_IN_WEI_UNITS 46 | - Coordinator: 47 | proxy: 48 | constructor: 49 | initialOwner: $THRESHOLD_COUNCIL_ON_POLYGON # Upgrades owner 50 | _data: $encode:initialize,$MAX_DKG_SIZE,$deployer 51 | constructor: 52 | _application: $TACoChildApplication 53 | _dkgTimeout: $DKG_TIMEOUT_IN_SECONDS 54 | _handoverTimeout: $HANDOVER_TIMEOUT_IN_SECONDS 55 | _currency: $DAI_ON_POLYGON 56 | _feeRatePerSecond: $PRIVATE_BETA_FEE_RATE 57 | - GlobalAllowList: 58 | constructor: 59 | _coordinator: $Coordinator 60 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/bqeth.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: bqeth-mainnet 3 | chain_id: 137 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: bqeth-mainnet.json 8 | 9 | constants: 10 | # See deployment/artifacts/mainnet.json 11 | COORDINATOR_PROXY: "0xE74259e3dafe30bAA8700238e324b47aC98FE755" 12 | 13 | # See https://github.com/nucypher/tdec/issues/137#issuecomment-1881525878 14 | # and https://app.safe.global/home?safe=matic:0x861aa915C785dEe04684444560fC7A2AB43a1543 15 | NUCO_MULTISIG: "0x861aa915C785dEe04684444560fC7A2AB43a1543" 16 | 17 | # DAI Token on Polygon PoS - References: 18 | # - https://polygonscan.com/token/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 19 | # - https://github.com/search?q=0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063&type=code 20 | DAI_ON_POLYGON: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" 21 | 22 | # Threshold Network - References: 23 | # - https://docs.threshold.network/resources/contract-addresses/mainnet/threshold-dao 24 | # - https://github.com/keep-network/tbtc-v2/issues/594 25 | TREASURY_GUILD_ON_POLYGON: "0xc3Bf49eBA094AF346830dF4dbB42a07dE378EeB6" 26 | THRESHOLD_COUNCIL_ON_POLYGON: "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f" 27 | 28 | # Subscription Parameters 29 | # See https://github.com/nucypher/tdec/issues/169 30 | 31 | MAX_NODES: 30 32 | 33 | # - Fee parameters: 34 | INITIAL_BASE_FEE_RATE: 4050925925925 # $0.35 per day, in DAI units per second (in Python: 35*10**16 // 86400) 35 | ENCRYPTOR_FEE_RATE: 63419583967 # $2 per year, in DAI units per second (in Python: 2 * 10**18 // 86400 // 365) 36 | BASE_FEE_RATE_INCREASE_PER_PERIOD: 247 # 5%/year ~ 2.47%/semester, expressed in basis points (0.01%) 37 | # - Duration parameters --> 1 period = 6 months 38 | SIX_MONTHS_IN_SECONDS: 15811200 # 183 days (~6 months) 39 | THREE_MONTHS_IN_SECONDS: 7776000 # 90 day (~3 months) 40 | 41 | contracts: 42 | - GlobalAllowList: 43 | constructor: 44 | _coordinator: $COORDINATOR_PROXY 45 | - StandardSubscription: 46 | proxy: 47 | constructor: 48 | initialOwner: $THRESHOLD_COUNCIL_ON_POLYGON # Upgrades owner 49 | _data: $encode:initialize,$TREASURY_GUILD_ON_POLYGON 50 | constructor: 51 | _coordinator: $COORDINATOR_PROXY 52 | _accessController: $GlobalAllowList 53 | _feeToken: $DAI_ON_POLYGON 54 | _adopterSetter: $NUCO_MULTISIG 55 | _initialBaseFeeRate: $INITIAL_BASE_FEE_RATE 56 | _baseFeeRateIncrease: $BASE_FEE_RATE_INCREASE_PER_PERIOD 57 | _encryptorFeeRate: $ENCRYPTOR_FEE_RATE 58 | _maxNodes: $MAX_NODES 59 | _subscriptionPeriodDuration: $SIX_MONTHS_IN_SECONDS 60 | _yellowPeriodDuration: $THREE_MONTHS_IN_SECONDS 61 | _redPeriodDuration: $THREE_MONTHS_IN_SECONDS 62 | -------------------------------------------------------------------------------- /contracts/test/StandardSubscriptionTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../contracts/coordination/IEncryptionAuthorizer.sol"; 6 | import "../contracts/coordination/Coordinator.sol"; 7 | import "../contracts/coordination/IFeeModel.sol"; 8 | 9 | contract CoordinatorForStandardSubscriptionMock { 10 | struct Ritual { 11 | uint32 endTimestamp; 12 | IEncryptionAuthorizer accessController; 13 | Coordinator.RitualState state; 14 | } 15 | 16 | IFeeModel public feeModel; 17 | 18 | mapping(uint32 => Ritual) public rituals; 19 | 20 | function setFeeModel(IFeeModel _feeModel) external { 21 | feeModel = _feeModel; 22 | } 23 | 24 | function processRitualExtending( 25 | address initiator, 26 | uint32 ritualId, 27 | uint256 numberOfProviders, 28 | uint32 duration 29 | ) external { 30 | feeModel.processRitualExtending(initiator, ritualId, numberOfProviders, duration); 31 | } 32 | 33 | function processRitualPayment( 34 | address initiator, 35 | uint32 ritualId, 36 | uint256 numberOfProviders, 37 | uint32 duration 38 | ) external { 39 | feeModel.processRitualPayment(initiator, ritualId, numberOfProviders, duration); 40 | } 41 | 42 | function setRitual( 43 | uint32 _ritualId, 44 | Coordinator.RitualState _state, 45 | uint32 _endTimestamp, 46 | IEncryptionAuthorizer _accessController 47 | ) external { 48 | Ritual storage ritual = rituals[_ritualId]; 49 | ritual.state = _state; 50 | ritual.endTimestamp = _endTimestamp; 51 | ritual.accessController = _accessController; 52 | } 53 | 54 | function getAccessController(uint32 _ritualId) external view returns (IEncryptionAuthorizer) { 55 | return rituals[_ritualId].accessController; 56 | } 57 | 58 | function getRitualState(uint32 _ritualId) external view returns (Coordinator.RitualState) { 59 | return rituals[_ritualId].state; 60 | } 61 | 62 | function getFeeModel(uint32) external view returns (IFeeModel) { 63 | return feeModel; 64 | } 65 | 66 | function getTimestamps( 67 | uint32 _ritualId 68 | ) external view returns (uint32 initTimestamp, uint32 endTimestamp) { 69 | initTimestamp = 0; 70 | endTimestamp = rituals[_ritualId].endTimestamp; 71 | } 72 | 73 | function numberOfRituals() external pure returns (uint256) { 74 | return 1; 75 | } 76 | 77 | function getAuthority(uint32) external view returns (address) { 78 | // solhint-disable-next-line avoid-tx-origin 79 | return tx.origin; 80 | } 81 | 82 | function isRitualActive(uint32) external view returns (bool) { 83 | return true; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /contracts/test/AdjudicatorTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../contracts/Adjudicator.sol"; 6 | import "../contracts/lib/SignatureVerifier.sol"; 7 | 8 | /** 9 | * @notice Contract for testing the Adjudicator contract 10 | */ 11 | contract TACoApplicationForAdjudicatorMock { 12 | uint32 public immutable secondsPerPeriod = 1; 13 | mapping(address => uint96) public stakingProviderInfo; 14 | mapping(address => uint256) public rewardInfo; 15 | mapping(address => address) internal _stakingProviderFromOperator; 16 | 17 | function operatorToStakingProvider(address _operator) public view returns (address) { 18 | return _stakingProviderFromOperator[_operator]; 19 | } 20 | 21 | function setStakingProviderInfo( 22 | address _stakingProvider, 23 | uint96 _amount, 24 | address _operator 25 | ) public { 26 | stakingProviderInfo[_stakingProvider] = _amount; 27 | if (_operator == address(0)) { 28 | _operator = _stakingProvider; 29 | } 30 | _stakingProviderFromOperator[_operator] = _stakingProvider; 31 | } 32 | 33 | function authorizedStake(address _stakingProvider) public view returns (uint96) { 34 | return stakingProviderInfo[_stakingProvider]; 35 | } 36 | 37 | function slash(address _stakingProvider, uint96 _penalty, address _investigator) external { 38 | stakingProviderInfo[_stakingProvider] -= _penalty; 39 | rewardInfo[_investigator] += 1; 40 | } 41 | } 42 | 43 | ///** 44 | //* @notice Upgrade to this contract must lead to fail 45 | //*/ 46 | //contract AdjudicatorBad is Upgradeable { 47 | // 48 | // mapping (bytes32 => bool) public evaluatedCFrags; 49 | // mapping (address => uint256) public penaltyHistory; 50 | // 51 | //} 52 | // 53 | // 54 | ///** 55 | //* @notice Contract for testing upgrading the Adjudicator contract 56 | //*/ 57 | //contract AdjudicatorV2Mock is Adjudicator { 58 | // 59 | // uint256 public valueToCheck; 60 | // 61 | // constructor( 62 | // SignatureVerifier.HashAlgorithm _hashAlgorithm, 63 | // uint256 _basePenalty, 64 | // uint256 _percentagePenalty, 65 | // uint256 _penaltyHistoryCoefficient 66 | // ) 67 | // Adjudicator( 68 | // _hashAlgorithm, 69 | // _basePenalty, 70 | // _percentagePenalty, 71 | // _penaltyHistoryCoefficient 72 | // ) 73 | // { 74 | // } 75 | // 76 | // function setValueToCheck(uint256 _valueToCheck) public { 77 | // valueToCheck = _valueToCheck; 78 | // } 79 | // 80 | // function verifyState(address _testTarget) override public { 81 | // super.verifyState(_testTarget); 82 | // require(uint256(delegateGet(_testTarget, this.valueToCheck.selector)) == valueToCheck); 83 | // } 84 | //} 85 | -------------------------------------------------------------------------------- /deployment/constructor_params/mainnet/new-subscription.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | name: new-susbcription-mainnet 3 | chain_id: 137 4 | 5 | artifacts: 6 | dir: ./deployment/artifacts/ 7 | filename: new-susbcription-mainnet.json 8 | 9 | constants: 10 | # See deployment/artifacts/mainnet.json 11 | COORDINATOR_PROXY: "0xE74259e3dafe30bAA8700238e324b47aC98FE755" 12 | 13 | # See https://github.com/nucypher/tdec/issues/137#issuecomment-1881525878 14 | # and https://app.safe.global/home?safe=matic:0x861aa915C785dEe04684444560fC7A2AB43a1543 15 | NUCO_MULTISIG: "0x861aa915C785dEe04684444560fC7A2AB43a1543" 16 | 17 | # DAI Token on Polygon PoS - References: 18 | # - https://polygonscan.com/token/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 19 | # - https://github.com/search?q=0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063&type=code 20 | DAI_ON_POLYGON: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" 21 | 22 | # Threshold Network - References: 23 | # - https://docs.threshold.network/resources/contract-addresses/mainnet/threshold-dao 24 | # - https://github.com/keep-network/tbtc-v2/issues/594 25 | TREASURY_GUILD_ON_POLYGON: "0xc3Bf49eBA094AF346830dF4dbB42a07dE378EeB6" 26 | THRESHOLD_COUNCIL_ON_POLYGON: "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f" 27 | 28 | # Subscription Parameters 29 | # See https://github.com/nucypher/tdec/issues/169 30 | 31 | MAX_NODES: 30 32 | 33 | # - Fee parameters: 34 | INITIAL_BASE_FEE_RATE: 5787037037037 # $0.5 per day, in DAI units per second (in Python: 50*10**16 // 86400) 35 | ENCRYPTOR_FEE_RATE: 79274479959 # $2.5 per year, in DAI units per second (in Python: 250 * 10**16 // 86400 // 365) 36 | BASE_FEE_RATE_INCREASE_PER_PERIOD: 500 # 5%/year, expressed in basis points (0.01%) 37 | # - Duration parameters --> 1 period = 365 days 38 | ONE_YEAR_IN_SECONDS: 31536000 # 365 days (~1 year) 39 | THREE_MONTHS_IN_SECONDS: 7776000 # 90 day (~3 months) 40 | 41 | contracts: 42 | - GlobalAllowList: 43 | proxy: 44 | constructor: 45 | initialOwner: $THRESHOLD_COUNCIL_ON_POLYGON # Upgrades owner 46 | _data: "0x" 47 | constructor: 48 | _coordinator: $COORDINATOR_PROXY 49 | - StandardSubscription: 50 | proxy: 51 | constructor: 52 | initialOwner: $THRESHOLD_COUNCIL_ON_POLYGON # Upgrades owner 53 | _data: $encode:initialize,$TREASURY_GUILD_ON_POLYGON 54 | constructor: 55 | _coordinator: $COORDINATOR_PROXY 56 | _accessController: $GlobalAllowList 57 | _feeToken: $DAI_ON_POLYGON 58 | _adopterSetter: $NUCO_MULTISIG 59 | _initialBaseFeeRate: $INITIAL_BASE_FEE_RATE 60 | _baseFeeRateIncrease: $BASE_FEE_RATE_INCREASE_PER_PERIOD 61 | _encryptorFeeRate: $ENCRYPTOR_FEE_RATE 62 | _maxNodes: $MAX_NODES 63 | _subscriptionPeriodDuration: $ONE_YEAR_IN_SECONDS 64 | _yellowPeriodDuration: $THREE_MONTHS_IN_SECONDS 65 | _redPeriodDuration: $THREE_MONTHS_IN_SECONDS 66 | -------------------------------------------------------------------------------- /scripts/lynx/configure_staking.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | 4 | from ape import networks, project 5 | 6 | from deployment.constants import ARTIFACTS_DIR, LYNX_NODES 7 | from deployment.params import Transactor 8 | from deployment.registry import contracts_from_registry 9 | from deployment.utils import check_plugins 10 | 11 | LYNX_REGISTRY_FILEPATH = ARTIFACTS_DIR / "lynx.json" 12 | 13 | 14 | def configure_sepolia_root(transactor: Transactor) -> int: 15 | """Configures ThresholdStaking and TACoApplication on Sepolia.""" 16 | # Set up lynx stakes on Sepolia 17 | eth_network = networks.ethereum.sepolia 18 | with eth_network.use_provider("infura"): 19 | deployments = contracts_from_registry( 20 | filepath=LYNX_REGISTRY_FILEPATH, chain_id=eth_network.chain_id 21 | ) 22 | 23 | taco_application_contract = deployments[project.TACoApplication.contract_type.name] 24 | threshold_staking_contract = deployments[project.TestnetThresholdStaking.contract_type.name] 25 | 26 | min_stake_size = taco_application_contract.minimumAuthorization() 27 | for staking_provider, operator in LYNX_NODES.items(): 28 | # staking 29 | transactor.transact( 30 | threshold_staking_contract.setRoles, 31 | staking_provider, 32 | transactor.get_account().address, 33 | staking_provider, 34 | staking_provider, 35 | ) 36 | 37 | transactor.transact( 38 | threshold_staking_contract.authorizationIncreased, 39 | staking_provider, 40 | 0, 41 | min_stake_size, 42 | ) 43 | 44 | # bonding 45 | transactor.transact(taco_application_contract.bondOperator, staking_provider, operator) 46 | 47 | return min_stake_size 48 | 49 | 50 | def configure_amoy_root(transactor: Transactor, stake_size: int): 51 | """Configures MockTACoApplication on Amoy.""" 52 | # Set up lynx stakes on Amoy 53 | poly_network = networks.polygon.amoy 54 | with poly_network.use_provider("infura"): 55 | deployments = contracts_from_registry( 56 | filepath=LYNX_REGISTRY_FILEPATH, chain_id=poly_network.chain_id 57 | ) 58 | 59 | mock_taco_application_contract = deployments[project.MockPolygonChild.contract_type.name] 60 | 61 | for staking_provider, operator in LYNX_NODES.items(): 62 | # staking 63 | transactor.transact( 64 | mock_taco_application_contract.updateAuthorization, staking_provider, stake_size 65 | ) 66 | 67 | # bonding 68 | transactor.transact( 69 | mock_taco_application_contract.updateOperator, staking_provider, operator 70 | ) 71 | 72 | 73 | def main(): 74 | check_plugins() 75 | transactor = Transactor() 76 | stake_size = configure_sepolia_root(transactor) 77 | configure_amoy_root(transactor, stake_size) 78 | -------------------------------------------------------------------------------- /scripts/tapir/configure_staking.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from ape import networks, project 4 | 5 | from deployment.constants import ARTIFACTS_DIR, TAPIR_NODES 6 | from deployment.params import Transactor 7 | from deployment.registry import contracts_from_registry 8 | from deployment.utils import check_plugins 9 | 10 | TAPIR_REGISTRY_FILEPATH = ARTIFACTS_DIR / "tapir.json" 11 | 12 | 13 | def configure_sepolia_root(transactor: Transactor) -> int: 14 | """Configures ThresholdStaking and TACoApplication on Sepolia.""" 15 | # Set up Tapir stakes on Sepolia 16 | eth_network = networks.ethereum.sepolia 17 | with eth_network.use_provider("infura"): 18 | deployments = contracts_from_registry( 19 | filepath=TAPIR_REGISTRY_FILEPATH, chain_id=eth_network.chain_id 20 | ) 21 | 22 | taco_application_contract = deployments[project.TACoApplication.contract_type.name] 23 | threshold_staking_contract = deployments[project.TestnetThresholdStaking.contract_type.name] 24 | 25 | min_stake_size = taco_application_contract.minimumAuthorization() 26 | for staking_provider, operator in TAPIR_NODES.items(): 27 | # staking 28 | transactor.transact( 29 | threshold_staking_contract.setRoles, 30 | staking_provider, 31 | transactor.get_account().address, 32 | staking_provider, 33 | staking_provider, 34 | ) 35 | 36 | transactor.transact( 37 | threshold_staking_contract.authorizationIncreased, 38 | staking_provider, 39 | 0, 40 | min_stake_size, 41 | ) 42 | 43 | # bonding 44 | transactor.transact(taco_application_contract.bondOperator, staking_provider, operator) 45 | 46 | return min_stake_size 47 | 48 | 49 | def configure_amoy_root(transactor: Transactor, stake_size: int): 50 | """Configures MockTACoApplication on Amoy.""" 51 | # Set up Tapir stakes on Amoy 52 | poly_network = networks.polygon.amoy 53 | with poly_network.use_provider("infura"): 54 | deployments = contracts_from_registry( 55 | filepath=TAPIR_REGISTRY_FILEPATH, chain_id=poly_network.chain_id 56 | ) 57 | 58 | mock_taco_application_contract = deployments[project.MockPolygonChild.contract_type.name] 59 | 60 | for staking_provider, operator in TAPIR_NODES.items(): 61 | # staking 62 | transactor.transact( 63 | mock_taco_application_contract.updateAuthorization, staking_provider, stake_size 64 | ) 65 | 66 | # bonding 67 | transactor.transact( 68 | mock_taco_application_contract.updateOperator, staking_provider, operator 69 | ) 70 | 71 | 72 | def main(): 73 | check_plugins() 74 | transactor = Transactor() 75 | stake_size = configure_sepolia_root(transactor) 76 | configure_amoy_root(transactor, stake_size) 77 | -------------------------------------------------------------------------------- /tests/lib/test_snapshot.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of nucypher. 3 | 4 | nucypher is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | nucypher is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with nucypher. If not, see . 16 | """ 17 | 18 | import itertools 19 | 20 | import pytest 21 | from web3 import Web3 22 | 23 | 24 | @pytest.fixture(scope="module") 25 | def snapshot(accounts, project): 26 | contract = accounts[0].deploy(project.SnapshotMock) 27 | return contract 28 | 29 | 30 | timestamps = (0x00000001, 0x00001000, 0xFF000000, 0xFFFF0001) 31 | 32 | values = ( 33 | 0x000000000000000000000000, 34 | 0x000000000001000000000001, 35 | 0xFF0000000000000000000000, 36 | 0xFFFF00000000000000000001, 37 | ) 38 | 39 | 40 | @pytest.mark.parametrize("block_number, value", itertools.product(timestamps, values)) 41 | def test_snapshot(accounts, snapshot, block_number, value): 42 | 43 | # Testing basic encoding and decoding of snapshots 44 | def encode(_time, _value): 45 | return snapshot.encodeSnapshot(_time, _value) 46 | 47 | def decode(_snapshot): 48 | return snapshot.decodeSnapshot(_snapshot) 49 | 50 | encoded_snapshot = encode(block_number, value) 51 | assert decode(encoded_snapshot) == (block_number, value) 52 | expected_encoded_snapshot_as_bytes = block_number.to_bytes(4, "big") + value.to_bytes(12, "big") 53 | assert Web3.to_bytes(encoded_snapshot).rjust(16, b"\x00") == expected_encoded_snapshot_as_bytes 54 | 55 | # Testing adding new snapshots 56 | account = accounts[0] 57 | 58 | data = [(block_number + i * 10, value + i) for i in range(10)] 59 | for i, (block_i, value_i) in enumerate(data): 60 | snapshot.addSnapshot(block_i, value_i, sender=account) 61 | 62 | assert snapshot.length() == i + 1 63 | assert snapshot.call_view_method("history", i) == encode(block_i, value_i) 64 | assert snapshot.lastSnapshot() == (block_i, value_i) 65 | 66 | # Testing getValueAt: simple case, when asking for the exact block number that was recorded 67 | for i, (block_i, value_i) in enumerate(data): 68 | assert snapshot.getValueAt(block_i) == value_i 69 | assert snapshot.call_view_method("history", i) == encode(block_i, value_i) 70 | 71 | # Testing getValueAt: general case, when retrieving block numbers in-between snapshots 72 | # Special cases are before first snapshot (where value should be 0) and after the last one 73 | prior_value = 0 74 | for block_i, value_i in data: 75 | assert snapshot.getValueAt(block_i - 1) == prior_value 76 | prior_value = value_i 77 | 78 | last_block, last_value = snapshot.lastSnapshot() 79 | assert snapshot.getValueAt(last_block + 100) == last_value 80 | 81 | # Clear history for next test 82 | snapshot.deleteHistory(sender=account) 83 | -------------------------------------------------------------------------------- /contracts/test/TACoChildApplicationTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../contracts/coordination/ITACoRootToChild.sol"; 6 | import "../contracts/coordination/ITACoChildToRoot.sol"; 7 | 8 | /** 9 | * @notice Contract for testing TACo child application contract 10 | */ 11 | contract RootApplicationForTACoChildApplicationMock { 12 | ITACoRootToChild public childApplication; 13 | 14 | mapping(address => bool) public confirmations; 15 | mapping(address => bool) public penalties; 16 | mapping(address => bool) public releases; 17 | 18 | function setChildApplication(ITACoRootToChild _childApplication) external { 19 | childApplication = _childApplication; 20 | } 21 | 22 | function updateOperator(address _stakingProvider, address _operator) external { 23 | childApplication.updateOperator(_stakingProvider, _operator); 24 | } 25 | 26 | function updateAuthorization(address _stakingProvider, uint96 _authorized) external { 27 | childApplication.updateAuthorization(_stakingProvider, _authorized); 28 | } 29 | 30 | function updateAuthorization( 31 | address _stakingProvider, 32 | uint96 _authorized, 33 | uint96 _deauthorizing, 34 | uint64 _endDeauthorization 35 | ) external { 36 | childApplication.updateAuthorization( 37 | _stakingProvider, 38 | _authorized, 39 | _deauthorizing, 40 | _endDeauthorization 41 | ); 42 | } 43 | 44 | function confirmOperatorAddress(address _operator) external { 45 | confirmations[_operator] = true; 46 | } 47 | 48 | function penalize(address _stakingProvider) external { 49 | penalties[_stakingProvider] = true; 50 | } 51 | 52 | function release(address _stakingProvider) external { 53 | releases[_stakingProvider] = true; 54 | } 55 | 56 | function resetRelease(address _stakingProvider) external { 57 | releases[_stakingProvider] = false; 58 | } 59 | 60 | function resetConfirmation(address _operator) external { 61 | confirmations[_operator] = false; 62 | } 63 | 64 | function callChildRelease(address _stakingProvider) external { 65 | childApplication.release(_stakingProvider); 66 | } 67 | } 68 | 69 | contract CoordinatorForTACoChildApplicationMock { 70 | ITACoChildToRoot public immutable application; 71 | 72 | mapping(uint32 ritualId => mapping(address provider => bool participant)) internal rituals; 73 | 74 | constructor(ITACoChildToRoot _application) { 75 | application = _application; 76 | } 77 | 78 | function confirmOperatorAddress(address _operator) external { 79 | application.confirmOperatorAddress(_operator); 80 | } 81 | 82 | function setRitualParticipant(uint32 ritualId, address provider) external { 83 | rituals[ritualId][provider] = true; 84 | } 85 | 86 | function isRitualActive(uint32) external pure returns (bool) { 87 | return true; 88 | } 89 | 90 | function isParticipant(uint32 ritualId, address provider) external view returns (bool) { 91 | return rituals[ritualId][provider]; 92 | } 93 | 94 | function release(address _stakingProvider) external { 95 | application.release(_stakingProvider); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/subscription/AbstractSubscription.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../Coordinator.sol"; 6 | import "../IFeeModel.sol"; 7 | 8 | /** 9 | * @title Base Subscription contract 10 | * @notice Manages the subscription information for rituals. 11 | */ 12 | abstract contract AbstractSubscription is IFeeModel { 13 | Coordinator public immutable coordinator; 14 | 15 | uint32 public immutable subscriptionPeriodDuration; 16 | uint32 public immutable yellowPeriodDuration; 17 | uint32 public immutable redPeriodDuration; 18 | 19 | uint256[20] private gap; 20 | 21 | /** 22 | * @notice Sets subscription parameters 23 | * @dev The coordinator and fee token contracts cannot be zero addresses 24 | * @param _coordinator The address of the coordinator contract 25 | * @param _subscriptionPeriodDuration Maximum duration of subscription period 26 | * @param _yellowPeriodDuration Duration of yellow period 27 | * @param _redPeriodDuration Duration of red period 28 | */ 29 | constructor( 30 | Coordinator _coordinator, 31 | uint32 _subscriptionPeriodDuration, 32 | uint32 _yellowPeriodDuration, 33 | uint32 _redPeriodDuration 34 | ) { 35 | require(address(_coordinator) != address(0), "Coordinator cannot be the zero address"); 36 | coordinator = _coordinator; 37 | subscriptionPeriodDuration = _subscriptionPeriodDuration; 38 | yellowPeriodDuration = _yellowPeriodDuration; 39 | redPeriodDuration = _redPeriodDuration; 40 | } 41 | 42 | modifier onlyCoordinator() { 43 | require(msg.sender == address(coordinator), "Only the Coordinator can call this method"); 44 | _; 45 | } 46 | 47 | modifier onlyAccessController() virtual; 48 | 49 | modifier onlyActiveRitual(uint32 ritualId) virtual; 50 | 51 | function getEndOfSubscription() public view virtual returns (uint32); 52 | 53 | function processRitualExtending( 54 | address, 55 | uint32 ritualId, 56 | uint256, 57 | uint32 58 | ) external view override onlyCoordinator onlyActiveRitual(ritualId) { 59 | (, uint32 endTimestamp) = coordinator.getTimestamps(ritualId); 60 | require( 61 | getEndOfSubscription() + yellowPeriodDuration + redPeriodDuration >= endTimestamp, 62 | "Ritual parameters exceed available in package" 63 | ); 64 | } 65 | 66 | /** 67 | * @dev This function is called before the setAuthorizations function 68 | */ 69 | function beforeSetAuthorization( 70 | uint32 ritualId, 71 | address[] calldata, 72 | bool 73 | ) public virtual override onlyAccessController onlyActiveRitual(ritualId) { 74 | require(block.timestamp <= getEndOfSubscription(), "Subscription has expired"); 75 | } 76 | 77 | /** 78 | * @dev This function is called before the isAuthorized function 79 | * @param ritualId The ID of the ritual 80 | */ 81 | function beforeIsAuthorized( 82 | uint32 ritualId 83 | ) public view virtual override onlyAccessController onlyActiveRitual(ritualId) { 84 | require( 85 | block.timestamp <= getEndOfSubscription() + yellowPeriodDuration, 86 | "Yellow period has expired" 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /deployment/constants.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from pathlib import Path 3 | 4 | from ape import project 5 | 6 | import deployment 7 | 8 | # 9 | # Filesystem 10 | # 11 | 12 | DEPLOYMENT_DIR = Path(deployment.__file__).parent 13 | CONSTRUCTOR_PARAMS_DIR = DEPLOYMENT_DIR / "constructor_params" 14 | ARTIFACTS_DIR = DEPLOYMENT_DIR / "artifacts" 15 | 16 | # 17 | # Domains 18 | # 19 | 20 | LYNX = "lynx" 21 | TAPIR = "tapir" 22 | MAINNET = "mainnet" 23 | 24 | SUPPORTED_TACO_DOMAINS = [LYNX, TAPIR, MAINNET] 25 | 26 | # 27 | # Testnet 28 | # 29 | 30 | LYNX_NODES = { 31 | # staking provider -> operator 32 | "0xb15d5a4e2be34f4be154a1b08a94ab920ffd8a41": "0x890069745E9497C6f99Db68C4588deC5669F3d3E", 33 | "0x210eeac07542f815ebb6fd6689637d8ca2689392": "0xf48F720A2Ed237c24F5A7686543D90596bb8D44D", 34 | "0x48C8039c32F4c6f5cb206A5911C8Ae814929C16B": "0xce057adc39dcD1b3eA28661194E8963481CC48b2", 35 | } 36 | 37 | TAPIR_NODES = { 38 | # staking provider -> operator 39 | "0x05Be6D76d2282D24691E28E3Dc1c1A9709d70fa1": "0x91d12AB1EffBa82A4756ea029D40DE3fCD8f0255", 40 | "0xd274f0060256c186479f2b9f51615003cbcd19E6": "0xB6e188a88948d2d9bB5A3Eb05B7aC96A85A8BF07", 41 | "0xA7165c0229544c84b417e53a1D3ab717EA4b4587": "0x5d01059e669081861F8D9A4082a3A2ed6EB46A4B", 42 | "0x18F3a9ae64339E4FcfeBe1ac89Bc51aC3c83C22E": "0x131617ed5894Fe9f5A4B97a276ec99430A0a8B23", 43 | "0xcbE2F626d84c556AbA674FABBbBDdbED6B39d87b": "0xb057B982fB575509047e90cf5087c9B863a2022d", 44 | } 45 | 46 | # same derivation as TAPIR_NODES above, but subsequent indices 44/1/0/0/5 and 44/1/0/0/6 47 | ADDITIONAL_TEMPORARY_TAPIR_NODES = { 48 | # staking provider (derived) -> operator (randomly generated) 49 | "0x542Dc81be607e6622B9B04fF3e424E8E24922b6c": "0x6FDCee672743c492A78dF8E579ceFD0CFa6E9aa2", 50 | "0xF6e7E806fC96E907ef9CA203c35cD308672234Ce": "0x60Fc5414a4063a9AEAEDf9AC203f5F3ab5aA4Bc7", 51 | "0xb947c457a14d38229C7C5188bcc0B8925458AF04": "0xd8b9aB3f5Da9C0428a95d56b53f8D996c03a7093", 52 | } 53 | 54 | # 55 | # Contracts 56 | # 57 | 58 | OZ_DEPENDENCY = project.dependencies["openzeppelin"]["5.0.0"] 59 | 60 | # EIP1967 Admin slot - https://eips.ethereum.org/EIPS/eip-1967#admin-address 61 | EIP1967_ADMIN_SLOT = 0xB53127684A568B3173AE13B9F8A6016E243E63B6E8EE1178D6A717850B5D6103 62 | 63 | ACCESS_CONTROLLERS = ["GlobalAllowList", "OpenAccessAuthorizer", "ManagedAllowList"] 64 | 65 | # 66 | # Sampling 67 | # 68 | 69 | PORTER_SAMPLING_ENDPOINTS = { 70 | MAINNET: "https://porter.nucypher.io/bucket_sampling", 71 | LYNX: "https://porter-lynx.nucypher.io/get_ursulas", 72 | TAPIR: "https://porter-tapir.nucypher.io/get_ursulas", 73 | } 74 | 75 | # 76 | # Domain Seednodes 77 | # 78 | 79 | NETWORK_SEEDNODE_STATUS_JSON_URI = { 80 | MAINNET: "https://mainnet.nucypher.network:9151/status?json=true", 81 | LYNX: "https://lynx.nucypher.network:9151/status?json=true", 82 | TAPIR: "https://tapir.nucypher.network:9151/status?json=true", 83 | } 84 | 85 | # 86 | # DKG Ritual states as defined in the Coordinator contract 87 | # 88 | 89 | 90 | class RitualState(IntEnum): 91 | NON_INITIATED = 0 92 | DKG_AWAITING_TRANSCRIPTS = 1 93 | DKG_AWAITING_AGGREGATIONS = 2 94 | DKG_TIMEOUT = 3 95 | DKG_INVALID = 4 96 | ACTIVE = 5 97 | EXPIRED = 6 98 | 99 | 100 | HEARTBEAT_ARTIFACT_FILENAME = "heartbeat-rituals.json" 101 | -------------------------------------------------------------------------------- /contracts/threshold/IApplicationWithDecreaseDelay.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity ^0.8.9; 17 | 18 | import "@threshold/contracts/staking/IApplication.sol"; 19 | 20 | /// @title Interface for Threshold Network applications with delay after decrease request 21 | interface IApplicationWithDecreaseDelay is IApplication { 22 | /// @notice Returns authorization-related parameters of the application. 23 | /// @dev The minimum authorization is also returned by `minimumAuthorization()` 24 | /// function, as a requirement of `IApplication` interface. 25 | /// @return _minimumAuthorization The minimum authorization amount required 26 | /// so that operator can participate in the application. 27 | /// @return authorizationDecreaseDelay Delay in seconds that needs to pass 28 | /// between the time authorization decrease is requested and the 29 | /// time that request gets approved. Protects against free-riders 30 | /// earning rewards and not being active in the network. 31 | /// @return authorizationDecreaseChangePeriod Authorization decrease change 32 | /// period in seconds. It is the time, before authorization decrease 33 | /// delay end, during which the pending authorization decrease 34 | /// request can be overwritten. 35 | /// If set to 0, pending authorization decrease request can not be 36 | /// overwritten until the entire `authorizationDecreaseDelay` ends. 37 | /// If set to value equal `authorizationDecreaseDelay`, request can 38 | /// always be overwritten. 39 | function authorizationParameters() 40 | external 41 | view 42 | returns ( 43 | uint96 _minimumAuthorization, 44 | uint64 authorizationDecreaseDelay, 45 | uint64 authorizationDecreaseChangePeriod 46 | ); 47 | 48 | /// @notice Returns the amount of stake that is pending authorization 49 | /// decrease for the given staking provider. If no authorization 50 | /// decrease has been requested, returns zero. 51 | function pendingAuthorizationDecrease(address _stakingProvider) external view returns (uint96); 52 | 53 | /// @notice Returns the remaining time in seconds that needs to pass before 54 | /// the requested authorization decrease can be approved. 55 | function remainingAuthorizationDecreaseDelay( 56 | address stakingProvider 57 | ) external view returns (uint64); 58 | 59 | /// @notice Approves the previously registered authorization decrease 60 | /// request. Reverts if authorization decrease delay has not passed 61 | /// yet or if the authorization decrease was not requested for the 62 | /// given staking provider. 63 | function approveAuthorizationDecrease(address stakingProvider) external; 64 | } 65 | -------------------------------------------------------------------------------- /tests/lib/test_umbral_deserializer.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of nucypher. 3 | 4 | nucypher is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | nucypher is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with nucypher. If not, see . 16 | """ 17 | 18 | 19 | import os 20 | 21 | import ape 22 | import pytest 23 | from nucypher_core import MessageKit 24 | from nucypher_core.umbral import SecretKey, Signer, generate_kfrags, reencrypt 25 | 26 | 27 | @pytest.fixture() 28 | def deserializer(project, accounts): 29 | contract = accounts[0].deploy(project.UmbralDeserializerMock) 30 | return contract 31 | 32 | 33 | @pytest.fixture(scope="module") 34 | def fragments(): 35 | delegating_privkey = SecretKey.random() 36 | delegating_pubkey = delegating_privkey.public_key() 37 | signing_privkey = SecretKey.random() 38 | signer = Signer(signing_privkey) 39 | priv_key_bob = SecretKey.random() 40 | pub_key_bob = priv_key_bob.public_key() 41 | kfrags = generate_kfrags( 42 | delegating_sk=delegating_privkey, 43 | signer=signer, 44 | receiving_pk=pub_key_bob, 45 | threshold=2, 46 | shares=4, 47 | sign_delegating_key=False, 48 | sign_receiving_key=False, 49 | ) 50 | 51 | capsule = MessageKit(delegating_pubkey, b"unused").capsule 52 | cfrag = reencrypt(capsule, kfrags[0]) 53 | return capsule, cfrag 54 | 55 | 56 | def test_capsule(deserializer, fragments): 57 | # Wrong number of bytes to deserialize capsule 58 | with ape.reverts(): 59 | deserializer.toCapsule(os.urandom(97)) 60 | with ape.reverts(): 61 | deserializer.toCapsule(os.urandom(99)) 62 | 63 | # Check random capsule bytes 64 | capsule_bytes = os.urandom(98) 65 | result = deserializer.toCapsule(capsule_bytes) 66 | assert capsule_bytes == bytes().join(bytes(item) for item in result) 67 | 68 | # Check real capsule 69 | capsule, _cfrag = fragments 70 | capsule_bytes = capsule.to_bytes_simple() 71 | result = deserializer.toCapsule(capsule_bytes) 72 | assert b"".join(result) == capsule_bytes 73 | 74 | 75 | def test_cfrag(deserializer, fragments): 76 | # Wrong number of bytes to deserialize cfrag 77 | with ape.reverts(): 78 | deserializer.toCapsuleFrag(os.urandom(358)) 79 | 80 | # Check random cfrag bytes 81 | cfrag_bytes = os.urandom(131) 82 | proof_bytes = os.urandom(228) 83 | full_cfrag_bytes = cfrag_bytes + proof_bytes 84 | result = deserializer.toCapsuleFrag(full_cfrag_bytes) 85 | assert cfrag_bytes == bytes().join(result) 86 | result = deserializer.toCorrectnessProofFromCapsuleFrag(full_cfrag_bytes) 87 | assert proof_bytes == bytes().join(result) 88 | 89 | # Check real cfrag 90 | _capsule, cfrag = fragments 91 | cfrag_bytes = bytes(cfrag) 92 | result_frag = deserializer.toCapsuleFrag(cfrag_bytes) 93 | result_proof = deserializer.toCorrectnessProofFromCapsuleFrag(cfrag_bytes) 94 | assert cfrag_bytes == b"".join(result_frag) + b"".join(result_proof) 95 | 96 | 97 | # TODO: Missing test for precomputed_data 98 | -------------------------------------------------------------------------------- /tests/application/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of nucypher. 3 | 4 | nucypher is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | nucypher is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with nucypher. If not, see . 16 | """ 17 | 18 | 19 | import pytest 20 | from web3 import Web3 21 | 22 | MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether") 23 | 24 | MIN_OPERATOR_SECONDS = 24 * 60 * 60 25 | TOTAL_SUPPLY = Web3.to_wei(11_000_000_000, "ether") 26 | 27 | REWARD_DURATION = 60 * 60 * 24 * 7 # one week in seconds 28 | DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds 29 | 30 | PENALTY_DEFAULT = 1000 # 10% penalty 31 | PENALTY_INCREMENT = 2500 # 25% penalty increment 32 | PENALTY_DURATION = 60 * 60 * 24 # 1 day in seconds 33 | 34 | 35 | @pytest.fixture() 36 | def token(project, accounts): 37 | # Create an ERC20 token 38 | token = accounts[0].deploy(project.TestToken, TOTAL_SUPPLY) 39 | return token 40 | 41 | 42 | @pytest.fixture() 43 | def threshold_staking(project, accounts): 44 | threshold_staking = accounts[0].deploy(project.ThresholdStakingForTACoApplicationMock) 45 | return threshold_staking 46 | 47 | 48 | def encode_function_data(initializer=None, *args): 49 | """Encodes the function call so we can work with an initializer. 50 | Args: 51 | initializer ([ape.Contract.ContractMethodHandler], optional): 52 | The initializer function we want to call. Example: `box.store`. 53 | Defaults to None. 54 | args (Any, optional): 55 | The arguments to pass to the initializer function 56 | Returns: 57 | [bytes]: Return the encoded bytes. 58 | """ 59 | if not len(args): 60 | args = b"" 61 | 62 | if initializer: 63 | return initializer.encode_input(*args) 64 | 65 | return b"" 66 | 67 | 68 | @pytest.fixture() 69 | def taco_application(project, creator, token, threshold_staking, oz_dependency, chain): 70 | contract = creator.deploy( 71 | project.TACoApplication, 72 | token.address, 73 | threshold_staking.address, 74 | MIN_AUTHORIZATION, 75 | MIN_OPERATOR_SECONDS, 76 | REWARD_DURATION, 77 | DEAUTHORIZATION_DURATION, 78 | PENALTY_DEFAULT, 79 | PENALTY_DURATION, 80 | PENALTY_INCREMENT, 81 | ) 82 | 83 | encoded_initializer_function = encode_function_data() 84 | proxy = oz_dependency.TransparentUpgradeableProxy.deploy( 85 | contract.address, 86 | creator, 87 | encoded_initializer_function, 88 | sender=creator, 89 | ) 90 | proxy_contract = project.TACoApplication.at(proxy.address) 91 | 92 | threshold_staking.setApplication(proxy_contract.address, sender=creator) 93 | proxy_contract.initialize(sender=creator) 94 | 95 | return proxy_contract 96 | 97 | 98 | @pytest.fixture() 99 | def child_application(project, creator, taco_application): 100 | contract = project.ChildApplicationForTACoApplicationMock.deploy( 101 | taco_application.address, sender=creator 102 | ) 103 | taco_application.setChildApplication(contract.address, sender=creator) 104 | return contract 105 | -------------------------------------------------------------------------------- /contracts/contracts/TestnetThresholdStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@threshold/contracts/staking/IApplication.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | contract TestnetThresholdStaking is Ownable { 9 | struct StakingProviderInfo { 10 | address owner; 11 | address payable beneficiary; 12 | address authorizer; 13 | uint96 tStake; 14 | uint96 keepInTStake; 15 | uint96 nuInTStake; 16 | } 17 | 18 | IApplication public application; 19 | 20 | mapping(address => StakingProviderInfo) public stakingProviderInfo; 21 | 22 | constructor() Ownable(msg.sender) {} 23 | 24 | function setApplication(IApplication _application) external onlyOwner { 25 | application = _application; 26 | } 27 | 28 | function stakedNu(address) external view returns (uint256) { 29 | return 0; 30 | } 31 | 32 | function authorizationIncreased( 33 | address _stakingProvider, 34 | uint96 _fromAmount, 35 | uint96 _toAmount 36 | ) external onlyOwner { 37 | application.authorizationIncreased(_stakingProvider, _fromAmount, _toAmount); 38 | } 39 | 40 | function authorizationDecreaseRequested( 41 | address _stakingProvider, 42 | uint96 _fromAmount, 43 | uint96 _toAmount 44 | ) external onlyOwner { 45 | application.authorizationDecreaseRequested(_stakingProvider, _fromAmount, _toAmount); 46 | } 47 | 48 | function approveAuthorizationDecrease(address) external pure returns (uint96) { 49 | return 0; 50 | } 51 | 52 | function setRoles( 53 | address _stakingProvider, 54 | address _owner, 55 | address payable _beneficiary, 56 | address _authorizer 57 | ) external onlyOwner { 58 | StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; 59 | info.owner = _owner; 60 | info.beneficiary = _beneficiary; 61 | info.authorizer = _authorizer; 62 | } 63 | 64 | /** 65 | * @dev If the function is called with only the _stakingProvider parameter, 66 | * we presume that the caller wants that address set for the other roles as well. 67 | */ 68 | function setRoles(address _stakingProvider) external onlyOwner { 69 | StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; 70 | info.owner = _stakingProvider; 71 | info.beneficiary = payable(_stakingProvider); 72 | info.authorizer = _stakingProvider; 73 | } 74 | 75 | function setStakes( 76 | address _stakingProvider, 77 | uint96 _tStake, 78 | uint96 _keepInTStake, 79 | uint96 _nuInTStake 80 | ) external onlyOwner { 81 | StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; 82 | info.tStake = _tStake; 83 | info.keepInTStake = _keepInTStake; 84 | info.nuInTStake = _nuInTStake; 85 | } 86 | 87 | function authorizedStake( 88 | address /* _stakingProvider */, 89 | address /* _application */ 90 | ) external view returns (uint96) { 91 | return 0; 92 | } 93 | 94 | function stakes( 95 | address _stakingProvider 96 | ) external view returns (uint96 tStake, uint96 keepInTStake, uint96 nuInTStake) { 97 | StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; 98 | tStake = info.tStake; 99 | keepInTStake = info.keepInTStake; 100 | nuInTStake = info.nuInTStake; 101 | } 102 | 103 | function rolesOf( 104 | address _stakingProvider 105 | ) external view returns (address owner, address payable beneficiary, address authorizer) { 106 | StakingProviderInfo storage info = stakingProviderInfo[_stakingProvider]; 107 | owner = info.owner; 108 | beneficiary = info.beneficiary; 109 | authorizer = info.authorizer; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /scripts/ci/deploy_child.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import filecmp 3 | from pathlib import Path 4 | 5 | from ape import accounts, networks, project 6 | 7 | from deployment.constants import ARTIFACTS_DIR, CONSTRUCTOR_PARAMS_DIR 8 | from deployment.params import Deployer 9 | from deployment.registry import ConflictResolution, merge_registries 10 | 11 | VERIFY = False 12 | CONSTRUCTOR_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "ci" / "child.yml" 13 | UPGRADE_PARAMS_FILEPATH = CONSTRUCTOR_PARAMS_DIR / "ci" / "upgrade-coordinator.yml" 14 | 15 | UPGRADED_COORDINATOR_FILE_NAME = "ci-upgrade-coordinator.json" 16 | 17 | ORIGINAL_DEPLOYMENT_ARTIFACT = ARTIFACTS_DIR / "ci.json" 18 | UPGRADED_DEPLOYMENT_ARTIFACT = ARTIFACTS_DIR / UPGRADED_COORDINATOR_FILE_NAME 19 | FINAL_DEPLOYMENT_ARTIFACT = ARTIFACTS_DIR / "ci-final.json" 20 | 21 | 22 | def create_upgrade_coordinator_yaml( 23 | output_file: Path, taco_child_application_address: str, dkg_timeout: int, handover_timeout: int 24 | ) -> None: 25 | """ 26 | Creates a YAML file for the upgrade process. 27 | """ 28 | yaml_text = f"""deployment: 29 | name: ci-upgrade-coordinator 30 | chain_id: 80002 31 | 32 | artifacts: 33 | dir: ./deployment/artifacts/ 34 | filename: {UPGRADED_COORDINATOR_FILE_NAME} 35 | 36 | contracts: 37 | - Coordinator: 38 | constructor: 39 | _application: "{taco_child_application_address}" 40 | _dkgTimeout: {dkg_timeout} 41 | _handoverTimeout: {handover_timeout} 42 | """ 43 | with open(output_file, "w") as file: 44 | file.write(yaml_text) 45 | 46 | 47 | def main(): 48 | with networks.ethereum.local.use_provider("test"): 49 | test_account = accounts.test_accounts[0] 50 | 51 | deployer = Deployer.from_yaml( 52 | filepath=CONSTRUCTOR_PARAMS_FILEPATH, 53 | verify=VERIFY, 54 | account=test_account, 55 | autosign=True, 56 | ) 57 | 58 | mock_polygon_child = deployer.deploy(project.MockPolygonChild) 59 | 60 | taco_child_application = deployer.deploy(project.TACoChildApplication) 61 | 62 | deployer.transact(mock_polygon_child.setChildApplication, taco_child_application.address) 63 | 64 | ritual_token = deployer.deploy(project.LynxRitualToken) 65 | 66 | coordinator = deployer.deploy(project.Coordinator) 67 | 68 | global_allow_list = deployer.deploy(project.GlobalAllowList) 69 | 70 | deployments = [ 71 | mock_polygon_child, 72 | taco_child_application, 73 | ritual_token, 74 | coordinator, 75 | global_allow_list, 76 | ] 77 | 78 | deployer.finalize(deployments=deployments) 79 | 80 | # dynamically create the YAML file for the upgrade of the Coordinator contract 81 | create_upgrade_coordinator_yaml( 82 | UPGRADE_PARAMS_FILEPATH, 83 | str(taco_child_application.address), 84 | coordinator.dkgTimeout(), 85 | coordinator.handoverTimeout(), 86 | ) 87 | 88 | with networks.ethereum.local.use_provider("test"): 89 | test_account = accounts.test_accounts[0] 90 | upgrade_deployer = Deployer.from_yaml( 91 | filepath=UPGRADE_PARAMS_FILEPATH, 92 | verify=VERIFY, 93 | account=test_account, 94 | autosign=True, 95 | ) 96 | 97 | upgraded_coordinator = upgrade_deployer.upgrade(project.Coordinator, coordinator.address) 98 | 99 | upgraded_deployments = [ 100 | upgraded_coordinator, 101 | ] 102 | 103 | upgrade_deployer.finalize(deployments=upgraded_deployments) 104 | 105 | # merge registries 106 | merge_registries( 107 | registry_1_filepath=deployer.registry_filepath, 108 | registry_2_filepath=upgrade_deployer.registry_filepath, 109 | output_filepath=FINAL_DEPLOYMENT_ARTIFACT, 110 | force_conflict_resolution=ConflictResolution.USE_2, 111 | ) 112 | 113 | # diff 114 | assert filecmp.cmp(ORIGINAL_DEPLOYMENT_ARTIFACT, FINAL_DEPLOYMENT_ARTIFACT, shallow=False) 115 | 116 | # remove created files 117 | deployer.registry_filepath.unlink() 118 | upgrade_deployer.registry_filepath.unlink() 119 | FINAL_DEPLOYMENT_ARTIFACT.unlink() 120 | UPGRADE_PARAMS_FILEPATH.unlink() 121 | -------------------------------------------------------------------------------- /contracts/contracts/coordination/subscription/EncryptorSlotsSubscription.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./AbstractSubscription.sol"; 6 | 7 | /** 8 | * @title Subscription that includes payment for enryptor slots 9 | * @notice Manages the subscription information for rituals. 10 | */ 11 | abstract contract EncryptorSlotsSubscription is AbstractSubscription { 12 | uint32 public startOfSubscription; 13 | uint256 public usedEncryptorSlots; 14 | // example of storage layout 15 | // mapping(uint256 periodNumber => Billing billing) public billingInfo; 16 | 17 | uint256[20] private gap; 18 | 19 | /** 20 | * @notice Sets the coordinator and fee token contracts 21 | * @dev The coordinator and fee token contracts cannot be zero addresses 22 | * @param _coordinator The address of the coordinator contract 23 | * @param _subscriptionPeriodDuration Maximum duration of subscription period 24 | * @param _yellowPeriodDuration Duration of yellow period 25 | * @param _redPeriodDuration Duration of red period 26 | */ 27 | constructor( 28 | Coordinator _coordinator, 29 | uint32 _subscriptionPeriodDuration, 30 | uint32 _yellowPeriodDuration, 31 | uint32 _redPeriodDuration 32 | ) 33 | AbstractSubscription( 34 | _coordinator, 35 | _subscriptionPeriodDuration, 36 | _yellowPeriodDuration, 37 | _redPeriodDuration 38 | ) 39 | {} 40 | 41 | function isPeriodPaid(uint256 periodNumber) public view virtual returns (bool); 42 | 43 | function getPaidEncryptorSlots(uint256 periodNumber) public view virtual returns (uint256); 44 | 45 | function getCurrentPeriodNumber() public view returns (uint256) { 46 | if (startOfSubscription == 0) { 47 | return 0; 48 | } 49 | return (block.timestamp - startOfSubscription) / subscriptionPeriodDuration; 50 | } 51 | 52 | function getEndOfSubscription() public view override returns (uint32 endOfSubscription) { 53 | if (startOfSubscription == 0) { 54 | return 0; 55 | } 56 | 57 | uint256 currentPeriodNumber = getCurrentPeriodNumber(); 58 | if (currentPeriodNumber == 0 && !isPeriodPaid(currentPeriodNumber)) { 59 | return 0; 60 | } 61 | 62 | if (isPeriodPaid(currentPeriodNumber)) { 63 | while (isPeriodPaid(currentPeriodNumber)) { 64 | currentPeriodNumber++; 65 | } 66 | } else { 67 | while (!isPeriodPaid(currentPeriodNumber)) { 68 | currentPeriodNumber--; 69 | } 70 | currentPeriodNumber++; 71 | } 72 | endOfSubscription = uint32( 73 | startOfSubscription + currentPeriodNumber * subscriptionPeriodDuration 74 | ); 75 | } 76 | 77 | function beforeSetAuthorization( 78 | uint32 ritualId, 79 | address[] calldata addresses, 80 | bool value 81 | ) public virtual override { 82 | super.beforeSetAuthorization(ritualId, addresses, value); 83 | if (value) { 84 | uint256 currentPeriodNumber = getCurrentPeriodNumber(); 85 | uint256 encryptorSlots = isPeriodPaid(currentPeriodNumber) 86 | ? getPaidEncryptorSlots(currentPeriodNumber) 87 | : 0; 88 | usedEncryptorSlots += addresses.length; 89 | require(usedEncryptorSlots <= encryptorSlots, "Encryptors slots filled up"); 90 | } else { 91 | if (usedEncryptorSlots >= addresses.length) { 92 | usedEncryptorSlots -= addresses.length; 93 | } else { 94 | usedEncryptorSlots = 0; 95 | } 96 | } 97 | } 98 | 99 | function beforeIsAuthorized(uint32 ritualId) public view virtual override { 100 | super.beforeIsAuthorized(ritualId); 101 | // used encryptor slots must be paid 102 | if (block.timestamp <= getEndOfSubscription()) { 103 | uint256 currentPeriodNumber = getCurrentPeriodNumber(); 104 | require( 105 | usedEncryptorSlots <= getPaidEncryptorSlots(currentPeriodNumber), 106 | "Encryptors slots filled up" 107 | ); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /deployment/legacy.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from urllib.parse import urlencode 4 | 5 | import requests 6 | from eth_typing import ChecksumAddress 7 | from eth_utils import to_checksum_address 8 | 9 | from deployment.registry import ChainId, RegistryEntry, write_registry 10 | from deployment.utils import _load_json 11 | 12 | 13 | def get_creation_info(api_key: str, chain_id: int, contract_address: ChecksumAddress) -> tuple: 14 | networks = { 15 | 1: ("", "etherscan.io"), 16 | 5: ("-goerli", "etherscan.io"), 17 | 80002: ("-testnet", "polygonscan.com"), 18 | } 19 | 20 | params = { 21 | "module": "account", 22 | "action": "txlist", 23 | "address": contract_address, 24 | "page": 1, 25 | "sort": "asc", 26 | "apikey": api_key, 27 | } 28 | 29 | network, explorer = networks[chain_id] 30 | base_url = f"https://api{network}.{explorer}/api" 31 | url = f"{base_url}?{urlencode(params)}" 32 | response = requests.get(url) 33 | data = response.json() 34 | 35 | if data["status"] == "1" and data["result"]: 36 | # If there are transactions, the first one will be the contract creation transaction 37 | tx = data["result"][0] 38 | tx_hash = tx["hash"] 39 | block_number = tx["blockNumber"] 40 | deployer = tx["from"] 41 | else: 42 | raise ValueError(f"Could not find contract creation transaction for {contract_address}") 43 | 44 | return tx_hash, block_number, to_checksum_address(deployer) 45 | 46 | 47 | def convert_legacy_registry( 48 | legacy_filepath: Path, 49 | output_filepath: Path, 50 | chain_id: ChainId, 51 | ) -> None: 52 | """Converts a legacy nucypher-style contract registry to a new-style registry.""" 53 | 54 | if not legacy_filepath.exists(): 55 | raise FileNotFoundError(f"Legacy registry not found at {legacy_filepath}") 56 | api_key = os.environ.get("ETHERSCAN_API_KEY") 57 | if not api_key: 58 | raise ValueError("Please set the ETHERSCAN_API_KEY environment variable.") 59 | 60 | legacy_registry_entries = _load_json(filepath=legacy_filepath) 61 | new_registry_entries = list() 62 | for entry in legacy_registry_entries: 63 | name, address, abi = entry[0], entry[2], entry[3] 64 | tx_hash, block_number, deployer = get_creation_info( 65 | api_key=api_key, chain_id=chain_id, contract_address=address 66 | ) 67 | new_registry_entry = RegistryEntry( 68 | chain_id=chain_id, 69 | name=name, 70 | address=address, 71 | abi=abi, 72 | tx_hash=tx_hash, 73 | block_number=block_number, 74 | deployer=deployer, 75 | ) 76 | new_registry_entries.append(new_registry_entry) 77 | write_registry(entries=new_registry_entries, filepath=output_filepath) 78 | print(f"Converted legacy registry to {output_filepath}") 79 | 80 | 81 | def convert_legacy_npm_artifacts(directory: Path, chain_id: ChainId, output_filepath: Path) -> None: 82 | if output_filepath.exists(): 83 | raise FileExistsError(f"Registry already exists at {output_filepath}") 84 | 85 | if not directory.exists(): 86 | raise FileNotFoundError(f"Directory not found at {directory}") 87 | 88 | api_key = os.environ.get("ETHERSCAN_API_KEY") 89 | if not api_key: 90 | raise ValueError("Please set the ETHERSCAN_API_KEY environment variable.") 91 | 92 | entries = list() 93 | for filepath in directory.glob("*.json"): 94 | data = _load_json(filepath=filepath) 95 | 96 | name = filepath.name.replace(".json", "") 97 | abi = data["abi"] 98 | address = data["address"] 99 | 100 | tx_hash, block_number, deployer = get_creation_info( 101 | api_key=api_key, chain_id=chain_id, contract_address=address 102 | ) 103 | 104 | entry = RegistryEntry( 105 | chain_id=chain_id, 106 | name=name, 107 | address=address, 108 | abi=abi, 109 | tx_hash=tx_hash, 110 | block_number=block_number, 111 | deployer=deployer, 112 | ) 113 | 114 | entries.append(entry) 115 | 116 | write_registry(entries=entries, filepath=output_filepath) 117 | print(f"Converted legacy registry to {output_filepath}") 118 | --------------------------------------------------------------------------------