From 68b88701dd04d00f8aed157582b27090074d0129 Mon Sep 17 00:00:00 2001 From: Felix Hieser <47272854+FHieser@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:20:53 +0200 Subject: [PATCH 01/67] [Audit] Feature/Authorizer-Update * Feature: Roleauthorizer_v1.1 upgrade * Test Folder restructured * Rename folder mistake * adapt rest of references * Implement Function Lock * Delete TestFolderRefactoring.md * Implement Function Lock * Adapt functionality * Create Tests * Include the rest of the new functions * Change public Role to Id 1 * Deprecate Functions * Rename to key to Permission * Clean up last functions that were still from old implementation * Refactor function postions * Test Permissioned Modifier * Adapt onlyRole to permissioned in Authorizer * Cleanup and missed tests * Adapt modules to use permissioned modifier * Cleanup comments and todos * Update Staking Module * rename to t.sol * Update Recurring Payments * Update Paymentrouter * Comment Cleanup * Add Event references * Update KPIRewarder * Update Optimistic Oracle * Update Bounties * Update PP_Streaming * Update TokenVault * Update BondingCurveBase * Adapt comments * Update RedeemingBondingCurveBase * Update comments * Adapt Event references in tests * Update Bancor Redeeming * Update BondingSurface Redeeming * Update comments * Update Bonding Surface Redeeming Restricted * Fix Typo * Update RoleAuthorizerE2E.t.sol * Update MetaTxAndMulticallE2E.t.sol * Adapt comments * Make the test of the upgraded Authorizer the new standard * Update VotingRoleManagerE2E.t.sol * Update BountyManagerE2E.t.sol * Update BondingCurveFundingManagerE2E.t.sol * Update BondingCurveTokenRescueE2E.t.sol * Update BondingSurfaceFundingManagerE2E.t.sol * Update StakingManagerLifecycle.t.sol * Update KPIRewarderLifecycle.t.sol * Remove Module__CallerNotAuthorized from VotingRoles * Remove Module__CallerNotAuthorized from IModule * Update comments * Remove Authorization from ModuleManagerBase * Adapt Orchestrator to use permissioned functions * Update KPIRewarderLifecycle.t.sol * Make supportsInterface check mandatory for ModuleTest * Update Template * Adapt E2E tests to take regular Authorizer * Remove PIM_Workflowfactory * Remove Restricted Bancor Redeeming * Remove TokenGatedRoleAuthorizer * Adapt comments * Fix Merge with dev * Make FM_Oracle and PP_Queue buildable * Adapt correct file ending * Update OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.t.sol * Test modifier in places * Do leftover todos - Removing leftover Role declarations - moving Authorization comment to downstream implementation - cleanup comments * Adapt based on Pablos comments * Remove deprecated functions * Fix test * Revert "Remove TokenGatedRoleAuthorizer" This reverts commit b5fa277c016e18d0bf10132fd1304ac8592bccb8. * Adapt comments * Adapt Token Gated Roles * Update TokenGatedRoleAuthorizerE2E.t.sol * Update AUT_TokenGated_Roles_v1.sol * Adapt tests * Turn down the rate of assume problems * Addressed Pablos comments * Fix test It was the || * Adapt the function positioning * Adapt natspec * Fix typo Damn copy paste * Fix test for real * Update comments * Adapt natspec * Update Natspec for Token Gated Roles * Apply suggestions from code review Co-authored-by: Marvin Kruse * Apply suggestions from code review Co-authored-by: Marvin Kruse * Rename RoleIdCounter to LastAssignedRoleId * Address Marvins comments * Update Voting Roles variable naming * Adapt to inverter standard * Adapt OnlyCallableByMotion modifier and error * Apply suggestions from code review Co-authored-by: Marvin Kruse * Adapt comments * Remove Pim Workflow as it is outdated * Adapt comments * Apply suggestions from code review Co-authored-by: Marvin Kruse * Update Authorizer Comments.md * Delete Authorizer Comments.md * Adapt documentation of TokenGated_Roles --------- Co-authored-by: Marvin Kruse * Move Interface to correct position * Adapt threshold in motion * remove redundant section * Fix merge * Add _validateThreshold to addVoter * Revert "Adapt threshold in motion" This reverts commit 7217a67fa064b0c4aa54b26a8c120176ce767437. And extends it by a natspec section * Move _addVoter to its own subfunction --------- Co-authored-by: Marvin Kruse --- remappings.txt | 1 + .../MetadataCollection_v1.s.sol | 11 - .../ModuleBeaconDeployer_v1.s.sol | 22 - .../SingletonDeployer_v1.s.sol | 8 - .../DeployAndSetupNavBasedPimWorkflow.s.sol | 881 -------- .../WorkflowDeployment.md | 65 - .../example.dev.navBasedPimWorkflow.env | 107 - .../interfaces/IPIM_WorkflowFactory_v1.sol | 139 -- .../PIM_WorkflowFactory_v1.sol | 311 --- src/modules/authorizer/IAuthorizer_v1.sol | 506 ++++- .../extensions/AUT_EXT_VotingRoles_v1.sol | 383 ++-- .../interfaces/IAUT_EXT_VotingRoles_v1.sol | 287 +++ src/modules/authorizer/role/AUT_Roles_v1.sol | 621 ++++-- .../role/AUT_TokenGated_Roles_v1.sol | 464 ++-- .../interfaces/IAUT_EXT_VotingRoles_v1.sol | 239 --- .../interfaces/IAUT_TokenGated_Roles_v1.sol | 278 ++- src/modules/base/IModule_v1.sol | 82 +- src/modules/base/Module_v1.sol | 180 +- ...M_BC_Bancor_Redeeming_VirtualSupply_v1.sol | 68 +- ...deeming_Restricted_Repayer_Seizable_v1.sol | 232 +- .../FM_BC_BondingSurface_Redeeming_v1.sol | 6 +- ...cted_Bancor_Redeeming_VirtualSupply_v1.sol | 85 - .../abstracts/BondingCurveBase_v1.sol | 20 +- .../RedeemingBondingCurveBase_v1.sol | 18 +- .../interfaces/IBondingCurveBase_v1.sol | 15 +- ...M_BC_Bancor_Redeeming_VirtualSupply_v1.sol | 4 +- ...deeming_Restricted_Repayer_Seizable_v1.sol | 46 +- .../IFM_BC_BondingSurface_Redeeming_v1.sol | 4 +- .../IRedeemingBondingCurveBase_v1.sol | 14 +- .../IVirtualIssuanceSupplyBase_v1.sol | 1 + .../depositVault/FM_DepositVault_v1.sol | 3 +- .../extensions/FM_EXT_TokenVault_v1.sol | 2 +- .../interfaces/IFM_EXT_TokenVault_v1.sol | 2 +- .../oracle/FM_PC_Oracle_Redeeming_v1.sol | 197 +- .../interfaces/IFM_PC_Oracle_Redeeming_v1.sol | 131 +- .../logicModule/LM_Oracle_Permissioned_v1.sol | 85 +- src/modules/logicModule/LM_PC_Bounties_v2.sol | 21 +- .../logicModule/LM_PC_KPIRewarder_v2.sol | 12 +- .../logicModule/LM_PC_PaymentRouter_v2.sol | 7 +- .../LM_PC_RecurringPayments_v2.sol | 11 +- src/modules/logicModule/LM_PC_Staking_v2.sol | 9 +- .../IOptimisticOracleIntegrator.sol | 4 + .../OptimisticOracleIntegrator.sol | 14 +- .../interfaces/ILM_Oracle_Permissioned_v1.sol | 51 +- .../interfaces/ILM_PC_Bounties_v2.sol | 9 +- .../interfaces/ILM_PC_KPIRewarder_v2.sol | 50 +- .../interfaces/ILM_PC_PaymentRouter_v2.sol | 2 + .../ILM_PC_RecurringPayments_v2.sol | 2 + .../interfaces/ILM_PC_Staking_v2.sol | 4 + .../PP_Queue_ManualExecution_v1.sol | 40 +- src/modules/paymentProcessor/PP_Queue_v1.sol | 89 +- .../paymentProcessor/PP_Streaming_v2.sol | 7 +- .../IPP_Queue_ManualExecution_v1.sol | 40 +- .../interfaces/IPP_Queue_v1.sol | 59 +- .../interfaces/IPP_Streaming_v2.sol | 11 + src/orchestrator/Orchestrator_v1.sol | 73 +- .../abstracts/ModuleManagerBase_v1.sol | 28 +- .../interfaces/IModuleManagerBase_v1.sol | 3 - .../interfaces/IOrchestrator_v1.sol | 84 +- src/templates/modules/ILM_PC_Template_v1.sol | 5 +- src/templates/modules/LM_PC_Template_v1.sol | 36 +- src/templates/tests/unit/FM_Template_v1.t.sol | 2 +- .../tests/unit/LM_PC_Template_v1.t.sol | 49 +- src/templates/tests/unit/PP_Template_v1.t.sol | 2 +- test/e2e/authorizer/RoleAuthorizerE2E.t.sol | 155 +- .../TokenGatedRoleAuthorizerE2E.t.sol | 69 +- .../extensions/VotingRoleManagerE2E.t.sol | 40 +- .../BondingCurveFundingManagerE2E.t.sol | 18 + .../BondingCurveTokenRescueE2E.t.sol | 26 +- .../BondingSurfaceFundingManagerE2E.t.sol | 151 +- ...ManualQueueBasedPaymentProcessorE2E.t.sol} | 214 +- test/e2e/logicModule/BountyManagerE2E.t.sol | 77 +- .../logicModule/KPIRewarderLifecycle.t.sol | 47 +- .../logicModule/StakingManagerLifecycle.t.sol | 19 +- test/e2e/module/MetaTxAndMulticallE2E.t.sol | 113 +- test/e2e/proxies/InverterBeaconE2E.t.sol | 4 +- .../authorizer/AUT_Roles_v1_Exposed.sol | 40 + .../AUT_TokenGated_Roles_v1_Exposed.sol | 73 + .../modules/authorizer/AuthorizerV1Mock.sol | 218 +- .../modules/authorizer/TokenInterfaceMock.sol | 24 + test/mocks/modules/base/ModuleV1Mock.sol | 33 +- ..._Restricted_Repayer_SeizableV1_Exposed.sol | 4 - ...d_Bancor_Redeeming_VirtualSupplyV1Mock.sol | 136 -- .../orchestrator/Orchestrator_v1_Exposed.sol | 34 + test/tools/CallIntercepter.sol | 46 + test/tools/OZErrors.sol | 17 + test/tools/TypeSanityHelper.sol | 76 + .../PIM_WorkflowFactory_v1.t.sol | 572 ----- test/unit/modules/ModuleTest.sol | 43 + .../extensions/AUT_EXT_VotingRoles_v1.t.sol | 155 +- .../authorizer/role/AUT_Roles_v1.t.sol | 1898 +++++++++-------- .../role/AUT_TokenGated_Roles_v1.t.sol | 1354 ++++++------ test/unit/modules/base/Module_v1.t.sol | 344 +-- ...BC_Bancor_Redeeming_VirtualSupply_v1.t.sol | 389 +++- ...eming_Restricted_Repayer_Seizable_v1.t.sol | 1118 ++++------ .../FM_BC_BondingSurface_Redeeming_v1.t.sol | 74 +- ...ed_Bancor_Redeeming_VirtualSupply_v1.t.sol | 352 --- .../abstracts/BondingCurveBase_v1.t.sol | 276 ++- .../RedeemingBondingCurveBase_v1.t.sol | 184 +- .../depositVault/FM_DepositVault_v1.t.sol | 2 +- .../extensions/FM_EXT_TokenVault_v1.t.sol | 18 +- .../oracle/FM_PC_Oracle_Redeeming_v1.t.sol | 414 +--- .../LM_Oracle_Permissioned_v1.t.sol | 99 +- .../logicModule/LM_PC_Bounties_v2.t.sol | 361 ++-- .../logicModule/LM_PC_KPIRewarder_v2.t.sol | 192 +- .../logicModule/LM_PC_PaymentRouter_v2.t.sol | 96 +- .../LM_PC_RecurringPayments_v2.t.sol | 67 +- .../logicModule/LM_PC_Staking_v2.t.sol | 88 +- .../abstract/ERCPaymentClientBase_v2.t.sol | 2 +- .../oracle/OptimisticOracleIntegrator.t.sol | 94 +- .../PP_Queue_ManualExecution_v1.t.sol | 4 +- .../paymentProcessor/PP_Queue_v1.t.sol | 205 +- .../paymentProcessor/PP_Simple_v2.t.sol | 12 +- .../paymentProcessor/PP_Streaming_v2.t.sol | 61 +- test/unit/orchestrator/Orchestrator_v1.t.sol | 295 ++- .../abstracts/ModuleManagerBase_v1.t.sol | 81 +- ...roxyAdmin.sol => InverterProxyAdmin.t.sol} | 0 117 files changed, 7867 insertions(+), 8859 deletions(-) delete mode 100644 script/workflowDeploymentAndSetupScripts/DeployAndSetupNavBasedPimWorkflow.s.sol delete mode 100644 script/workflowDeploymentAndSetupScripts/WorkflowDeployment.md delete mode 100644 script/workflowDeploymentAndSetupScripts/example.dev.navBasedPimWorkflow.env delete mode 100644 src/factories/interfaces/IPIM_WorkflowFactory_v1.sol delete mode 100644 src/factories/workflow-specific/PIM_WorkflowFactory_v1.sol create mode 100644 src/modules/authorizer/extensions/interfaces/IAUT_EXT_VotingRoles_v1.sol delete mode 100644 src/modules/authorizer/role/interfaces/IAUT_EXT_VotingRoles_v1.sol delete mode 100644 src/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.sol rename test/e2e/fundingManager/oracle/{OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.sol => OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.t.sol} (76%) create mode 100644 test/mocks/modules/authorizer/AUT_Roles_v1_Exposed.sol create mode 100644 test/mocks/modules/authorizer/AUT_TokenGated_Roles_v1_Exposed.sol create mode 100644 test/mocks/modules/authorizer/TokenInterfaceMock.sol delete mode 100644 test/mocks/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock.sol create mode 100644 test/mocks/orchestrator/Orchestrator_v1_Exposed.sol create mode 100644 test/tools/CallIntercepter.sol create mode 100644 test/tools/OZErrors.sol create mode 100644 test/tools/TypeSanityHelper.sol delete mode 100644 test/unit/factories/workflow-specific/PIM_WorkflowFactory_v1.t.sol delete mode 100644 test/unit/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.t.sol rename test/unit/proxies/{InverterProxyAdmin.sol => InverterProxyAdmin.t.sol} (100%) diff --git a/remappings.txt b/remappings.txt index 8407e5472..9d4bf4949 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,6 +3,7 @@ @df/=lib/deterministic-factory/src/ @uma/=lib/uma-protocol/packages/core/contracts/ @ex/=src/external/ +@aut/=src/modules/authorizer/ @fm/=src/modules/fundingManager/ @pp/=src/modules/paymentProcessor/ @lm/=src/modules/logicModule/ diff --git a/script/deploymentSuite/MetadataCollection_v1.s.sol b/script/deploymentSuite/MetadataCollection_v1.s.sol index 3a2b230df..b9552e2e5 100644 --- a/script/deploymentSuite/MetadataCollection_v1.s.sol +++ b/script/deploymentSuite/MetadataCollection_v1.s.sol @@ -108,17 +108,6 @@ contract MetadataCollection_v1 { "FM_BC_Bancor_Redeeming_VirtualSupply_v1" ); - // RestrictedBancorRedeemingVirtualSupplyFundingManager - IModule_v1.Metadata public - restrictedBancorRedeemingVirtualSupplyFundingManagerMetadata = - IModule_v1.Metadata( - 1, - 0, - 0, - "https://github.com/InverterNetwork/contracts", - "FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1" - ); - // BondingSurfaceRedeemingFundingManager IModule_v1.Metadata public bondingSurfaceRedeemingFundingManagerMetadata = IModule_v1.Metadata( diff --git a/script/deploymentSuite/ModuleBeaconDeployer_v1.s.sol b/script/deploymentSuite/ModuleBeaconDeployer_v1.s.sol index bd5fdc461..414d57d09 100644 --- a/script/deploymentSuite/ModuleBeaconDeployer_v1.s.sol +++ b/script/deploymentSuite/ModuleBeaconDeployer_v1.s.sol @@ -133,28 +133,6 @@ contract ModuleBeaconDeployer_v1 is ) ); - // RestrictedBancorRedeemingVirtualSupplyFundingManager - initialMetadataRegistration.push( - restrictedBancorRedeemingVirtualSupplyFundingManagerMetadata - ); - initialBeaconRegistration.push( - IInverterBeacon_v1( - proxyAndBeaconDeployer.deployInverterBeacon( - restrictedBancorRedeemingVirtualSupplyFundingManagerMetadata - .title, - reverter, - governor, - impl_mod_FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1, - restrictedBancorRedeemingVirtualSupplyFundingManagerMetadata - .majorVersion, - restrictedBancorRedeemingVirtualSupplyFundingManagerMetadata - .minorVersion, - restrictedBancorRedeemingVirtualSupplyFundingManagerMetadata - .patchVersion - ) - ) - ); - // BondingSurfaceRedeemingFundingManager initialMetadataRegistration.push( bondingSurfaceRedeemingFundingManagerMetadata diff --git a/script/deploymentSuite/SingletonDeployer_v1.s.sol b/script/deploymentSuite/SingletonDeployer_v1.s.sol index fc35b4372..a71053645 100644 --- a/script/deploymentSuite/SingletonDeployer_v1.s.sol +++ b/script/deploymentSuite/SingletonDeployer_v1.s.sol @@ -53,7 +53,6 @@ contract SingletonDeployer_v1 is ProtocolConstants_v1 { // Funding Managers address public impl_mod_FM_BC_Bancor_Redeeming_VirtualSupply_v1; - address public impl_mod_FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1; address public impl_mod_FM_BC_BondingSurface_Redeeming_v1; address public impl_mod_FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1; @@ -186,13 +185,6 @@ contract SingletonDeployer_v1 is ProtocolConstants_v1 { "FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol:FM_BC_Bancor_Redeeming_VirtualSupply_v1" ) ); - impl_mod_FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1 = - deployAndLogWithCreate2( - "FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1", - vm.getCode( - "FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.sol:FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1" - ) - ); impl_mod_FM_BC_BondingSurface_Redeeming_v1 = deployAndLogWithCreate2( "FM_BC_BondingSurface_Redeeming_v1", diff --git a/script/workflowDeploymentAndSetupScripts/DeployAndSetupNavBasedPimWorkflow.s.sol b/script/workflowDeploymentAndSetupScripts/DeployAndSetupNavBasedPimWorkflow.s.sol deleted file mode 100644 index 43fda4e95..000000000 --- a/script/workflowDeploymentAndSetupScripts/DeployAndSetupNavBasedPimWorkflow.s.sol +++ /dev/null @@ -1,881 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -// Internal -import {ERC20IssuanceUpgradeable_Blacklist_v1} from - "@ex/token/ERC20IssuanceUpgradeable_Blacklist_v1.sol"; -import {IOrchestratorFactory_v1} from - "src/factories/interfaces/IOrchestratorFactory_v1.sol"; -import {AUT_Roles_v1} from "src/modules/authorizer/role/AUT_Roles_v1.sol"; -import {PP_Queue_ManualExecution_v1} from "@pp/PP_Queue_ManualExecution_v1.sol"; -import {FM_PC_Oracle_Redeeming_v1} from - "@fm/oracle/FM_PC_Oracle_Redeeming_v1.sol"; -import {LM_Oracle_Permissioned_v1} from "@lm/LM_Oracle_Permissioned_v1.sol"; -import {IOrchestrator_v1} from - "src/orchestrator/interfaces/IOrchestrator_v1.sol"; -import {IModule_v1} from "src/modules/base/IModule_v1.sol"; -import {DeploymentScript} from "script/deploymentScript/DeploymentScript.s.sol"; - -// External -import "forge-std/Script.sol"; -import {TransparentUpgradeableProxy} from - "@oz/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {IERC20Metadata} from "@oz/token/ERC20/extensions/IERC20Metadata.sol"; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - -contract DeployAndSetupNavBasedPimWorkflow is DeploymentScript { - // Issuance Token - ERC20IssuanceUpgradeable_Blacklist_v1 internal _issuanceToken; - string internal _tokenName; - string internal _tokenSymbol; - uint internal _maxSupply; - uint8 internal _tokenDecimals; - address internal _tokenOwner; - address internal _tokenProxyAdmin; - address internal _blacklistManager; - - // Role Authorizer - address internal _workflowAdmin; - - // Collateral Token (external) - address internal _collateralToken; - - // Oracle Based Funding Manager - address internal _projectTreasury; - uint internal _buyFeeInBps; - uint internal _maxBuyFeeInBps; - uint internal _sellFeeInBps; - uint internal _maxSellFeeInBps; - bool internal _isDirectOperationOnly; - bytes internal _workflowConfigData; - address internal _whitelistRole; - address internal _whitelistRoleAdmin; - address internal _queueExecutorRole; - address internal _queueExecutorRoleAdmin; - - // Manual Queue Payment Processor - address internal _cancelledOrdersTreasury; - address internal _failedOrdersTreasury; - address internal _queueOperatorRole; - address internal _queueOperatorRoleAdmin; - - // Oracle - address internal _priceSetterRole; - address internal _priceSetterRoleAdmin; - - // Workflow config - bool internal _independentUpdateBool; - address internal _independentUpdateAdmin; - - // Orchestrator - IOrchestrator_v1 internal _orchestrator; - - // Workflow Modules - FM_PC_Oracle_Redeeming_v1 internal _fundingManager; - AUT_Roles_v1 internal _authorizer; - PP_Queue_ManualExecution_v1 internal _paymentProcessor; - LM_Oracle_Permissioned_v1 internal _oracleModule; - - function run() public override { - console2.log("\n==============================================="); - console2.log(" DEPLOYING NAV BASED PIM WORKFLOW"); - console2.log(" ================================\n"); - - // Load and validate deployment variables - _loadAndValidateDeploymentVariables(); - - // Deploy external issuance token - _deployToken(); - - // Deploy workflow - _deployWorkflow(); - - // Setup workflow - _setupWorkflow(); - - // Set workflow admin - _setupWorkflowAdmin(); - - console2.log("================================================"); - console2.log(" DEPLOYMENT SUMMARY "); - console2.log("================================================"); - console2.log(" Status: SUCCESSFUL "); - console2.log(" Chain ID: ", block.chainid); - console2.log(" All contracts deployed and configured "); - console2.log("================================================\n"); - } - - // ------------------------------------------------------------------------- - // Deployment functions - - function _deployToken() internal { - console2.log("Token Deployment"); - console2.log("----------------"); - console2.log(" Deploying issuance token..."); - - // Deploy the implementation contract - vm.startBroadcast(deployerPrivateKey); - ERC20IssuanceUpgradeable_Blacklist_v1 implementation = - new ERC20IssuanceUpgradeable_Blacklist_v1(); - // Deploy a simple proxy that delegates to the implementation - address proxy = address( - new TransparentUpgradeableProxy( - address(implementation), - _tokenProxyAdmin, - abi.encodeWithSelector( - ERC20IssuanceUpgradeable_Blacklist_v1 - .__ERC20IssuanceBlacklist_init - .selector, - _tokenName, - _tokenSymbol, - _tokenDecimals, - _maxSupply - ) - ) - ); - vm.stopBroadcast(); - - // Store issuance token - _issuanceToken = ERC20IssuanceUpgradeable_Blacklist_v1(proxy); - console2.log(" [OK] Issuance token deployed successfully"); - _logAddress(" Issuance token deployed at", proxy); - } - - function _deployWorkflow() internal { - console2.log("\nWorkflow Deployment"); - console2.log("-------------------"); - // --------------------------------------------------------------------- - // Deploy workflow - - // Get the orchestrator factory - IOrchestratorFactory_v1 orchestratorFactory = - IOrchestratorFactory_v1(getDeployedOrchestratorFactory()); - - console2.log(" Deploying Inverter Protocol workflow..."); - // Create workflow - vm.startBroadcast(deployerPrivateKey); - _orchestrator = orchestratorFactory.createOrchestrator( - _getWorkflowConfig(), - _getFundingManagerConfig(), - _getAuthorizerConfig(), - _getPaymentProcessorConfig(), - _getOptionalModulesConfigs() - ); - vm.stopBroadcast(); - - // --------------------------------------------------------------------- - // Store workflow addresses - - // Store modules - // Store funding manager - _fundingManager = - FM_PC_Oracle_Redeeming_v1(address(_orchestrator.fundingManager())); - // Store Authorizer - _authorizer = AUT_Roles_v1(address(_orchestrator.authorizer())); - // Store Payment Processor - _paymentProcessor = PP_Queue_ManualExecution_v1( - address(_orchestrator.paymentProcessor()) - ); - // Store Oracle module - _oracleModule = - LM_Oracle_Permissioned_v1(address(_orchestrator.listModules()[0])); - - // --------------------------------------------------------------------- - // Validate workflow deployment - - console2.log(" Validating deployed workflow..."); - _validateWorkflowDeployment(); - console2.log(" [OK] Workflow deployed successfully\n"); - - // --------------------------------------------------------------------- - // Log workflow addresses - console2.log("Deployed Contract Addresses"); - console2.log("---------------------------"); - _logAddress(" * Orchestrator", address(_orchestrator)); - _logAddress(" * Funding Manager", address(_fundingManager)); - _logAddress(" * Authorizer", address(_authorizer)); - _logAddress(" * Payment Processor", address(_paymentProcessor)); - _logAddress(" * Oracle Module", address(_oracleModule)); - _logAddress(" * Issuance Token", address(_issuanceToken)); - console2.log("\n"); - } - - // ------------------------------------------------------------------------- - // Setup functions - - function _setupWorkflowAdmin() internal { - console2.log("Administrative Setup"); - console2.log("--------------------"); - - console2.log("Setting up workflow admin..."); - vm.startBroadcast(deployerPrivateKey); - _authorizer.grantRole(_authorizer.getAdminRole(), _workflowAdmin); - require( - _authorizer.checkForRole(_authorizer.getAdminRole(), _workflowAdmin) - == true, - "Workflow admin not set correctly" - ); - console2.log(" [OK] Workflow admin configured"); - console2.log(" Workflow admin: ", _workflowAdmin); - - // Revoke admin role from deployer - console2.log("\n Revoking admin role from deployer..."); - _authorizer.revokeRole(_authorizer.getAdminRole(), deployer); - require( - _authorizer.checkForRole(_authorizer.getAdminRole(), deployer) - == false, - "Admin role not revoked from deployer" - ); - vm.stopBroadcast(); - console2.log(" [OK] Admin role revoked from deployer\n"); - } - - function _setupWorkflow() internal { - console2.log("Workflow Configuration"); - console2.log("----------------------"); - // Setup token - _setupAndVerifyToken(); - // Setup Oracle - _setupAndVerifyOracle(); - // Setup Funding Manager - _setupAndVerifyFundingManager(); - // Setup Payment Processor - _setupAndVerifyPaymentProcessor(); - console2.log("\n [OK] Workflow setup successful\n"); - } - - function _setupAndVerifyPaymentProcessor() internal { - console2.log("\n Payment Processor Setup"); - // --------------------------------------------------------------------- - // Roles - vm.startBroadcast(deployerPrivateKey); - // Set Queue Operator Role - _paymentProcessor.grantModuleRole( - _paymentProcessor.getQueueOperatorRole(), _queueOperatorRole - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_paymentProcessor), - _paymentProcessor.getQueueOperatorRole() - ), - _queueOperatorRole - ), - "Queue operator role not set correctly" - ); - _logAddress(" * Queue Operator", _queueOperatorRole); - - // Set Queue Operator Role Admin - _paymentProcessor.grantModuleRole( - _paymentProcessor.getQueueOperatorRoleAdmin(), - _queueOperatorRoleAdmin - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_paymentProcessor), - _paymentProcessor.getQueueOperatorRoleAdmin() - ), - _queueOperatorRoleAdmin - ), - "Queue operator role admin not set correctly" - ); - _logAddress(" * Queue Op Admin", _queueOperatorRoleAdmin); - - // Set admin role for queue operator role - _authorizer.transferAdminRole( - _authorizer.generateRoleId( - address(_paymentProcessor), - _paymentProcessor.getQueueOperatorRole() - ), - _authorizer.generateRoleId( - address(_paymentProcessor), - _paymentProcessor.getQueueOperatorRoleAdmin() - ) - ); - require( - _authorizer.getRoleAdmin( - _authorizer.generateRoleId( - address(_paymentProcessor), - _paymentProcessor.getQueueOperatorRole() - ) - ) - == _authorizer.generateRoleId( - address(_paymentProcessor), - _paymentProcessor.getQueueOperatorRoleAdmin() - ), - "Admin role for queue operator role not set correctly" - ); - vm.stopBroadcast(); - } - - function _setupAndVerifyFundingManager() internal { - console2.log("\n Funding Manager Setup"); - // --------------------------------------------------------------------- - // Setters - vm.startBroadcast(deployerPrivateKey); - // Set Oracle address - _fundingManager.setOracleAddress(address(_oracleModule)); - require( - _fundingManager.getOracle() == address(_oracleModule), - "Funding manager oracle not set correctly" - ); - _logAddress(" * Oracle", address(_oracleModule)); - // --------------------------------------------------------------------- - // Roles - - // Set Whitelist Role - _fundingManager.grantModuleRole( - _fundingManager.getWhitelistRole(), _whitelistRole - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_fundingManager), _fundingManager.getWhitelistRole() - ), - _whitelistRole - ), - "Whitelist role not set correctly" - ); - _logAddress(" * Whitelist Role", _whitelistRole); - - // Set Whitelist Role Admin - _fundingManager.grantModuleRole( - _fundingManager.getWhitelistRoleAdmin(), _whitelistRoleAdmin - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getWhitelistRoleAdmin() - ), - _whitelistRoleAdmin - ), - "Whitelist role admin not set correctly" - ); - _logAddress(" * Whitelist Admin", _whitelistRoleAdmin); - - // Set admin role for whitelist role - _authorizer.transferAdminRole( - _authorizer.generateRoleId( - address(_fundingManager), _fundingManager.getWhitelistRole() - ), - _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getWhitelistRoleAdmin() - ) - ); - require( - _authorizer.getRoleAdmin( - _authorizer.generateRoleId( - address(_fundingManager), _fundingManager.getWhitelistRole() - ) - ) - == _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getWhitelistRoleAdmin() - ), - "Admin role for whitelist role not set correctly" - ); - - // Set Queue Executor Role - _fundingManager.grantModuleRole( - _fundingManager.getQueueExecutorRole(), _queueExecutorRole - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getQueueExecutorRole() - ), - _queueExecutorRole - ), - "Queue executor role not set correctly" - ); - _logAddress(" * Queue Executor", _queueExecutorRole); - - // Set Queue Executor Role Admin - _fundingManager.grantModuleRole( - _fundingManager.getQueueExecutorRoleAdmin(), _queueExecutorRoleAdmin - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getQueueExecutorRoleAdmin() - ), - _queueExecutorRoleAdmin - ), - "Queue executor role admin not set correctly" - ); - _logAddress(" * Queue Exec Admin", _queueExecutorRoleAdmin); - - // Set admin role for queue executor role - _authorizer.transferAdminRole( - _authorizer.generateRoleId( - address(_fundingManager), _fundingManager.getQueueExecutorRole() - ), - _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getQueueExecutorRoleAdmin() - ) - ); - require( - _authorizer.getRoleAdmin( - _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getQueueExecutorRole() - ) - ) - == _authorizer.generateRoleId( - address(_fundingManager), - _fundingManager.getQueueExecutorRoleAdmin() - ), - "Admin role for queue executor role not set correctly" - ); - vm.stopBroadcast(); - } - - function _setupAndVerifyOracle() internal { - console2.log("\n Oracle Setup"); - // --------------------------------------------------------------------- - // Roles - - vm.startBroadcast(deployerPrivateKey); - // Set price setter role - _oracleModule.grantModuleRole( - _oracleModule.getPriceSetterRole(), _priceSetterRole - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_oracleModule), _oracleModule.getPriceSetterRole() - ), - _priceSetterRole - ), - "Price setter role not set correctly" - ); - _logAddress(" * Price Setter", _priceSetterRole); - - // Set price setter role admin - _oracleModule.grantModuleRole( - _oracleModule.getPriceSetterRoleAdmin(), _priceSetterRoleAdmin - ); - require( - _authorizer.checkForRole( - _authorizer.generateRoleId( - address(_oracleModule), - _oracleModule.getPriceSetterRoleAdmin() - ), - _priceSetterRoleAdmin - ), - "Price setter role admin not set correctly" - ); - _logAddress(" * Price Setter Admin", _priceSetterRoleAdmin); - - // Set admin role for price setter role - _authorizer.transferAdminRole( - _authorizer.generateRoleId( - address(_oracleModule), _oracleModule.getPriceSetterRole() - ), - _authorizer.generateRoleId( - address(_oracleModule), _oracleModule.getPriceSetterRoleAdmin() - ) - ); - require( - _authorizer.getRoleAdmin( - _authorizer.generateRoleId( - address(_oracleModule), _oracleModule.getPriceSetterRole() - ) - ) - == _authorizer.generateRoleId( - address(_oracleModule), _oracleModule.getPriceSetterRoleAdmin() - ), - "Admin role for price setter role not set correctly" - ); - vm.stopBroadcast(); - } - - function _setupAndVerifyToken() internal { - console2.log("\n Issuance Token Setup"); - // --------------------------------------------------------------------- - // Setters - - // Set minter - vm.startBroadcast(deployerPrivateKey); - _issuanceToken.setMinter(address(_fundingManager), true); - require( - _issuanceToken.allowedMinters(address(_fundingManager)), - "Minter not set correctly" - ); - _logAddress(" * Minter", address(_fundingManager)); - // --------------------------------------------------------------------- - // Roles - - // Set Blacklist Manager - _issuanceToken.setBlacklistManager(_blacklistManager, true); - require( - _issuanceToken.isBlacklistManager(_blacklistManager) == true, - "Blacklist manager not set correctly" - ); - _logAddress(" * Blacklist Manager", _blacklistManager); - // Set new owner - _issuanceToken.transferOwnership(_tokenOwner); - require( - _issuanceToken.owner() == _tokenOwner, - "Token owner not set correctly" - ); - _logAddress(" * Owner", _tokenOwner); - vm.stopBroadcast(); - } - - // ------------------------------------------------------------------------- - // Workflow config functions - - function _getWorkflowConfig() - internal - view - returns (IOrchestratorFactory_v1.WorkflowConfig memory workflowConfig_) - { - workflowConfig_ = IOrchestratorFactory_v1.WorkflowConfig( - _independentUpdateBool, _independentUpdateAdmin - ); - } - - function _getFundingManagerConfig() - internal - view - returns ( - IOrchestratorFactory_v1.ModuleConfig memory fundingManagerConfig_ - ) - { - // Get the funding manager config - fundingManagerConfig_ = IOrchestratorFactory_v1.ModuleConfig( - IModule_v1.Metadata( - 1, - 0, - 0, - "https://github.com/InverterNetwork/contracts", - "FM_PC_Oracle_Redeeming_v1" - ), - abi.encode( - _projectTreasury, - _issuanceToken, - _collateralToken, - _buyFeeInBps, - _sellFeeInBps, - _maxSellFeeInBps, - _maxBuyFeeInBps, - _isDirectOperationOnly - ) - ); - } - - function _getAuthorizerConfig() - internal - view - returns (IOrchestratorFactory_v1.ModuleConfig memory authorizerConfig_) - { - // Get the authorizer config - authorizerConfig_ = IOrchestratorFactory_v1.ModuleConfig( - IModule_v1.Metadata( - 1, - 0, - 0, - "https://github.com/InverterNetwork/contracts", - "AUT_Roles_v1" - ), - abi.encode(deployer) // Set deployer as initial admin. Later replaced by the workflow admin. - ); - } - - function _getPaymentProcessorConfig() - internal - view - returns ( - IOrchestratorFactory_v1.ModuleConfig memory paymentProcessorConfig_ - ) - { - paymentProcessorConfig_ = IOrchestratorFactory_v1.ModuleConfig( - IModule_v1.Metadata( - 1, - 0, - 0, - "https://github.com/InverterNetwork/contracts", - "PP_Queue_ManualExecution_v1" - ), - abi.encode(_cancelledOrdersTreasury, _failedOrdersTreasury) - ); - } - - function _getOptionalModulesConfigs() - internal - view - returns ( - IOrchestratorFactory_v1.ModuleConfig[] memory optionalModulesConfigs_ - ) - { - optionalModulesConfigs_ = new IOrchestratorFactory_v1.ModuleConfig[](1); - optionalModulesConfigs_[0] = IOrchestratorFactory_v1.ModuleConfig( - IModule_v1.Metadata( - 1, - 0, - 0, - "https://github.com/InverterNetwork/contracts", - "LM_Oracle_Permissioned_v1" - ), - abi.encode(_collateralToken) - ); - } - - // ------------------------------------------------------------------------- - // Validation functions - - function _loadAndValidateDeploymentVariables() internal { - console2.log("Environment Setup"); - console2.log("-----------------"); - console2.log(" Loading and validating environment variables..."); - - // Load and validate environment variables of workflow - _loadAndValidateEnvironmentVariablesOfWorkflow(); - - // Validate Inverter Protocol deployment variables from ENV - _validateInverterProtocolDeploymentVariables(); - - // Load Inverter Protocol deployed contracts - loadDeployedContracts(); - console2.log( - " [OK] Loading Inverter Protocol Infrastructure Contracts\n" - ); - } - - function _validateWorkflowDeployment() internal view { - // Validate deployments - _validateFundingManagerDeployment(); - _validateAuthorizerDeployment(); - _validatePaymentProcessorDeployment(); - _validateOracleDeployment(); - console2.log(" [OK] Deployed workflow validated"); - } - - function _validateOracleDeployment() internal view { - // Validate oracle deployment - require( - _oracleModule.getCollateralTokenDecimals() - == IERC20Metadata(_collateralToken).decimals(), - "Oracle collateral token decimals not set correctly" - ); - require( - _oracleModule.orchestrator() == _orchestrator, - "Oracle orchestrator not set correctly" - ); - } - - function _validatePaymentProcessorDeployment() internal view { - // Validate payment processor deployment - require( - _paymentProcessor.getCanceledOrdersTreasury() - == _cancelledOrdersTreasury, - "Payment processor cancelled orders treasury not set" - ); - require( - _paymentProcessor.getFailedOrdersTreasury() == _failedOrdersTreasury, - "Payment processor failed orders treasury not set" - ); - require( - _paymentProcessor.orchestrator() == _orchestrator, - "Payment processor orchestrator not set correctly" - ); - } - - function _validateAuthorizerDeployment() internal view { - // Validate authorizer deployment - require( - _authorizer.checkForRole(_authorizer.getAdminRole(), deployer), - "Authorizer admin role not set correctly" - ); - require( - _authorizer.orchestrator() == _orchestrator, - "Authorizer orchestrator not set correctly" - ); - } - - function _validateFundingManagerDeployment() internal view { - // Validate funding manager deployment - require( - _fundingManager.getProjectTreasury() == _projectTreasury, - "Funding manager project treasury not set" - ); - require( - _fundingManager.getIssuanceToken() == address(_issuanceToken), - "Funding manager issuance token not set" - ); - require( - _fundingManager.token() == IERC20(_collateralToken), - "Funding manager token not set" - ); - require( - _fundingManager.getBuyFee() == _buyFeeInBps, - "Funding manager buy fee not set" - ); - require( - _fundingManager.getSellFee() == _sellFeeInBps, - "Funding manager sell fee not set" - ); - require( - _fundingManager.getMaxProjectBuyFee() == _maxBuyFeeInBps, - "Funding manager max project buy fee not set" - ); - require( - _fundingManager.getMaxProjectSellFee() == _maxSellFeeInBps, - "Funding manager max project sell fee not set" - ); - require( - _fundingManager.getIsDirectOperationsOnly() - == _isDirectOperationOnly, - "Funding manager is direct operations only not set" - ); - require( - _fundingManager.orchestrator() == _orchestrator, - "Funding manager orchestrator not set correctly" - ); - } - - function _validateInverterProtocolDeploymentVariables() internal view { - _validateValidChainId(block.chainid); - console2.log(" [OK] Validating Inverter Protocol deployment address"); - } - - function _validateValidChainId(uint chainId_) internal view { - for (uint i = 0; i < deployedMainnets.length; i++) { - if (chainId_ == deployedMainnets[i]) { - return; - } - } - for (uint i = 0; i < deployedTestnets.length; i++) { - if (chainId_ == deployedTestnets[i]) { - return; - } - } - // revert("Invalid chain id"); - } - - function _loadAndValidateEnvironmentVariablesOfWorkflow() internal { - // Issuance Token - // --------------------------------------------------------------------- - _tokenName = vm.envString("TOKEN_NAME_STRING"); - require(bytes(_tokenName).length > 0, "Token name not set"); - - _tokenSymbol = vm.envString("TOKEN_SYMBOL_STRING"); - require(bytes(_tokenSymbol).length > 0, "Token symbol not set"); - - _tokenDecimals = uint8(vm.envUint("TOKEN_DECIMALS")); - require(_tokenDecimals > 0, "Token decimals not set"); - - _maxSupply = vm.envUint("MAX_SUPPLY"); - require(_maxSupply > 0, "Max supply not set"); - - _tokenOwner = vm.envAddress("TOKEN_OWNER_ADDRESS"); - require(_tokenOwner != address(0), "Token owner not set"); - - _tokenProxyAdmin = vm.envAddress("TOKEN_PROXY_ADMIN_ADDRESS"); - require(_tokenProxyAdmin != address(0), "Token proxy admin not set"); - - _blacklistManager = vm.envAddress("BLACKLIST_MANAGER_ADDRESS"); - require(_blacklistManager != address(0), "Blacklist manager not set"); - - // Role Authorizer - // --------------------------------------------------------------------- - _workflowAdmin = vm.envAddress("WORKFLOW_ADMIN_ADDRESS"); - require(_workflowAdmin != address(0), "Workflow admin not set"); - - // Collateral Token (external) - // --------------------------------------------------------------------- - _collateralToken = vm.envAddress("COLLATERAL_TOKEN_ADDRESS"); - require(_collateralToken != address(0), "Collateral token not set"); - - // Oracle Based Funding Manager - // --------------------------------------------------------------------- - _projectTreasury = vm.envAddress("PROJECT_TREASURY_ADDRESS"); - require(_projectTreasury != address(0), "Project treasury not set"); - - _buyFeeInBps = vm.envUint("BUY_FEE_IN_BPS"); - - _maxBuyFeeInBps = vm.envUint("MAX_BUY_FEE_IN_BPS"); - - _sellFeeInBps = vm.envUint("SELL_FEE_IN_BPS"); - - _maxSellFeeInBps = vm.envUint("MAX_SELL_FEE_IN_BPS"); - - _isDirectOperationOnly = vm.envBool("IS_DIRECT_OPERATION_ONLY_BOOL"); - - _whitelistRole = vm.envAddress("WHITELIST_ROLE_ADDRESS"); - require(_whitelistRole != address(0), "Whitelist role not set"); - - _whitelistRoleAdmin = vm.envAddress("WHITELIST_ROLE_ADMIN_ADDRESS"); - require( - _whitelistRoleAdmin != address(0), "Whitelist role admin not set" - ); - - _queueExecutorRole = vm.envAddress("QUEUE_EXECUTOR_ROLE_ADDRESS"); - require(_queueExecutorRole != address(0), "Queue executor role not set"); - - _queueExecutorRoleAdmin = - vm.envAddress("QUEUE_EXECUTOR_ROLE_ADMIN_ADDRESS"); - require( - _queueExecutorRoleAdmin != address(0), - "Queue executor role admin not set" - ); - - // Oracle - // --------------------------------------------------------------------- - _priceSetterRole = vm.envAddress("PRICE_SETTER_ROLE_ADDRESS"); - require(_priceSetterRole != address(0), "Price setter role not set"); - - _priceSetterRoleAdmin = vm.envAddress("PRICE_SETTER_ROLE_ADMIN_ADDRESS"); - require( - _priceSetterRoleAdmin != address(0), - "Price setter role admin not set" - ); - - // Manual Queue Payment Processor - // --------------------------------------------------------------------- - _cancelledOrdersTreasury = - vm.envAddress("CANCELLED_ORDER_TREASURY_ADDRESS"); - require( - _cancelledOrdersTreasury != address(0), - "Cancelled orders treasury not set" - ); - - _failedOrdersTreasury = vm.envAddress("FAILED_ORDER_TREASURY_ADDRESS"); - require( - _failedOrdersTreasury != address(0), - "Failed orders treasury not set" - ); - - _queueOperatorRole = vm.envAddress("QUEUE_OPERATOR_ROLE_ADDRESS"); - require(_queueOperatorRole != address(0), "Queue operator role not set"); - - _queueOperatorRoleAdmin = - vm.envAddress("QUEUE_EXECUTOR_ROLE_ADMIN_ADDRESS"); - require( - _queueOperatorRoleAdmin != address(0), - "Queue operator role admin not set" - ); - - // Workflow config - // --------------------------------------------------------------------- - _independentUpdateBool = vm.envBool("INDEPENDENT_UPDATE_BOOL"); - _independentUpdateAdmin = - vm.envAddress("INDEPENDENT_UPDATE_ADMIN_ADDRESS"); - - console2.log(" [OK] Environment variables loaded and validated"); - } - - // ------------------------------------------------------------------------- - // Logging functions - - function _logAddress(string memory label, address addr) internal view { - console2.log(string.concat(label, ": "), addr); - } -} diff --git a/script/workflowDeploymentAndSetupScripts/WorkflowDeployment.md b/script/workflowDeploymentAndSetupScripts/WorkflowDeployment.md deleted file mode 100644 index 2c73a4a42..000000000 --- a/script/workflowDeploymentAndSetupScripts/WorkflowDeployment.md +++ /dev/null @@ -1,65 +0,0 @@ -# Inverter Protocol: Workflow Deployment Guide - -## Introduction - -This document provides instructions for deploying workflows within the Inverter Protocol. It outlines the necessary steps and environment variable configurations for successful deployment. - ---- - -## Yield Bearing Stable Token Workflow - -Follow these steps to deploy the Yield Bearing Stable Token workflow: - -1. **Prepare Environment File:** - * Duplicate the example environment file: `example.dev.navBasedPimWorkflow.env` - * Rename the duplicated file to: `dev.navBasedPimWorkflow.env` (removing the `example.` prefix). - -2. **Configure Deployment Variables:** - * Open the new `.env.navBasedPimWorkflow` file. - * Locate the **Workflow Deployment Parameters** section. - * Replace all placeholder/demo values in this section with your actual deployment-specific variables. - * Locate the **Etherscan API Keys** section. - * Replace the placeholder/demo values to your Etherscan API key. - -3. **Load Environment Variables:** - * Source the environment file in your terminal session. This loads the variables you configured in the previous step. - ```bash - source script/workflowDeploymentAndSetupScripts/dev.navBasedPimWorkflow.env - ``` - -4. **Deploy and Verify the Workflow:** - * Execute the deployment script using `forge`. This command performs the deployment, setup, and contract verification. - * You will need to provide the following command-line arguments (ensure the corresponding environment variables like `$OPTIMISM_SEPOLIA_RPC_URL`, `$OPTIMISM_ETHERSCAN_API_KEY`, and `$VERIFIER_URL` are set, either from the sourced file or your shell environment): - * `--rpc-url`: The RPC endpoint URL for your target blockchain network (e.g., `$OPTIMISM_SEPOLIA_RPC_URL` for Optimism Sepolia). - * `--etherscan-api-key`: Your Etherscan API key for the target network (e.g., `$OPTIMISM_ETHERSCAN_API_KEY`). - * `--verifier-url`: The Etherscan API URL used for verification on the target network (e.g., `$OPTIMISM_SEPOLIA_ETHERSCAN_URL`). - - * Run the following command: - ```bash - forge script script/workflowDeploymentAndSetupScripts/DeployAndSetupNavBasedPimWorkflow.s.sol \ - --rpc-url $OPTIMISM_SEPOLIA_RPC_URL \ - -vvv \ - --broadcast \ - --etherscan-api-key $OPTIMISM_ETHERSCAN_API_KEY \ - --verifier-url $OPTIMISM_SEPOLIA_ETHERSCAN_URL \ - --verify - ``` - *(Note: Replace `$OPTIMISM_SEPOLIA_RPC_URL`, `$OPTIMISM_ETHERSCAN_API_KEY`, and `$OPTIMISM_SEPOLIA_ETHERSCAN_URL` with the actual environment variables containing your specific URLs and key if they differ from the example names.)* - ---- - -### Troubleshooting: Verification Failures - -Contract verification can sometimes fail due to intermittent Etherscan issues. If a contract doesn't verify automatically during deployment: - -1. Identify the address (``) of the contract that failed verification (usually visible in the `forge script` output). -2. Run the `forge verify-contract` command manually for that specific address: - - ```bash - forge verify-contract \ - --rpc-url $RPC_URL \ - --etherscan-api-key $ETHERSCAN_API_KEY \ - --verifier-url $VERIFIER_URL \ - --watch - ``` - *(Ensure you use the same `$RPC_URL`, `$ETHERSCAN_API_KEY`, and `$VERIFIER_URL` values corresponding to the network where the contract was deployed.)* \ No newline at end of file diff --git a/script/workflowDeploymentAndSetupScripts/example.dev.navBasedPimWorkflow.env b/script/workflowDeploymentAndSetupScripts/example.dev.navBasedPimWorkflow.env deleted file mode 100644 index 7d6d5a9f7..000000000 --- a/script/workflowDeploymentAndSetupScripts/example.dev.navBasedPimWorkflow.env +++ /dev/null @@ -1,107 +0,0 @@ -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Development Environment Variables -# -# WARNING: This file is part of the git repo. DO NOT INCLUDE SENSITIVE DATA! -# -# The environment variables are read by -# - Solidity scripts in script/ -# - forge commands -# -# Note that the variables need to be exported in order for make to read them -# directly. -# -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -# ------------------------------------------------------------------------------ - -# RPC endpoints (these are public links, substitute with alchemy or similar for less rate limiting) - -# Local -export RPC_URL="http://127.0.0.1:8545" # Local anvil node - -# Ethereum -export ETHEREUM_RPC_URL=https://cloudflare-eth.com -export SEPOLIA_RPC_URL=https://rpc.ankr.com/eth_sepolia/919d3fa62bc6fc3bddabdc497f7e6fcd1c242ae86588bcdede81c171daab5df7 - -# Optimism -export OPTIMISM_SEPOLIA_RPC_URL=https://sepolia.optimism.io -export OPTIMISM_RPC_URL=https://mainnet.optimism.io - -# Polygon -export POLYGON_AMOY_RPC_URL=https://rpc-amoy.polygon.technology/ -export POLYGON_CARDONA_RPC_URL=https://rpc.cardona.zkevm-rpc.com - - -# ------------------------------------------------------------------------------ -# Wallets - -# Deployer Wallet -# (Note that for this example we are reusing anvil's default wallets.) -export DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 - -# ------------------------------------------------------------------------------ -# Contract Verification - -# Etherscan API Keys -export ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 -export POLYGONSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 -export OPTIMISM_ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 - -# Optimisim Etherscan URLs -export OPTIMISM_SEPOLIA_ETHERSCAN_URL=https://api-sepolia-optimistic.etherscan.io/api -export OPTIMISM_ETHERSCAN_URL=https://api-optimistic.etherscan.io/api - -# Ethereum Etherscan URLs -export SEPOLIA_ETHERSCAN_URL=https://api-sepolia.etherscan.io/api -export ETHEREUM_ETHERSCAN_URL=https://api.etherscan.io/api - - -# ------------------------------------------------------------------------------ -# Command to run a deployment script -# In general, the command to run a deployment script will look like this: -# forge script script/deploymentScript/DeploymentScript.s.sol --rpc-url $SEPOLIA_RPC_URL --chain-id 11155111 --private-key $WALLET_DEPLOYER_PK --etherscan-api-key $ETHERSCAN_API_KEY --verify --broadcast --legacy -vvv - - -# ------------------------------------------------------------------------------ -# Workflow Deployment Parameters - -# Issuance Token -export TOKEN_NAME_STRING='Test Token' -export TOKEN_SYMBOL_STRING='T-Token' -export TOKEN_DECIMALS=18 -export MAX_SUPPLY=115792089237316195423570985008687907853269984665640564039457584007913129639935 -export TOKEN_OWNER_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export TOKEN_PROXY_ADMIN_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export BLACKLIST_MANAGER_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 - -# Role Authorizer -export WORKFLOW_ADMIN_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 - -# Collateral Token (external) -export COLLATERAL_TOKEN_ADDRESS=0x065775C7aB4E60ad1776A30DCfB15325d231Ce4F - -# Oracle Based Funding Manager -export PROJECT_TREASURY_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export BUY_FEE_IN_BPS=0 -export MAX_BUY_FEE_IN_BPS=0 -export SELL_FEE_IN_BPS=0 -export MAX_SELL_FEE_IN_BPS=0 -export IS_DIRECT_OPERATION_ONLY_BOOL=true -export WHITELIST_ROLE_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export WHITELIST_ROLE_ADMIN_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export QUEUE_EXECUTOR_ROLE_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export QUEUE_EXECUTOR_ROLE_ADMIN_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 - -# Manual Queue Payment Processor -export CANCELLED_ORDER_TREASURY_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export FAILED_ORDER_TREASURY_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export QUEUE_OPERATOR_ROLE_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export QUEUE_OPERATOR_ROLE_ADMIN=ABC123ABC123ABC123ABC123ABC123ABC1 - -# Permissoned Oracle -export PRICE_SETTER_ROLE_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 -export PRICE_SETTER_ROLE_ADMIN_ADDRESS=ABC123ABC123ABC123ABC123ABC123ABC1 - -# Workflow config -export INDEPENDENT_UPDATE_BOOL=false -export INDEPENDENT_UPDATE_ADMIN_ADDRESS=0x0000000000000000000000000000000000000000 diff --git a/src/factories/interfaces/IPIM_WorkflowFactory_v1.sol b/src/factories/interfaces/IPIM_WorkflowFactory_v1.sol deleted file mode 100644 index 729a6855e..000000000 --- a/src/factories/interfaces/IPIM_WorkflowFactory_v1.sol +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; - -// Internal Interfaces -import {IOrchestrator_v1} from - "src/orchestrator/interfaces/IOrchestrator_v1.sol"; -import {IModule_v1} from "src/modules/base/IModule_v1.sol"; -import {IInverterBeacon_v1} from "src/proxies/interfaces/IInverterBeacon_v1.sol"; -import {IOrchestratorFactory_v1} from - "src/factories/interfaces/IOrchestratorFactory_v1.sol"; -import {IFM_BC_Bancor_Redeeming_VirtualSupply_v1} from - "@fm/bondingCurve/interfaces/IFM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; -import {IPIM_WorkflowFactory_v1} from - "src/factories/interfaces/IPIM_WorkflowFactory_v1.sol"; -import {IERC20Issuance_v1} from - "src/external/token/interfaces/IERC20Issuance_v1.sol"; - -// Internal Dependencies -import {ERC20Issuance_v1} from "src/external/token/ERC20Issuance_v1.sol"; - -// External Interfaces -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - -interface IPIM_WorkflowFactory_v1 { - //-------------------------------------------------------------------------- - // Errors - /// @notice Error thrown when an unpermissioned address tries to claim fees or to transfer role. - error PIM_WorkflowFactory__OnlyPimFeeRecipient(); - /// @notice Error thrown when the curve is deployed with an invalid configuration. - error PIM_WorkflowFactory__InvalidConfiguration(); - - //-------------------------------------------------------------------------- - // Events - - /// @notice Event emitted when a new PIM workflow is created. - /// @param bondingCurve The address of the bonding curve. - /// @param issuanceToken The address of the issuance token. - /// @param deployer The address of the deployer. - /// @param recipient The address of the recipient. - /// @param isRenouncedIssuanceToken If ownership over the issuance token should be renounced. - /// @param isRenouncedWorkflow If admin rights over the workflow should be renounced. - event PIMWorkflowCreated( - address indexed bondingCurve, - address indexed issuanceToken, - address indexed deployer, - address recipient, - bool isRenouncedIssuanceToken, - bool isRenouncedWorkflow - ); - - /// @notice Event emitted when factory owner sets new fee. - /// @param oldRecipient The previous pim fee recipient. - /// @param newRecipient The new pim fee recipient. - event PimFeeRecipientUpdated( - address indexed oldRecipient, address indexed newRecipient - ); - - /// @notice Event emitted when PIM fee (buy/sell fees) is claimed. - /// @param claimer The address of the claimer. - /// @param amount The amount claimed. - event PimFeeClaimed(address indexed claimer, uint amount); - - //-------------------------------------------------------------------------- - // Structs - - /// @notice Struct for the issuance token parameters. - /// @param name The name of the issuance token. - /// @param symbol The symbol of the issuance token. - /// @param decimals The decimals of the issuance token. - /// @param maxSupply The maximum supply of the issuance token. - struct IssuanceTokenParams { - string name; - string symbol; - uint8 decimals; - uint maxSupply; - } - - /// @notice Struct for the issuance token parameters. - /// @param fundingManagerMetadata The {FundingManager_v1}'s metadata. - /// @param authorizerMetadata The {Authorizer_v1}'s metadata. - /// @param bcProperties The bonding curve's properties. - /// @param issuanceTokenParams The issuance token's parameters. - /// @param recipient The recipient of the initial issuance token supply. - /// @param admin Is set as token owner and workflow admin unless renounced. - /// @param collateralToken The collateral token. - /// @param firstCollateralIn Amount of collateral that is used for the first purchase from the bonding curve. - /// @param isRenouncedIssuanceToken If ownership over the issuance token should be renounced. - /// @param isRenouncedWorkflow If admin rights over the workflow should be renounced. - /// @param withInitialLiquidity If true initial liquidity will be added to the bonding curve. - /// In this case the recipient will receive the initial issuance token supply. - /// If false initial liquidity will not be added to the bonding curve and initial token. - struct PIMConfig { - IModule_v1.Metadata fundingManagerMetadata; - IModule_v1.Metadata authorizerMetadata; - IFM_BC_Bancor_Redeeming_VirtualSupply_v1.BondingCurveProperties - bcProperties; - IssuanceTokenParams issuanceTokenParams; - address admin; - address recipient; - address collateralToken; - uint firstCollateralIn; - bool isRenouncedIssuanceToken; - bool isRenouncedWorkflow; - bool withInitialLiquidity; - } - - //-------------------------------------------------------------------------- - // Functions - - /// @notice Deploys a workflow with a bonding curve and an issuance token. - /// @param workflowConfig The workflow's config data. - /// @param paymentProcessorConfig The config data for the {Orchestrator_v1}'s {IPaymentProcessor_v1} instance. - /// @param moduleConfigs Variable length set of optional module's config data. - /// @param PIMConfig The configuration for the issuance token and the bonding curve. - /// @return Returns the address of {Orchestrator_v1} and the address of the issuance token. - function createPIMWorkflow( - IOrchestratorFactory_v1.WorkflowConfig memory workflowConfig, - IOrchestratorFactory_v1.ModuleConfig memory paymentProcessorConfig, - IOrchestratorFactory_v1.ModuleConfig[] memory moduleConfigs, - IPIM_WorkflowFactory_v1.PIMConfig memory PIMConfig - ) external returns (IOrchestrator_v1, ERC20Issuance_v1); - - /// @notice Updates who can claim the buy/sell fees of a given bonding curve. - /// @dev Only callable by the currently eligible fee recipient. - /// @param fundingManager The address of the bonding curve from which to withdraw fees. - /// @param to The address that should be eligible to claim fees in the future. - function transferPimFeeEligibility(address fundingManager, address to) - external; - - /// @notice Withdraws the buy/sell fees of a given bonding curve. - /// @dev Only callable by the currently eligible fee recipient. - /// @param fundingManager The address of the bonding curve from which to withdraw fees. - /// @param to The address to which the fees are sent. - function withdrawPimFee(address fundingManager, address to) external; - - /// @notice Returns the address of the {OrchestratorFractory_v1}. - /// @return Address of the {OrchestratorFractory_v1}. - function orchestratorFactory() external view returns (address); -} diff --git a/src/factories/workflow-specific/PIM_WorkflowFactory_v1.sol b/src/factories/workflow-specific/PIM_WorkflowFactory_v1.sol deleted file mode 100644 index 99212117b..000000000 --- a/src/factories/workflow-specific/PIM_WorkflowFactory_v1.sol +++ /dev/null @@ -1,311 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.23; - -// Internal Interfaces -import {IOrchestratorFactory_v1} from - "src/factories/interfaces/IOrchestratorFactory_v1.sol"; -import {IOrchestrator_v1} from - "src/orchestrator/interfaces/IOrchestrator_v1.sol"; -import {IFM_BC_Bancor_Redeeming_VirtualSupply_v1} from - "@fm/bondingCurve/interfaces/IFM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; -import {IPIM_WorkflowFactory_v1} from - "src/factories/interfaces/IPIM_WorkflowFactory_v1.sol"; -import {IBondingCurveBase_v1} from - "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; - -// External Interfaces -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - -// External Implementations -import {ERC20Issuance_v1} from "src/external/token/ERC20Issuance_v1.sol"; - -// External Dependencies -import {Ownable2Step} from "@oz/access/Ownable2Step.sol"; -import {Ownable} from "@oz/access/Ownable.sol"; -import {ERC2771Context, Context} from "@oz/metatx/ERC2771Context.sol"; - -/** - * @title Inverter PIM Workflow Factory - * - * @notice Facilitates the creation and configuration and setup of PIM workflows, including the deployment of - * bonding curves and issuance tokens. It also provides functionalities for updating fee recipients and - * withdrawing fees from bonding curves. - * - * @dev An owned factory for deploying PIM workflows. Deploying the PIM workflow includes: - * - Deploying the bonding curve and issuance token. - * - Enable bonding curve to mint issuance token. - * - Transfer initial collateral supply from msg.sender to bonding curve and mint issuance token to recipient. - * - If applicable make first purchase. - * - If flag is set, renounce admin role by setting factory as admin. - * - * @custom:security-contact security@inverter.network - * For any security concerns or findings, please refer to our Security Policy at - * security.inverter.network or email us directly. - * - * @custom:author Inverter Network - */ -contract PIM_WorkflowFactory_v1 is - Ownable2Step, - ERC2771Context, - IPIM_WorkflowFactory_v1 -{ - //-------------------------------------------------------------------------- - // State Variables - - /// @dev store address of {Orchestratorfactory_v1}. - address public orchestratorFactory; - - /// @dev mapping of Funding Manager address to `feeRecipient` address. - mapping(address fundingManager => address feeRecipient) private - _pimFeeRecipients; - - //-------------------------------------------------------------------------- - // Modifiers - - /// @dev Modifier to guarantee the caller is the fee recipient for the given funding manager. - modifier onlyPimFeeRecipient(address fundingManager) { - if (_msgSender() != _pimFeeRecipients[fundingManager]) { - revert PIM_WorkflowFactory__OnlyPimFeeRecipient(); - } - _; - } - - //-------------------------------------------------------------------------- - // Constructor - - constructor( - address _orchestratorFactory, - address _owner, - address _trustedForwarder - ) Ownable(_owner) ERC2771Context(_trustedForwarder) { - orchestratorFactory = _orchestratorFactory; - } - - //-------------------------------------------------------------------------- - // Public Mutating Functions - - /// @inheritdoc IPIM_WorkflowFactory_v1 - function createPIMWorkflow( - IOrchestratorFactory_v1.WorkflowConfig memory workflowConfig, - IOrchestratorFactory_v1.ModuleConfig memory paymentProcessorConfig, - IOrchestratorFactory_v1.ModuleConfig[] memory moduleConfigs, - IPIM_WorkflowFactory_v1.PIMConfig memory PIMConfig - ) - external - returns (IOrchestrator_v1 orchestrator, ERC20Issuance_v1 issuanceToken) - { - // deploy issuance token - issuanceToken = new ERC20Issuance_v1( - PIMConfig.issuanceTokenParams.name, - PIMConfig.issuanceTokenParams.symbol, - PIMConfig.issuanceTokenParams.decimals, - PIMConfig.issuanceTokenParams.maxSupply - ); - issuanceToken.setMinter(address(this), true); - - // assemble fundingManager config, authorizer config and deploy orchestrator - IOrchestratorFactory_v1.ModuleConfig memory fundingManagerConfig = - IOrchestratorFactory_v1.ModuleConfig( - PIMConfig.fundingManagerMetadata, - abi.encode( - address(issuanceToken), - PIMConfig.bcProperties, - PIMConfig.collateralToken - ) - ); - IOrchestratorFactory_v1.ModuleConfig memory authorizerConfig = - IOrchestratorFactory_v1.ModuleConfig( - PIMConfig.authorizerMetadata, abi.encode(address(this)) - ); - orchestrator = IOrchestratorFactory_v1(orchestratorFactory) - .createOrchestrator( - workflowConfig, - fundingManagerConfig, - authorizerConfig, - paymentProcessorConfig, - moduleConfigs - ); - - // get bonding curve / funding manager - address fundingManager = address(orchestrator.fundingManager()); - - // enable bonding curve to mint issuance token - issuanceToken.setMinter(fundingManager, true); - - // transfer initial collateral supply from msg.sender to bonding curve and mint issuance token to recipient - _manageInitialSupplies( - IBondingCurveBase_v1(fundingManager), - IERC20(PIMConfig.collateralToken), - issuanceToken, - PIMConfig.bcProperties.initialCollateralSupply, - PIMConfig.bcProperties.initialIssuanceSupply, - PIMConfig.recipient, - PIMConfig.withInitialLiquidity - ); - - // if applicable make first purchase - _manageInitialPurchase( - IBondingCurveBase_v1(fundingManager), - IERC20(PIMConfig.collateralToken), - PIMConfig.firstCollateralIn, - PIMConfig.recipient - ); - - // disable factory to mint issuance token - issuanceToken.setMinter(address(this), false); - - // if isRenouncedToken flag is set burn owner role, else transfer ownership to specified admin - if (PIMConfig.isRenouncedIssuanceToken) { - _transferTokenOwnership(issuanceToken, address(0)); - } else { - _transferTokenOwnership(issuanceToken, PIMConfig.admin); - } - - // if isRenouncedWorkflow flag is set factory keeps admin rights over workflow, else transfer admin rights to - // specified admin - if (PIMConfig.isRenouncedWorkflow) { - // record the admin as fee recipient eligible to claim buy/sell fees - _pimFeeRecipients[fundingManager] = PIMConfig.admin; - } else { - _transferWorkflowAdminRights(orchestrator, PIMConfig.admin); - } - - emit IPIM_WorkflowFactory_v1.PIMWorkflowCreated( - fundingManager, - address(issuanceToken), - _msgSender(), - PIMConfig.recipient, - PIMConfig.isRenouncedIssuanceToken, - PIMConfig.isRenouncedWorkflow - ); - - return (orchestrator, issuanceToken); - } - - //-------------------------------------------------------------------------- - // Permissioned Functions - - /// @inheritdoc IPIM_WorkflowFactory_v1 - function withdrawPimFee(address fundingManager, address to) - external - onlyPimFeeRecipient(fundingManager) - { - uint amount = - IBondingCurveBase_v1(fundingManager).projectCollateralFeeCollected(); - IBondingCurveBase_v1(fundingManager).withdrawProjectCollateralFee( - to, amount - ); - emit IPIM_WorkflowFactory_v1.PimFeeClaimed(_msgSender(), amount); - } - - /// @inheritdoc IPIM_WorkflowFactory_v1 - function transferPimFeeEligibility(address fundingManager, address to) - external - onlyPimFeeRecipient(fundingManager) - { - _pimFeeRecipients[fundingManager] = to; - emit IPIM_WorkflowFactory_v1.PimFeeRecipientUpdated(_msgSender(), to); - } - - //-------------------------------------------------------------------------- - // Internal Functions - - function _manageInitialSupplies( - IBondingCurveBase_v1 fundingManager, - IERC20 collateralToken, - ERC20Issuance_v1 issuanceToken, - uint initialCollateralSupply, - uint initialIssuanceSupply, - address recipient, - bool withInitialLiquidity - ) private { - if (withInitialLiquidity) { - // collateral token is paid for by the msg.sender - collateralToken.transferFrom( - _msgSender(), address(fundingManager), initialCollateralSupply - ); - // issuance token is minted to the the specified recipient - issuanceToken.mint(recipient, initialIssuanceSupply); - } else { - // issuance token is minted to the burn address - issuanceToken.mint(address(0xDEAD), initialIssuanceSupply); - } - } - - function _manageInitialPurchase( - IBondingCurveBase_v1 fundingManager, - IERC20 collateralToken, - uint firstCollateralIn, - address recipient - ) private { - // transfer initial collateral amount from deployer to factory - collateralToken.transferFrom( - _msgSender(), address(this), firstCollateralIn - ); - - // set allowance for curve to spend factory's tokens - collateralToken.approve(address(fundingManager), firstCollateralIn); - - // make first purchase - IBondingCurveBase_v1(fundingManager).buyFor( - recipient, firstCollateralIn, 1 - ); - } - - function _transferTokenOwnership( - ERC20Issuance_v1 issuanceToken, - address newAdmin - ) private { - if (newAdmin == address(0)) { - issuanceToken.renounceOwnership(); - } else { - issuanceToken.transferOwnership(newAdmin); - } - } - - function _transferWorkflowAdminRights( - IOrchestrator_v1 orchestrator, - address newAdmin - ) private { - bytes32 adminRole = orchestrator.authorizer().getAdminRole(); - // if renounced flag is set, add zero address as admin (because workflow must have at least one admin set) - orchestrator.authorizer().grantRole(adminRole, newAdmin); - // and revoke admin role from factory - orchestrator.authorizer().revokeRole(adminRole, address(this)); - } - - //-------------------------------------------------------------------------- - // ERC2771 Context - - /// Needs to be overridden, because they are imported via the Ownable2Step as well. - function _msgSender() - internal - view - virtual - override(ERC2771Context, Context) - returns (address sender) - { - return ERC2771Context._msgSender(); - } - - /// Needs to be overridden, because they are imported via the Ownable2Step as well. - function _msgData() - internal - view - virtual - override(ERC2771Context, Context) - returns (bytes calldata) - { - return ERC2771Context._msgData(); - } - - function _contextSuffixLength() - internal - view - virtual - override(ERC2771Context, Context) - returns (uint) - { - return ERC2771Context._contextSuffixLength(); - } -} diff --git a/src/modules/authorizer/IAuthorizer_v1.sol b/src/modules/authorizer/IAuthorizer_v1.sol index 96675d979..d5d5e35a1 100644 --- a/src/modules/authorizer/IAuthorizer_v1.sol +++ b/src/modules/authorizer/IAuthorizer_v1.sol @@ -4,15 +4,245 @@ pragma solidity ^0.8.0; import {IAccessControlEnumerable} from "@oz/access/extensions/IAccessControlEnumerable.sol"; +/** + * @title Inverter Authorizer Interface + * + * @notice Provides the access control mechanism for managing roles and + * permissions across different modules within the Inverter Network, + * ensuring secure and controlled access to critical functionalities. + * + * @dev Inherits functionality from: + * - IAuthorizer_v1: Implementation interface. + * - Module_v1: Inverter network base module functionality. + * - AccessControlEnumerableUpgradeable: Access control functionality. + * + * Key features: + * + * - Role creation and management. This includes the ability to + * create roles, revoke roles, assigning and revoking role + * admins, which can add and remove role members. + * + * - Role-based access control. This includes the ability to grant + * roles access to functions that implement the permissioned + * modifier. Functions can also be set to public access by + * adding the public role to the function permissions. + * + * @custom:guide + * The following guide explains in detail how to use the key features + * of this module: + * + * - ROLE MANAGEMENT: + * - Roles: + * A role has the following properties: + * - A unique identifier (ID) + * - A label + * - A list of members + * - A associated admin role + * The id is a value assigned by the authorizer module and + * is used to reference the role in the different functions + * of the authorizer module. + * The label is a string that is emitted as an event when + * the role is created. It is used to make the role human + * readable in the frontend and has no practical use in the + * onchain live setup. + * The members are the addresses that inhabit the role. + * The admin role is the role that can add and remove new + * members to the role. + * + * - Native Roles: + * There are three native roles that are created with the + * authorizer module: + * - The default admin role + * - The public role + * - The burn admin role + * The default admin role is the role that is + * + * - Role Creation: + * A role can be created by calling the createRole function. + * The function takes the following parameters: + * - The role name + * - The role id of the role that will become the admin of + * the new role. + * - The addresses of the initial members of the new role. + * The rolename in this context refers to the label of the + * role and is therefor not referenceable onchain. + * The function can only be called by a permissioned address + * (See permissioned section below). + * + * - Role Labeling: + * The label of a role can be overwritten by calling the + * labelRole function. With this a new event is emitted, + * that signals the frontend that the label has been + * updated. + * The function can only be called by a permissioned address + * (See permissioned section below). + * + * - Role granting and revoking: + * A role can be granted to a address by calling the + * grantRole function. The function takes the following + * parameters: + * - The role id of the role to grant + * - The address to grant the role to + * The grantRole function can only be called by according + * admin of the role. + * A role can be revoked from an address by calling the + * revokeRole function. The function takes the following + * parameters: + * - The role id of the role to revoke + * - The address to revoke the role from + * The revokeRole function can only be called by according + * admin of the role. + * + * - Transferal and Burning of Admin Roles: + * The admin role of a role can be transferred by calling + * the transferAdminRole function. The function takes the + * following parameters: + * - The role id of the role to transfer the admin from + * - The role id of the role to transfer the admin to + * The transferAdminRole function can only be called by + * according admin of the role. + * The admin role can be burned by calling the + * burnRoleAdmin function. + * The function takes the following parameters: + * - The role id of the role to burn the admin from + * If the admin role is burned, then no members can be added + * or removed from a role anymore. + * Remmeber: This step is irreversible. + * + * - ROLE BASED ACCESS CONTROL: + * - Permissioned + * Most of the state altering functions in a workflow are + * permissioned functions. This means that only roles that + * have been granted the according function permission can + * call the function. The permissioned status is enforced + * by the `permissioned` modifier. + * Some of the native roles have special rights in this + * system. The default admin role can access every + * permissioned function regardless of wether the default + * admin role was granted the permission or not. If the + * public role is granted the permission to a function, then + * every caller can access the function, regardless of + * wether they inhabit a already added role or not. + * Note: As a workflow is intialized without any native + * permissions, some functions that could be perceived as + * "this should be publicly accessible" are not. Examples + * for this could be the "buy" and "sell" functions of some + * funding manager modules or the stake and unstake + * functions of the staking logic module. For these + * functions, the public role has to be added to the access + * of the respective function. + * + * - Adding access permissions + * Adding access permissions is done by calling the + * `addAccessPermission` function. This function takes the + * following parameters: + * - The contract for which the permission is added + * - The function selector of the target function + * - The role ID of the role that will receive the + * permission + * Example: Adding the role "BOUNTY_MANAGER" to the + * "createBounty" function of the "bountyManager" contract + * would look like this: + * authorizer.addAccessPermission( + * address(bountyManager), + * bountyManager.createBounty.selector, + * bountyManagerId); + * + * - Removing access permissions + * Removing access permissions is done by calling the + * `removeAccessPermission` function. This function takes + * the following parameters: + * - The contract for which the permission is removed + * - The function selector of the target function + * - The role ID of the role that will lose the permission + * Example: Removing the role "BOUNTY_MANAGER" from the + * "createBounty" function of the "bountyManager" contract + * would look like this: + * authorizer.removeAccessPermission( + * address(bountyManager), + * bountyManager.createBounty.selector, + * bountyManagerId); + * + * - Making a function public + * A function can be made public by calling the + * `addAccessPermission` function with the public role as + * target role. + * Example: Making the "buy" function of the funding manager + * contract public would look like this: + * authorizer.addAccessPermission( + * address(fundingManager), + * fundingManager.buy.selector, + * authorizer.PUBLIC_ROLE()); + * The public role can be removed in the same way as any + * other role. + * + * - MIXED UTILITY: + * - The createRoleAndAddAccessPermissions function + * This function is a convenience function that combines + * the creation of a new role and the adding of access + * permissions. It takes the following parameters: + * - The name of the role + * - The role id of the role that will become the admin of + * the new role. + * - The addresses of the initial members of the new role. + * - The addresses of the targets contracts. + * - The selectors of the functions. + * Note: The selectors of the functions are linked to the + * respective target contracts. As the selectors are passed + * as a 2 Dimensional array, the first dimension is coupled + * to target contract and the second one contains the + * actual selectors for that target contract. + * Example: The target contracts are the fundingManager at + * position 0 in the array and the logic module at position + * 1. The selectors therefor contain two arrays, one for + * position 0 and one for position 1. + * Example: authorizer.createRoleAndAddAccessPermissions( + * "newRole", + * authorizer.DEFAULT_ADMIN_ROLE(), + * [initialMember1, initialMember2], + * [fundingManager.address, logicModule.address], + * [ + * [ + * fundingManager.buy.selector, + * fundingManager.sell.selector + * ], + * [logicModule.execute.selector] + * ] + * ) + * + * @custom:security-contact security@inverter.network + * In case of any concerns or findings, please refer to + * our Security Policy at security.inverter.network or + * email us directly! + * + * @custom:version v1.1.0 + * + * @custom:inverter-standard-version v0.1.0 + * + * @author Inverter Network + */ interface IAuthorizer_v1 is IAccessControlEnumerable { - //-------------------------------------------------------------------------- + // ======================================================================== // Errors + /// @notice The provided initial admin address is invalid. + error Module__Authorizer__InvalidInitialAdmin(); + + /// @notice The provided role ID is the default admin role. + error Module__Authorizer__CannotModifyAdminRoleAccess(); + + /// @notice The provided role ID is not existing. + error Module__Authorizer__RoleIdNotExisting(); + + /// @notice The provided input length is not valid. + error Module__Authorizer__InvalidInputLength(); + /// @notice The function is only callable by an active Module. - /// @param module The address of the module. - error Module__Authorizer__NotActiveModule(address module); + /// @param module_ The address of the module. + error Module__Authorizer__NotActiveModule(address module_); - /// @notice The function is only callable if the Module is self-managing its roles. + /// @notice The function is only callable if the Module is self-managing + /// its roles. error Module__Authorizer__ModuleNotSelfManaged(); /// @notice There always needs to be at least one admin. @@ -21,99 +251,199 @@ interface IAuthorizer_v1 is IAccessControlEnumerable { /// @notice The orchestrator cannot own itself. error Module__Authorizer__OrchestratorCannotHaveAdminRole(); - /// @notice The provided initial admin address is invalid. - error Module__Authorizer__InvalidInitialAdmin(); + // ======================================================================== + // Events + + /// @notice Emits when a role is added to a function permission. + /// @param target_ The address of the target contract. + /// @param functionSelector_ The selector of the function. + /// @param roleId_ The ID of the role. + event AccessPermissionAdded( + address target_, bytes4 functionSelector_, bytes32 roleId_ + ); + + /// @notice Emits when a role is removed from a function permission. + /// @param target_ The address of the target contract. + /// @param functionSelector_ The selector of the function. + /// @param roleId_ The ID of the role. + event AccessPermissionRemoved( + address target_, bytes4 functionSelector_, bytes32 roleId_ + ); + + /// @notice Emits when a role is created. + /// @param roleId_ The ID of the role. + /// @param roleName The name of the role. + event RoleCreated(bytes32 roleId_, string roleName); + + /// @notice Emits when a role is labeled. + /// @param roleId_ The ID of the role. + /// @param newRoleName The new name of the role. + event RoleLabeled(bytes32 roleId_, string newRoleName); + + /// @notice Emits when a role admin is burned. + /// @param roleId_ The ID of the role for which the admin was burned. + event RoleAdminBurned(bytes32 roleId_); + + // ======================================================================== + // Public Getter Functions + + // ------------------------------------------------------------------------ + // Getter - Role Management + + /// @notice Returns the role ID of the admin role. + /// @return defaultAdminId_ The role ID of the default admin. + function getAdminRole() external view returns (bytes32 defaultAdminId_); + + // ------------------------------------------------------------------------ + // Getter - Authorization - //-------------------------------------------------------------------------- - // Functions - - /// @notice Checks whether an address holds the required role to execute - /// the current transaction. - /// @dev The calling contract needs to generate the right role ID using its - /// own address and the role identifier. - /// In modules, this function should be used instead of `hasRole`, as - /// there are Authorizer-specific checks that need to be performed. - /// @param role The identifier of the role we want to check - /// @param who The address on which to perform the check. - /// @return bool Returns if the address holds the role - function checkForRole(bytes32 role, address who) + /// @notice Returns the permissions of the given function in the target + /// contract. + /// @param target_ The address of the target contract. + /// @param selector_ The selector of the function. + /// @return permissions_ The roleIds that are permissioned to call the + /// function. + function getPermissions(address target_, bytes4 selector_) external view - returns (bool); + returns (bytes32[] memory permissions_); - /// @notice Helper function to generate a bytes32 role hash for a module role. - /// @param module The address of the module to generate the hash for. - /// @param role The ID number of the role to generate the hash for. - /// @return bytes32 Returns the generated role hash. - function generateRoleId(address module, bytes32 role) + /// @notice Returns the number of created role IDs. + /// @return lastAssignedRoleId_ The number of created role IDs. + function getLastAssignedRoleId() external - pure - returns (bytes32); - - /// @notice Used by a Module to grant a role to a user. - /// @param role The identifier of the role to grant. - /// @param target The address to which to grant the role. - function grantRoleFromModule(bytes32 role, address target) external; - - /// @notice Used by a Module to grant a role to a set of users. - /// @param role The identifier of the role to grant. - /// @param targets The addresses to which to grant the role. - function grantRoleFromModuleBatched( - bytes32 role, - address[] calldata targets - ) external; + view + returns (uint lastAssignedRoleId_); - /// @notice Used by a Module to revoke a role from a user. - /// @param role The identifier of the role to revoke. - /// @param target The address to revoke the role from. - function revokeRoleFromModule(bytes32 role, address target) external; - - /// @notice Used by a Module to revoke a role from a set of users. - /// @param role The identifier of the role to revoke. - /// @param targets The address to revoke the role from. - function revokeRoleFromModuleBatched( - bytes32 role, - address[] calldata targets - ) external; + /// @notice Returns whether the given roleId has the permission to call the + /// given function in the target contract. + /// @param target_ The address of the target contract. + /// @param selector_ The selector of the function. + /// @param roleId_ The roleId that we want to check. + /// @return isRolePermissioned_ Returns whether the roleId is permissioned + /// to call the function. + function isRolePermissioned( + address target_, + bytes4 selector_, + bytes32 roleId_ + ) external view returns (bool isRolePermissioned_); + + /// @notice Checks whether the given caller address holds the required role + /// to execute the given function in the target contract. + /// @dev Returns true if the address holds the Default Admin role. + /// Returns true if the function permissions contain the public + /// role. + /// @param caller_ The address of the caller. + /// @param target_ The address of the target contract. + /// @param selector_ The selector of the function. + /// @return hasPermission_ Returns if the address can call the function. + function hasPermission(address caller_, address target_, bytes4 selector_) + external + view + returns (bool hasPermission_); + + // ======================================================================== + // Mutating Functions + + // ------------------------------------------------------------------------ + // Mutating - Role Management + + /// @notice Creates a new role and adds initial members to it. + /// @dev Function access controlled by authorizer. + /// @dev The role of the admin has to be created already. + /// @param roleName_ The name of the role to create. + /// @param respectiveAdminRole_ The role ID of the admin role. + /// @param initialMembers_ The addresses of the initial members. + /// @return newRoleId_ The ID of the newly created role. + function createRole( + string memory roleName_, + bytes32 respectiveAdminRole_, + address[] memory initialMembers_ + ) external returns (bytes32 newRoleId_); + + /// @notice Changes the name of a role. + /// @dev Labels are emitted as events and are therefore not accessible + /// on-chain. + /// @dev Function access controlled by authorizer. + /// @dev The role has to be created already. + /// @param roleId_ The ID of the role to change the name of. + /// @param newRoleName_ The new name of the role. + function labelRole(bytes32 roleId_, string memory newRoleName_) external; /// @notice Transfer the admin rights to a given role. - /// @param roleId The role on which to peform the admin transfer. - /// @param newAdmin The new role to which to transfer admin access to. - function transferAdminRole(bytes32 roleId, bytes32 newAdmin) external; - - /// @notice Irreversibly burns the admin of a given role. - /// @param role The role to remove admin access from. - /// @dev The module itself can still grant and revoke it's own roles. This only burns third-party access to - /// the role. - function burnAdminFromModuleRole(bytes32 role) external; - - /// @notice Grants a global role to a target. - /// @param role The role to grant. - /// @param target The address to grant the role to. - /// @dev Only the addresses with the Admin role should be able to call this function. - function grantGlobalRole(bytes32 role, address target) external; - - /// @notice Grants a global role to a set of targets. - /// @param role The role to grant. - /// @param targets The addresses to grant the role to. - /// @dev Only the addresses with the Admin role should be able to call this function. - function grantGlobalRoleBatched(bytes32 role, address[] calldata targets) + /// @dev Only callable by the Admin of the role. + /// @dev The role has to be created already. + /// @dev The admin of the roleId_ can not be burned. + /// @param roleId_ The role on which to peform the admin transfer. + /// @param newAdminRoleId_ The new role to which to transfer admin + /// access to. + function transferAdminRole(bytes32 roleId_, bytes32 newAdminRoleId_) external; - /// @notice Revokes a global role from a target. - /// @param role The role to grant. - /// @param target The address to grant the role to. - /// @dev Only the addresses with the Admin role should be able to call this function. - function revokeGlobalRole(bytes32 role, address target) external; - - /// @notice Revokes a global role from a set of targets. - /// @param role The role to grant. - /// @param targets The addresses to grant the role to. - /// @dev Only the addresses with the Admin role should be able to call this function. - function revokeGlobalRoleBatched(bytes32 role, address[] calldata targets) - external; + /// @notice Burns the admin of the given roleId. + /// @dev Only callable by the Admin of the role. + /// @dev The role has to be created already. + /// @dev Does nothing if the admin was already burned. + /// @param roleId_ The role for which to burn the admin. + function burnRoleAdmin(bytes32 roleId_) external; - /// @notice Returns the role ID of the admin role. - /// @return The role ID. - function getAdminRole() external view returns (bytes32); + // ------------------------------------------------------------------------ + // Mutating - Authorization + + /// @notice Adds a new permission to the given roleId to call the given + /// function in the target contract. + /// @dev Function access controlled by authorizer. + /// @dev The roleId must have already been created. + /// @dev Does nothing if the roleId permission is already added to the + ///function. + /// @param target_ The address of the target contract. + /// @param selector_ The selector of the function. + /// @param roleId_ The roleId that will receive the permission. + function addAccessPermission( + address target_, + bytes4 selector_, + bytes32 roleId_ + ) external; + + /// @notice Removes a permission from the given roleid to call the given + /// function in the target contract. + /// @dev Function access controlled by authorizer. + /// @dev Does nothing if the roleId is not linked to the function. + /// @param target_ The address of the target contract. + /// @param selector_ The selector of the function. + /// @param roleId_ The roleId to remove. + function removeAccessPermission( + address target_, + bytes4 selector_, + bytes32 roleId_ + ) external; + + // ------------------------------------------------------------------------ + // Mutating - Mixed Utility + + /// @notice Creates a new role, adds initial members to it and adds + /// permission to call to the respective functions. + /// @dev Function access controlled by authorizer. + /// @dev The role of the admin has to be created already. + /// @dev The array of targets corresponds with the two dimensional array + /// of selectors. The first position of targets therefore is + /// assigned to the first position of the selector array. The + /// second dimension of the selector array contains all the + /// function selectors of the target, which get the newly created + /// role added as a permissioned role. + /// @dev The target contracts array must have the same length as the + /// selector array. + /// @param roleName_ The name of the role to create. + /// @param respectiveAdminRole_ The role ID of the admin role. + /// @param initialMembers_ The addresses of the initial members. + /// @param targets_ The addresses of the target contracts. + /// @param selectors_ The selectors of the functions. + /// @return newRoleId_ The ID of the newly created role. + function createRoleAndAddAccessPermissions( + string memory roleName_, + bytes32 respectiveAdminRole_, + address[] memory initialMembers_, + address[] memory targets_, + bytes4[][] memory selectors_ + ) external returns (bytes32 newRoleId_); } diff --git a/src/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.sol b/src/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.sol index c3c910b03..fe9168f26 100644 --- a/src/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.sol +++ b/src/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.sol @@ -6,75 +6,75 @@ import {IModule_v1} from "src/modules/base/IModule_v1.sol"; import {IOrchestrator_v1} from "src/orchestrator/interfaces/IOrchestrator_v1.sol"; import {IAUT_EXT_VotingRoles_v1} from - "src/modules/authorizer/role/interfaces/IAUT_EXT_VotingRoles_v1.sol"; + "src/modules/authorizer/extensions/interfaces/IAUT_EXT_VotingRoles_v1.sol"; // Internal Dependencies import {ERC165Upgradeable, Module_v1} from "src/modules/base/Module_v1.sol"; /** * @title Inverter Voting Role Manager * - * @notice Facilitates voting and motion management within the Inverter Network, - * allowing designated voters to participate in governance through proposals, - * voting, and execution of decisions. + * @notice Facilitates voting and motion management within the Inverter + * Network, allowing designated voters to participate in governance + * through proposals, voting, and execution of decisions. * - * @dev Supports setting thresholds for decision-making, managing voter lists, - * creating motions, casting votes, and executing actions based on collective - * decisions. This structure enhances governance transparency and efficacy. + * @dev Supports setting thresholds for decision-making, managing voter + * lists, creating _motions, casting votes, and executing actions + * based on collective decisions. This structure enhances governance + * transparency and efficacy. * * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer to our Security Policy - * at security.inverter.network or email us directly! + * In case of any concerns or findings, please refer to + * our Security Policy at security.inverter.network or + * email us directly! * * @author Inverter Network */ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) + function supportsInterface(bytes4 interfaceId_) public view virtual override(Module_v1) - returns (bool) + returns (bool isInterfaceId_) { - return interfaceId == type(IAUT_EXT_VotingRoles_v1).interfaceId - || super.supportsInterface(interfaceId); + return interfaceId_ == type(IAUT_EXT_VotingRoles_v1).interfaceId + || super.supportsInterface(interfaceId_); } - //-------------------------------------------------------------------------- + //========================================================================== // Modifiers - /// @dev Reverts if caller is not the module itself. + /// @notice Reverts if caller is not the module itself. modifier onlySelf() { if (_msgSender() != address(this)) { - revert Module__CallerNotAuthorized( - bytes32("onlySelf"), _msgSender() - ); + revert Module__VotingRoleManager__OnlySelfCallAllowed(); } _; } - /// @dev Reverts if caller is not a voter. + /// @notice Reverts if caller is not a voter. modifier onlyVoter() { - if (!isVoter[_msgSender()]) { + if (!_isVoter[_msgSender()]) { revert Module__VotingRoleManager__CallerNotVoter(); } _; } - /// @dev Reverts if voter address is invalid. - /// @param voter The address to check. - modifier isValidVoterAddress(address voter) { + /// @notice Reverts if voter address is invalid. + /// @param voter_ The address to check. + modifier isValidVoterAddress(address voter_) { if ( - voter == address(0) || voter == address(this) - || voter == address(orchestrator()) + voter_ == address(0) || voter_ == address(this) + || voter_ == address(orchestrator()) ) { revert Module__VotingRoleManager__InvalidVoterAddress(); } _; } - //-------------------------------------------------------------------------- + //========================================================================== // Constants /// @inheritdoc IAUT_EXT_VotingRoles_v1 @@ -83,48 +83,48 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { /// @inheritdoc IAUT_EXT_VotingRoles_v1 uint public constant MIN_VOTING_DURATION = 1 days; - //-------------------------------------------------------------------------- + //========================================================================== // Storage - /// @inheritdoc IAUT_EXT_VotingRoles_v1 - mapping(address => bool) public isVoter; + /// @notice Mapping that stores if an address is a voter. + mapping(address voter => bool isVoter) internal _isVoter; - /// @inheritdoc IAUT_EXT_VotingRoles_v1 - mapping(bytes32 => Motion) public motions; + /// @notice Mapping that stores the motions. + mapping(bytes32 motionId => Motion motion) internal _motions; - /// @inheritdoc IAUT_EXT_VotingRoles_v1 - uint public motionCount; + /// @notice The counter for motions. + uint internal _motionCount; - /// @inheritdoc IAUT_EXT_VotingRoles_v1 - uint public voterCount; + /// @notice The counter for voters. + uint internal _voterCount; - /// @inheritdoc IAUT_EXT_VotingRoles_v1 - uint public threshold; + /// @notice The threshold for motions. + uint internal _threshold; - /// @inheritdoc IAUT_EXT_VotingRoles_v1 - uint public voteDuration; + /// @notice The duration for voting. + uint internal _voteDuration; /// @dev Storage gap for future upgrades. uint[50] private __gap; - //-------------------------------------------------------------------------- + //========================================================================== // Initialization /// @inheritdoc Module_v1 function init( IOrchestrator_v1 orchestrator_, - Metadata memory metadata, - bytes memory configData + Metadata memory metadata_, + bytes memory configData_ ) external override initializer { - __Module_init(orchestrator_, metadata); + __Module_init(orchestrator_, metadata_); // Decode configData to list of voters, the required threshold, and the // voting duration. address[] memory voters; - uint threshold_; - uint voteDuration_; - (voters, threshold_, voteDuration_) = - abi.decode(configData, (address[], uint, uint)); + uint threshold; + uint voteDuration; + (voters, threshold, voteDuration) = + abi.decode(configData_, (address[], uint, uint)); uint votersLen = voters.length; @@ -133,13 +133,13 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { revert Module__VotingRoleManager__EmptyVoters(); } - // Revert if the threshold is set incorrectly - _validateThreshold(votersLen, threshold_); + // Revert if the threshold is set incorrectly. + _validateThreshold(votersLen, threshold); // Revert if votingDuration outside of bounds. if ( - voteDuration_ < MIN_VOTING_DURATION - || voteDuration_ > MAX_VOTING_DURATION + voteDuration < MIN_VOTING_DURATION + || voteDuration > MAX_VOTING_DURATION ) { revert Module__VotingRoleManager__InvalidVotingDuration(); } @@ -156,184 +156,232 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { revert Module__VotingRoleManager__InvalidVoterAddress(); } - if (isVoter[voter]) { + if (_isVoter[voter]) { revert Module__VotingRoleManager__IsAlreadyVoter(); } - isVoter[voter] = true; + _isVoter[voter] = true; emit VoterAdded(voter); } // Write count of voters to storage. - voterCount = votersLen; + _voterCount = votersLen; // Write threshold to storage. - threshold = threshold_; - emit ThresholdUpdated(0, threshold_); + _threshold = threshold; + emit ThresholdUpdated(0, threshold); - // Write voteDuration to storage. - voteDuration = voteDuration_; - emit VoteDurationUpdated(0, voteDuration_); + // Write _voteDuration to storage. + _voteDuration = voteDuration; + emit VoteDurationUpdated(0, voteDuration); + } + + //========================================================================== + // Getter Functions + + //-------------------------------------------------------------------------- + // Getter - State Access Functions + + /// @inheritdoc IAUT_EXT_VotingRoles_v1 + function isVoter(address who_) external view returns (bool isVoter_) { + return _isVoter[who_]; + } + + /// @inheritdoc IAUT_EXT_VotingRoles_v1 + function getMotion(bytes32 id_) + external + view + returns ( + address target_, + bytes memory action_, + uint startTimestamp_, + uint endTimestamp_, + uint requiredThreshold_, + uint forVotes_, + uint againstVotes_, + uint abstainVotes_, + uint executedAt_, + bool executionResult_, + bytes memory executionReturnData_ + ) + { + Motion storage motion_ = _motions[id_]; + + return ( + motion_.target, + motion_.action, + motion_.startTimestamp, + motion_.endTimestamp, + motion_.requiredThreshold, + motion_.forVotes, + motion_.againstVotes, + motion_.abstainVotes, + motion_.executedAt, + motion_.executionResult, + motion_.executionReturnData + ); + } + + /// @inheritdoc IAUT_EXT_VotingRoles_v1 + function getMotionCount() external view returns (uint motionCount_) { + return _motionCount; + } + + /// @inheritdoc IAUT_EXT_VotingRoles_v1 + function getVoterCount() external view returns (uint voterCount_) { + return _voterCount; + } + + /// @inheritdoc IAUT_EXT_VotingRoles_v1 + function getThreshold() external view returns (uint threshold_) { + return _threshold; + } + + /// @inheritdoc IAUT_EXT_VotingRoles_v1 + function getVoteDuration() external view returns (uint voteDuration_) { + return _voteDuration; } //-------------------------------------------------------------------------- // Data Retrieval Functions /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function getReceipt(bytes32 _ID, address voter) + function getReceipt(bytes32 id_, address voter_) public view - returns (Receipt memory) + returns (Receipt memory receipt_) { - Receipt memory _r = motions[_ID].receipts[voter]; + Receipt memory r = _motions[id_].receipts[voter_]; - return (_r); + return (r); } + //========================================================================== + // Mutating Functions + //-------------------------------------------------------------------------- - // Configuration Functions + // Mutating - Configuration Functions /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function setThreshold(uint newThreshold) public onlySelf { - // Revert if the threshold is set incorrectly - _validateThreshold(voterCount, newThreshold); + function setThreshold(uint newThreshold_) public onlySelf { + // Revert if the threshold is set incorrectly. + _validateThreshold(_voterCount, newThreshold_); - emit ThresholdUpdated(threshold, newThreshold); - threshold = newThreshold; + emit ThresholdUpdated(_threshold, newThreshold_); + _threshold = newThreshold_; } /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function setVotingDuration(uint newVoteDuration) external onlySelf { + function setVotingDuration(uint newVoteDuration_) external onlySelf { // Revert if votingDuration outside of bounds. if ( - newVoteDuration < MIN_VOTING_DURATION - || newVoteDuration > MAX_VOTING_DURATION + newVoteDuration_ < MIN_VOTING_DURATION + || newVoteDuration_ > MAX_VOTING_DURATION ) { revert Module__VotingRoleManager__InvalidVotingDuration(); } - emit VoteDurationUpdated(voteDuration, newVoteDuration); - voteDuration = newVoteDuration; + emit VoteDurationUpdated(_voteDuration, newVoteDuration_); + _voteDuration = newVoteDuration_; } //-------------------------------------------------------------------------- - // Voter Management Functions + // Mutating - Voter Management Functions /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function addVoter(address who) public onlySelf isValidVoterAddress(who) { - if (!isVoter[who]) { - isVoter[who] = true; - unchecked { - ++voterCount; - } - emit VoterAdded(who); + function addVoter(address who_) public onlySelf isValidVoterAddress(who_) { + if (!_isVoter[who_]) { + _addVoter(who_); + // Validate threshold after adding voter. + _validateThreshold(_voterCount, _threshold); } } /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function addVoterAndUpdateThreshold(address who, uint newThreshold) + function addVoterAndUpdateThreshold(address who_, uint newThreshold_) external { - // Add the new voter - addVoter(who); - - // Set the new threshold (also validates it) - setThreshold(newThreshold); + if (!_isVoter[who_]) { + // Add the new voter. + _addVoter(who_); + } + // Set the new threshold (also validates it). + setThreshold(newThreshold_); } /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function removeVoter(address who) public onlySelf { - _removeVoter(who); + function removeVoter(address who_) public onlySelf { + _removeVoter(who_); - // Revert if the threshold would be invalid after this - _validateThreshold(voterCount, threshold); + // Revert if the threshold would be invalid after this. + _validateThreshold(_voterCount, _threshold); } /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function removeVoterAndUpdateThreshold(address who, uint newThreshold) + function removeVoterAndUpdateThreshold(address who_, uint newThreshold_) external onlySelf { - _removeVoter(who); - - // Set the new threshold (also validates it) - setThreshold(newThreshold); - } + _removeVoter(who_); - //-------------------------------------------------------------------------- - // Internal Functions - - /// @dev Removes a voter from the list of voters. - /// @param who The address of the voter to remove. - function _removeVoter(address who) internal { - // Revert if trying to remove the last voter - if (voterCount == 1) { - revert Module__VotingRoleManager__EmptyVoters(); - } - - if (isVoter[who]) { - delete isVoter[who]; - unchecked { - --voterCount; - } - emit VoterRemoved(who); - } + // Set the new threshold (also validates it). + setThreshold(newThreshold_); } //-------------------------------------------------------------------------- - // Governance Functions + // Mutating - Governance Functions /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function createMotion(address target, bytes calldata action) + function createMotion(address target_, bytes calldata action_) external onlyVoter - returns (bytes32) + returns (bytes32 motionId_) { // Cache motion's id. bytes32 motionId = - keccak256(abi.encodePacked(target, action, motionCount)); + keccak256(abi.encodePacked(target_, action_, _motionCount)); // Get pointer to motion. // Note that the motion instance is uninitialized. - Motion storage motion_ = motions[motionId]; + Motion storage motion_ = _motions[motionId]; // Initialize motion. - motion_.target = target; - motion_.action = action; + motion_.target = target_; + motion_.action = action_; motion_.startTimestamp = block.timestamp; - motion_.endTimestamp = block.timestamp + voteDuration; - motion_.requiredThreshold = threshold; + motion_.endTimestamp = block.timestamp + _voteDuration; + motion_.requiredThreshold = _threshold; emit MotionCreated(motionId); // Increase the motion count. unchecked { - ++motionCount; + ++_motionCount; } return motionId; } /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function castVote(bytes32 motionId, uint8 support) external onlyVoter { + function castVote(bytes32 motionId_, uint8 support_) external onlyVoter { // Revert if support invalid. - // 0 = for - // 1 = against - // 2 = abstain - if (support > 2) { + // - 0 = for + // - 1 = against + // - 2 = abstain + if (support_ > 2) { revert Module__VotingRoleManager__InvalidSupport(); } // Get pointer to the motion. - Motion storage motion_ = motions[motionId]; + Motion storage motion_ = _motions[motionId_]; - // Revert if motionID invalid + // Revert if motionID invalid. if (motion_.startTimestamp == 0) { revert Module__VotingRoleManager__InvalidMotionId(); } - // Revert if voting duration exceeded + // Revert if voting duration exceeded. if (block.timestamp > motion_.endTimestamp) { revert Module__VotingRoleManager__MotionVotingPhaseClosed(); } @@ -343,15 +391,15 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { revert Module__VotingRoleManager__AttemptedDoubleVote(); } - if (support == 0) { + if (support_ == 0) { unchecked { ++motion_.forVotes; } - } else if (support == 1) { + } else if (support_ == 1) { unchecked { ++motion_.againstVotes; } - } else if (support == 2) { + } else if (support_ == 2) { unchecked { ++motion_.abstainVotes; } @@ -359,15 +407,15 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { address voter = _msgSender(); - motion_.receipts[voter] = Receipt(true, support); + motion_.receipts[voter] = Receipt(true, support_); - emit VoteCast(motionId, voter, support); + emit VoteCast(motionId_, voter, support_); } /// @inheritdoc IAUT_EXT_VotingRoles_v1 - function executeMotion(bytes32 motionId) external { + function executeMotion(bytes32 motionId_) external { // Get pointer to the motion. - Motion storage motion_ = motions[motionId]; + Motion storage motion_ = _motions[motionId_]; // Revert if motionId invalid. if (motion_.startTimestamp == 0) { @@ -379,7 +427,7 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { revert Module__VotingRoleManager__MotionInVotingPhase(); } - // Revert if necessary threshold was not reached + // Revert if necessary threshold was not reached. if (motion_.forVotes < motion_.requiredThreshold) { revert Module__VotingRoleManager__ThresholdNotReached(); } @@ -389,7 +437,7 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { revert Module__VotingRoleManager__MotionAlreadyExecuted(); } - // Updating executedAt here to prevent reentrancy + // Updating executedAt here to prevent reentrancy. motion_.executedAt = block.timestamp; // Execute `action` on `target`. @@ -401,25 +449,54 @@ contract AUT_EXT_VotingRoles_v1 is IAUT_EXT_VotingRoles_v1, Module_v1 { motion_.executionResult = result; motion_.executionReturnData = returnData; - emit MotionExecuted(motionId); + emit MotionExecuted(motionId_); } - //-------------------------------------------------------------------------- - // Internal + //========================================================================== + // Internal Functions + + /// @notice Removes a voter from the list of voters. + /// @param who_ The address of the voter to remove. + function _removeVoter(address who_) internal { + // Revert if trying to remove the last voter. + if (_voterCount == 1) { + revert Module__VotingRoleManager__EmptyVoters(); + } + + if (_isVoter[who_]) { + delete _isVoter[who_]; + unchecked { + --_voterCount; + } + emit VoterRemoved(who_); + } + } - /// @dev Internal function to validate the threshold. - /// @param _voters The number of voters. - /// @param _threshold The threshold. - function _validateThreshold(uint _voters, uint _threshold) internal pure { - // Revert if one of these conditions is met + /// @notice Internal function to validate the threshold. + /// @param voters_ The number of voters. + /// @param threshold_ The threshold. + function _validateThreshold(uint voters_, uint threshold_) internal pure { + // Revert if one of these conditions is met: // - Threshold is higher than the amount of voters // - There are less than 3 voters and the threshold is set to 0 // - There are 3 or more voters and the threshold is less than 2 if ( - _threshold > _voters || (_voters >= 3 && _threshold < 2) - || (_voters < 3 && _threshold == 0) + threshold_ > voters_ || (voters_ >= 3 && threshold_ < 2) + || (voters_ < 3 && threshold_ == 0) ) { revert Module__VotingRoleManager__InvalidThreshold(); } } + + /// @notice Internal function to add a voter to the list of voters. + /// @dev This function does not validate the threshold. + /// @param voter_ The address of the voter to add. + function _addVoter(address voter_) internal { + _isVoter[voter_] = true; + unchecked { + ++_voterCount; + } + + emit VoterAdded(voter_); + } } diff --git a/src/modules/authorizer/extensions/interfaces/IAUT_EXT_VotingRoles_v1.sol b/src/modules/authorizer/extensions/interfaces/IAUT_EXT_VotingRoles_v1.sol new file mode 100644 index 000000000..62de56948 --- /dev/null +++ b/src/modules/authorizer/extensions/interfaces/IAUT_EXT_VotingRoles_v1.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +interface IAUT_EXT_VotingRoles_v1 { + // ======================================================================== + // Structs + + /// @notice A motion is a proposal to execute an action on a target + /// contract. + /// @param target The address of the contract to execute the action on. + /// @param action The action data to execute on the target contract. + /// @param startTimestamp The timestamp at which the motion starts. + /// @param endTimestamp The timestamp at which the motion ends. + /// @param requiredThreshold The required threshold of votes to pass the + /// motion. + /// @param forVotes The number of votes in favor of the motion. + /// @param againstVotes The number of votes against the motion. + /// @param abstainVotes The number of votes abstaining from the motion. + /// @param receipts The receipts of votes for the motion address. + /// @param executedAt The timestamp at which the motion was executed. + /// @param executionResult The result of the execution. + /// @param executionReturnData The return data of the execution. + struct Motion { + address target; + bytes action; + uint startTimestamp; + uint endTimestamp; + uint requiredThreshold; + uint forVotes; + uint againstVotes; + uint abstainVotes; + mapping(address => Receipt) receipts; + uint executedAt; + bool executionResult; + bytes executionReturnData; + } + + /// @notice A receipt is a vote cast for a motion. + /// @param hasVoted Whether the voter has already voted. + /// @param support The value that indicates wether the voter supports the + /// motion. + struct Receipt { + bool hasVoted; + uint8 support; + } + // ======================================================================== + // Errors + + /// @notice This function is only callable by a motion of this contract. + error Module__VotingRoleManager__OnlySelfCallAllowed(); + + /// @notice The action would leave an empty voter list. + error Module__VotingRoleManager__EmptyVoters(); + + /// @notice The supplied voter address is invalid. + error Module__VotingRoleManager__InvalidVoterAddress(); + + /// @notice The threshold cannot exceed the amount of voters. + /// or be too low to be considered safe. + error Module__VotingRoleManager__InvalidThreshold(); + + /// @notice The supplied voting duration is invalid. + error Module__VotingRoleManager__InvalidVotingDuration(); + + /// @notice The function can only be called by a voter. + error Module__VotingRoleManager__CallerNotVoter(); + + /// @notice The address is already a voter. + error Module__VotingRoleManager__IsAlreadyVoter(); + + /// @notice The value given as vote is invalid. + error Module__VotingRoleManager__InvalidSupport(); + + /// @notice The supplied ID is referencing a motion that doesn't exist. + error Module__VotingRoleManager__InvalidMotionId(); + + /// @notice A user cannot vote twice. + error Module__VotingRoleManager__AttemptedDoubleVote(); + + /// @notice A motion cannot be executed if the voting duration hasn't + /// passed. + error Module__VotingRoleManager__MotionInVotingPhase(); + + /// @notice A motion cannot be voted on if the duration has been exceeded. + error Module__VotingRoleManager__MotionVotingPhaseClosed(); + + /// @notice A motion cannot be executed twice. + error Module__VotingRoleManager__MotionAlreadyExecuted(); + + /// @notice A motion cannot be executed if it didn't reach the threshold. + error Module__VotingRoleManager__ThresholdNotReached(); + + // ======================================================================== + // Events + + /// @notice Event emitted when a new voter address gets added. + /// @param who_ The added address. + event VoterAdded(address indexed who_); + + /// @notice Event emitted when a voter address gets removed. + /// @param who_ The removed address. + event VoterRemoved(address indexed who_); + + /// @notice Event emitted when the required threshold changes. + /// @param oldThreshold_ The old threshold. + /// @param newThreshold_ The new threshold. + event ThresholdUpdated(uint oldThreshold_, uint newThreshold_); + + /// @notice Event emitted when the voting duration changes. + /// @param oldVotingDuration_ The old voting duration. + /// @param newVotingDuration_ The new voting duration. + event VoteDurationUpdated(uint oldVotingDuration_, uint newVotingDuration_); + + /// @notice Event emitted when a motion is created. + /// @param motionId_ The motion ID. + event MotionCreated(bytes32 indexed motionId_); + + /// @notice Event emitted when a vote is cast for a motion. + /// @param motionId_ The motion ID. + /// @param voter_ The address of a voter. + /// @param support_ Value that indicates how the voter supports the motion. + event VoteCast( + bytes32 indexed motionId_, + address indexed voter_, + uint8 indexed support_ + ); + + /// @notice Event emitted when a motion is executed. + /// @param motionId_ The motion ID. + event MotionExecuted(bytes32 indexed motionId_); + + // ======================================================================== + // Public Getter Functions + + //-------------------------------------------------------------------------- + // Getter - Constants + + /// @notice The maximum voting duration. + /// @return maxVotingDuration_ The maximum voting duration. + function MAX_VOTING_DURATION() + external + view + returns (uint maxVotingDuration_); + + /// @notice The minimum voting duration. + /// @return minVotingDuration_ The minimum voting duration. + function MIN_VOTING_DURATION() + external + view + returns (uint minVotingDuration_); + + //-------------------------------------------------------------------------- + // Getter - State Access Functions + + /// @notice Checks whether an address is a voter. + /// @param who_ The address to check. + /// @return isVoter_ Whether the address is a voter. + function isVoter(address who_) external view returns (bool isVoter_); + + /// @notice Gets the motion data. + /// @param motionId_ The ID of the motion. + /// @return target_ The address of the contract to execute the action on. + /// @return action_ The action data to execute on the target contract. + /// @return startTimestamp_ The timestamp at which the motion starts. + /// @return endTimestamp_ The timestamp at which the motion ends. + /// @return requiredThreshold_ The required threshold of votes to pass the + /// motion. + /// @return forVotes_ The number of votes in favor of the motion. + /// @return againstVotes_ The number of votes against the motion. + /// @return abstainVotes_ The number of votes abstaining from the motion. + /// @return executedAt_ The timestamp at which the motion was executed. + /// @return executionResult_ The result of the execution. + /// @return executionReturnData_ The return data of the execution. + function getMotion(bytes32 motionId_) + external + view + returns ( + address target_, + bytes memory action_, + uint startTimestamp_, + uint endTimestamp_, + uint requiredThreshold_, + uint forVotes_, + uint againstVotes_, + uint abstainVotes_, + uint executedAt_, + bool executionResult_, + bytes memory executionReturnData_ + ); + + /// @notice Gets the number of motions. + /// @return motionCount_ The number of motions. + function getMotionCount() external view returns (uint motionCount_); + + /// @notice Gets the number of voters. + /// @return voterCount_ The number of voters. + function getVoterCount() external view returns (uint voterCount_); + + /// @notice Gets the threshold. + /// @return threshold_ The threshold. + function getThreshold() external view returns (uint threshold_); + + /// @notice Gets the voting duration. + /// @return voteDuration_ The voting duration. + function getVoteDuration() external view returns (uint voteDuration_); + + /// @notice Gets the receipt of a voter for a motion. + /// @param id_ The ID of the motion. + /// @param voter_ The address of the voter. + /// @return receipt_ The receipt of the voter. + function getReceipt(bytes32 id_, address voter_) + external + view + returns (Receipt memory receipt_); + + //========================================================================== + // Mutating Functions + + //-------------------------------------------------------------------------- + // Mutating - Configuration Functions + + /// @notice Sets the threshold. + /// @param newThreshold_ The new threshold. + function setThreshold(uint newThreshold_) external; + + /// @notice Sets the voting duration. + /// @param newVoteDuration_ The new voting duration. + function setVotingDuration(uint newVoteDuration_) external; + + //-------------------------------------------------------------------------- + // Mutating - Voter Management Functions + + /// @notice Adds a voter. + /// @dev Beware that adding a voter has implications for already + /// existing motions and might change how easy a threshold can be + /// reached / a motion can be executed. + /// @param who_ The address to add. + function addVoter(address who_) external; + + /// @notice Adds a voter and updates the threshold. + /// @dev Beware that adding a voter has implications for already + /// existing motions and might change how easy a threshold can be + /// reached / a motion can be executed. + /// @param who_ The address to add. + /// @param newThreshold_ The new threshold. + function addVoterAndUpdateThreshold(address who_, uint newThreshold_) + external; + + /// @notice Removes a voter. + /// @dev Beware that removing a voter has implications for already + /// existing motions and might change how easy a threshold can be + /// reached / a motion can be executed. This can even lead to a + /// threshold in which a motion can't be executed anymore. + /// @param who_ The address to remove. + function removeVoter(address who_) external; + + /// @notice Removes a voter and updates the threshold. + /// @dev Beware that removing a voter has implications for already + /// existing motions and might change how easy a threshold can be + /// reached / a motion can be executed. This can even lead to a + /// threshold in which a motion can't be executed anymore. + /// @param who_ The address to remove. + /// @param newThreshold_ The new threshold. + function removeVoterAndUpdateThreshold(address who_, uint newThreshold_) + external; + + //-------------------------------------------------------------------------- + // Mutating - Governance Functions + + /// @notice Creates a motion. + /// @param target_ The address of the contract to execute the action on. + /// @param action_ The action data to execute on the target contract. + /// @return motionId_ The ID of the created motion. + function createMotion(address target_, bytes calldata action_) + external + returns (bytes32 motionId_); + + /// @notice Casts a vote for a motion. + /// @param motionId_ The ID of the motion. + /// @param support_ The value that indicates wether the voter supports the + /// motion. + function castVote(bytes32 motionId_, uint8 support_) external; + + /// @notice Executes a motion. + /// @param motionId_ The ID of the motion. + function executeMotion(bytes32 motionId_) external; +} diff --git a/src/modules/authorizer/role/AUT_Roles_v1.sol b/src/modules/authorizer/role/AUT_Roles_v1.sol index 420736f50..d42d76d9a 100644 --- a/src/modules/authorizer/role/AUT_Roles_v1.sol +++ b/src/modules/authorizer/role/AUT_Roles_v1.sol @@ -23,305 +23,608 @@ import {AccessControlEnumerableUpgradeable} from /** * @title Inverter Roles Authorizer * - * @notice Provides a robust access control mechanism for managing roles and permissions - * across different modules within the Inverter Network, ensuring secure and - * controlled access to critical functionalities. + * @notice Provides the access control mechanism for managing roles and + * permissions across different modules within the Inverter Network, + * ensuring secure and controlled access to critical functionalities. + * + * @dev Inherits functionality from: + * - IAuthorizer_v1: Implementation interface. + * - Module_v1: Inverter network base module functionality. + * - AccessControlEnumerableUpgradeable: Access control functionality. + * + * Key features: + * - Role creation and management. This includes the ability to + * create roles, revoke roles, assigning and revoking role + * admins, which can add and remove role members. + * - Role-based access control. This includes the ability to grant + * roles access to functions that implement the permissioned + * modifier. Functions can also be set to public access by + * adding the public role to the function permissions. + * + * @custom:guide + * The following guide explains in detail how to use the key features + * of this module: + * + * - ROLE MANAGEMENT: + * - Roles: + * A role has the following properties: + * - A unique identifier (ID) + * - A label + * - A list of members + * - A associated admin role + * The id is a value assigned by the authorizer module and + * is used to reference the role in the different functions + * of the authorizer module. + * The label is a string that is emitted as an event when + * the role is created. It is used to make the role human + * readable in the frontend and has no practical use in the + * onchain live setup. + * The members are the addresses that inhabit the role. + * The admin role is the role that can add and remove new + * members to the role. + * + * - Native Roles: + * There are three native roles that are created with the + * authorizer module: + * - The default admin role + * - The public role + * - The burn admin role + * The default admin role is the role that is + * + * - Role Creation: + * A role can be created by calling the createRole function. + * The function takes the following parameters: + * - The role name + * - The role id of the role that will become the admin of + * the new role. + * - The addresses of the initial members of the new role. + * The rolename in this context refers to the label of the + * role and is therefor not referenceable onchain. + * The function can only be called by a permissioned address + * (See permissioned section below). + * + * - Role Labeling: + * The label of a role can be overwritten by calling the + * labelRole function. With this a new event is emitted, + * that signals the frontend that the label has been + * updated. + * The function can only be called by a permissioned address + * (See permissioned section below). + * + * - Role granting and revoking: + * A role can be granted to a address by calling the + * grantRole function. The function takes the following + * parameters: + * - The role id of the role to grant + * - The address to grant the role to + * The grantRole function can only be called by according + * admin of the role. + * A role can be revoked from an address by calling the + * revokeRole function. The function takes the following + * parameters: + * - The role id of the role to revoke + * - The address to revoke the role from + * The revokeRole function can only be called by according + * admin of the role. + * + * - Transferal and Burning of Admin Roles: + * The admin role of a role can be transferred by calling + * the transferAdminRole function. The function takes the + * following parameters: + * - The role id of the role to transfer the admin from + * - The role id of the role to transfer the admin to + * The transferAdminRole function can only be called by + * according admin of the role. + * The admin role can be burned by calling the + * burnRoleAdmin function. + * The function takes the following parameters: + * - The role id of the role to burn the admin from + * If the admin role is burned, then no members can be added + * or removed from a role anymore. + * Remmeber: This step is irreversible. + * + * - ROLE BASED ACCESS CONTROL: + * - Permissioned + * Most of the state altering functions in a workflow are + * permissioned functions. This means that only roles that + * have been granted the according function permission can + * call the function. The permissioned status is enforced + * by the `permissioned` modifier. + * Some of the native roles have special rights in this + * system. The default admin role can access every + * permissioned function regardless of wether the default + * admin role was granted the permission or not. If the + * public role is granted the permission to a function, then + * every caller can access the function, regardless of + * wether they inhabit a already added role or not. + * Note: As a workflow is intialized without any native + * permissions, some functions that could be perceived as + * "this should be publicly accessible" are not. Examples + * for this could be the "buy" and "sell" functions of some + * funding manager modules or the stake and unstake + * functions of the staking logic module. For these + * functions, the public role has to be added to the access + * of the respective function. + * + * - Adding access permissions + * Adding access permissions is done by calling the + * `addAccessPermission` function. This function takes the + * following parameters: + * - The contract for which the permission is added + * - The function selector of the target function + * - The role ID of the role that will receive the + * permission + * Example: Adding the role "BOUNTY_MANAGER" to the + * "createBounty" function of the "bountyManager" contract + * would look like this: + * authorizer.addAccessPermission( + * address(bountyManager), + * bountyManager.createBounty.selector, + * bountyManagerId); + * + * - Removing access permissions + * Removing access permissions is done by calling the + * `removeAccessPermission` function. This function takes + * the following parameters: + * - The contract for which the permission is removed + * - The function selector of the target function + * - The role ID of the role that will lose the permission + * Example: Removing the role "BOUNTY_MANAGER" from the + * "createBounty" function of the "bountyManager" contract + * would look like this: + * authorizer.removeAccessPermission( + * address(bountyManager), + * bountyManager.createBounty.selector, + * bountyManagerId); + * + * - Making a function public + * A function can be made public by calling the + * `addAccessPermission` function with the public role as + * target role. + * Example: Making the "buy" function of the funding manager + * contract public would look like this: + * authorizer.addAccessPermission( + * address(fundingManager), + * fundingManager.buy.selector, + * authorizer.PUBLIC_ROLE()); + * The public role can be removed in the same way as any + * other role. + * + * - MIXED UTILITY: + * - The createRoleAndAddAccessPermissions function + * This function is a convenience function that combines + * the creation of a new role and the adding of access + * permissions. It takes the following parameters: + * - The name of the role + * - The role id of the role that will become the admin of + * the new role. + * - The addresses of the initial members of the new role. + * - The addresses of the targets contracts. + * - The selectors of the functions. + * Note: The selectors of the functions are linked to the + * respective target contracts. As the selectors are passed + * as a 2 Dimensional array, the first dimension is coupled + * to target contract and the second one contains the + * actual selectors for that target contract. + * Example: The target contracts are the fundingManager at + * position 0 in the array and the logic module at position + * 1. The selectors therefor contain two arrays, one for + * position 0 and one for position 1. + * Example: authorizer.createRoleAndAddAccessPermissions( + * "newRole", + * authorizer.DEFAULT_ADMIN_ROLE(), + * [initialMember1, initialMember2], + * [fundingManager.address, logicModule.address], + * [ + * [ + * fundingManager.buy.selector, + * fundingManager.sell.selector + * ], + * [logicModule.execute.selector] + * ] + * ) * - * @dev Extends {AccessControlEnumerableUpgradeable} and integrates with {Module_v1} to - * offer fine-grained access control through role-based permissions. Utilizes - * ERC2771 for meta-transactions to enhance module interaction experiences. * * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer to our Security Policy - * at security.inverter.network or email us directly! + * In case of any concerns or findings, please refer to + * our Security Policy at security.inverter.network or + * email us directly! + * + * @custom:version v1.1.0 + * + * @custom:inverter-standard-version v0.1.0 * * @author Inverter Network */ contract AUT_Roles_v1 is IAuthorizer_v1, - AccessControlEnumerableUpgradeable, - Module_v1 + Module_v1, + AccessControlEnumerableUpgradeable { /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) + function supportsInterface(bytes4 interfaceId_) public view virtual override(Module_v1, AccessControlEnumerableUpgradeable) - returns (bool) + returns (bool isInterfaceId_) { - return interfaceId == type(IAuthorizer_v1).interfaceId - || super.supportsInterface(interfaceId); + return interfaceId_ == type(IAuthorizer_v1).interfaceId + || super.supportsInterface(interfaceId_); } - //-------------------------------------------------------------------------- - // Storage - /// @notice The role that is used as a placeholder for a burned admin role. - bytes32 public constant BURN_ADMIN_ROLE = - 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - - /// @dev Storage gap for future upgrades. - uint[50] private __gap; - - //-------------------------------------------------------------------------- + // ======================================================================== // Modifiers - /// @dev Verifies that the caller is an active module. - /// @param module The address of the module. - modifier onlyModule(address module) { - if (!orchestrator().isModule(module)) { - revert Module__Authorizer__NotActiveModule(module); + /// @notice Verifies that the roleId is not the default admin role. + /// @param roleId_ The id of the role. + modifier idNotDefaultAdmin(bytes32 roleId_) { + if (roleId_ == DEFAULT_ADMIN_ROLE) { + revert Module__Authorizer__CannotModifyAdminRoleAccess(); } _; } - /// @dev Verifies that the admin being removed is not the last one. - /// @param role The id number of the role. - modifier notLastAdmin(bytes32 role) { - if ( - role == DEFAULT_ADMIN_ROLE - && getRoleMemberCount(DEFAULT_ADMIN_ROLE) <= 1 - ) { - revert Module__Authorizer__AdminRoleCannotBeEmpty(); + /// @notice Verifies that the roleId is already existing. + /// @param roleId_ The id of the role. + modifier idExists(bytes32 roleId_) { + // If the given roleId is greater than the last assigned roleId, then + // it is not existing. + if (uint(roleId_) > _lastAssignedRoleId) { + revert Module__Authorizer__RoleIdNotExisting(); } _; } - /// @dev Verifies that the admin being added is not the {Orchestrator_v1}. - /// @param role The id number of the role. - /// @param who The user we want to check on. - modifier noSelfAdmin(bytes32 role, address who) { - if (role == DEFAULT_ADMIN_ROLE && who == address(orchestrator())) { - revert Module__Authorizer__OrchestratorCannotHaveAdminRole(); - } - _; - } + // ======================================================================== + // Storage - //-------------------------------------------------------------------------- + /// @notice The public role. + bytes32 public constant PUBLIC_ROLE = bytes32(uint(1)); + + /// @notice The burned admin role. + bytes32 public constant BURN_ADMIN_ROLE = + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + /// @notice Mapping that stores the role IDs that can be used to call + /// functions on a target contract. + /// @dev target The address of the target contract. + /// @dev selector The function selector of the function to call. + /// @dev roleIds The role IDs that can be used to call the function. + mapping(address target => mapping(bytes4 selector => bytes32[] roleIds)) + internal _permissions; + + /// @notice The counter for role IDs. + /// @dev This is used to generate unique role IDs for each role. + /// @dev Starts at 1, which symbolizes two roles: PUBLIC_ROLE and + /// DEFAULT_ADMIN_ROLE, but is immediately incremented when a role + /// is created. + uint internal _lastAssignedRoleId; + + /// @dev Storage gap for future upgrades. + uint[50] private __gap; + + // ======================================================================== // Initialization /// @inheritdoc Module_v1 function init( IOrchestrator_v1 orchestrator_, - Metadata memory metadata, - bytes memory configData + Metadata memory metadata_, + bytes memory configData_ ) external override initializer { - __Module_init(orchestrator_, metadata); + __Module_init(orchestrator_, metadata_); - (address initialAdmin) = abi.decode(configData, (address)); + (address initialAdmin) = abi.decode(configData_, (address)); __RoleAuthorizer_init(initialAdmin); } /// @notice Initializes the role authorizer. - /// @param initialAdmin The initial admin of the role authorizer. - function __RoleAuthorizer_init(address initialAdmin) + /// @param initialAdmin_ The initial admin of the role authorizer. + function __RoleAuthorizer_init(address initialAdmin_) internal onlyInitializing { - if (initialAdmin == address(0)) { + if (initialAdmin_ == address(0)) { revert Module__Authorizer__InvalidInitialAdmin(); } - // Note about DEFAULT_ADMIN_ROLE: The Admin of the workflow holds the DEFAULT_ADMIN_ROLE, and has admin - // privileges on all Modules in the contract. - // It is defined in the AccessControl contract and identified with bytes32("0x00") - // Modules can opt out of this on a per-role basis by setting the admin role to "BURN_ADMIN_ROLE". + // Start with 1 to account for the two native roles: + // DEFAULT_ADMIN_ROLE at 0 and PUBLIC_ROLE at 1. + _lastAssignedRoleId = 1; + + // Note about DEFAULT_ADMIN_ROLE: + // The admin of the workflow holds the DEFAULT_ADMIN_ROLE, and has + // admin privileges on all modules in the contract. + // It is defined in the AccessControl contract and identified with + // bytes32("0x00"). + // Modules can opt out of this on a per-role basis by setting the admin + // role to "BURN_ADMIN_ROLE". - // make the BURN_ADMIN_ROLE immutable + // make the BURN_ADMIN_ROLE immutable. _setRoleAdmin(BURN_ADMIN_ROLE, BURN_ADMIN_ROLE); // set the initial admin as the DEFAULT_ADMIN_ROLE - _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin); + _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin_); } - //-------------------------------------------------------------------------- - // Public functions + // ======================================================================== + // Public Getter Functions + + // ------------------------------------------------------------------------ + // Getter - Role Management /// @inheritdoc IAuthorizer_v1 - function checkForRole(bytes32 role, address who) - external - view - virtual - returns (bool) - { - return hasRole(role, who); + function getAdminRole() external pure returns (bytes32 defaultAdminId_) { + return DEFAULT_ADMIN_ROLE; } + // ------------------------------------------------------------------------ + // Getter - Authorization + /// @inheritdoc IAuthorizer_v1 - function generateRoleId(address module, bytes32 role) - public - pure - returns (bytes32) + function getPermissions(address target_, bytes4 selector_) + external + view + virtual + returns (bytes32[] memory permissions_) { - // Generate Role ID from module and role - return keccak256(abi.encodePacked(module, role)); + permissions_ = _permissions[target_][selector_]; } /// @inheritdoc IAuthorizer_v1 - function grantRoleFromModule(bytes32 role, address target) + function getLastAssignedRoleId() external - onlyModule(_msgSender()) + view + returns (uint lastAssignedRoleId_) { - bytes32 roleId = generateRoleId(_msgSender(), role); - _grantRole(roleId, target); + lastAssignedRoleId_ = _lastAssignedRoleId; } /// @inheritdoc IAuthorizer_v1 - function grantRoleFromModuleBatched( - bytes32 role, - address[] calldata targets - ) external onlyModule(_msgSender()) { - bytes32 roleId = generateRoleId(_msgSender(), role); - for (uint i = 0; i < targets.length; i++) { - _grantRole(roleId, targets[i]); + function isRolePermissioned( + address target_, + bytes4 selector_, + bytes32 roleId_ + ) public view virtual returns (bool isRolePermissioned_) { + bytes32[] memory permissions_ = _permissions[target_][selector_]; + for (uint i = 0; i < permissions_.length; i++) { + if (permissions_[i] == roleId_) { + return true; + } } + return false; } /// @inheritdoc IAuthorizer_v1 - function revokeRoleFromModule(bytes32 role, address target) + function hasPermission(address caller_, address target_, bytes4 selector_) external - onlyModule(_msgSender()) + view + virtual + returns (bool hasPermission_) { - bytes32 roleId = generateRoleId(_msgSender(), role); - _revokeRole(roleId, target); + // If caller is the admin, they can call any function. + if (hasRole(DEFAULT_ADMIN_ROLE, caller_)) { + return true; + } + + bytes32[] memory roleIds = _permissions[target_][selector_]; + uint permissionLength = roleIds.length; + + // If there are no roles, the caller cannot call the function. + if (permissionLength == 0) { + return false; + } + + // Go through each role and check if the caller has permission. + for (uint i = 0; i < permissionLength; i++) { + if ( + // Return true if the role is the public role + // or if the caller has the role. + roleIds[i] == PUBLIC_ROLE || hasRole(roleIds[i], caller_) + ) { + return true; + } + } + // Caller does not have any of the roles, so they cannot call the + // function. + return false; } + // ======================================================================== + // Mutating Functions + + // ------------------------------------------------------------------------ + // Mutating - Role Management + /// @inheritdoc IAuthorizer_v1 - function revokeRoleFromModuleBatched( - bytes32 role, - address[] calldata targets - ) external onlyModule(_msgSender()) { - bytes32 roleId = generateRoleId(_msgSender(), role); - for (uint i = 0; i < targets.length; i++) { - _revokeRole(roleId, targets[i]); + function createRole( + string memory roleName_, + bytes32 respectiveAdminRole_, + address[] memory initialMembers_ + ) + public + virtual + permissioned + idExists(respectiveAdminRole_) + returns (bytes32 newRoleId_) + { + newRoleId_ = bytes32(++_lastAssignedRoleId); + + emit RoleCreated(newRoleId_, roleName_); + + _setRoleAdmin(newRoleId_, respectiveAdminRole_); + + uint length = initialMembers_.length; + for (uint i = 0; i < length; i++) { + _grantRole(newRoleId_, initialMembers_[i]); } } /// @inheritdoc IAuthorizer_v1 - function transferAdminRole(bytes32 roleId, bytes32 newAdmin) + function labelRole(bytes32 roleId_, string memory newRoleName_) external - onlyRole(getRoleAdmin(roleId)) + permissioned + idExists(roleId_) { - _setRoleAdmin(roleId, newAdmin); + emit RoleLabeled(roleId_, newRoleName_); } /// @inheritdoc IAuthorizer_v1 - function burnAdminFromModuleRole(bytes32 role) + function transferAdminRole(bytes32 roleId_, bytes32 newAdminRoleId_) external - onlyModule(_msgSender()) + onlyRole(getRoleAdmin(roleId_)) + idExists(roleId_) + idExists(newAdminRoleId_) { - bytes32 roleId = generateRoleId(_msgSender(), role); - _setRoleAdmin(roleId, BURN_ADMIN_ROLE); + _setRoleAdmin(roleId_, newAdminRoleId_); } /// @inheritdoc IAuthorizer_v1 - function grantGlobalRole(bytes32 role, address target) + function burnRoleAdmin(bytes32 roleId_) external - onlyRole(DEFAULT_ADMIN_ROLE) + onlyRole(getRoleAdmin(roleId_)) + idExists(roleId_) { - bytes32 roleId = generateRoleId(address(orchestrator()), role); - _grantRole(roleId, target); + // Burn admin from the role. + _setRoleAdmin(roleId_, BURN_ADMIN_ROLE); + emit RoleAdminBurned(roleId_); } + // ------------------------------------------------------------------------ + // Mutating - Authorization + /// @inheritdoc IAuthorizer_v1 - function grantGlobalRoleBatched(bytes32 role, address[] calldata targets) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - bytes32 roleId = generateRoleId(address(orchestrator()), role); - for (uint i = 0; i < targets.length; i++) { - _grantRole(roleId, targets[i]); + function addAccessPermission( + address target_, + bytes4 selector_, + bytes32 roleId_ + ) public permissioned idNotDefaultAdmin(roleId_) idExists(roleId_) { + // if RoleId already has a permission, do nothing. + if (isRolePermissioned(target_, selector_, roleId_)) { + return; } + + _permissions[target_][selector_].push(roleId_); + emit AccessPermissionAdded(target_, selector_, roleId_); } /// @inheritdoc IAuthorizer_v1 - function revokeGlobalRole(bytes32 role, address target) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - bytes32 roleId = generateRoleId(address(orchestrator()), role); - _revokeRole(roleId, target); + function removeAccessPermission( + address target_, + bytes4 selector_, + bytes32 roleId_ + ) public permissioned { + bytes32[] memory permissions = _permissions[target_][selector_]; + uint permissionsLength = permissions.length; + + for (uint i = 0; i < permissionsLength; i++) { + if (permissions[i] == roleId_) { + // Replace the element to be removed with the last one. + _permissions[target_][selector_][i] = + _permissions[target_][selector_][permissionsLength - 1]; + // Remove the last element. + _permissions[target_][selector_].pop(); + + // Emit Event and exit the function once the value is removed. + emit AccessPermissionRemoved(target_, selector_, roleId_); + return; + } + } + // Do nothing if the value is not found. } + // ------------------------------------------------------------------------ + // Mutating - Mixed Utility + /// @inheritdoc IAuthorizer_v1 - function revokeGlobalRoleBatched(bytes32 role, address[] calldata targets) + function createRoleAndAddAccessPermissions( + string memory roleName_, + bytes32 respectiveAdminRole_, + address[] memory initialMembers_, + address[] memory targets_, + bytes4[][] memory selectors_ + ) external - onlyRole(DEFAULT_ADMIN_ROLE) + permissioned + idExists(respectiveAdminRole_) + returns (bytes32 newRoleId_) { - bytes32 roleId = generateRoleId(address(orchestrator()), role); - for (uint i = 0; i < targets.length; i++) { - _revokeRole(roleId, targets[i]); + uint targetsLength = targets_.length; + if (targetsLength != selectors_.length) { + revert Module__Authorizer__InvalidInputLength(); } - } - /// @inheritdoc IAuthorizer_v1 - function getAdminRole() public pure returns (bytes32) { - return DEFAULT_ADMIN_ROLE; - } + newRoleId_ = + createRole(roleName_, respectiveAdminRole_, initialMembers_); - //-------------------------------------------------------------------------- - // Overloaded and overridden functions + // Run through all target and selector combinations and add permission + // to role id. - /// @notice Overrides {_revokeRole} to prevent having an empty `ADMIN` role. - /// @param role The id number of the role. - /// @param who The user we want to check on. - /// @return bool Returns if revoke has been succesful. - function _revokeRole(bytes32 role, address who) - internal - virtual - override - notLastAdmin(role) - returns (bool) - { - return super._revokeRole(role, who); + for (uint i = 0; i < targetsLength; i++) { + for (uint j = 0; j < selectors_[i].length; j++) { + addAccessPermission(targets_[i], selectors_[i][j], newRoleId_); + } + } } - /// @notice Overrides {_grantRole} to prevent having the {Orchestrator_v1} having the `OWNER` role. - /// @param role The id of the role. - /// @param who The user we want to check on. - /// @return bool Returns if grant has been succesful. - function _grantRole(bytes32 role, address who) + // ======================================================================== + // Internal Functions + + // ------------------------------------------------------------------------ + // Internal - Upstream Function Implementations + + /// @notice Overrides {_grantRole} to make sure only existing roles can be + /// granted. + /// @param role_ The id of the role. + /// @param who_ The user we want to check on. + /// @return success_ Returns if grant has been successful. + function _grantRole(bytes32 role_, address who_) internal virtual override - noSelfAdmin(role, who) - returns (bool) + idExists(role_) + returns (bool success_) { - return super._grantRole(role, who); + return super._grantRole(role_, who_); } //-------------------------------------------------------------------------- - // ERC2771 Context Upgradeable + // Internal - ERC2771 Context Upgradeable - /// Needs to be overridden, because they are imported via the AccessControlEnumerableUpgradeable as well. + /// @dev Needs to be overridden, because they are imported via the + /// AccessControlEnumerableUpgradeable as well. function _msgSender() internal view virtual override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) + returns (address sender_) { return ERC2771ContextUpgradeable._msgSender(); } - /// Needs to be overridden, because they are imported via the AccessControlEnumerableUpgradeable as well. + /// @dev Needs to be overridden, because they are imported via the + /// AccessControlEnumerableUpgradeable as well. function _msgData() internal view virtual override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) + returns (bytes calldata msgData_) { return ERC2771ContextUpgradeable._msgData(); } + /// @dev Needs to be overridden, because they are imported via the + /// AccessControlEnumerableUpgradeable as well. function _contextSuffixLength() internal view virtual override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (uint) + returns (uint contextSuffixLength_) { return ERC2771ContextUpgradeable._contextSuffixLength(); } diff --git a/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol b/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol index bbd9e1881..c15fa3bd7 100644 --- a/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol +++ b/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol @@ -21,275 +21,428 @@ import {AccessControlUpgradeable} from import {AccessControlEnumerableUpgradeable} from "@oz-up/access/extensions/AccessControlEnumerableUpgradeable.sol"; +/** + * @title Token Interface + * + * @notice This Interface is an abstraction of token based contracts that is + * referenced in the Token-Gated Role Authorizer. + * + * @dev It only contains the balanceOf function, which should be + * implemented by any of the following token contracts and their + * derivatives: + * - ERC20 + * - ERC721 + * This interface is used to ensure that the token-gated role + * authorizer can be used with any token contract that implements + * the balanceOf function. + * + * @author Inverter Network + */ interface TokenInterface { - function balanceOf(address _owner) external view returns (uint balance); + /// @notice Returns the balance of the given address. + /// @param owner_ The address to check the balance of. + /// @return balance_ The balance of the given address. + function balanceOf(address owner_) external view returns (uint balance_); } /** * @title Inverter Token-Gated Role Authorizer * - * @notice Extends the Inverter's role-based access control to include token gating, - * enabling roles to be conditionally assigned based on token ownership. - * This mechanism allows for dynamic permissioning tied to specific token - * holdings. + * @notice Extends the Inverter's role-based access control to include token + * gating, enabling roles to be conditionally assigned based on token + * ownership. This mechanism allows for dynamic permissioning tied to + * specific token holdings. + * + * @dev Inherits functionality from: + * - {IAUT_TokenGated_Roles_v1}: Implementation interface. + * - {AUT_Roles_v1}: Inverter's role-based access control. + * + * Key feeatures: + * - Token-based access checks before role assignment. + * - Supports both {ERC20} and {ERC721} tokens. * - * @dev Builds on {AUT_Roles_v1} by integrating token-based access checks before - * role assignment. Utilizes checks on token balances to gate access, - * supporting both {ERC20} and {ERC721} tokens as qualifiers for role eligibility. + * @custom:guide + * The following guide explains in detail how to use the key features + * of this module: + * + * - TOKEN BASED ACCESS CONTROL: + * - Token Gated Role: + * With this contract it is possible to extend the base + * functionality of the {AUT_Roles_v1} contract to make a + * role token gated. A token gated role behaves in all + * respects like a regular role, but handles the membership + * of that role differently. A member of a token gated role + * is only allowed to access the role functionalities if + * they hold a certain amount of a token. + * The implementation of this contract uses a few tricks to + * achieve this. Without going into too much detail, this + * is the main part that is needed to understand the basic + * mechanism: + * In the contract, token gating is implemented by storing + * the token address in the role’s members property, instead + * of directly listing user addresses. This setup allows the + * contract to check the token balance of a user against a + * defined threshold when access is requested. + * Example: We want to restrict a role to users who hold a + * certain amount of Token A. Therefore, we configure the + * role to be token-gated and set a required token amount + * the user needs to hold as threshold. When we then call + * 'grantRole' with the address of Token A, the role becomes + * accessible only to users whose wallet holds at least the + * specified amount of that token. + * + * - Making a role token gated: + * Making a role token gated is done by calling the + * `setTokenGated` function. This function takes the + * following parameters: + * - The role id of the role that we change the token gated + * status of. + * - The boolean that indicates if the role should be token + * gated or not. + * This function can only be called by a permissioned address + * (See permissioned section in the {AUT_Roles_v1} contract). + * Also the role can not contain any members, when it is + * switched to and from token gated. + * Example: Making the role "Whitelisted" token gated would + * look like this: + * authorizer.setTokenGated(whitelistedRoleId, true); + * + * - Setting the token threshold: + * Setting the token threshold needed to pass the token gate + * is done by calling the setTokenThreshold function. This + * function takes the following parameters: + * - The role id of the role to set the threshold for. + * - The address of the token to set the threshold for. + * - The threshold value to set. + * This function can only be called by a permissioned address + * (See permissioned section in the {AUT_Roles_v1} contract). + * This function can be called anytime, even if the role is + * not token gated yet. + * Example: Setting the threshold for the token "USDC" to + * 100 would look like this: + * authorizer.setTokenThreshold( + * whitelistedRoleId, USDC, 100); + * + * - Adding a token to the token gate: + * Adding a token to the token gate is done by calling the + * grantRole function. This function takes the following + * parameters: + * - The role id of the role to grant. + * - The address of the token to grant the role to. + * The grantRole function can only be called by according + * admin of the role. In addition, the given address needs + * to be a contract, already have a threshold set and + * contain the balanceOf function. + * If the role is not token gated then grantRole will + * behave like the regular grantRole function. + * Example: Adding a token gate to the token gated role + * "Whitelisted" would look like this: + * authorizer.grantRole(whitelistedRoleId, address(USDC)); + * + * - Removing a token from the token gate: + * Removing a token from the token gate is done by calling + * the revokeRole function. This function behaves like the + * regular revokeRole function, except that it sets the + * threshold for the role and token combination to 0 as + * well. + * Example: Removing the token from the role "Whitelisted" + * would look like this: + * authorizer.revokeRole(whitelistedRoleId, address(USDC)); + * + * - Reversing a token gate: + * In case the token gated status of a role needs to be + * reverted, the setTokenGated function can be used. The + * same restrictions as for the setTokenGated function apply + * here as well (see above). + * Example: Reversing the token gated status of the role + * "Whitelisted" would look like this: + * authorizer.setTokenGated(whitelistedRoleId, false); * * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer to our Security Policy - * at security.inverter.network or email us directly! + * In case of any concerns or findings, please refer to + * our Security Policy at security.inverter.network or + * email us directly! + * + * @custom:version v1.0.0 + * + * @custom:inverter-standard-version v0.1.0 * * @author Inverter Network */ contract AUT_TokenGated_Roles_v1 is IAUT_TokenGated_Roles_v1, AUT_Roles_v1 { /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) + function supportsInterface(bytes4 interfaceId_) public view virtual override(AUT_Roles_v1) - returns (bool) + returns (bool isInterfaceId_) { - return interfaceId == type(IAUT_TokenGated_Roles_v1).interfaceId - || super.supportsInterface(interfaceId); + return interfaceId_ == type(IAUT_TokenGated_Roles_v1).interfaceId + || super.supportsInterface(interfaceId_); } /* - * This Module expands on the AUT_Roles_v1 by adding the possibility to set a role as "Token-Gated" - * Instead of whitelisting a user address, the whitelisted addresses will correspond to a token address, and on - * authorization the contract will check on ownership of one of the specifed tokens. + * This Module expands on the AUT_Roles_v1 by adding the possibility to set + * a role as "Token-Gated". Instead of whitelisting a user address, the + * whitelisted addresses will correspond to a token address, and on + * authorization the contract will check on ownership of one of the specifed + * tokens. */ - //-------------------------------------------------------------------------- + // ======================================================================== // Modifiers - /// @dev Modifier to guarantee function is only callable when the role is empty. - /// @param roleId The ID of the role to be checked. - modifier onlyEmptyRole(bytes32 roleId) { - // Check that the role is empty - if (getRoleMemberCount(roleId) != 0) { + /// @notice Modifier to guarantee function is only callable when the role is + /// empty. + /// @param roleId_ The ID of the role to be checked. + modifier onlyEmptyRole(bytes32 roleId_) { + // Check that the role is empty. + if (getRoleMemberCount(roleId_) != 0) { revert Module__AUT_TokenGated_Roles__RoleNotEmpty(); } _; } - /// @dev Modifier to guarantee function is only callable when the role is token-gated. - /// @param roleId The ID of the role to be checked. - modifier onlyTokenGated(bytes32 roleId) { - if (!isTokenGated[roleId]) { + /// @notice Modifier to guarantee that the role is not the public role. + /// @param roleId_ The ID of the role to be checked. + modifier notPublicRole(bytes32 roleId_) { + if (PUBLIC_ROLE == roleId_) { + revert Module__AUT_TokenGated_Roles__RoleIsPublic(); + } + _; + } + + /// @notice Modifier to guarantee function is only callable when the role + /// is token-gated. + /// @param roleId_ The ID of the role to be checked. + modifier onlyTokenGated(bytes32 roleId_) { + if (!_isTokenGated[roleId_]) { revert Module__AUT_TokenGated_Roles__RoleNotTokenGated(); } _; } - /// @dev Modifier to guarantee function is only callable when the threshold is valid. - /// @param threshold The threshold to be checked. - modifier validThreshold(uint threshold) { - // Since base ERC721 does not have a total/max supply, we can only enforce that the value should be non-zero - if (threshold == 0) { - revert Module__AUT_TokenGated_Roles__InvalidThreshold(threshold); + /// @notice Modifier to guarantee function is only callable when the + /// threshold is valid. + /// @param threshold_ The threshold to be checked. + modifier validThreshold(uint threshold_) { + // Since base ERC721 does not have a total/max supply, we can only + // enforce that the value should be non-zero. + if (threshold_ == 0) { + revert Module__AUT_TokenGated_Roles__InvalidThreshold(threshold_); } _; } - //-------------------------------------------------------------------------- + // ======================================================================== // Storage /// @dev Stores if a role is token gated. - mapping(bytes32 => bool) public isTokenGated; + mapping(bytes32 => bool) internal _isTokenGated; /// @dev Stores the threshold amount for each token in a role. - mapping(bytes32 => uint) public thresholdMap; + mapping(bytes32 => uint) internal _thresholdMap; /// @dev Storage gap for future upgrades. uint[50] private __gap; - //-------------------------------------------------------------------------- - // View functions + // ======================================================================== + // Public Getter Functions /// @inheritdoc IAUT_TokenGated_Roles_v1 - function hasTokenRole(bytes32 role, address who) + function isTokenGated(bytes32 roleId_) external view - onlyTokenGated(role) - returns (bool) + returns (bool isTokenGated_) { - return _hasTokenRole(role, who); + return _isTokenGated[roleId_]; } /// @inheritdoc IAUT_TokenGated_Roles_v1 - function getThresholdValue(bytes32 roleId, address token) - public + function hasTokenRole(bytes32 roleId_, address who_) + external view - returns (uint) + onlyTokenGated(roleId_) + returns (bool hasRole_) { - bytes32 thresholdId = keccak256(abi.encodePacked(roleId, token)); - return thresholdMap[thresholdId]; + return _hasTokenRole(roleId_, who_); } - //-------------------------------------------------------------------------- - // State-altering functions - /// @inheritdoc IAUT_TokenGated_Roles_v1 - function makeRoleTokenGatedFromModule(bytes32 role) + function getThresholdValue(bytes32 roleId_, address token_) public - onlyModule(_msgSender()) - onlyEmptyRole(generateRoleId(_msgSender(), role)) + view + returns (uint threshold_) { - bytes32 roleId = generateRoleId(_msgSender(), role); - - isTokenGated[roleId] = true; - emit ChangedTokenGating(roleId, true); - } - - /// @inheritdoc IAUT_TokenGated_Roles_v1 - function grantTokenRoleFromModule( - bytes32 role, - address token, - uint threshold - ) external onlyModule(_msgSender()) { - bytes32 roleId = generateRoleId(_msgSender(), role); - _setThreshold(roleId, token, threshold); - _grantRole(roleId, token); + bytes32 thresholdId = keccak256(abi.encodePacked(roleId_, token_)); + return _thresholdMap[thresholdId]; } - /// @inheritdoc IAUT_TokenGated_Roles_v1 - function setThresholdFromModule(bytes32 role, address token, uint threshold) - public - onlyModule(_msgSender()) - { - bytes32 roleId = generateRoleId(_msgSender(), role); - _setThreshold(roleId, token, threshold); - } + // ======================================================================== + // Mutating Functions - //-------------------------------------------------------------------------- - // Setters for the Admin + // ------------------------------------------------------------------------ + // Mutating - TokenGated Settings /// @inheritdoc IAUT_TokenGated_Roles_v1 - function setTokenGated(bytes32 role, bool to) + function setTokenGated(bytes32 roleId_, bool to_) public - onlyRole(getRoleAdmin(role)) - onlyEmptyRole(role) + permissioned + idExists(roleId_) + onlyEmptyRole(roleId_) + notPublicRole(roleId_) { - isTokenGated[role] = to; - emit ChangedTokenGating(role, to); + _isTokenGated[roleId_] = to_; + emit ChangedTokenGating(roleId_, to_); } /// @inheritdoc IAUT_TokenGated_Roles_v1 - function setThreshold(bytes32 roleId, address token, uint threshold) + function setThreshold(bytes32 roleId_, address token_, uint threshold_) public - onlyRole(getRoleAdmin(roleId)) + permissioned + idExists(roleId_) { - _setThreshold(roleId, token, threshold); + _setThreshold(roleId_, token_, threshold_); } //-------------------------------------------------------------------------- // Overloaded and overridden functions + /// @inheritdoc IAccessControl + /// @notice In case the role is token gated, it will check if {who_} holds a + /// balance above the threshold for at least one of the required + ///tokens. + /// @param roleId_ The id number of the role. + /// @param who_ The user we want to check on. + /// @return hasRole_ Returns if the account has the role. + function hasRole(bytes32 roleId_, address who_) + public + view + virtual + override(AccessControlUpgradeable, IAccessControl) + returns (bool hasRole_) + { + if (_isTokenGated[roleId_]) { + return _hasTokenRole(roleId_, who_); + } else { + return super.hasRole(roleId_, who_); + } + } + /// @notice Grants a role to an address. - /// @param role The role to grant. - /// @param who The address to grant the role to. - /// @return bool Returns true if the role has been granted succesfully. - /// @dev Overrides {_grantRole} from {AUT_ROLES_v1} to enforce interface implementation and threshold existence - /// when role is token-gated. - /// @dev Please note: current check for validating a valid token is not conclusive and could be - /// circumvented through a `callback()` function. - function _grantRole(bytes32 role, address who) + /// @param roleId_ The role to grant. + /// @param who_ The address to grant the role to. + /// @return success_ Returns true if the role has been granted succesfully. + /// @dev Overrides {_grantRole} from {AUT_ROLES_v1} to enforce interface + /// implementation and threshold existence when role is token-gated. + /// @dev Please note: current check for validating a valid token is not + /// conclusive and could be circumvented through a `callback()` + /// function. + function _grantRole(bytes32 roleId_, address who_) internal virtual override - returns (bool) + returns (bool success_) { - if (isTokenGated[role]) { - // Make sure that a threshold has been set before granting the role - if (getThresholdValue(role, who) == 0) { - revert Module__AUT_TokenGated_Roles__TokenRoleMustHaveThreshold( - role, who - ); - } - - // Check that address has code attached + if (_isTokenGated[roleId_]) { + // Check that address has code attached. uint32 size; assembly { - size := extcodesize(who) + size := extcodesize(who_) } if (size == 0) { - revert Module__AUT_TokenGated_Roles__InvalidToken(who); + revert Module__AUT_TokenGated_Roles__InvalidToken(who_); + } + + // Make sure that a threshold has been set before granting the role. + if (getThresholdValue(roleId_, who_) == 0) { + revert Module__AUT_TokenGated_Roles__TokenRoleMustHaveThreshold( + roleId_, who_ + ); } - // Execute a balanceOf call to the address - (bool success, bytes memory data) = who.call( + // Execute a balanceOf call to the address. + (bool success, bytes memory data) = who_.call( abi.encodeWithSelector( TokenInterface.balanceOf.selector, address(this) ) ); // If the call was either unsuccessful or the return data is not - // 32 bytes long (i.e. not a uint256), it's deemed invalid + // 32 bytes long (i.e. not a uint256), it's deemed invalid. if (!success || data.length != 32) { - revert Module__AUT_TokenGated_Roles__InvalidToken(who); + revert Module__AUT_TokenGated_Roles__InvalidToken(who_); } } - return super._grantRole(role, who); + return super._grantRole(roleId_, who_); } - /// @param role The id number of the role. - /// @param who The user we want to check on. - /// @return bool Returns if revoke has been succesful. + /// @notice Revokes a role from an address. /// @dev Overrides {_revokeRole} to clean up threshold data on revoking. - function _revokeRole(bytes32 role, address who) + /// @param roleId_ The id number of the role. + /// @param who_ The user we want to check on. + /// @return success_ Returns if revoke has been succesful. + + function _revokeRole(bytes32 roleId_, address who_) internal virtual override - returns (bool) + returns (bool success_) { - if (isTokenGated[role]) { - // Set the threshold to 0 before revoking the role from the token - bytes32 thresholdId = keccak256(abi.encodePacked(role, who)); - thresholdMap[thresholdId] = 0; - emit ChangedTokenThreshold(role, who, 0); + if (_isTokenGated[roleId_]) { + // Set the threshold to 0 before revoking the role from the token. + bytes32 thresholdId = keccak256(abi.encodePacked(roleId_, who_)); + _thresholdMap[thresholdId] = 0; + emit ChangedTokenThreshold(roleId_, who_, 0); } - return super._revokeRole(role, who); + return super._revokeRole(roleId_, who_); } //-------------------------------------------------------------------------- // Internal Functions /// @notice Sets the minimum threshold for a token-gated role. - /// @param roleId The ID of the role to be modified. - /// @param token The token for which to the threshold. - /// @param threshold The user will need to have at least this number to qualify for the role. - /// @dev This function does not validate the threshold. It is technically possible to set a threshold above the - /// total supply of the token. - function _setThreshold(bytes32 roleId, address token, uint threshold) + /// @dev This function does not validate the threshold. It is + /// technically possible to set a threshold above the total supply + /// of the token. + /// @param roleId_ The ID of the role to be modified. + /// @param token_ The token for which to the threshold. + /// @param threshold_ The user will need to have at least this number to + /// qualify for the role. + + function _setThreshold(bytes32 roleId_, address token_, uint threshold_) internal - onlyTokenGated(roleId) - validThreshold(threshold) + onlyTokenGated(roleId_) + validThreshold(threshold_) { - bytes32 thresholdId = keccak256(abi.encodePacked(roleId, token)); - thresholdMap[thresholdId] = threshold; - emit ChangedTokenThreshold(roleId, token, threshold); + bytes32 thresholdId = keccak256(abi.encodePacked(roleId_, token_)); + _thresholdMap[thresholdId] = threshold_; + emit ChangedTokenThreshold(roleId_, token_, threshold_); } - /// @notice Internal function that checks if an account qualifies for a token-gated role. - /// @param role The role to be checked. - /// @param who The account to be checked. - function _hasTokenRole(bytes32 role, address who) + /// @notice Internal function that checks if an account qualifies for a + /// token-gated role. + /// @param roleId_ The id of the role to be checked. + /// @param who_ The account to be checked. + /// @return hasRokenRole_ Returns if the account has the role. + function _hasTokenRole(bytes32 roleId_, address who_) internal view - returns (bool) + returns (bool hasRokenRole_) { - uint numberOfAllowedTokens = getRoleMemberCount(role); + uint numberOfAllowedTokens = getRoleMemberCount(roleId_); + address tokenAddr; + bytes32 thresholdId; + uint tokenThreshold; for (uint i; i < numberOfAllowedTokens; ++i) { - address tokenAddr = getRoleMember(role, i); - bytes32 thresholdId = keccak256(abi.encodePacked(role, tokenAddr)); - uint tokenThreshold = thresholdMap[thresholdId]; + tokenAddr = getRoleMember(roleId_, i); + thresholdId = keccak256(abi.encodePacked(roleId_, tokenAddr)); + tokenThreshold = _thresholdMap[thresholdId]; - // Should work with both ERC20 and ERC721 - try TokenInterface(tokenAddr).balanceOf(who) returns ( + // Should work with both ERC20 and ERC721. + try TokenInterface(tokenAddr).balanceOf(who_) returns ( uint tokenBalance ) { if (tokenBalance >= tokenThreshold) { @@ -297,28 +450,13 @@ contract AUT_TokenGated_Roles_v1 is IAUT_TokenGated_Roles_v1, AUT_Roles_v1 { } } catch { // If the call fails, we continue to the next token. - // Emitting an event here would make this function (and the functions calling it) non-view. - // note we already enforce Interface implementation when granting the role. + // Emitting an event here would make this function + // (and the functions calling it) non-view. + // note we already enforce Interface implementation when + // granting the role. } } return false; } - - /// @inheritdoc IAuthorizer_v1 - /// @notice In case the role is token gated, it will check if {who} holds a balance - /// above the threshold for at least one of the required tokens. - function checkForRole(bytes32 role, address who) - external - view - virtual - override(AUT_Roles_v1, IAuthorizer_v1) - returns (bool) - { - if (isTokenGated[role]) { - return _hasTokenRole(role, who); - } else { - return hasRole(role, who); - } - } } diff --git a/src/modules/authorizer/role/interfaces/IAUT_EXT_VotingRoles_v1.sol b/src/modules/authorizer/role/interfaces/IAUT_EXT_VotingRoles_v1.sol deleted file mode 100644 index 548e6104b..000000000 --- a/src/modules/authorizer/role/interfaces/IAUT_EXT_VotingRoles_v1.sol +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; - -interface IAUT_EXT_VotingRoles_v1 { - //-------------------------------------------------------------------------- - // Structs - - /// @notice A motion is a proposal to execute an action on a target contract. - /// @param target The address of the contract to execute the action on. - /// @param action The action data to execute on the target contract. - /// @param startTimestamp The timestamp at which the motion starts. - /// @param endTimestamp The timestamp at which the motion ends. - /// @param requiredThreshold The required threshold of votes to pass the motion. - /// @param forVotes The number of votes in favor of the motion. - /// @param againstVotes The number of votes against the motion. - /// @param abstainVotes The number of votes abstaining from the motion. - /// @param receipts The receipts of votes for the motion address => Receipt - /// @param executedAt The timestamp at which the motion was executed. - /// @param executionResult The result of the execution. - /// @param executionReturnData The return data of the execution. - struct Motion { - address target; - bytes action; - uint startTimestamp; - uint endTimestamp; - uint requiredThreshold; - uint forVotes; - uint againstVotes; - uint abstainVotes; - mapping(address => Receipt) receipts; - uint executedAt; - bool executionResult; - bytes executionReturnData; - } - - /// @notice A receipt is a vote cast for a motion. - /// @param hasVoted Whether the voter has already voted. - /// @param support The value that indicates wether the voter supports the motion. - struct Receipt { - bool hasVoted; - uint8 support; - } - - //-------------------------------------------------------------------------- - // Errors - - /// @notice The action would leave an empty voter list. - error Module__VotingRoleManager__EmptyVoters(); - - /// @notice The supplied voter address is invalid. - error Module__VotingRoleManager__InvalidVoterAddress(); - - /// @notice The threshold cannot exceed the amount of voters. - /// or be too low to be considered safe. - error Module__VotingRoleManager__InvalidThreshold(); - - /// @notice The supplied voting duration is invalid. - error Module__VotingRoleManager__InvalidVotingDuration(); - - /// @notice The function can only be called by a voter. - error Module__VotingRoleManager__CallerNotVoter(); - - /// @notice The address is already a voter. - error Module__VotingRoleManager__IsAlreadyVoter(); - - /// @notice The value given as vote is invalid. - error Module__VotingRoleManager__InvalidSupport(); - - /// @notice The supplied ID is referencing a motion that doesn't exist. - error Module__VotingRoleManager__InvalidMotionId(); - - /// @notice A user cannot vote twice. - error Module__VotingRoleManager__AttemptedDoubleVote(); - - /// @notice A motion cannot be executed if the voting duration hasn't passed. - error Module__VotingRoleManager__MotionInVotingPhase(); - - /// @notice A motion cannot be voted on if the duration has been exceeded. - error Module__VotingRoleManager__MotionVotingPhaseClosed(); - - /// @notice A motion cannot be executed twice. - error Module__VotingRoleManager__MotionAlreadyExecuted(); - - /// @notice A motion cannot be executed if it didn't reach the threshold. - error Module__VotingRoleManager__ThresholdNotReached(); - - //-------------------------------------------------------------------------- - // Events - - /// @notice Event emitted when a new voter address gets added. - /// @param who The added address. - event VoterAdded(address indexed who); - - /// @notice Event emitted when a voter address gets removed. - /// @param who The removed address. - event VoterRemoved(address indexed who); - - /// @notice Event emitted when the required threshold changes. - /// @param oldThreshold The old threshold. - /// @param newThreshold The new threshold. - event ThresholdUpdated(uint oldThreshold, uint newThreshold); - - /// @notice Event emitted when the voting duration changes. - /// @param oldVotingDuration The old voting duration. - /// @param newVotingDuration The new voting duration. - event VoteDurationUpdated(uint oldVotingDuration, uint newVotingDuration); - - /// @notice Event emitted when a motion is created. - /// @param motionId The motion ID. - event MotionCreated(bytes32 indexed motionId); - - /// @notice Event emitted when a vote is cast for a motion. - /// @param motionId The motion ID. - /// @param voter The address of a voter. - /// @param motionId Value that indicates how the voter supports the motion. - event VoteCast( - bytes32 indexed motionId, address indexed voter, uint8 indexed support - ); - - /// @notice Event emitted when a motion is executed. - /// @param motionId The motion ID. - event MotionExecuted(bytes32 indexed motionId); - - //-------------------------------------------------------------------------- - // Functions - - /// @notice The maximum voting duration. - /// @return The maximum voting duration. - function MAX_VOTING_DURATION() external view returns (uint); - - /// @notice The minimum voting duration. - /// @return The minimum voting duration. - function MIN_VOTING_DURATION() external view returns (uint); - - /// @notice Checks whether an address is a voter. - /// @param who The address to check. - /// @return Whether the address is a voter. - function isVoter(address who) external view returns (bool); - - /// @notice Adds a voter. - /// @param who The address to add. - function addVoter(address who) external; - - /// @notice Adds a voter and updates the threshold. - /// @param who The address to add. - /// @param newThreshold The new threshold. - function addVoterAndUpdateThreshold(address who, uint newThreshold) - external; - - /// @notice Removes a voter. - /// @param who The address to remove. - function removeVoter(address who) external; - - /// @notice Removes a voter and updates the threshold. - /// @param who The address to remove. - /// @param newThreshold The new threshold. - function removeVoterAndUpdateThreshold(address who, uint newThreshold) - external; - - /// @notice Gets the motion data. - /// @param motionId The ID of the motion. - /// @return target The address of the contract to execute the action on. - /// @return action The action data to execute on the target contract. - /// @return startTimestamp The timestamp at which the motion starts. - /// @return endTimestamp The timestamp at which the motion ends. - /// @return requiredThreshold The required threshold of votes to pass the motion. - /// @return forVotes The number of votes in favor of the motion. - /// @return againstVotes The number of votes against the motion. - /// @return abstainVotes The number of votes abstaining from the motion. - /// @return executedAt The timestamp at which the motion was executed. - /// @return executionResult The result of the execution. - /// @return executionReturnData The return data of the execution. - function motions(bytes32 motionId) - external - view - returns ( - address, - bytes memory, - uint, - uint, - uint, - uint, - uint, - uint, - uint, - bool, - bytes memory - ); - - /// @notice Gets the number of motions. - /// @return The number of motions. - function motionCount() external view returns (uint); - - /// @notice Gets the number of voters. - /// @return The number of voters. - function voterCount() external view returns (uint); - - /// @notice Gets the threshold. - /// @return The threshold. - function threshold() external view returns (uint); - - /// @notice Gets the receipt of a voter for a motion. - /// @param _ID The ID of the motion. - /// @param voter The address of the voter. - /// @return The receipt of the voter. - function getReceipt(bytes32 _ID, address voter) - external - view - returns (Receipt memory); - - /// @notice Gets the voting duration. - /// @return The voting duration. - function voteDuration() external view returns (uint); - - /// @notice Sets the threshold. - /// @param newThreshold The new threshold. - function setThreshold(uint newThreshold) external; - - /// @notice Sets the voting duration. - /// @param newVoteDuration The new voting duration. - function setVotingDuration(uint newVoteDuration) external; - - /// @notice Creates a motion. - /// @param target The address of the contract to execute the action on. - /// @param action The action data to execute on the target contract. - /// @return The ID of the created motion. - function createMotion(address target, bytes calldata action) - external - returns (bytes32); - - /// @notice Casts a vote for a motion. - /// @param motionId The ID of the motion. - /// @param support The value that indicates wether the voter supports the motion. - function castVote(bytes32 motionId, uint8 support) external; - - /// @notice Executes a motion. - /// @param motionId The ID of the motion. - function executeMotion(bytes32 motionId) external; -} diff --git a/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol b/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol index 365ece78e..4374616c1 100644 --- a/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol +++ b/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol @@ -3,107 +3,231 @@ pragma solidity ^0.8.0; import {IAuthorizer_v1} from "@aut/IAuthorizer_v1.sol"; +/** + * @title Inverter Token-Gated Role Authorizer Interface + * + * @notice Extends the Inverter's role-based access control to include token + * gating, enabling roles to be conditionally assigned based on token + * ownership. This mechanism allows for dynamic permissioning tied to + * specific token holdings. + * + * @dev Inherits functionality from: + * - {IAUT_TokenGated_Roles_v1}: Implementation interface. + * - {AUT_Roles_v1}: Inverter's role-based access control. + * + * Key feeatures: + * - Token-based access checks before role assignment. + * - Supports both {ERC20} and {ERC721} tokens. + * + * @custom:guide + * The following guide explains in detail how to use the key features + * of this module: + * + * - TOKEN BASED ACCESS CONTROL: + * - Token Gated Role: + * With this contract it is possible to extend the base + * functionality of the {AUT_Roles_v1} contract to make a + * role token gated. A token gated role behaves in all + * respects like a regular role, but handles the membership + * of that role differently. A member of a token gated role + * is only allowed to access the role functionalities if + * they hold a certain amount of a token. + * The implementation of this contract uses a few tricks to + * achieve this. Without going into too much detail, this + * is the main part that is needed to understand the basic + * mechanism: + * In the contract the token gating is done by instead of + * saving the members of the role directly in the members + * property of the role, the contract saves the token + * address that will gate the role in the property. That way + * the token address can be looked up and the threshold + * amount compared to the token balance of incoming users. + * Example: We want to adapt a role so that it can only be + * accessed by users that hold a certain amount of token A. + * So we make the role token gated and set a threshold of + * how many tokens a address needs to hold to be able to + * access the role. The moment we use the grantRole function + * to add the address of token A to the members property of + * the role, the role will only be accessible by users that + * hold the threshold amount of token A. + * + * - Making a role token gated: + * Making a role token gated is done by calling the + * `setTokenGated` function. This function takes the + * following parameters: + * - The role id of the role that we change the token gated + * status of. + * - The boolean that indicates if the role should be token + * gated or not. + * This function can only be called by a permissioned address + * (See permissioned section in the {AUT_Roles_v1} contract). + * Also the role can not contain any members, when it is + * switched to and from token gated. + * Example: Making the role "Whitelisted" token gated would + * look like this: + * authorizer.setTokenGated(whitelistedRoleId, true); + * + * - Setting the token threshold: + * Setting the token threshold needed to pass the token gate + * is done by calling the setTokenThreshold function. This + * function takes the following parameters: + * - The role id of the role to set the threshold for. + * - The address of the token to set the threshold for. + * - The threshold value to set. + * This function can only be called by a permissioned address + * (See permissioned section in the {AUT_Roles_v1} contract). + * This function can be called anytime, even if the role is + * not token gated yet. + * Example: Setting the threshold for the token "USDC" to + * 100 would look like this: + * authorizer.setTokenThreshold( + * whitelistedRoleId, USDC, 100); + * + * - Adding a token to the token gate: + * Adding a token to the token gate is done by calling the + * grantRole function. This function takes the following + * parameters: + * - The role id of the role to grant. + * - The address of the token to grant the role to. + * The grantRole function can only be called by according + * admin of the role. In addition, the given address needs + * to be a contract, already have a threshold set and + * contain the balanceOf function. + * If the role is not token gated then grantRole will + * behave like the regular grantRole function. + * Example: Adding a token gate to the token gated role + * "Whitelisted" would look like this: + * authorizer.grantRole(whitelistedRoleId, address(USDC)); + * + * - Removing a token from the token gate: + * Removing a token from the token gate is done by calling + * the revokeRole function. This function behaves like the + * regular revokeRole function, except that it sets the + * threshold for the role and token combination to 0 as + * well. + * Example: Removing the token from the role "Whitelisted" + * would look like this: + * authorizer.revokeRole(whitelistedRoleId, address(USDC)); + * + * - Reversing a token gate: + * In case the token gated status of a role needs to be + * reverted, the setTokenGated function can be used. The + * same restrictions as for the setTokenGated function apply + * here as well (see above). + * Example: Reversing the token gated status of the role + * "Whitelisted" would look like this: + * authorizer.setTokenGated(whitelistedRoleId, false); + * + * @custom:security-contact security@inverter.network + * In case of any concerns or findings, please refer to + * our Security Policy at security.inverter.network or + * email us directly! + * + * @custom:version v1.0.0 + * + * @custom:inverter-standard-version v0.1.0 + * + * @author Inverter Network + */ interface IAUT_TokenGated_Roles_v1 is IAuthorizer_v1 { - //-------------------------------------------------------------------------- - // Events - - /// @notice Event emitted when the token-gating of a role changes. - /// @param role The role that was modified. - /// @param newValue The new value of the role. - event ChangedTokenGating(bytes32 role, bool newValue); - - /// @notice Event emitted when the threshold of a token-gated role changes. - /// @param role The role that was modified. - /// @param token The token for which the threshold was modified. - /// @param newValue The new value of the threshold. - event ChangedTokenThreshold(bytes32 role, address token, uint newValue); - - //-------------------------------------------------------------------------- + //======================================================================= // Errors /// @notice The function is only callable by an active Module. error Module__AUT_TokenGated_Roles__RoleNotTokenGated(); - /// @notice The function is only callable if the Module is self-managing its roles. + /// @notice The function is only callable if the Module is self-managing + /// its roles. error Module__AUT_TokenGated_Roles__RoleNotEmpty(); + /// @notice The function is only callable if the Module is not the public + /// role. + error Module__AUT_TokenGated_Roles__RoleIsPublic(); + /// @notice The token doesn't support balance query. - /// @param token The token address that is not supported. - error Module__AUT_TokenGated_Roles__InvalidToken(address token); + /// @param token_ The token address that is not supported. + error Module__AUT_TokenGated_Roles__InvalidToken(address token_); /// @notice The given threshold is invalid. - /// @param threshold The threshold that is not valid. - error Module__AUT_TokenGated_Roles__InvalidThreshold(uint threshold); + /// @param threshold_ The threshold that is not valid. + error Module__AUT_TokenGated_Roles__InvalidThreshold(uint threshold_); /// @notice The role is token-gated but no threshold is set. - /// @param role The role that doesnt have threshold. - /// @param token The token for which the threshold was not set. + /// @param roleId_ The role that doesnt have threshold. + /// @param token_ The token for which the threshold was not set. error Module__AUT_TokenGated_Roles__TokenRoleMustHaveThreshold( - bytes32 role, address token + bytes32 roleId_, address token_ + ); + + //======================================================================= + // Events + + /// @notice Event emitted when the token-gating of a role changes. + /// @param roleId_ The role id of the role that was modified. + /// @param newValue_ The new value of the role. + event ChangedTokenGating(bytes32 roleId_, bool newValue_); + + /// @notice Event emitted when the threshold of a token-gated role changes. + /// @param roleId_ The role id of the role that was modified. + /// @param token_ The token for which the threshold was modified. + /// @param newValue_ The new value of the threshold. + event ChangedTokenThreshold( + bytes32 roleId_, address token_, uint newValue_ ); - //-------------------------------------------------------------------------- - // Public functions + // ======================================================================== + // Public Getter Functions + + /// @notice Returns if a role is token-gated or not. + /// @param roleId_ The ID of the role to be checked. + /// @return isTokenGated_ True if the role is token-gated. + function isTokenGated(bytes32 roleId_) + external + view + returns (bool isTokenGated_); /// @notice Checks if an account qualifies for a token-gated role. - /// @param role The role to be checked. - /// @param who The account to be checked. - /// @return True if the account qualifies for the role. - function hasTokenRole(bytes32 role, address who) + /// @param roleId_ The role to be checked. + /// @param who_ The account to be checked. + /// @return hasTokenRole_ True if the account qualifies for the role. + function hasTokenRole(bytes32 roleId_, address who_) external view - returns (bool); - - /// @notice Returns the threshold balance for a given token necessary to qualify for a - /// specific role. If the value is 0, the supplied token is not part of the. - /// role's token gating. - /// @dev In case the queried role is not token gated, all calls will return 0. - /// @param roleId The role to be checked on. - /// @param token The token to check the threshold for. - /// @return The threshold amount necessary to qualify for a given token role. - function getThresholdValue(bytes32 roleId, address token) + returns (bool hasTokenRole_); + + /// @notice Returns the threshold balance for a given token necessary to + /// qualify for a specific role. If the value is 0, the supplied + /// token is not part of the role's token gating. + /// @dev In case the queried role is not token gated, all calls will + /// return 0. + /// @param roleId_ The role to be checked on. + /// @param token_ The token to check the threshold for. + /// @return threshold_ The threshold amount necessary to qualify for a + /// given token role. + function getThresholdValue(bytes32 roleId_, address token_) external - returns (uint); - - /// @notice Sets up a token-gated empty role. - /// @param role The role to be made token-gated. - /// @dev This function is only callable by an active Module for itself. Admin should use `setTokenGated()`. - /// @dev Calling this function does not specify WHICH token to use for gating. That has to be done - /// with 'grantTokenFromModule()'. - function makeRoleTokenGatedFromModule(bytes32 role) external; - - /// @notice One-step setup for Modules to create a token-gated role and set its threshold. - /// Please be aware that using tokens that are transferable and have active markets could - /// make the token-gated authorization vulnerable to flash loans, potentially bypassing. - /// the authorization mechanism. - /// @param role The role to be made token-gated. - /// @param token The token for which the threshold will be set. - /// @param threshold The minimum balance of the token required to qualify for the role. - function grantTokenRoleFromModule( - bytes32 role, - address token, - uint threshold - ) external; - - /// @notice Allows a Module to set the threshold of one of it's roles. - /// @param role The token-gated role. - /// @param token The token for which the threshold will be set. - /// @param threshold The new minimum balance of the token required to qualify for the role. - function setThresholdFromModule(bytes32 role, address token, uint threshold) - external; + returns (uint threshold_); + + // ======================================================================== + // Mutating Functions /// @notice Sets if a role is token-gated or not. - /// @param role The ID of the role to be modified. - /// @param to The new value to be set. - /// @dev Admin access for rescue purposes. If the role has active members, they need to be reovked first. - function setTokenGated(bytes32 role, bool to) external; + /// @dev Admin access for rescue purposes. If the role has active + /// members, they need to be reovked first. + /// @param roleId_ The ID of the role to be modified. + /// @param to_ The new value to be set. + + function setTokenGated(bytes32 roleId_, bool to_) external; /// @notice Sets the minimum threshold for a token-gated role. - /// @param roleId The ID of the role to be modified. - /// @param token The token for which to the threshold. - /// @param threshold The user will need to have at least this number to qualify for the role. - /// @dev This function does not validate the threshold. It is technically possible to set a threshold above - /// the total supply of the token. - function setThreshold(bytes32 roleId, address token, uint threshold) + /// @dev This function does not validate the threshold. It is + /// technically possible to set a threshold above the total supply + /// of the token. + /// @param roleId_ The ID of the role to be modified. + /// @param token_ The token for which to the threshold. + /// @param threshold_ The user will need to have at least this number to + /// qualify for the role. + + function setThreshold(bytes32 roleId_, address token_, uint threshold_) external; } diff --git a/src/modules/base/IModule_v1.sol b/src/modules/base/IModule_v1.sol index 9c2bf04d1..5f2796cbf 100644 --- a/src/modules/base/IModule_v1.sol +++ b/src/modules/base/IModule_v1.sol @@ -6,7 +6,7 @@ import {IOrchestrator_v1} from "src/orchestrator/interfaces/IOrchestrator_v1.sol"; interface IModule_v1 { - //-------------------------------------------------------------------------- + // ======================================================================== // Structs /// @notice The module's metadata. @@ -23,34 +23,11 @@ interface IModule_v1 { string title; } - //-------------------------------------------------------------------------- - // Events - - /// @notice Module has been initialized. - /// @param parentOrchestrator The address of the {Orchestrator_v1} the module is linked to. - /// @param metadata The metadata of the module. - event ModuleInitialized( - address indexed parentOrchestrator, Metadata metadata - ); - - /// @notice Event emitted when protocol fee has been transferred to the treasury. - /// @param token The token received as protocol fee. - /// @param treasury The protocol treasury address receiving the token fee amount. - /// @param feeAmount The fee amount transferred to the treasury. - event ProtocolFeeTransferred( - address indexed token, address indexed treasury, uint feeAmount - ); - - //-------------------------------------------------------------------------- + // ======================================================================== // Errors /// @notice Function is only callable by authorized caller. - /// @param role The role that is required. - /// @param caller The address that is required to have the role. - error Module__CallerNotAuthorized(bytes32 role, address caller); - - /// @notice Function is only callable by the {Orchestrator_v1}. - error Module__OnlyCallableByOrchestrator(); + error Module__CallerNotPermissioned(); /// @notice Function is only callable by a {IERC20PaymentClientBase_v2}. error Module__OnlyCallableByPaymentClient(); @@ -68,8 +45,29 @@ interface IModule_v1 { /// @dev Invalid Address. error Module__InvalidAddress(); - //-------------------------------------------------------------------------- - // Functions + /// @dev The given function is no longer supported. + error Module__FunctionDeprecated(); + + // ======================================================================== + // Events + + /// @notice Module has been initialized. + /// @param parentOrchestrator The address of the {Orchestrator_v1} the module is linked to. + /// @param metadata The metadata of the module. + event ModuleInitialized( + address indexed parentOrchestrator, Metadata metadata + ); + + /// @notice Event emitted when protocol fee has been transferred to the treasury. + /// @param token The token received as protocol fee. + /// @param treasury The protocol treasury address receiving the token fee amount. + /// @param feeAmount The fee amount transferred to the treasury. + event ProtocolFeeTransferred( + address indexed token, address indexed treasury, uint feeAmount + ); + + // ======================================================================== + // Initialization /// @notice The module's initializer function. /// @dev CAN be overridden by downstream contract. @@ -84,6 +82,12 @@ interface IModule_v1 { bytes memory configData ) external; + // ======================================================================== + // Public Getter Functions + + // ------------------------------------------------------------------------ + // Getter - Module State + /// @notice Returns the module's identifier. /// @dev The identifier is defined as the keccak256 hash of the module's /// abi packed encoded major version, url and title. @@ -107,26 +111,4 @@ interface IModule_v1 { /// @notice Returns the module's {Orchestrator_v1} interface, {IOrchestrator_v1}. /// @return The module's {Orchestrator_1}. function orchestrator() external view returns (IOrchestrator_v1); - - /// @notice Grants a module role to a target address. - /// @param role The role to grant. - /// @param target The target address to grant the role to. - function grantModuleRole(bytes32 role, address target) external; - - /// @notice Grants a module role to multiple target addresses. - /// @param role The role to grant. - /// @param targets The target addresses to grant the role to. - function grantModuleRoleBatched(bytes32 role, address[] calldata targets) - external; - - /// @notice Revokes a module role from a target address. - /// @param role The role to revoke. - /// @param target The target address to revoke the role from. - function revokeModuleRole(bytes32 role, address target) external; - - /// @notice Revokes a module role from multiple target addresses. - /// @param role The role to revoke. - /// @param targets The target addresses to revoke the role from. - function revokeModuleRoleBatched(bytes32 role, address[] calldata targets) - external; } diff --git a/src/modules/base/Module_v1.sol b/src/modules/base/Module_v1.sol index 8c0cffbca..128c71aa8 100644 --- a/src/modules/base/Module_v1.sol +++ b/src/modules/base/Module_v1.sol @@ -64,7 +64,7 @@ abstract contract Module_v1 is || super.supportsInterface(interfaceId); } - //-------------------------------------------------------------------------- + // ======================================================================== // Storage // // Variables are prefixed with `__Module_`. @@ -82,19 +82,15 @@ abstract contract Module_v1 is /// @dev Storage gap for future upgrades. uint[50] private __gap; - //-------------------------------------------------------------------------- + // ======================================================================== // Modifiers // // Note that the modifiers declared here are available in dowstream // contracts too. To not make unnecessary modifiers available, this contract // inlines argument validations not needed in downstream contracts. - /// @dev Modifier to guarantee function is only callable by addresses - /// authorized via {Orchestrator_v1}. - modifier onlyOrchestratorAdmin() { - _checkRoleModifier( - __Module_orchestrator.authorizer().getAdminRole(), _msgSender() - ); + modifier permissioned() { + _checkAuthorization(_msgSender(), _msgData()); _; } @@ -105,38 +101,6 @@ abstract contract Module_v1 is _; } - /// @dev Modifier to guarantee function is only callable by addresses that hold a specific module-assigned role. - modifier onlyModuleRole(bytes32 role) { - _checkRoleModifier( - __Module_orchestrator.authorizer().generateRoleId( - address(this), role - ), - _msgSender() - ); - _; - } - - /// @dev Modifier to guarantee function is only callable by addresses that hold a specific module-assigned role. - modifier onlyModuleRoleAdmin(bytes32 role) { - bytes32 moduleRole = __Module_orchestrator.authorizer().generateRoleId( - address(this), role - ); - _checkRoleModifier( - __Module_orchestrator.authorizer().getRoleAdmin(moduleRole), - _msgSender() - ); - _; - } - - /// @dev Modifier to guarantee function is only callable by the {Orchestrator_v1}. - /// @dev onlyOrchestrator functions MUST only access the module's storage, i.e. - /// `__Module_` variables. - /// @dev Note to use function prefix `__Module_`. - modifier onlyOrchestrator() { - _onlyOrchestratorModifier(); - _; - } - /// @dev Checks if the given Address is valid. /// @param to The address to check. modifier validAddress(address to) { @@ -144,7 +108,7 @@ abstract contract Module_v1 is _; } - //-------------------------------------------------------------------------- + // ======================================================================== // Initialization constructor() ERC2771ContextUpgradeable(address(0)) { @@ -182,8 +146,11 @@ abstract contract Module_v1 is emit ModuleInitialized(address(orchestrator_), metadata); } - //-------------------------------------------------------------------------- - // Public View Functions + // ======================================================================== + // Public Getter Functions + + // ------------------------------------------------------------------------ + // Getter - Module State /// @inheritdoc IModule_v1 function identifier() public view returns (bytes32) { @@ -215,47 +182,45 @@ abstract contract Module_v1 is } //-------------------------------------------------------------------------- - // Role Management - - /// @inheritdoc IModule_v1 - function grantModuleRole(bytes32 role, address target) - external - onlyModuleRoleAdmin(role) - { - __Module_orchestrator.authorizer().grantRoleFromModule(role, target); - } + // Getter - ERC2771 Context Upgradeable Overrides - /// @inheritdoc IModule_v1 - function grantModuleRoleBatched(bytes32 role, address[] calldata targets) - external - onlyModuleRoleAdmin(role) - { - __Module_orchestrator.authorizer().grantRoleFromModuleBatched( - role, targets - ); - } - - /// @inheritdoc IModule_v1 - function revokeModuleRole(bytes32 role, address target) - external - onlyModuleRoleAdmin(role) + /// @notice Checks if the provided address is the trusted forwarder. + /// @param forwarder The contract address to be verified. + /// @return bool Is the given address the trusted forwarder. + /// @dev We imitate here the EIP2771 Standard to enable metatransactions + /// As it currently stands we dont want to feed the forwarder address to each module individually and we decided to + /// move this to the orchestrator. + function isTrustedForwarder(address forwarder) + public + view + virtual + override(ERC2771ContextUpgradeable) + returns (bool) { - __Module_orchestrator.authorizer().revokeRoleFromModule(role, target); + return __Module_orchestrator.isTrustedForwarder(forwarder); } - /// @inheritdoc IModule_v1 - function revokeModuleRoleBatched(bytes32 role, address[] calldata targets) - external - onlyModuleRoleAdmin(role) + /// @notice Returns the trusted forwarder. + /// @return address The trusted forwarder. + /// @dev We imitate here the EIP2771 Standard to enable metatransactions. + /// As it currently stands we dont want to feed the forwarder address to each module individually and we decided to + /// move this to the orchestrator. + function trustedForwarder() + public + view + virtual + override(ERC2771ContextUpgradeable) + returns (address) { - __Module_orchestrator.authorizer().revokeRoleFromModuleBatched( - role, targets - ); + return __Module_orchestrator.trustedForwarder(); } - //-------------------------------------------------------------------------- + // ======================================================================== // Internal Functions + // ------------------------------------------------------------------------ + // Internal - Fees + /// @notice Returns the collateral fee for the specified workflow module function and the according treasury /// address of this workflow. /// @param functionSelector The function selector of the target function. @@ -296,22 +261,29 @@ abstract contract Module_v1 is ); } - /// @dev Checks if the caller has the specified role. - /// @param role The role to check. - /// @param addr The address to check. - function _checkRoleModifier(bytes32 role, address addr) internal view { - if (!__Module_orchestrator.authorizer().checkForRole(role, addr)) { - revert Module__CallerNotAuthorized(role, addr); - } - } + // ------------------------------------------------------------------------ + // Internal - Authorization - /// @dev Checks if the caller is the orchestrator. - function _onlyOrchestratorModifier() internal view { - if (_msgSender() != address(__Module_orchestrator)) { - revert Module__OnlyCallableByOrchestrator(); + /// @notice Checks if the caller can call the function that implements the locked modifier. + /// @param caller_ The address of the caller. + /// @param data_ The data of the call. + function _checkAuthorization(address caller_, bytes calldata data_) + internal + view + { + // If caller cannot call the function, revert. + if ( + !__Module_orchestrator.authorizer().hasPermission( + caller_, address(this), bytes4(data_[0:4]) + ) + ) { + revert Module__CallerNotPermissioned(); } } + // ------------------------------------------------------------------------ + // Internal - Modifiers + /// @dev Checks if the given address is an valid address. /// @param to The address to check. function _validAddressModifier(address to) internal view { @@ -334,38 +306,4 @@ abstract contract Module_v1 is ) ) revert Module__OnlyCallableByPaymentClient(); } - - //-------------------------------------------------------------------------- - // ERC2771 Context Upgradeable - - /// @notice Checks if the provided address is the trusted forwarder. - /// @param forwarder The contract address to be verified. - /// @return bool Is the given address the trusted forwarder. - /// @dev We imitate here the EIP2771 Standard to enable metatransactions - /// As it currently stands we dont want to feed the forwarder address to each module individually and we decided to - /// move this to the orchestrator. - function isTrustedForwarder(address forwarder) - public - view - virtual - override(ERC2771ContextUpgradeable) - returns (bool) - { - return __Module_orchestrator.isTrustedForwarder(forwarder); - } - - /// @notice Returns the trusted forwarder. - /// @return address The trusted forwarder. - /// @dev We imitate here the EIP2771 Standard to enable metatransactions. - /// As it currently stands we dont want to feed the forwarder address to each module individually and we decided to - /// move this to the orchestrator. - function trustedForwarder() - public - view - virtual - override(ERC2771ContextUpgradeable) - returns (address) - { - return __Module_orchestrator.trustedForwarder(); - } } diff --git a/src/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol b/src/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol index e7c069338..10dae21a6 100644 --- a/src/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol +++ b/src/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol @@ -217,8 +217,9 @@ contract FM_BC_Bancor_Redeeming_VirtualSupply_v1 is public virtual override(BondingCurveBase_v1, IBondingCurveBase_v1) - validReceiver(_receiver) + permissioned buyingIsEnabled + validReceiver(_receiver) { (uint amountIssued, uint collateralFeeAmount) = _buyOrder(_receiver, _depositAmount, _minAmountOut); @@ -239,9 +240,13 @@ contract FM_BC_Bancor_Redeeming_VirtualSupply_v1 is public virtual override(BondingCurveBase_v1, IBondingCurveBase_v1) + permissioned buyingIsEnabled { - buyFor(_msgSender(), _depositAmount, _minAmountOut); + (uint amountIssued, uint collateralFeeAmount) = + _buyOrder(_msgSender(), _depositAmount, _minAmountOut); + _addVirtualIssuanceAmount(amountIssued); + _addVirtualCollateralAmount(_depositAmount - collateralFeeAmount); } /// @notice Redeem tokens and direct the proceeds to a specified receiver address. This function is subject @@ -258,8 +263,9 @@ contract FM_BC_Bancor_Redeeming_VirtualSupply_v1 is public virtual override(RedeemingBondingCurveBase_v1) - validReceiver(_receiver) + permissioned sellingIsEnabled + validReceiver(_receiver) { (uint redeemAmount, uint issuanceFeeAmount) = _sellOrder(_receiver, _depositAmount, _minAmountOut); @@ -280,9 +286,13 @@ contract FM_BC_Bancor_Redeeming_VirtualSupply_v1 is public virtual override(RedeemingBondingCurveBase_v1) + permissioned sellingIsEnabled { - sellTo(_msgSender(), _depositAmount, _minAmountOut); + (uint redeemAmount, uint issuanceFeeAmount) = + _sellOrder(_msgSender(), _depositAmount, _minAmountOut); + _subVirtualIssuanceAmount(_depositAmount - issuanceFeeAmount); + _subVirtualCollateralAmount(redeemAmount); } // ------------------------------------------------------------------------- @@ -366,42 +376,27 @@ contract FM_BC_Bancor_Redeeming_VirtualSupply_v1 is } // ------------------------------------------------------------------------- - // OnlyOrchestrator Functions - - /// @inheritdoc IFundingManager_v1 - function transferOrchestratorToken(address to_, uint amount_) - external - virtual - onlyPaymentClient - { - if ( - amount_ - > token().balanceOf(address(this)) - projectCollateralFeeCollected - ) { - revert InvalidOrchestratorTokenWithdrawAmount(); - } - token().safeTransfer(to_, amount_); - - emit TransferOrchestratorToken(to_, amount_); - } + // Permissioned Functions /// @inheritdoc IVirtualIssuanceSupplyBase_v1 + /// @dev Function access controlled by authorizer. function setVirtualIssuanceSupply(uint virtualSupply_) external virtual override(VirtualIssuanceSupplyBase_v1) - onlyOrchestratorAdmin + permissioned onlyWhenCurveInteractionsAreClosed { _setVirtualIssuanceSupply(virtualSupply_); } /// @inheritdoc IVirtualCollateralSupplyBase_v1 + /// @dev Function access controlled by authorizer. function setVirtualCollateralSupply(uint virtualSupply_) external virtual override(VirtualCollateralSupplyBase_v1) - onlyOrchestratorAdmin + permissioned onlyWhenCurveInteractionsAreClosed { _setVirtualCollateralSupply(virtualSupply_); @@ -411,22 +406,43 @@ contract FM_BC_Bancor_Redeeming_VirtualSupply_v1 is function setReserveRatioForBuying(uint32 reserveRatio_) external virtual - onlyOrchestratorAdmin + permissioned onlyWhenCurveInteractionsAreClosed { _setReserveRatioForBuying(reserveRatio_); } /// @inheritdoc IFM_BC_Bancor_Redeeming_VirtualSupply_v1 + /// @dev Function access controlled by authorizer. function setReserveRatioForSelling(uint32 reserveRatio_) external virtual - onlyOrchestratorAdmin + permissioned onlyWhenCurveInteractionsAreClosed { _setReserveRatioForSelling(reserveRatio_); } + // ------------------------------------------------------------------------- + // PaymentClient Functions + + /// @inheritdoc IFundingManager_v1 + function transferOrchestratorToken(address to_, uint amount_) + external + virtual + onlyPaymentClient + { + if ( + amount_ + > token().balanceOf(address(this)) - projectCollateralFeeCollected + ) { + revert InvalidOrchestratorTokenWithdrawAmount(); + } + token().safeTransfer(to_, amount_); + + emit TransferOrchestratorToken(to_, amount_); + } + // ------------------------------------------------------------------------- // Upstream Function Implementations diff --git a/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol b/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol index 16ea56712..57d1484b5 100644 --- a/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol +++ b/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.23; // Internal +import {IModule_v1} from "src/modules/base/IModule_v1.sol"; import {Module_v1} from "src/modules/base/Module_v1.sol"; import {FM_BC_BondingSurface_Redeeming_v1} from "@fm/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.sol"; @@ -92,14 +93,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is uint64 public constant MAX_FEE = 100; /// @notice Time interval between seizes. uint64 public constant SEIZE_DELAY = 7 days; - /// @notice Role associated with the managing of the bonding curve values. - bytes32 public constant RISK_MANAGER_ROLE = "RISK_MANAGER"; - /// @notice Role associated with the managing of setting withdraw addresses - /// and setting the fee. - bytes32 public constant COVER_MANAGER_ROLE = "COVER_MANAGER"; - /// @notice Role that can use buy and sell regardless wether these - /// functions are restricted or not - bytes32 public constant CURVE_INTERACTION_ROLE = "CURVE_USER"; // ======================================================================== // Storage @@ -118,8 +111,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is uint internal _lastSeizeTimestamp; /// @notice Address of the reserve pool. address internal _tokenVault; - /// @notice Restricts buying and selling functionalities to specific role. - bool internal _buyAndSellIsRestricted; /// @notice Storage gap for future upgrades. uint[50] private __gap; @@ -127,12 +118,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is // ======================================================================== // Modifiers - /// @notice Modifier to ensure buy and sell restrictions are met. - modifier onlyIfNotBuyAndSellRestricted() { - _onlyIfNotBuyAndSellRestrictedModifier(); - _; - } - /// @notice Modifier to ensure only the LiquidityVaultController can call /// the function. modifier onlyLiquidityVaultController() { @@ -156,27 +141,23 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is BondingCurveProperties memory bondingCurveProperties; address liquidityVaultController; uint64 newSeize; - // Indicates whether buying and selling is restricted to the - // CURVE_INTERACTION_ROLE or open to anyone. - bool buyAndSellIsRestricted; ( issuanceToken, acceptedToken, bondingCurveProperties, liquidityVaultController, - newSeize, - buyAndSellIsRestricted + newSeize ) = abi.decode( configData_, - (address, address, BondingCurveProperties, address, uint64, bool) + (address, address, BondingCurveProperties, address, uint64) ); __Module_init(orchestrator_, metadata_); __FM_BC_BondingSurface_Redeeming_v1_Init( issuanceToken, acceptedToken, bondingCurveProperties ); __FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Init( - liquidityVaultController, newSeize, buyAndSellIsRestricted + liquidityVaultController, newSeize ); } @@ -186,18 +167,12 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is /// @param liquidityVaultController_ The address of the /// LiquidityVaultController. /// @param newSeize_ The new seize value. - /// @param buyAndSellIsRestricted_ Whether buy and sell is restricted. function __FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Init( address liquidityVaultController_, - uint64 newSeize_, - bool buyAndSellIsRestricted_ + uint64 newSeize_ ) internal onlyInitializing { _liquidityVaultController = liquidityVaultController_; - // Set buy and sell restriction to restricted if true. By default buy - // and sell are unrestricted. - _buyAndSellIsRestricted = buyAndSellIsRestricted_; - _setSeize(newSeize_); } @@ -239,15 +214,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is return address(_tokenVault); } - /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function isBuyAndSellRestricted() - public - view - returns (bool buyAndSellIsRestricted_) - { - return _buyAndSellIsRestricted; - } - /// @inheritdoc IRepayer_v1 function getRepayableAmount() external @@ -261,61 +227,18 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is // Public Mutating Functions // ------------------------------------------------------------------------ - // Mutating - Token Manipulation Functions - - /// @inheritdoc IBondingCurveBase_v1 - /// @dev The buy functionality can be restricted to the - /// CURVE_INTERACTION_ROLE. - function buyFor(address receiver_, uint depositAmount_, uint minAmountOut_) - public - virtual - override(BondingCurveBase_v1, IBondingCurveBase_v1) - onlyIfNotBuyAndSellRestricted - { - super.buyFor(receiver_, depositAmount_, minAmountOut_); - } - - /// @inheritdoc IBondingCurveBase_v1 - /// @dev The buy functionality can be restricted to the - /// CURVE_INTERACTION_ROLE. - function buy(uint depositAmount_, uint minAmountOut_) - public - virtual - override(BondingCurveBase_v1, IBondingCurveBase_v1) - { - buyFor(_msgSender(), depositAmount_, minAmountOut_); - } - - /// @inheritdoc IRedeemingBondingCurveBase_v1 - /// @dev The sell functionality can be restricted to the - /// CURVE_INTERACTION_ROLE. - function sellTo(address receiver_, uint depositAmount_, uint minAmountOut_) - public - virtual - override(RedeemingBondingCurveBase_v1, IRedeemingBondingCurveBase_v1) - onlyIfNotBuyAndSellRestricted - { - super.sellTo(receiver_, depositAmount_, minAmountOut_); - } - - /// @inheritdoc IRedeemingBondingCurveBase_v1 - /// @dev The sell functionality can be restricted to the - /// CURVE_INTERACTION_ROLE. - function sell(uint depositAmount_, uint minAmountOut_) - public - virtual - override(RedeemingBondingCurveBase_v1, IRedeemingBondingCurveBase_v1) - { - sellTo(_msgSender(), depositAmount_, minAmountOut_); - } + // Mutating - Permissioned Functions /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function burnIssuanceToken(uint amount_) external { + function burnIssuanceToken(uint amount_) external permissioned { _burn(_msgSender(), amount_); } /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function burnIssuanceTokenFor(address owner_, uint amount_) external { + function burnIssuanceTokenFor(address owner_, uint amount_) + external + permissioned + { if (owner_ != _msgSender()) { // Does not update allowance if set to infinite. _spendAllowance(owner_, _msgSender(), amount_); @@ -324,48 +247,8 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is _burn(owner_, amount_); } - // ------------------------------------------------------------------------ - // Mutating - OnlyLiquidityVaultController Functions - - /// @inheritdoc IRepayer_v1 - function transferRepayment(address to_, uint amount_) - external - validReceiver(to_) - onlyLiquidityVaultController - { - if (amount_ > _getRepayableAmount()) { - revert Repayer__InsufficientCollateralForRepayerTransfer(); - } - __Module_orchestrator.fundingManager().token().safeTransfer( - to_, amount_ - ); - if (MIN_RESERVE > token().balanceOf(address(this))) { - revert FM_BC_BondingSurface_Redeeming_v1__MinReserveReached(); - } - - emit RepaymentTransfer(to_, amount_); - } - - // ------------------------------------------------------------------------ - // Mutating - OnlyCoverManager Functions - - /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function restrictBuyAndSell() external onlyModuleRole(COVER_MANAGER_ROLE) { - _buyAndSellIsRestricted = true; - emit BuyAndSellIsRestricted(); - } - - /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function unrestrictBuyAndSell() - external - onlyModuleRole(COVER_MANAGER_ROLE) - { - _buyAndSellIsRestricted = false; - emit BuyAndSellIsUnrestricted(); - } - /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function seize(uint amount_) public onlyModuleRole(COVER_MANAGER_ROLE) { + function seize(uint amount_) public permissioned { uint seizableAmount = getSeizableAmount(); if (amount_ > seizableAmount) { revert @@ -394,17 +277,14 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is } /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function adjustSeize(uint64 seize_) - public - onlyModuleRole(COVER_MANAGER_ROLE) - { + function adjustSeize(uint64 seize_) public permissioned { _setSeize(seize_); } /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 function setLiquidityVaultControllerContract(address lvc_) external - onlyModuleRole(COVER_MANAGER_ROLE) + permissioned { if (address(lvc_) == address(0) || address(lvc_) == address(this)) { revert @@ -418,10 +298,7 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is } /// @inheritdoc IRepayer_v1 - function setRepayableAmount(uint amount_) - external - onlyModuleRole(COVER_MANAGER_ROLE) - { + function setRepayableAmount(uint amount_) external permissioned { if (amount_ > _getSmallerCaCr()) { revert IFM_BC_BondingSurface_Redeeming_v1 @@ -431,65 +308,41 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is _repayableAmount = amount_; } - // ------------------------------------------------------------------------ - // Mutating - RedeemingBondingCurveBase_v1 Overrides - - /// @inheritdoc IRedeemingBondingCurveBase_v1 - function setSellFee(uint fee_) - external - virtual - override(RedeemingBondingCurveBase_v1, IRedeemingBondingCurveBase_v1) - onlyModuleRole(COVER_MANAGER_ROLE) - { - _setSellFee(fee_); + /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + function setTokenVault(address tokenVault_) external permissioned { + _setTokenVault(tokenVault_); } // ------------------------------------------------------------------------ - // Mutating - OnlyRiskManager Functions + // Mutating - OnlyLiquidityVaultController Functions - /// @inheritdoc IFM_BC_BondingSurface_Redeeming_v1 - function setCapitalRequired(uint newCapitalRequired_) - public - override( - FM_BC_BondingSurface_Redeeming_v1, IFM_BC_BondingSurface_Redeeming_v1 - ) - onlyModuleRole(RISK_MANAGER_ROLE) + /// @inheritdoc IRepayer_v1 + function transferRepayment(address to_, uint amount_) + external + validReceiver(to_) + onlyLiquidityVaultController { - _setCapitalRequired(newCapitalRequired_); - } + if (amount_ > _getRepayableAmount()) { + revert Repayer__InsufficientCollateralForRepayerTransfer(); + } + __Module_orchestrator.fundingManager().token().safeTransfer( + to_, amount_ + ); + if (MIN_RESERVE > token().balanceOf(address(this))) { + revert FM_BC_BondingSurface_Redeeming_v1__MinReserveReached(); + } - /// @inheritdoc IFM_BC_BondingSurface_Redeeming_v1 - function setBasePriceMultiplier(uint newBasePriceMultiplier_) - public - override( - FM_BC_BondingSurface_Redeeming_v1, IFM_BC_BondingSurface_Redeeming_v1 - ) - onlyModuleRole(RISK_MANAGER_ROLE) - { - _setBasePriceMultiplier(newBasePriceMultiplier_); + emit RepaymentTransfer(to_, amount_); } // ------------------------------------------------------------------------ - // Mutating - OnlyOrchestratorAdmin Functions - - /// @inheritdoc IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - function setTokenVault(address tokenVault_) - external - onlyOrchestratorAdmin - { - _setTokenVault(tokenVault_); - } + // Mutating - Out of Order /// @inheritdoc IBondingCurveBase_v1 function withdrawProjectCollateralFee( address, /* receiver_ */ uint /* amount_ */ - ) - public - view - override(BondingCurveBase_v1, IBondingCurveBase_v1) - onlyOrchestratorAdmin - { + ) public view override(BondingCurveBase_v1, IBondingCurveBase_v1) { revert FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__InvalidFunctionality( ); @@ -575,19 +428,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is } } - /// @notice Validate if buy and sell is restricted, and if so - /// check if the caller has the CURVE_INTERACTION_ROLE. - function _onlyIfNotBuyAndSellRestrictedModifier() internal view { - if (_buyAndSellIsRestricted) { - _checkRoleModifier( - __Module_orchestrator.authorizer().generateRoleId( - address(this), CURVE_INTERACTION_ROLE - ), - _msgSender() - ); - } - } - // ------------------------------------------------------------------------ // Internal - BondingCurveBase_v1 Overrides diff --git a/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.sol b/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.sol index 4c0d023c5..211cfdfda 100644 --- a/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.sol +++ b/src/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.sol @@ -265,13 +265,13 @@ contract FM_BC_BondingSurface_Redeeming_v1 is // Mutating Functions // ------------------------------------------------------------------------ - // Mutating - OnlyOrchestratorAdmin Functions + // Mutating - Permissioned Functions /// @inheritdoc IFM_BC_BondingSurface_Redeeming_v1 function setCapitalRequired(uint newCapitalRequired_) public virtual - onlyOrchestratorAdmin + permissioned { _setCapitalRequired(newCapitalRequired_); } @@ -280,7 +280,7 @@ contract FM_BC_BondingSurface_Redeeming_v1 is function setBasePriceMultiplier(uint newBasePriceMultiplier_) public virtual - onlyOrchestratorAdmin + permissioned { _setBasePriceMultiplier(newBasePriceMultiplier_); } diff --git a/src/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.sol b/src/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.sol deleted file mode 100644 index 98ac7fbbb..000000000 --- a/src/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.23; - -// Internal Dependencies - -import { - FM_BC_Bancor_Redeeming_VirtualSupply_v1, - IFM_BC_Bancor_Redeeming_VirtualSupply_v1, - IFundingManager_v1 -} from "@fm/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; - -/** - * @title Inverter Restricted Bancor Virtual Supply Bonding Curve Funding Manager - * - * @notice This contract enables the issuance and redeeming of tokens on a - * bonding curve, using a virtual supply for both the issuance and - * the collateral as input. It integrates Aragon's Bancor Formula to - * manage the calculations for token issuance and redemption rates - * based on specified reserve ratios. - * - * @dev It overrides the `buyFor()` and `sellTo()` functions of its parent - * contract to limit them to callers holding a "Curve Interaction" - * role. Since the upstream functions `buy()` and `sell()` call these - * functions internally, they also become gated. - * - * PLEASE NOTE: This means that the workflow itself can only mint - * tokens through buying and selling by somebody with the - * `CURVE_INTERACTION_ROLE`, but NOT that there are no other ways to - * mint tokens. The Bonding Curve uses an external token contract, and - * there is no guarantee that said uses an external token contract, and - * there is no guarantee that said contract won't have an additional - * way to mint tokens (and potentially sell them on the cruve to - * receive backing collateral) - * - * @custom:security-contact security@inverter.network - * In case of any concerns or findings, please refer - * to our Security Policy at security.inverter.network - * or email us directly! - * - * @custom:version 1.1.2 - * - * @author Inverter Network - */ -contract FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1 is - FM_BC_Bancor_Redeeming_VirtualSupply_v1 -{ - // ------------------------------------------------------------------------- - // Errors - - /// @notice The feature is deactivated in this implementation. - error Module__FM_BC_Restricted_Bancor_Redeeming_VirtualSupply__FeatureDeactivated( - ); - - // ------------------------------------------------------------------------- - // Storage - - /// @dev Minter/Burner Role. - bytes32 public constant CURVE_INTERACTION_ROLE = "CURVE_USER"; - - /// @dev Storage gap for future upgrades. - uint[50] private __gap; - - // ------------------------------------------------------------------------- - // Public Functions - - /// @inheritdoc FM_BC_Bancor_Redeeming_VirtualSupply_v1 - /// @dev Adds additional role check to the buyFor function. - function buyFor(address _receiver, uint _depositAmount, uint _minAmountOut) - public - override - onlyModuleRole(CURVE_INTERACTION_ROLE) - { - super.buyFor(_receiver, _depositAmount, _minAmountOut); - } - - /// @inheritdoc FM_BC_Bancor_Redeeming_VirtualSupply_v1 - /// @dev Adds addtional role check to the sellTo function. - function sellTo(address _receiver, uint _depositAmount, uint _minAmountOut) - public - override - onlyModuleRole(CURVE_INTERACTION_ROLE) - { - super.sellTo(_receiver, _depositAmount, _minAmountOut); - } -} diff --git a/src/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.sol b/src/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.sol index ad543b158..333d5351f 100644 --- a/src/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.sol +++ b/src/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.sol @@ -99,6 +99,7 @@ abstract contract BondingCurveBase_v1 is IBondingCurveBase_v1, Module_v1 { function buyFor(address _receiver, uint _depositAmount, uint _minAmountOut) public virtual + permissioned buyingIsEnabled validReceiver(_receiver) { @@ -106,27 +107,32 @@ abstract contract BondingCurveBase_v1 is IBondingCurveBase_v1, Module_v1 { } /// @inheritdoc IBondingCurveBase_v1 - function buy(uint _depositAmount, uint _minAmountOut) public virtual { - buyFor(_msgSender(), _depositAmount, _minAmountOut); + function buy(uint _depositAmount, uint _minAmountOut) + public + virtual + permissioned + buyingIsEnabled + { + _buyOrder(_msgSender(), _depositAmount, _minAmountOut); } // ------------------------------------------------------------------------- - // OnlyOrchestrator Functions + // Permissioned Functions /// @inheritdoc IBondingCurveBase_v1 - function openBuy() external virtual onlyOrchestratorAdmin { + function openBuy() external virtual permissioned { buyIsOpen = true; emit BuyingEnabled(); } /// @inheritdoc IBondingCurveBase_v1 - function closeBuy() external virtual onlyOrchestratorAdmin { + function closeBuy() external virtual permissioned { buyIsOpen = false; emit BuyingDisabled(); } /// @inheritdoc IBondingCurveBase_v1 - function setBuyFee(uint _fee) external virtual onlyOrchestratorAdmin { + function setBuyFee(uint _fee) external virtual permissioned { _setBuyFee(_fee); } @@ -170,8 +176,8 @@ abstract contract BondingCurveBase_v1 is IBondingCurveBase_v1, Module_v1 { function withdrawProjectCollateralFee(address _receiver, uint _amount) public virtual + permissioned validReceiver(_receiver) - onlyOrchestratorAdmin { if (_amount > projectCollateralFeeCollected) { revert Module__BondingCurveBase__InvalidWithdrawAmount(); diff --git a/src/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.sol b/src/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.sol index ea205bcba..8212f1b44 100644 --- a/src/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.sol +++ b/src/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.sol @@ -85,6 +85,7 @@ abstract contract RedeemingBondingCurveBase_v1 is function sellTo(address _receiver, uint _depositAmount, uint _minAmountOut) public virtual + permissioned sellingIsEnabled validReceiver(_receiver) { @@ -92,27 +93,32 @@ abstract contract RedeemingBondingCurveBase_v1 is } /// @inheritdoc IRedeemingBondingCurveBase_v1 - function sell(uint _depositAmount, uint _minAmountOut) public virtual { - sellTo(_msgSender(), _depositAmount, _minAmountOut); + function sell(uint _depositAmount, uint _minAmountOut) + public + virtual + permissioned + sellingIsEnabled + { + _sellOrder(_msgSender(), _depositAmount, _minAmountOut); } // ------------------------------------------------------------------------- - // OnlyOrchestrator Functions + // Permissioned Functions /// @inheritdoc IRedeemingBondingCurveBase_v1 - function openSell() external virtual onlyOrchestratorAdmin { + function openSell() external virtual permissioned { sellIsOpen = true; emit SellingEnabled(); } /// @inheritdoc IRedeemingBondingCurveBase_v1 - function closeSell() external virtual onlyOrchestratorAdmin { + function closeSell() external virtual permissioned { sellIsOpen = false; emit SellingDisabled(); } /// @inheritdoc IRedeemingBondingCurveBase_v1 - function setSellFee(uint _fee) external virtual onlyOrchestratorAdmin { + function setSellFee(uint _fee) external virtual permissioned { _setSellFee(_fee); } diff --git a/src/modules/fundingManager/bondingCurve/interfaces/IBondingCurveBase_v1.sol b/src/modules/fundingManager/bondingCurve/interfaces/IBondingCurveBase_v1.sol index 863c956c9..9554a5331 100644 --- a/src/modules/fundingManager/bondingCurve/interfaces/IBondingCurveBase_v1.sol +++ b/src/modules/fundingManager/bondingCurve/interfaces/IBondingCurveBase_v1.sol @@ -98,6 +98,7 @@ interface IBondingCurveBase_v1 { // Functions /// @notice Buy tokens on behalf of a specified receiver address. + /// @dev Function access controlled by authorizer. /// @dev Redirects to the internal function `_buyOrder` by passing the receiver address and deposit amount. /// @param _receiver The address that will receive the bought tokens. /// @param _depositAmount The amount of collateral token deposited. @@ -106,24 +107,25 @@ interface IBondingCurveBase_v1 { external; /// @notice Buy tokens for the sender's address. + /// @dev Function access controlled by authorizer. /// @dev Redirects to the internal function `_buyOrder` by passing the sender's address and deposit amount. /// @param _depositAmount The amount of collateral token depoisited. /// @param _minAmountOut The minimum acceptable amount the user expects to receive from the transaction. function buy(uint _depositAmount, uint _minAmountOut) external; /// @notice Opens the buying functionality for the token. - /// @dev Only callable by the {Orchestrator_v1} admin. - /// Reverts if buying is already open. + /// @dev Function access controlled by authorizer. + /// @dev Reverts if buying is already open. function openBuy() external; /// @notice Closes the buying functionality for the token. - /// @dev Only callable by the {Orchestrator_v1} admin. - /// Reverts if buying is already closed. + /// @dev Function access controlled by authorizer. + /// @dev Reverts if buying is already closed. function closeBuy() external; /// @notice Sets the fee percentage for buying tokens, payed in collateral. - /// @dev Only callable by the {Orchestrator_v1} admin. - /// The fee cannot exceed 10000 basis points. Reverts if an invalid fee is provided. + /// @dev Function access controlled by authorizer. + /// @dev The fee cannot exceed 10000 basis points. Reverts if an invalid fee is provided. /// @param _fee The fee in basis points. function setBuyFee(uint _fee) external; @@ -141,6 +143,7 @@ interface IBondingCurveBase_v1 { returns (uint mintAmount); /// @notice Withdraw project collateral fee to the receiver address. + /// @dev Function access controlled by authorizer. /// @param _receiver The address that will receive the fee. /// @param _amount The amount of fee to withdraw. function withdrawProjectCollateralFee(address _receiver, uint _amount) diff --git a/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_Bancor_Redeeming_VirtualSupply_v1.sol b/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_Bancor_Redeeming_VirtualSupply_v1.sol index fdaf3d1d5..b37b0c8a0 100644 --- a/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_Bancor_Redeeming_VirtualSupply_v1.sol +++ b/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_Bancor_Redeeming_VirtualSupply_v1.sol @@ -111,14 +111,14 @@ interface IFM_BC_Bancor_Redeeming_VirtualSupply_v1 { /// @notice Set the reserve ratio used for issuing tokens on a bonding /// curve. - /// @dev This function can only be called by the {Orchestrator_v1} admin. + /// @dev Function access controlled by authorizer. /// @param reserveRatio_ The new reserve ratio for buying, expressed in /// PPM. function setReserveRatioForBuying(uint32 reserveRatio_) external; /// @notice Set the reserve ratio used for redeeming tokens on a bonding /// curve. - /// @dev This function can only be called by the {Orchestrator_v1} admin. + /// @dev Function access controlled by authorizer. /// @param reserveRatio_ The new reserve ratio for selling, expressed in /// PPM. function setReserveRatioForSelling(uint32 reserveRatio_) external; diff --git a/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol b/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol index 23a5642ef..45645702f 100644 --- a/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol +++ b/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.sol @@ -78,9 +78,6 @@ interface IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is /// @notice Emits when the token vault gets updated. event TokenVaultSet(address tokenVault); - /// @notice Emits when buy and sell restriction is set. - event BuyAndSellIsRestricted(); - /// @notice Emits when buy and sell restriction is removed. event BuyAndSellIsUnrestricted(); @@ -118,61 +115,48 @@ interface IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 is /// @return tokenVault_ The address of the token vault. function getTokenVault() external view returns (address tokenVault_); - /// @notice Returns whether buy and sell is restricted. - /// @return buyAndSellIsRestricted_ Whether buy and sell is restricted. - function isBuyAndSellRestricted() - external - view - returns (bool buyAndSellIsRestricted_); - // ======================================================================== // Public Mutating Functions - // Mutating - Token Manipulation Functions + // ------------------------------------------------------------------------- + // Mutating - Permissioned Functions /// @notice Burn amount of tokens from message sender. + /// @dev Function access controlled by authorizer. /// @param amount_ Amount token to be burned. function burnIssuanceToken(uint amount_) external; /// @notice Burn `amount` tokens belonging to `owner`. + /// @dev Function access controlled by authorizer. /// @param owner_ Address whose tokens will be burnt. /// @param amount_ Burn amount. function burnIssuanceTokenFor(address owner_, uint amount_) external; - // ------------------------------------------------------------------------- - // Mutating - OnlyCoverManager Functions - - /// @notice Restricts buying and selling functionalities to the - /// CURVE_INTERACTION_ROLE. - /// @dev Only callable by the COVER_MANAGER_ROLE. - function restrictBuyAndSell() external; - - /// @notice Unrestricts buying and selling functionalities to the - /// CURVE_INTERACTION_ROLE. - /// @dev Only callable by the COVER_MANAGER_ROLE. - function unrestrictBuyAndSell() external; - - /// @notice Allows the COVER_MANAGER_ROLE to seize assets from this pool. - /// @dev As the COVER_MANAGER_ROLE has ability to basically rug - /// the projects, a timelock and max. - /// seizable percentage has been added. + /// @notice Seizes assets from this pool. + /// @dev Function access controlled by authorizer. + /// @dev This function has a timelock and max. + /// seizable percentage to prevent rugging the projects. /// @param amount_ Number of tokens to be removed from the pool. function seize(uint amount_) external; /// @notice Adjust the seize percentage, which is seizable from the /// contract. + /// @dev Function access controlled by authorizer. /// @param seize_ The seize in percentage, expressed as BPS. function adjustSeize(uint64 seize_) external; /// @notice Sets a new liquidity valut controller address. + /// @dev Function access controlled by authorizer. /// @param lvc_ Address of the liquidity vault controller. function setLiquidityVaultControllerContract(address lvc_) external; - // ------------------------------------------------------------------------- - // Mutating - OnlyOrchestratorAdmin Functions + /// @notice Sets the Repayable amount. + /// @dev Function access controlled by authorizer. + /// @param amount_ The new Repayable amount. + function setRepayableAmount(uint amount_) external; /// @notice Sets the token vault address. - /// @dev Only callable by OrchestratorAdmin. + /// @dev Function access controlled by authorizer. /// @param tokenVault_ The address of the token vault. function setTokenVault(address tokenVault_) external; } diff --git a/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_v1.sol b/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_v1.sol index eb69c5897..445ce8b81 100644 --- a/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_v1.sol +++ b/src/modules/fundingManager/bondingCurve/interfaces/IFM_BC_BondingSurface_Redeeming_v1.sol @@ -135,13 +135,15 @@ interface IFM_BC_BondingSurface_Redeeming_v1 is // Public Mutating Functions // ------------------------------------------------------------------------ - // Mutating - OnlyOrchestratorAdmin Functions + // Mutating - Permissioned Functions /// @notice Update the capital required used for the bonding curve. + /// @dev Function access controlled by authorizer. /// @param newCapitalRequired_ The new capital required. function setCapitalRequired(uint newCapitalRequired_) external; /// @notice Update the base price multiplier used for the bonding curve. + /// @dev Function access controlled by authorizer. /// @param newBasePriceMultiplier_ The new base price multiplier. function setBasePriceMultiplier(uint newBasePriceMultiplier_) external; } diff --git a/src/modules/fundingManager/bondingCurve/interfaces/IRedeemingBondingCurveBase_v1.sol b/src/modules/fundingManager/bondingCurve/interfaces/IRedeemingBondingCurveBase_v1.sol index 4dfd770ce..bc6db48be 100644 --- a/src/modules/fundingManager/bondingCurve/interfaces/IRedeemingBondingCurveBase_v1.sol +++ b/src/modules/fundingManager/bondingCurve/interfaces/IRedeemingBondingCurveBase_v1.sol @@ -61,6 +61,7 @@ interface IRedeemingBondingCurveBase_v1 is IBondingCurveBase_v1 { // Functions /// @notice Redeem tokens and directs the proceeds to a specified receiver address. + /// @dev Function access controlled by authorizer. /// @dev This function wraps the `_sellOrder` internal function with specified parameters to handle /// the transaction and direct the proceeds. /// @param _receiver The address that will receive the redeemed tokens. @@ -70,24 +71,25 @@ interface IRedeemingBondingCurveBase_v1 is IBondingCurveBase_v1 { external; /// @notice Redeem collateral for the sender's address. + /// @dev Function access controlled by authorizer. /// @dev Redirects to the internal function `_sellOrder` by passing the sender's address and deposit amount. /// @param _depositAmount The amount of issued token deposited. /// @param _minAmountOut The minimum acceptable amount the user expects to receive from the transaction. function sell(uint _depositAmount, uint _minAmountOut) external; /// @notice Opens the selling functionality for the collateral. - /// @dev Only callable by the {Orchestrator_v1} admin. - /// Reverts if selling is already open. + /// @dev Function access controlled by authorizer. + /// @dev Reverts if selling is already open. function openSell() external; /// @notice Closes the selling functionality for the collateral. - /// @dev Only callable by the {Orchestrator_v1} admin. - /// Reverts if selling is already closed. + /// @dev Function access controlled by authorizer. + /// @dev Reverts if selling is already closed. function closeSell() external; /// @notice Sets the fee percentage for selling collateral, payed in collateral. - /// @dev Only callable by the {Orchestrator_v1} admin. - /// The fee cannot exceed 10000 basis points. Reverts if an invalid fee is provided. + /// @dev Function access controlled by authorizer. + /// @dev The fee cannot exceed 10000 basis points. Reverts if an invalid fee is provided. /// @param _fee The fee in basis points. function setSellFee(uint _fee) external; diff --git a/src/modules/fundingManager/bondingCurve/interfaces/IVirtualIssuanceSupplyBase_v1.sol b/src/modules/fundingManager/bondingCurve/interfaces/IVirtualIssuanceSupplyBase_v1.sol index 3fd3e7c1f..2eb493304 100644 --- a/src/modules/fundingManager/bondingCurve/interfaces/IVirtualIssuanceSupplyBase_v1.sol +++ b/src/modules/fundingManager/bondingCurve/interfaces/IVirtualIssuanceSupplyBase_v1.sol @@ -37,6 +37,7 @@ interface IVirtualIssuanceSupplyBase_v1 { //-------------------------------------------------------------------------- // Functions + /// @dev Function access controlled by authorizer. /// @notice Sets the virtual issuance supply to a new value. /// @dev This function calls the internal function `_setVirtualIssuanceSupply`. /// The function must be implemented by the downstream contract. The downstream contract should diff --git a/src/modules/fundingManager/depositVault/FM_DepositVault_v1.sol b/src/modules/fundingManager/depositVault/FM_DepositVault_v1.sol index e2d5b3fe1..17c7a30c3 100644 --- a/src/modules/fundingManager/depositVault/FM_DepositVault_v1.sol +++ b/src/modules/fundingManager/depositVault/FM_DepositVault_v1.sol @@ -118,7 +118,7 @@ contract FM_DepositVault_v1 is } //-------------------------------------------------------------------------- - // OnlyOrchestrator Mutating Functions + // PaymentClient Mutating Functions /// @inheritdoc IFundingManager_v1 function transferOrchestratorToken(address to, uint amount) @@ -127,7 +127,6 @@ contract FM_DepositVault_v1 is validAddress(to) { token().safeTransfer(to, amount); - emit TransferOrchestratorToken(to, amount); } diff --git a/src/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.sol b/src/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.sol index 8d6724e63..5c04a70d2 100644 --- a/src/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.sol +++ b/src/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.sol @@ -61,7 +61,7 @@ contract FM_EXT_TokenVault_v1 is IFM_EXT_TokenVault_v1, Module_v1 { function withdraw(address token_, uint amount_, address recipient_) external virtual - onlyOrchestratorAdmin + permissioned validAddress(token_) amountIsValid(amount_) validAddress(recipient_) diff --git a/src/modules/fundingManager/extensions/interfaces/IFM_EXT_TokenVault_v1.sol b/src/modules/fundingManager/extensions/interfaces/IFM_EXT_TokenVault_v1.sol index a880e6932..201bd4d04 100644 --- a/src/modules/fundingManager/extensions/interfaces/IFM_EXT_TokenVault_v1.sol +++ b/src/modules/fundingManager/extensions/interfaces/IFM_EXT_TokenVault_v1.sol @@ -41,7 +41,7 @@ interface IFM_EXT_TokenVault_v1 { // Public Mutating Functions /// @notice Allows for withdrawal of reserve tokens. - /// @dev This function is only callable by the orchestrator admin. + /// @dev Function access controlled by authorizer. /// @param token_ The token to withdraw. /// @param amount_ The amount of tokens to withdraw. /// @param recipient_ The address to send the tokens to. diff --git a/src/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.sol b/src/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.sol index 4c4a389b7..2b08e5828 100644 --- a/src/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.sol +++ b/src/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.sol @@ -57,9 +57,6 @@ import {ERC165Upgradeable} from * Mints new tokens during purchases and burns tokens during * sell operations at oracle-determined prices. * - * - Whitelisting system for controlled token distribution. - * Restricts token purchases and sales to approved addresses. - * * - Queue-based redemption and payment processing. * Creates payment orders in a queue and sends them to the payment * processor for executing token redemptions. @@ -89,84 +86,48 @@ import {ERC165Upgradeable} from * setter function. * - Example: module.setOracleAddress(oracleAddress); * - * 3. Setup Whitelist: + * 3. Enable Trading: + * - Purpose: Makes the the buy/sell functionality of the + * contract public. Trading must be explicitly + * enabled. + * - How: The OrchestratorAdmin must enable both buying + * and selling operations separately. + * - Example: authorizer.addAccessPermission(buy.selector); + * authorizer.addAccessPermission(sell.selector); + * module.openBuy(); + * module.openSell(); + * + * OPTIONAL setup steps for enhanced administration: + * + * 1. Setup Whitelist: * - Purpose: Implements access control for buy/sell * functions. Only whitelisted addresses can * participate in token buy & sell operations to * provide a security layer for controlled token * distribution and compliance. - * - How: The OrchestratorAdmin (or WHITELIST_ROLE_ADMIN - * if configured) must: - * 1. Retrieve the whitelist role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getWhitelistRole(), - * userAddress - * ); + * - How: The OrchestratorAdmin must: + * 1. Create a whitelist role + * 2. Add access permission for the buy() and + * sell() functions to the whitelist role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); + * - Notice: This assumes that the function access + * permissions currently don't contain the + * public role. * - * 4. Setup Queue Executors: + * 2. Setup Queue Executors: * - Purpose: Implements access control for authorized * addresses that can process the redemption * queue. - * - How: The OrchestratorAdmin (or - * QUEUE_EXECUTOR_ROLE_ADMIN if configured) must: - * 1. Retrieve the executor role identifier. - * 2. Grant the role to designated executors. - * - Example: module.grantModuleRole( - * module.getQueueExecutorRole(), - * executorAddress - * ); - * - * 5. Enable Trading: - * - Purpose: Activates the buy/sell functionality of the - * contract. Trading must be explicitly enabled. - * - How: The OrchestratorAdmin must enable both buying - * and selling operations separately. - * - Example: module.openBuy(); - * module.openSell(); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Whitelist Admin: - * - Purpose: Enables delegation of whitelist management to - * a dedicated admin role instead of relying on - * the OrchestratorAdmin. This allows for more - * granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getWhitelistRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getWhitelistRoleAdmin() - * ) - * ); - * - * 2. Custom Queue Executor Admin: - * - Purpose: Allows delegation of queue executor - * management to a dedicated admin role instead - * of the OrchestratorAdmin. This allows for - * more granular access control and operational - * flexibility. - * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the - * Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueExecutorRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueExecutorRoleAdmin() - * ) - * ); + * 1. Create a queue executor role + * 2. Add access permission for the executeRedemptionQueue() function. + * 3. Grant the role to designated executors. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to @@ -207,27 +168,6 @@ contract FM_PC_Oracle_Redeeming_v1 is // ------------------------------------------------------------------------- // Constants - /// @notice Role identifier for accounts who are whitelisted to buy and sell. - bytes32 internal constant WHITELIST_ROLE = "WHITELIST_ROLE"; - - /// @notice Role identifier for the admin authorized to assign the whitelist - /// role. - /// @dev This role should be set as the role admin for the WHITELIST_ROLE - /// within the Authorizer module. - bytes32 internal constant WHITELIST_ROLE_ADMIN = "WHITELIST_ROLE_ADMIN"; - - /// @notice Role identifier for accounts who are allowed to manually execute - /// the redemption queue. - bytes32 internal constant QUEUE_EXECUTOR_ROLE = "QUEUE_EXECUTOR_ROLE"; - - /// @notice Role identifier for the admin authorized to assign the queue - /// execution role. - /// role. - /// @dev This role should be set as the role admin for the - /// QUEUE_EXECUTOR_ROLE within the Authorizer module. - bytes32 internal constant QUEUE_EXECUTOR_ROLE_ADMIN = - "QUEUE_EXECUTOR_ROLE_ADMIN"; - /// @notice Flag used for the payment order. uint internal constant FLAG_ORDER_ID = 0; @@ -369,41 +309,6 @@ contract FM_PC_Oracle_Redeeming_v1 is // ------------------------------------------------------------------------- // Public View Functions - /// @inheritdoc IFM_PC_Oracle_Redeeming_v1 - function getWhitelistRole() public pure virtual returns (bytes32 role_) { - return WHITELIST_ROLE; - } - - /// @inheritdoc IFM_PC_Oracle_Redeeming_v1 - function getWhitelistRoleAdmin() - public - pure - virtual - returns (bytes32 role_) - { - return WHITELIST_ROLE_ADMIN; - } - - /// @inheritdoc IFM_PC_Oracle_Redeeming_v1 - function getQueueExecutorRole() - public - pure - virtual - returns (bytes32 role_) - { - return QUEUE_EXECUTOR_ROLE; - } - - /// @inheritdoc IFM_PC_Oracle_Redeeming_v1 - function getQueueExecutorRoleAdmin() - public - pure - virtual - returns (bytes32 role_) - { - return QUEUE_EXECUTOR_ROLE_ADMIN; - } - /// @inheritdoc IFundingManager_v1 function token() public view virtual override returns (IERC20 token_) { return _token; @@ -547,43 +452,21 @@ contract FM_PC_Oracle_Redeeming_v1 is // ------------------------------------------------------------------------- // Public Mutating Functions - /// @inheritdoc BondingCurveBase_v1 - function buy(uint collateralAmount_, uint minAmountOut_) - public - virtual - override(BondingCurveBase_v1, IBondingCurveBase_v1) - onlyModuleRole(WHITELIST_ROLE) - { - super.buyFor(_msgSender(), collateralAmount_, minAmountOut_); - } - /// @inheritdoc BondingCurveBase_v1 function buyFor(address receiver_, uint depositAmount_, uint minAmountOut_) public virtual override(BondingCurveBase_v1, IBondingCurveBase_v1) - onlyModuleRole(WHITELIST_ROLE) thirdPartyOperationsEnabled { super.buyFor(receiver_, depositAmount_, minAmountOut_); } - /// @inheritdoc RedeemingBondingCurveBase_v1 - function sell(uint depositAmount_, uint minAmountOut_) - public - virtual - override(RedeemingBondingCurveBase_v1, IRedeemingBondingCurveBase_v1) - onlyModuleRole(WHITELIST_ROLE) - { - super.sellTo(_msgSender(), depositAmount_, minAmountOut_); - } - /// @inheritdoc RedeemingBondingCurveBase_v1 function sellTo(address receiver_, uint depositAmount_, uint minAmountOut_) public virtual override(RedeemingBondingCurveBase_v1, IRedeemingBondingCurveBase_v1) - onlyModuleRole(WHITELIST_ROLE) thirdPartyOperationsEnabled { super.sellTo(receiver_, depositAmount_, minAmountOut_); @@ -626,17 +509,13 @@ contract FM_PC_Oracle_Redeeming_v1 is function setProjectTreasury(address projectTreasury_) external virtual - onlyOrchestratorAdmin + permissioned { _setProjectTreasury(projectTreasury_); } /// @inheritdoc IFM_PC_Oracle_Redeeming_v1 - function setOracleAddress(address oracle_) - external - virtual - onlyOrchestratorAdmin - { + function setOracleAddress(address oracle_) external virtual permissioned { _setOracleAddress(oracle_); } @@ -644,17 +523,13 @@ contract FM_PC_Oracle_Redeeming_v1 is function setIsDirectOperationsOnly(bool isDirectOperationsOnly_) public virtual - onlyOrchestratorAdmin + permissioned { _setIsDirectOperationsOnly(isDirectOperationsOnly_); } /// @inheritdoc IFM_PC_Oracle_Redeeming_v1 - function executeRedemptionQueue() - external - virtual - onlyModuleRole(QUEUE_EXECUTOR_ROLE) - { + function executeRedemptionQueue() external virtual permissioned { (bool success, bytes memory data) = address( __Module_orchestrator.paymentProcessor() ).call( diff --git a/src/modules/fundingManager/oracle/interfaces/IFM_PC_Oracle_Redeeming_v1.sol b/src/modules/fundingManager/oracle/interfaces/IFM_PC_Oracle_Redeeming_v1.sol index 1ecdf63ef..c50f57da3 100644 --- a/src/modules/fundingManager/oracle/interfaces/IFM_PC_Oracle_Redeeming_v1.sol +++ b/src/modules/fundingManager/oracle/interfaces/IFM_PC_Oracle_Redeeming_v1.sol @@ -31,9 +31,6 @@ import {IRedeemingBondingCurveBase_v1} from * Mints new tokens during purchases and burns tokens during * sell operations at oracle-determined prices. * - * - Whitelisting system for controlled token distribution. - * Restricts token purchases and sales to approved addresses. - * * - Queue-based redemption and payment processing. * Creates payment orders in a queue and sends them to the payment * processor for executing token redemptions. @@ -63,84 +60,48 @@ import {IRedeemingBondingCurveBase_v1} from * setter function. * - Example: module.setOracleAddress(oracleAddress); * - * 3. Setup Whitelist: + * 3. Enable Trading: + * - Purpose: Makes the the buy/sell functionality of the + * contract public. Trading must be explicitly + * enabled. + * - How: The OrchestratorAdmin must enable both buying + * and selling operations separately. + * - Example: authorizer.addAccessPermission(buy.selector); + * authorizer.addAccessPermission(sell.selector); + * module.openBuy(); + * module.openSell(); + * + * OPTIONAL setup steps for enhanced administration: + * + * 1. Setup Whitelist: * - Purpose: Implements access control for buy/sell * functions. Only whitelisted addresses can * participate in token buy & sell operations to * provide a security layer for controlled token * distribution and compliance. - * - How: The OrchestratorAdmin (or WHITELIST_ROLE_ADMIN - * if configured) must: - * 1. Retrieve the whitelist role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getWhitelistRole(), - * userAddress - * ); + * - How: The OrchestratorAdmin must: + * 1. Create a whitelist role + * 2. Add access permission for the buy() and + * sell() functions to the whitelist role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); + * - Notice: This assumes that the function access + * permissions currently don't contain the + * public role. * - * 4. Setup Queue Executors: + * 2. Setup Queue Executors: * - Purpose: Implements access control for authorized * addresses that can process the redemption * queue. - * - How: The OrchestratorAdmin (or - * QUEUE_EXECUTOR_ROLE_ADMIN if configured) must: - * 1. Retrieve the executor role identifier. - * 2. Grant the role to designated executors. - * - Example: module.grantModuleRole( - * module.getQueueExecutorRole(), - * executorAddress - * ); - * - * 5. Enable Trading: - * - Purpose: Activates the buy/sell functionality of the - * contract. Trading must be explicitly enabled. - * - How: The OrchestratorAdmin must enable both buying - * and selling operations separately. - * - Example: module.openBuy(); - * module.openSell(); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Whitelist Admin: - * - Purpose: Enables delegation of whitelist management to - * a dedicated admin role instead of relying on - * the OrchestratorAdmin. This allows for more - * granular access control and operational - * flexibility. - * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getWhitelistRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getWhitelistRoleAdmin() - * ) - * ); - * - * 2. Custom Queue Executor Admin: - * - Purpose: Allows delegation of queue executor - * management to a dedicated admin role instead - * of the OrchestratorAdmin. This allows for - * more granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the - * Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueExecutorRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueExecutorRoleAdmin() - * ) - * ); + * 1. Create a queue executor role + * 2. Add access permission for the executeRedemptionQueue() function. + * 3. Grant the role to designated executors. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to @@ -317,25 +278,6 @@ interface IFM_PC_Oracle_Redeeming_v1 is /// @return fee_ The current sell fee. function getSellFee() external view returns (uint fee_); - /// @notice Gets the whitelist role identifier - /// @return role_ The whitelist role identifier - function getWhitelistRole() external pure returns (bytes32 role_); - - /// @notice Gets the whitelist role admin identifier - /// @return role_ The whitelist role admin identifier - function getWhitelistRoleAdmin() external pure returns (bytes32 role_); - - /// @notice Gets the queue executor role identifier - /// @return role_ The queue executor role identifier - function getQueueExecutorRole() external pure returns (bytes32 role_); - - /// @notice Gets the queue executor role admin identifier - /// @return role_ The queue executor role admin identifier - function getQueueExecutorRoleAdmin() - external - pure - returns (bytes32 role_); - /// @notice Gets the oracle address. /// @return oracle_ The address of the oracle. function getOracle() external view returns (address oracle_); @@ -344,23 +286,28 @@ interface IFM_PC_Oracle_Redeeming_v1 is // External Functions /// @notice Allows depositing collateral to provide reserves for redemptions. + /// @dev This function is always publicly callable. /// @param amount_ The amount of collateral to deposit. function depositReserve(uint amount_) external; /// @notice Sets the project treasury address. - /// @param projectTreasury_ The address of the project treasury. + /// @dev Function access controlled by authorizer. + /// @param projectTreasury_ The address of the project treasury. function setProjectTreasury(address projectTreasury_) external; /// @notice Sets the oracle address. - /// @param oracle_ The address of the oracle. + /// @dev Function access controlled by authorizer. + /// @param oracle_ The address of the oracle. function setOracleAddress(address oracle_) external; /// @notice Toggles whether the contract only allows direct operations or not. + /// @dev Function access controlled by authorizer. /// @param isDirectOperationsOnly_ The new value for the flag. function setIsDirectOperationsOnly(bool isDirectOperationsOnly_) external; /// @notice Manually executes the redemption queue in the workflows Payment /// Processor. + /// @dev Function access controlled by authorizer. /// @dev If this function is called but the Payment Processor does not /// implement the option to manually execute the redemption queue /// then this function will revert. diff --git a/src/modules/logicModule/LM_Oracle_Permissioned_v1.sol b/src/modules/logicModule/LM_Oracle_Permissioned_v1.sol index e3356925c..4823f22e8 100644 --- a/src/modules/logicModule/LM_Oracle_Permissioned_v1.sol +++ b/src/modules/logicModule/LM_Oracle_Permissioned_v1.sol @@ -32,7 +32,7 @@ import {ERC165Upgradeable} from * and redemption operations. * * - Manual price setting. - * Prices are manually set by the price setter role and must be + * Prices are manually set and must be * non-zero values. * * - Price decimal denominations. @@ -44,41 +44,21 @@ import {ERC165Upgradeable} from * - To price redeeming 1 token at 0.5 collateral with 6 decimal * collateral: 500_000 * - * @custom:setup This module requires the following MANDATORY setup steps: + * @custom:setup OPTIONAL setup steps for enhanced administration: * * 1. Configure Price Setter Role: * - Purpose: The price setter role is authorized to set * prices for issuance and redemption operations. - * - How: The OrchestratorAdmin (or PRICE_SETTER_ROLE_ADMIN - * if configured) must: - * 1. Retrieve the price setter role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getPriceSetterRole(), - * operatorAddress - * ); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Price Setter Role Admin: - * - Purpose: Enables delegation of price setter role - * management to a dedicated admin role instead of - * relying on the OrchestratorAdmin. This allows - * for more granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getPriceSetterRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getPriceSetterRoleAdmin() - * ) - * ); + * 1. Create a price setter role + * 2. Add access permission for the + * setIssuancePrice(), setRedemptionPrice() + * and setIssuanceAndRedemptionPrice() + * functions to the price setter role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer @@ -107,20 +87,6 @@ contract LM_Oracle_Permissioned_v1 is ILM_Oracle_Permissioned_v1, Module_v1 { || super.supportsInterface(interfaceId); } - // ------------------------------------------------------------------------- - // Constants - - /// @notice Role identifier for accounts authorized to set prices. - /// @dev This role should be granted to trusted price feeders only. - bytes32 internal constant PRICE_SETTER_ROLE = "PRICE_SETTER_ROLE"; - - /// @notice Role identifier for the admin authorized to assign the price - /// setter role. - /// @dev This role should be set as the role admin within the Authorizer - /// module. - bytes32 internal constant PRICE_SETTER_ROLE_ADMIN = - "PRICE_SETTER_ROLE_ADMIN"; - // ------------------------------------------------------------------------- // State Variables @@ -184,39 +150,16 @@ contract LM_Oracle_Permissioned_v1 is ILM_Oracle_Permissioned_v1, Module_v1 { return _redemptionPrice; } - /// @inheritdoc ILM_Oracle_Permissioned_v1 - function getPriceSetterRole() external pure virtual returns (bytes32) { - return PRICE_SETTER_ROLE; - } - - /// @inheritdoc ILM_Oracle_Permissioned_v1 - function getPriceSetterRoleAdmin() - external - pure - virtual - returns (bytes32) - { - return PRICE_SETTER_ROLE_ADMIN; - } - //-------------------------------------------------------------------------- // Public Mutating Functions /// @inheritdoc ILM_Oracle_Permissioned_v1 - function setIssuancePrice(uint price_) - external - virtual - onlyModuleRole(PRICE_SETTER_ROLE) - { + function setIssuancePrice(uint price_) external virtual permissioned { _setIssuancePrice(price_); } /// @inheritdoc ILM_Oracle_Permissioned_v1 - function setRedemptionPrice(uint price_) - external - virtual - onlyModuleRole(PRICE_SETTER_ROLE) - { + function setRedemptionPrice(uint price_) external virtual permissioned { _setRedemptionPrice(price_); } @@ -224,7 +167,7 @@ contract LM_Oracle_Permissioned_v1 is ILM_Oracle_Permissioned_v1, Module_v1 { function setIssuanceAndRedemptionPrice( uint issuancePrice_, uint redemptionPrice_ - ) external virtual onlyModuleRole(PRICE_SETTER_ROLE) { + ) external virtual permissioned { _setIssuancePrice(issuancePrice_); _setRedemptionPrice(redemptionPrice_); } diff --git a/src/modules/logicModule/LM_PC_Bounties_v2.sol b/src/modules/logicModule/LM_PC_Bounties_v2.sol index 51b314424..8be2ad16c 100644 --- a/src/modules/logicModule/LM_PC_Bounties_v2.sol +++ b/src/modules/logicModule/LM_PC_Bounties_v2.sol @@ -234,13 +234,6 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { /// @dev Marks the beginning of the list. uint internal constant _SENTINEL = type(uint).max; - /// @dev Role for the bounty issuer. - bytes32 public constant BOUNTY_ISSUER_ROLE = "BOUNTY_ISSUER"; - /// @dev Role for the claimant. - bytes32 public constant CLAIMANT_ROLE = "CLAIMANT"; - /// @dev Role for the verifier. - bytes32 public constant VERIFIER_ROLE = "VERIFIER"; - //-------------------------------------------------------------------------- // Storage @@ -344,7 +337,7 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { bytes calldata details ) external - onlyModuleRole(BOUNTY_ISSUER_ROLE) + permissioned validPayoutAmounts(minimumPayoutAmount, maximumPayoutAmount) returns (uint id) { @@ -358,7 +351,7 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { bytes[] calldata detailArray ) external - onlyModuleRole(BOUNTY_ISSUER_ROLE) + permissioned validArrayLengths( minimumPayoutAmounts.length, maximumPayoutAmounts.length, @@ -384,7 +377,7 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { /// @inheritdoc ILM_PC_Bounties_v2 function updateBounty(uint bountyId, bytes calldata details) external - onlyModuleRole(BOUNTY_ISSUER_ROLE) + permissioned validBountyId(bountyId) notLocked(bountyId) { @@ -396,7 +389,7 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { /// @inheritdoc ILM_PC_Bounties_v2 function lockBounty(uint bountyId) external - onlyModuleRole(BOUNTY_ISSUER_ROLE) + permissioned validBountyId(bountyId) notLocked(bountyId) { @@ -412,7 +405,7 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { bytes calldata details ) external - onlyModuleRole(CLAIMANT_ROLE) + permissioned validBountyId(bountyId) notLocked(bountyId) returns (uint id) @@ -452,10 +445,10 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { Contributor[] calldata contributors ) external + permissioned validClaimId(claimId) notClaimed(claimId) notLocked(_claimRegistry[claimId].bountyId) - onlyModuleRole(CLAIMANT_ROLE) { _validContributorsForBounty( contributors, _bountyRegistry[_claimRegistry[claimId].bountyId] @@ -503,7 +496,7 @@ contract LM_PC_Bounties_v2 is ILM_PC_Bounties_v2, ERC20PaymentClientBase_v2 { /// @inheritdoc ILM_PC_Bounties_v2 function verifyClaim(uint claimId, Contributor[] calldata contributors) external - onlyModuleRole(VERIFIER_ROLE) + permissioned validClaimId(claimId) notClaimed(claimId) notLocked(_claimRegistry[claimId].bountyId) diff --git a/src/modules/logicModule/LM_PC_KPIRewarder_v2.sol b/src/modules/logicModule/LM_PC_KPIRewarder_v2.sol index 40dfbbea3..ac2bae73f 100644 --- a/src/modules/logicModule/LM_PC_KPIRewarder_v2.sol +++ b/src/modules/logicModule/LM_PC_KPIRewarder_v2.sol @@ -172,7 +172,7 @@ contract LM_PC_KPIRewarder_v2 is uint assertedValue, address asserter, uint targetKPI - ) public onlyModuleRole(ASSERTER_ROLE) returns (bytes32 assertionId) { + ) public permissioned returns (bytes32 assertionId) { // ================================================================== // Pre-check @@ -218,9 +218,9 @@ contract LM_PC_KPIRewarder_v2 is /// @inheritdoc ILM_PC_KPIRewarder_v2 /// @dev Top up funds to pay the optimistic oracle fee + function depositFeeFunds(uint amount) external - onlyOrchestratorAdmin nonReentrant validAmount(amount) { @@ -234,7 +234,7 @@ contract LM_PC_KPIRewarder_v2 is bool _continuous, uint[] calldata _trancheValues, uint[] calldata _trancheRewards - ) external onlyOrchestratorAdmin returns (uint) { + ) external permissioned returns (uint) { uint _numOfTranches = _trancheValues.length; if (_numOfTranches < 1 || _numOfTranches > 20) { @@ -288,6 +288,7 @@ contract LM_PC_KPIRewarder_v2 is external override nonReentrant + permissioned validAmount(amount) { // ================================================================== @@ -307,10 +308,7 @@ contract LM_PC_KPIRewarder_v2 is } /// @inheritdoc ILM_PC_KPIRewarder_v2 - function deleteStuckAssertion(bytes32 assertionId) - public - onlyOrchestratorAdmin - { + function deleteStuckAssertion(bytes32 assertionId) public permissioned { // Ensure the assertionId exists in this contract (since malicious assertions could callback this contract) if (assertionData[assertionId].dataId == bytes32(0x0)) { revert Module__LM_PC_KPIRewarder_v2__NonExistentAssertionId( diff --git a/src/modules/logicModule/LM_PC_PaymentRouter_v2.sol b/src/modules/logicModule/LM_PC_PaymentRouter_v2.sol index 2eb40e782..e3cf5546a 100644 --- a/src/modules/logicModule/LM_PC_PaymentRouter_v2.sol +++ b/src/modules/logicModule/LM_PC_PaymentRouter_v2.sol @@ -57,9 +57,6 @@ contract LM_PC_PaymentRouter_v2 is //-------------------------------------------------------------------------- // Storage - /// @dev The role that allows the pushing of payments. - bytes32 public constant PAYMENT_PUSHER_ROLE = "PAYMENT_PUSHER"; - uint8 public constant FLAG_START = 1; uint8 public constant FLAG_CLIFF = 2; uint8 public constant FLAG_END = 3; @@ -93,7 +90,7 @@ contract LM_PC_PaymentRouter_v2 is uint start, uint cliff, uint end - ) public onlyModuleRole(PAYMENT_PUSHER_ROLE) { + ) public permissioned { bytes32 flags; bytes32[] memory data; @@ -133,7 +130,7 @@ contract LM_PC_PaymentRouter_v2 is uint start, uint cliff, uint end - ) public onlyModuleRole(PAYMENT_PUSHER_ROLE) { + ) public permissioned { // Validate all arrays have the same length if ( recipients.length != numOfOrders diff --git a/src/modules/logicModule/LM_PC_RecurringPayments_v2.sol b/src/modules/logicModule/LM_PC_RecurringPayments_v2.sol index e9279a506..86fc18191 100644 --- a/src/modules/logicModule/LM_PC_RecurringPayments_v2.sol +++ b/src/modules/logicModule/LM_PC_RecurringPayments_v2.sol @@ -219,7 +219,7 @@ contract LM_PC_RecurringPayments_v2 is address recipient ) external - onlyOrchestratorAdmin + permissioned validAmount(amount) validStartEpoch(startEpoch) validRecipient(recipient) @@ -251,7 +251,7 @@ contract LM_PC_RecurringPayments_v2 is /// @inheritdoc ILM_PC_RecurringPayments_v2 function removeRecurringPayment(uint prevId, uint id) external - onlyOrchestratorAdmin + permissioned { // trigger to resolve the given Payment _triggerFor(id, _paymentList.getNextId(id)); @@ -269,11 +269,13 @@ contract LM_PC_RecurringPayments_v2 is // Trigger /// @inheritdoc ILM_PC_RecurringPayments_v2 + /// @dev This function is always publicly callable. function trigger() external { _triggerFor(_paymentList.getNextId(_SENTINEL), _SENTINEL); } /// @inheritdoc ILM_PC_RecurringPayments_v2 + /// @dev This function is always publicly callable. function triggerFor(uint startId, uint endId) external validId(startId) @@ -285,10 +287,13 @@ contract LM_PC_RecurringPayments_v2 is _triggerFor(startId, _paymentList.getNextId(endId)); } + //-------------------------------------------------------------------------- + // Internal Functions + /// @dev Triggers the given RecurringPayment. /// @param startId The id of the first RecurringPayment to trigger. /// @param endId The id of the last RecurringPayment to trigger. - function _triggerFor(uint startId, uint endId) private { + function _triggerFor(uint startId, uint endId) internal { // Set startId to be the current position in List uint currentId = startId; diff --git a/src/modules/logicModule/LM_PC_Staking_v2.sol b/src/modules/logicModule/LM_PC_Staking_v2.sol index 6909e22ad..2c3b61370 100644 --- a/src/modules/logicModule/LM_PC_Staking_v2.sol +++ b/src/modules/logicModule/LM_PC_Staking_v2.sol @@ -203,6 +203,7 @@ contract LM_PC_Staking_v2 is virtual nonReentrant validAmount(amount) + permissioned { address sender = _msgSender(); @@ -219,6 +220,7 @@ contract LM_PC_Staking_v2 is virtual nonReentrant validAmount(amount) + permissioned { address sender = _msgSender(); // Update rewardValue, updatedTimestamp and earned values @@ -250,15 +252,12 @@ contract LM_PC_Staking_v2 is } /// @inheritdoc ILM_PC_Staking_v2 - function setRewards(uint amount, uint duration) - external - onlyOrchestratorAdmin - { + function setRewards(uint amount, uint duration) external permissioned { _setRewards(amount, duration); } //-------------------------------------------------------------------------- - // Private Functions + // Internal Functions /// @dev Stakes tokens. /// @param depositFor The address of the user. diff --git a/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/IOptimisticOracleIntegrator.sol b/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/IOptimisticOracleIntegrator.sol index 59308e76f..47ef3c417 100644 --- a/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/IOptimisticOracleIntegrator.sol +++ b/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/IOptimisticOracleIntegrator.sol @@ -98,22 +98,26 @@ interface IOptimisticOracleIntegrator is // Setter Functions /// @notice Sets the default currency and amount for the bond. + /// @dev Function access controlled by authorizer. /// @param _newCurrency The address of the new default currency. /// @param _newBond The new bond amount. function setDefaultCurrencyAndBond(address _newCurrency, uint _newBond) external; /// @notice Sets the OptimisticOracleV3 instance where assertions will be published to. + /// @dev Function access controlled by authorizer. /// @param _newOO The address of the new OptimisticOracleV3 instance. function setOptimisticOracle(address _newOO) external; /// @notice Sets the default time assertions will be open for dispute. + /// @dev Function access controlled by authorizer. /// @param _newLiveness The new liveness in seconds. function setDefaultAssertionLiveness(uint64 _newLiveness) external; // State mutating functions /// @notice Asserts data for a specific dataId on behalf of an asserter address. + /// @dev Function access controlled by authorizer. /// @param dataId The id of the data to assert. /// @param data The data to assert. /// @param asserter The address doing the asserter. If zero defaults to _msgSender(). diff --git a/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/OptimisticOracleIntegrator.sol b/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/OptimisticOracleIntegrator.sol index 1c8e3bcba..81e872097 100644 --- a/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/OptimisticOracleIntegrator.sol +++ b/src/modules/logicModule/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/OptimisticOracleIntegrator.sol @@ -55,12 +55,6 @@ abstract contract OptimisticOracleIntegrator is || super.supportsInterface(interfaceId); } - //========================================================================== - // Constants - - /// @dev The role that is allowed to assert data. - bytes32 public constant ASSERTER_ROLE = keccak256("DATA_ASSERTER"); - //========================================================================== // Storage @@ -136,20 +130,20 @@ abstract contract OptimisticOracleIntegrator is /// @inheritdoc IOptimisticOracleIntegrator function setDefaultCurrencyAndBond(address _newCurrency, uint _newBond) public - onlyOrchestratorAdmin + permissioned { _setDefaultCurrencyAndBond(_newCurrency, _newBond); } /// @inheritdoc IOptimisticOracleIntegrator - function setOptimisticOracle(address _newOO) public onlyOrchestratorAdmin { + function setOptimisticOracle(address _newOO) public permissioned { _setOptimisticOracle(_newOO); } /// @inheritdoc IOptimisticOracleIntegrator function setDefaultAssertionLiveness(uint64 _newLiveness) public - onlyOrchestratorAdmin + permissioned { _setDefaultAssertionLiveness(_newLiveness); } @@ -204,7 +198,7 @@ abstract contract OptimisticOracleIntegrator is function assertDataFor(bytes32 dataId, bytes32 data_, address asserter) public virtual - onlyModuleRole(ASSERTER_ROLE) + permissioned returns (bytes32 assertionId) { asserter = asserter == address(0) ? _msgSender() : asserter; diff --git a/src/modules/logicModule/interfaces/ILM_Oracle_Permissioned_v1.sol b/src/modules/logicModule/interfaces/ILM_Oracle_Permissioned_v1.sol index a732871bb..b3f431712 100644 --- a/src/modules/logicModule/interfaces/ILM_Oracle_Permissioned_v1.sol +++ b/src/modules/logicModule/interfaces/ILM_Oracle_Permissioned_v1.sol @@ -34,41 +34,21 @@ import {IOraclePrice_v1} from "@lm/interfaces/IOraclePrice_v1.sol"; * - To price redeeming 1 token at 0.5 collateral with 6 decimal * collateral: 500_000 * - * @custom:setup This module requires the following MANDATORY setup steps: + * @custom:setup OPTIONAL setup steps for enhanced administration: * * 1. Configure Price Setter Role: * - Purpose: The price setter role is authorized to set * prices for issuance and redemption operations. - * - How: The OrchestratorAdmin (or PRICE_SETTER_ROLE_ADMIN - * if configured) must: - * 1. Retrieve the price setter role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getPriceSetterRole(), - * operatorAddress - * ); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Price Setter Role Admin: - * - Purpose: Enables delegation of price setter role - * management to a dedicated admin role instead of - * relying on the OrchestratorAdmin. This allows - * for more granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getPriceSetterRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getPriceSetterRoleAdmin() - * ) - * ); + * 1. Create a price setter role + * 2. Add access permission for the + * setIssuancePrice(), setRedemptionPrice() + * and setIssuanceAndRedemptionPrice() + * functions to the price setter role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer @@ -94,6 +74,7 @@ interface ILM_Oracle_Permissioned_v1 is IOraclePrice_v1 { /// @notice Sets the issuance price for token issuance (buying tokens) /// Price represents how much collateral is paid for 1 issuance token. + /// @dev Function access controlled by authorizer. /// @dev Must be non-zero and denominated in collateral token decimals. /// For example: With 6 decimal collateral token, /// - To price 1 issuance token at 1.5 collateral, use 1_500_000 @@ -103,6 +84,7 @@ interface ILM_Oracle_Permissioned_v1 is IOraclePrice_v1 { /// @notice Sets the redemption price for token redemption (selling tokens) /// Price represents how much collateral is returned for 1 issuance token. + /// @dev Function only callable by claim contributors /// @dev Must be non-zero and denominated in collateral token decimals. /// For example: With 6 decimal collateral token, /// - To price 1 issuance token at 1.5 collateral, use 1_500_000 @@ -112,6 +94,7 @@ interface ILM_Oracle_Permissioned_v1 is IOraclePrice_v1 { /// @notice Sets both issuance and redemption prices atomically, denominated /// in the collateral token decimals. + /// @dev Function only callable by claim contributors /// @dev Both prices must be non-zero. Both the issuance and redemption /// prices should be denominated in the collateral token decimals. /// For example, if the collateral token has 6 decimals and the @@ -131,12 +114,4 @@ interface ILM_Oracle_Permissioned_v1 is IOraclePrice_v1 { /// are denominated. /// @return decimals_ The decimals of the collateral token. function getCollateralTokenDecimals() external view returns (uint8); - - /// @notice Gets the price setter role identifier. - /// @return bytes32 The PRICE_SETTER_ROLE identifier - function getPriceSetterRole() external pure returns (bytes32); - - /// @notice Gets the price setter role admin identifier. - /// @return bytes32 The PRICE_SETTER_ROLE_ADMIN identifier - function getPriceSetterRoleAdmin() external pure returns (bytes32); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_Bounties_v2.sol b/src/modules/logicModule/interfaces/ILM_PC_Bounties_v2.sol index adac3ec7d..46c85b33d 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_Bounties_v2.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_Bounties_v2.sol @@ -188,6 +188,7 @@ interface ILM_PC_Bounties_v2 is IERC20PaymentClientBase_v2 { // Bounty Mutating Functions /// @notice Adds a new Bounty. + /// @dev Function access controlled by authorizer. /// @dev Reverts if an argument invalid. /// @param minimumPayoutAmount The minimum amount of tokens the Bounty will pay out upon being claimed. /// @param maximumPayoutAmount The maximum amount of tokens the Bounty will pay out upon being claimed. @@ -200,6 +201,7 @@ interface ILM_PC_Bounties_v2 is IERC20PaymentClientBase_v2 { ) external returns (uint); /// @notice Adds a new array of Bounties. + /// @dev Function access controlled by authorizer. /// @dev Reverts if an argument invalid. /// @param minimumPayoutAmounts The array of minimum amount of tokens the Bounty will pay out upon being claimed /// @param maximumPayoutAmounts The array of maximum amount of tokens the Bounty will pay out upon being claimed @@ -212,18 +214,21 @@ interface ILM_PC_Bounties_v2 is IERC20PaymentClientBase_v2 { ) external returns (uint[] memory ids); /// @notice Updates a Bounty's informations. + /// @dev Function access controlled by authorizer. /// @dev Reverts if an argument invalid. /// @param bountyId The id of the Bounty that will be updated. /// @param details The Bounty's details. function updateBounty(uint bountyId, bytes calldata details) external; /// @notice Locks the Bounty so it cant be claimed. + /// @dev Function access controlled by authorizer. /// @dev Only callable by authorized addresses. /// @dev Reverts if id invalid. /// @param bountyId The id of the Bounty that will be locked. function lockBounty(uint bountyId) external; /// @notice Adds a new Claim. + /// @dev Function access controlled by authorizer. /// @dev Reverts if an argument invalid. /// @param bountyId The id of the bounty this claim belongs to. /// @param contributors The contributor information for the Claim. @@ -236,6 +241,7 @@ interface ILM_PC_Bounties_v2 is IERC20PaymentClientBase_v2 { ) external returns (uint); /// @notice Updates a Claim's contributor informations. + /// @dev Function access controlled by authorizer. /// @dev Reverts if an argument invalid. /// @param claimId The id of the Claim that will be updated. /// @param contributors The contributor information for the Claim. @@ -245,13 +251,14 @@ interface ILM_PC_Bounties_v2 is IERC20PaymentClientBase_v2 { ) external; /// @notice Updates a Claim Details. + /// @dev Function only callable by claim contributors /// @param claimId The id of the Claim that will be updated. /// @param details The Claim's details. function updateClaimDetails(uint claimId, bytes calldata details) external; /// @notice Completes a Bounty by verifying a claim. - /// @dev Only callable by authorized addresses. + /// @dev Function access controlled by authorizer. /// @dev Reverts if id invalid. /// @dev contributors should be copied out of the given Claim. The parameter is used to prevent front running. /// @param claimId The id of the Claim that wants to claim the Bounty. diff --git a/src/modules/logicModule/interfaces/ILM_PC_KPIRewarder_v2.sol b/src/modules/logicModule/interfaces/ILM_PC_KPIRewarder_v2.sol index 23e3194b2..9697f1fcc 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_KPIRewarder_v2.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_KPIRewarder_v2.sol @@ -103,9 +103,34 @@ interface ILM_PC_KPIRewarder_v2 { event DeletedStuckAssertion(bytes32 indexed assertionId); //-------------------------------------------------------------------------- - // Functions + // Getter + + /// @notice Returns the KPI with the given number. + /// @param KPInum The number of the KPI to return. + /// @return The KPI. + function getKPI(uint KPInum) external view returns (KPI memory); + + /// @notice Returns the Assertion Configuration for a given assertionId. + /// @param assertionId The id of the Assertion to return. + /// @return The Assertion Configuration. + function getAssertionConfig(bytes32 assertionId) + external + view + returns (RewardRoundConfiguration memory); + + /// @notice Returns the current KPI counter. + /// @return The KPI counter. + function getKPICounter() external view returns (uint); + + /// @notice Returns the assertion pending flag. + /// @return The assertion pending flag. + function getAssertionPending() external view returns (bool); + + //-------------------------------------------------------------------------- + // Mutating /// @notice Posts an assertion to the Optimistic Oracle, specifying the KPI to use and the asserted value. + /// @dev Function access controlled by authorizer. /// @param dataId The dataId to be posted. /// @param assertedValue The target value that will be asserted and posted as data to the oracle. /// @param asserter The address of the asserter. @@ -119,6 +144,7 @@ interface ILM_PC_KPIRewarder_v2 { ) external returns (bytes32 assertionId); /// @notice Creates a KPI for the Rewarder. + /// @dev Function access controlled by authorizer. /// @param _continuous Should the tranche rewards be distributed continuously or in steps. /// @param _trancheValues The value at which the tranches end. /// @param _trancheRewards The rewards to be distributed at completion of each tranche. @@ -133,29 +159,9 @@ interface ILM_PC_KPIRewarder_v2 { /// @param amount The amount to deposit. function depositFeeFunds(uint amount) external; - /// @notice Returns the KPI with the given number. - /// @param KPInum The number of the KPI to return. - /// @return The KPI. - function getKPI(uint KPInum) external view returns (KPI memory); - - /// @notice Returns the Assertion Configuration for a given assertionId. - /// @param assertionId The id of the Assertion to return. - /// @return The Assertion Configuration. - function getAssertionConfig(bytes32 assertionId) - external - view - returns (RewardRoundConfiguration memory); - /// @notice Deletes a stuck assertion. + /// @dev Function access controlled by authorizer. /// @dev This function is only callable by the Orchestrator Admin. /// @param assertionId The id of the assertion to delete. function deleteStuckAssertion(bytes32 assertionId) external; - - /// @notice Returns the current KPI counter. - /// @return The KPI counter. - function getKPICounter() external view returns (uint); - - /// @notice Returns the assertion pending flag. - /// @return The assertion pending flag. - function getAssertionPending() external view returns (bool); } diff --git a/src/modules/logicModule/interfaces/ILM_PC_PaymentRouter_v2.sol b/src/modules/logicModule/interfaces/ILM_PC_PaymentRouter_v2.sol index 7010d9893..e56b7145f 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_PaymentRouter_v2.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_PaymentRouter_v2.sol @@ -12,6 +12,7 @@ interface ILM_PC_PaymentRouter_v2 { // Mutating Functions /// @notice Adds a new Payment Order. + /// @dev Function access controlled by authorizer. /// @dev Reverts if an argument invalid. /// @param recipient The address that will receive the payment. /// @param paymentToken The token in which to pay. @@ -30,6 +31,7 @@ interface ILM_PC_PaymentRouter_v2 { /// @notice Adds multiple Payment Orders in one batch. These PaymentOrders will share start, /// cliff and end timestamps. + /// @dev Function access controlled by authorizer. /// @dev Reverts if an argument invalid. The number of orders to be added in one batch is capped at 255. /// @param numOfOrders The number of orders to add. /// @param recipients The addresses that will receive the payments. diff --git a/src/modules/logicModule/interfaces/ILM_PC_RecurringPayments_v2.sol b/src/modules/logicModule/interfaces/ILM_PC_RecurringPayments_v2.sol index 9d3f77308..58d21ee28 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_RecurringPayments_v2.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_RecurringPayments_v2.sol @@ -127,6 +127,7 @@ interface ILM_PC_RecurringPayments_v2 { // Mutating Functions /// @notice Adds a recurring payment to the manager. + /// @dev Function access controlled by authorizer. /// @dev a new id is created for each Payment. /// @param amount Amount of tokens send to the recipient address. /// @param startEpoch Epoch in which the payment starts. Use getEpochFromTimestamp() or @@ -140,6 +141,7 @@ interface ILM_PC_RecurringPayments_v2 { ) external returns (uint id); /// @notice Removes a recurring Payment. + /// @dev Function access controlled by authorizer. /// @param prevId Id of the previous recurring payment in the payment list. /// @param id Id of the recurring payment that is to be removed. function removeRecurringPayment(uint prevId, uint id) external; diff --git a/src/modules/logicModule/interfaces/ILM_PC_Staking_v2.sol b/src/modules/logicModule/interfaces/ILM_PC_Staking_v2.sol index 23d3c40fb..978042eff 100644 --- a/src/modules/logicModule/interfaces/ILM_PC_Staking_v2.sol +++ b/src/modules/logicModule/interfaces/ILM_PC_Staking_v2.sol @@ -107,22 +107,26 @@ interface ILM_PC_Staking_v2 { // Mutating Functions /// @notice Stake a specified amount of tokens to earn rewards. + /// @dev Function access controlled by authorizer. /// @dev Should tokens already be staked, then the sending address will collect the rewards up until this point. /// @dev Fee on transfer tokens are currently not supported. /// @param amount How much token should be staked. function stake(uint amount) external; /// @notice Unstake a specified amount of tokens and collect rewards. + /// @dev Function access controlled by authorizer. /// @dev Reaps the rewards collected up to this point for the msg.Sender(). /// @dev Fee on transfer tokens are currently not supported. /// @param amount How much token should be unstaked. function unstake(uint amount) external; /// @notice Collects the rewards that are earned up until now. + /// @dev Function access controlled by authorizer. /// @dev Reaps the rewards collected up to this point for the msg.Sender(). function claimRewards() external; /// @notice Sets the rewards that are to be distributed. + /// @dev Function access controlled by authorizer. /// @dev Equally distributes the reward amount over the given time period. /// @param amount How much token should be distributed. /// @param duration How much time it will take to distribute the token. diff --git a/src/modules/paymentProcessor/PP_Queue_ManualExecution_v1.sol b/src/modules/paymentProcessor/PP_Queue_ManualExecution_v1.sol index 11c086150..4b03a22b1 100644 --- a/src/modules/paymentProcessor/PP_Queue_ManualExecution_v1.sol +++ b/src/modules/paymentProcessor/PP_Queue_ManualExecution_v1.sol @@ -47,42 +47,22 @@ import {IERC20} from "@oz/token/ERC20/IERC20.sol"; * - FAILED: The order has failed due to the transfer failing * (blacklisted address). * - * @custom:setup This module requires the following MANDATORY setup steps: + * @custom:setup OPTIONAL setup steps for enhanced administration: * * 1. Configure Queue Operators: * - Purpose: Queue operators are authorized to cancel payment * orders in the queue, and claim collateral for * failed payments. - * - How: The OrchestratorAdmin (or - * QUEUE_OPERATOR_ROLE_ADMIN if configured) must: - * 1. Retrieve the queue operator role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getQueueOperatorRole(), - * operatorAddress - * ); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Queue Operator Admin: - * - Purpose: Enables delegation of queue operator management - * to a dedicated admin role instead of relying on - * the OrchestratorAdmin. This allows for more - * granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRoleAdmin() - * ) - * ); + * 1. Create a Queue operator role + * 2. Add access permission for the + * claimPreviouslyUnclaimableToTreasury() and + * cancelPaymentOrderThroughQueueId() + * functions to the Queue operator role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to diff --git a/src/modules/paymentProcessor/PP_Queue_v1.sol b/src/modules/paymentProcessor/PP_Queue_v1.sol index 1ce1105f2..ec521d1df 100644 --- a/src/modules/paymentProcessor/PP_Queue_v1.sol +++ b/src/modules/paymentProcessor/PP_Queue_v1.sol @@ -48,42 +48,22 @@ import {LinkedIdList} from "src/modules/lib/LinkedIdList.sol"; * - FAILED: The order has failed due to the transfer failing * (blacklisted address). * - * @custom:setup This module requires the following MANDATORY setup steps: + * @custom:setup OPTIONAL setup steps for enhanced administration: * * 1. Configure Queue Operators: * - Purpose: Queue operators are authorized to cancel payment * orders in the queue, and claim collateral for * failed payments. - * - How: The OrchestratorAdmin (or - * QUEUE_OPERATOR_ROLE_ADMIN if configured) must: - * 1. Retrieve the queue operator role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getQueueOperatorRole(), - * operatorAddress - * ); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Queue Operator Admin: - * - Purpose: Enables delegation of queue operator management - * to a dedicated admin role instead of relying on - * the OrchestratorAdmin. This allows for more - * granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRoleAdmin() - * ) - * ); + * 1. Create a Queue operator role + * 2. Add access permission for the + * claimPreviouslyUnclaimableToTreasury() and + * cancelPaymentOrderThroughQueueId() + * functions to the Queue operator role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to @@ -125,17 +105,6 @@ contract PP_Queue_v1 is IPP_Queue_v1, Module_v1 { /// @notice Flag position in the flags byte. uint8 internal constant FLAG_ORDER_ID = 0; - /// @notice Role identifier for queue operations. - /// @dev This role cancels payments in the queue. - bytes32 internal constant QUEUE_OPERATOR_ROLE = "QUEUE_OPERATOR_ROLE"; - - /// @notice Role identifier for the admin authorized to assign the queue - /// operator role. - /// @dev This role should be set as the role admin for the - /// QUEUE_OPERATOR_ROLE within the Authorizer module. - bytes32 internal constant QUEUE_OPERATOR_ROLE_ADMIN = - "QUEUE_OPERATOR_ROLE_ADMIN"; - /// @notice BPS value. uint internal constant BPS = 10_000; @@ -328,26 +297,6 @@ contract PP_Queue_v1 is IPP_Queue_v1, Module_v1 { size_ = _queue[client_].length(); } - /// @inheritdoc IPP_Queue_v1 - function getQueueOperatorRole() - external - pure - virtual - returns (bytes32 role_) - { - return QUEUE_OPERATOR_ROLE; - } - - /// @inheritdoc IPP_Queue_v1 - function getQueueOperatorRoleAdmin() - external - pure - virtual - returns (bytes32 role_) - { - return QUEUE_OPERATOR_ROLE_ADMIN; - } - /// @inheritdoc IPaymentProcessor_v2 function unclaimable( address client_, @@ -372,7 +321,7 @@ contract PP_Queue_v1 is IPP_Queue_v1, Module_v1 { function setMaxOrdersPerExecution(uint maxOrdersPerExecution_) external virtual - onlyModuleRole(QUEUE_OPERATOR_ROLE) + permissioned { if (maxOrdersPerExecution_ == 0) { revert Module__PP_Queue_ZeroAmount(); @@ -384,16 +333,13 @@ contract PP_Queue_v1 is IPP_Queue_v1, Module_v1 { function setCanceledOrdersTreasury(address treasury_) external virtual - onlyOrchestratorAdmin + permissioned { _setCanceledOrdersTreasury(treasury_); } /// @inheritdoc IPP_Queue_v1 - function setFailedOrdersTreasury(address treasury_) - external - onlyOrchestratorAdmin - { + function setFailedOrdersTreasury(address treasury_) external permissioned { _setFailedOrdersTreasury(treasury_); } @@ -446,7 +392,7 @@ contract PP_Queue_v1 is IPP_Queue_v1, Module_v1 { address client_, address token_, address receiver_ - ) external virtual onlyModuleRole(QUEUE_OPERATOR_ROLE) { + ) external virtual permissioned { if (unclaimable(client_, token_, receiver_) == 0) { revert Module__PaymentProcessor__NothingToClaim(client_, receiver_); } @@ -472,12 +418,7 @@ contract PP_Queue_v1 is IPP_Queue_v1, Module_v1 { function cancelPaymentOrderThroughQueueId( uint orderId_, IERC20PaymentClientBase_v2 client_ - ) - external - virtual - onlyModuleRole(QUEUE_OPERATOR_ROLE) - returns (bool success_) - { + ) external virtual permissioned returns (bool success_) { // Validate that the order exists for the given queue ID and client. if (!_orderExists(orderId_, client_)) { revert Module__PP_Queue_InvalidOrderId(address(client_), orderId_); diff --git a/src/modules/paymentProcessor/PP_Streaming_v2.sol b/src/modules/paymentProcessor/PP_Streaming_v2.sol index 8936ea9d7..9789d17e9 100644 --- a/src/modules/paymentProcessor/PP_Streaming_v2.sol +++ b/src/modules/paymentProcessor/PP_Streaming_v2.sol @@ -276,7 +276,7 @@ contract PP_Streaming_v2 is Module_v1, IPP_Streaming_v2 { function removeAllPaymentReceiverPayments( address client, address paymentReceiver - ) external onlyOrchestratorAdmin { + ) external permissioned { if ( _findAddressInActiveStreams(client, paymentReceiver) == type(uint).max @@ -293,7 +293,7 @@ contract PP_Streaming_v2 is Module_v1, IPP_Streaming_v2 { address client, address paymentReceiver, uint streamId - ) external onlyOrchestratorAdmin { + ) external permissioned { // First, we give the streamed funds from this specific streamId to the beneficiary _claimForSpecificStream(client, paymentReceiver, streamId); @@ -436,9 +436,10 @@ contract PP_Streaming_v2 is Module_v1, IPP_Streaming_v2 { && _validOriginAndTargetChain(order.originChainId, order.targetChainId); } + /// @inheritdoc IPP_Streaming_v2 function setStreamingDefaults(uint newStart_, uint newCliff_, uint newEnd_) external - onlyOrchestratorAdmin + permissioned { _setDefaultTimes(newStart_, newCliff_, newEnd_); } diff --git a/src/modules/paymentProcessor/interfaces/IPP_Queue_ManualExecution_v1.sol b/src/modules/paymentProcessor/interfaces/IPP_Queue_ManualExecution_v1.sol index 8b7bb2f29..23ef0e8b3 100644 --- a/src/modules/paymentProcessor/interfaces/IPP_Queue_ManualExecution_v1.sol +++ b/src/modules/paymentProcessor/interfaces/IPP_Queue_ManualExecution_v1.sol @@ -37,42 +37,22 @@ import {IERC20PaymentClientBase_v2} from * - FAILED: The order has failed due to the transfer failing * (blacklisted address). * - * @custom:setup This module requires the following MANDATORY setup steps: + * @custom:setup OPTIONAL setup steps for enhanced administration: * * 1. Configure Queue Operators: * - Purpose: Queue operators are authorized to cancel payment * orders in the queue, and claim collateral for * failed payments. - * - How: The OrchestratorAdmin (or - * QUEUE_OPERATOR_ROLE_ADMIN if configured) must: - * 1. Retrieve the queue operator role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getQueueOperatorRole(), - * operatorAddress - * ); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Queue Operator Admin: - * - Purpose: Enables delegation of queue operator management - * to a dedicated admin role instead of relying on - * the OrchestratorAdmin. This allows for more - * granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRoleAdmin() - * ) - * ); + * 1. Create a Queue operator role + * 2. Add access permission for the + * claimPreviouslyUnclaimableToTreasury() and + * cancelPaymentOrderThroughQueueId() + * functions to the Queue operator role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to diff --git a/src/modules/paymentProcessor/interfaces/IPP_Queue_v1.sol b/src/modules/paymentProcessor/interfaces/IPP_Queue_v1.sol index 07c522708..c8b8dd95f 100644 --- a/src/modules/paymentProcessor/interfaces/IPP_Queue_v1.sol +++ b/src/modules/paymentProcessor/interfaces/IPP_Queue_v1.sol @@ -37,42 +37,22 @@ import {IERC20PaymentClientBase_v2} from * - FAILED: The order has failed due to the transfer failing * (blacklisted address). * - * @custom:setup This module requires the following MANDATORY setup steps: + * @custom:setup OPTIONAL setup steps for enhanced administration: * * 1. Configure Queue Operators: * - Purpose: Queue operators are authorized to cancel payment * orders in the queue, and claim collateral for * failed payments. - * - How: The OrchestratorAdmin (or - * QUEUE_OPERATOR_ROLE_ADMIN if configured) must: - * 1. Retrieve the queue operator role identifier. - * 2. Grant the role to desired addresses. - * - Example: module.grantModuleRole( - * module.getQueueOperatorRole(), - * operatorAddress - * ); - * - * OPTIONAL setup steps for enhanced administration: - * - * 1. Custom Queue Operator Admin: - * - Purpose: Enables delegation of queue operator management - * to a dedicated admin role instead of relying on - * the OrchestratorAdmin. This allows for more - * granular access control and operational - * flexibility. * - How: The OrchestratorAdmin must: - * 1. Generate the role IDs for both roles. - * 2. Transfer admin rights through the Authorizer. - * - Example: authorizer.transferAdminRole( - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRole() - * ), - * authorizer.generateRoleId( - * moduleAddress, - * module.getQueueOperatorRoleAdmin() - * ) - * ); + * 1. Create a Queue operator role + * 2. Add access permission for the + * claimPreviouslyUnclaimableToTreasury() and + * cancelPaymentOrderThroughQueueId() + * functions to the Queue operator role. + * 3. Grant the role to desired addresses. + * - Example: authorizer.createRole(); + * authorizer.addAccessPermission(); + * authorizer.grantRole(); * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to @@ -316,23 +296,12 @@ interface IPP_Queue_v1 is IPaymentProcessor_v2 { view returns (uint size_); - /// @notice Gets the role identifier for the queue operator role. - /// @return role_ The queue operator role identifier. - function getQueueOperatorRole() external pure returns (bytes32 role_); - - /// @notice Gets the role identifier for queue operator admin. - /// @return role_ The queue operator role admin identifier. - function getQueueOperatorRoleAdmin() - external - pure - returns (bytes32 role_); - /// @notice Cancels a payment order by its queue ID and sends the funds /// from the cancelled order to the canceled orders treasury. + /// @dev Function only callable by claim contributors /// @dev This function can only be excuted if the payment client /// has enough collateral to transfer the funds to the - /// canceled orders treasury. Additionally, the caller - /// must have the queue operator role. If the transfer fails, + /// canceled orders treasury. If the transfer fails, /// then the amount is added to the unclaimable amounts for /// the canceled orders treasury. /// @param orderId_ The ID of the order to cancel. @@ -359,17 +328,19 @@ interface IPP_Queue_v1 is IPaymentProcessor_v2 { /// @notice Set the treasury address which receives the collateral /// of canceled orders. + /// @dev Function only callable by claim contributors /// @param treasury_ The treasury address for canceled orders. function setCanceledOrdersTreasury(address treasury_) external; /// @notice Set the treasury address which receives the collateral /// of failed orders. + /// @dev Function only callable by claim contributors /// @param treasury_ The treasury address for failed orders. function setFailedOrdersTreasury(address treasury_) external; /// @notice Claim previously unclaimable amounts from a receiver to /// the failed orders treasury. - /// @dev This function is only callable by the queue operator. + /// @dev Function only callable by claim contributors /// @param client_ The client address. /// @param token_ The token address. /// @param receiver_ The receiver address. diff --git a/src/modules/paymentProcessor/interfaces/IPP_Streaming_v2.sol b/src/modules/paymentProcessor/interfaces/IPP_Streaming_v2.sol index 05a74259a..3c9e9f782 100644 --- a/src/modules/paymentProcessor/interfaces/IPP_Streaming_v2.sol +++ b/src/modules/paymentProcessor/interfaces/IPP_Streaming_v2.sol @@ -159,6 +159,7 @@ interface IPP_Streaming_v2 is IPaymentProcessor_v2 { /// @notice Deletes all payments related to a paymentReceiver & leaves currently streaming tokens in the /// {IERC20PaymentClientBase_v2}. + /// @dev Function access controlled by authorizer. /// @dev this function calls `_removePayment` which goes through all the payment orders for a `paymentReceiver`. /// For the payment orders that are completely streamed, their details are deleted in the /// `_claimForSpecificStrea` function and for others it is deleted in the `_removePayment` function only, @@ -172,6 +173,7 @@ interface IPP_Streaming_v2 is IPaymentProcessor_v2 { /// @notice Deletes a specific payment with id = streamId for a paymentReceiver & leaves currently streaming /// tokens in the {IERC20PaymentClientBase_v2}. + /// @dev Function access controlled by authorizer. /// @dev the detail of the wallet that is being removed is either deleted in the `_claimForSpecificStream` /// or later down in this function itself depending on the timestamp of when this function was called. /// @param client The {IERC20PaymentClientBase_v2} instance address from which we will remove the payment. @@ -183,6 +185,15 @@ interface IPP_Streaming_v2 is IPaymentProcessor_v2 { uint streamId ) external; + /// @notice Sets the default start time, cliff and end times for new + /// payment orders. + /// @dev Function access controlled by authorizer. + /// @param newStart_ The new default start time. + /// @param newCliff_ The new default cliff duration. + /// @param newEnd_ The new default end time. + function setStreamingDefaults(uint newStart_, uint newCliff_, uint newEnd_) + external; + /// @notice Getter for the start timestamp of a particular payment order with id = streamId associated /// with a particular paymentReceiver. /// @param client address of the payment client. diff --git a/src/orchestrator/Orchestrator_v1.sol b/src/orchestrator/Orchestrator_v1.sol index 381d486c4..066de0634 100644 --- a/src/orchestrator/Orchestrator_v1.sol +++ b/src/orchestrator/Orchestrator_v1.sol @@ -67,14 +67,8 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { //-------------------------------------------------------------------------- // Modifiers - /// @dev Modifier to guarantee function is only callable by the admin of the workflow - /// address. - modifier onlyOrchestratorAdmin() { - bytes32 adminRole = authorizer.getAdminRole(); - - if (!authorizer.hasRole(adminRole, _msgSender())) { - revert Orchestrator__CallerNotAuthorized(adminRole, _msgSender()); - } + modifier permissioned() { + _checkAuthorization(_msgSender(), _msgData()); _; } @@ -184,7 +178,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function initiateSetAuthorizerWithTimelock(IAuthorizer_v1 newAuthorizer) external - onlyOrchestratorAdmin + permissioned { address newAuthorizerAddress = address(newAuthorizer); _enforcePrivilegedModuleInterfaceCheck( @@ -198,7 +192,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function executeSetAuthorizer(IAuthorizer_v1 newAuthorizer) external - onlyOrchestratorAdmin + permissioned updatingModuleAlreadyStarted(address(newAuthorizer)) timelockExpired(address(newAuthorizer)) { @@ -221,7 +215,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function cancelAuthorizerUpdate(IAuthorizer_v1 authorizer_) external - onlyOrchestratorAdmin + permissioned { _cancelModuleUpdate(address(authorizer)); _cancelModuleUpdate(address(authorizer_)); @@ -230,7 +224,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function initiateSetFundingManagerWithTimelock( IFundingManager_v1 newFundingManager - ) external onlyOrchestratorAdmin { + ) external permissioned { address newFundingManagerAddress = address(newFundingManager); _enforcePrivilegedModuleInterfaceCheck( @@ -251,7 +245,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function executeSetFundingManager(IFundingManager_v1 newFundingManager) external - onlyOrchestratorAdmin + permissioned { address newFundingManagerAddress = address(newFundingManager); @@ -267,7 +261,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function cancelFundingManagerUpdate(IFundingManager_v1 fundingManager_) external - onlyOrchestratorAdmin + permissioned { _cancelModuleUpdate(address(fundingManager)); _cancelModuleUpdate(address(fundingManager_)); @@ -276,7 +270,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function initiateSetPaymentProcessorWithTimelock( IPaymentProcessor_v2 newPaymentProcessor - ) external onlyOrchestratorAdmin { + ) external permissioned { address newPaymentProcessorAddress = address(newPaymentProcessor); _enforcePrivilegedModuleInterfaceCheck( @@ -290,7 +284,7 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function executeSetPaymentProcessor( IPaymentProcessor_v2 newPaymentProcessor - ) external onlyOrchestratorAdmin { + ) external permissioned { address newPaymentProcessorAddress = address(newPaymentProcessor); _enforcePrivilegedModuleInterfaceCheck( @@ -306,45 +300,50 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { /// @inheritdoc IOrchestrator_v1 function cancelPaymentProcessorUpdate( IPaymentProcessor_v2 paymentProcessor_ - ) external onlyOrchestratorAdmin { + ) external permissioned { _cancelModuleUpdate(address(paymentProcessor)); _cancelModuleUpdate(address(paymentProcessor_)); } /// @inheritdoc IOrchestrator_v1 - function cancelModuleUpdate(address module_) external { + function initiateAddModuleWithTimelock(address module_) + external + permissioned + { _enforceNonPrivilegedModuleInterfaceCheck(module_); - _cancelModuleUpdate(module_); + _initiateAddModuleWithTimelock(module_); } /// @inheritdoc IOrchestrator_v1 - function initiateAddModuleWithTimelock(address module_) external { + function executeAddModule(address module_) external permissioned { _enforceNonPrivilegedModuleInterfaceCheck(module_); - _initiateAddModuleWithTimelock(module_); + _executeAddModule(module_); } /// @inheritdoc IOrchestrator_v1 function initiateRemoveModuleWithTimelock(address module_) external onlyLogicModules(module_) + permissioned { _initiateRemoveModuleWithTimelock(module_); } - /// @inheritdoc IOrchestrator_v1 - function executeAddModule(address module_) external { - _enforceNonPrivilegedModuleInterfaceCheck(module_); - _executeAddModule(module_); - } - /// @inheritdoc IOrchestrator_v1 function executeRemoveModule(address module_) external onlyLogicModules(module_) + permissioned { _executeRemoveModule(module_); } + /// @inheritdoc IOrchestrator_v1 + function cancelModuleUpdate(address module_) external permissioned { + _enforceNonPrivilegedModuleInterfaceCheck(module_); + _cancelModuleUpdate(module_); + } + //-------------------------------------------------------------------------- // Upstream Function Implementations @@ -359,9 +358,27 @@ contract Orchestrator_v1 is IOrchestrator_v1, ModuleManagerBase_v1 { return authorizer.hasRole(authorizer.getAdminRole(), who); } - //-------------------------------------------------------------------------- + // ======================================================================== // Internal Functions + // ------------------------------------------------------------------------ + // Internal - Authorization + + /// @notice Checks if the caller can call the function that implements the locked modifier. + /// @param caller_ The address of the caller. + /// @param data_ The data of the call. + function _checkAuthorization(address caller_, bytes calldata data_) + internal + view + { + // If caller cannot call the function, revert. + if ( + !authorizer.hasPermission(caller_, address(this), bytes4(data_[0:4])) + ) { + revert Orchestrator__NotPermissioned(); + } + } + /// @notice Enforces that the address is in fact a Module of the required type. /// @dev The function reverts if the given address is not a module of the required type. /// @param _contractAddr The address to be checked. diff --git a/src/orchestrator/abstracts/ModuleManagerBase_v1.sol b/src/orchestrator/abstracts/ModuleManagerBase_v1.sol index 4cb6cd988..2fccd37e7 100644 --- a/src/orchestrator/abstracts/ModuleManagerBase_v1.sol +++ b/src/orchestrator/abstracts/ModuleManagerBase_v1.sol @@ -53,14 +53,6 @@ abstract contract ModuleManagerBase_v1 is //-------------------------------------------------------------------------- // Modifiers - /// @dev Modifier to guarantee function is only callable by authorized address. - modifier __ModuleManager_onlyAuthorized() { - if (!__ModuleManager_isAuthorized(_msgSender())) { - revert ModuleManagerBase__CallerNotAuthorized(); - } - _; - } - /// @dev Modifier to guarantee that the caller is a module. modifier onlyModule() { if (!isModule(_msgSender())) { @@ -232,15 +224,13 @@ abstract contract ModuleManagerBase_v1 is } //-------------------------------------------------------------------------- - // onlyOrchestratorAdmin Functions + // Internal Functions /// @notice Cancels an initiated update for a module. - /// @dev Only callable by authorized address. /// @dev Fails if module update has not been initiated. /// @param module The module address to remove. function _cancelModuleUpdate(address module) internal - __ModuleManager_onlyAuthorized updatingModuleAlreadyStarted(module) { moduleAddressToTimelock[module].timelockActive = false; @@ -248,13 +238,11 @@ abstract contract ModuleManagerBase_v1 is } /// @notice Initiates adding of a module to the {Orchestrator_v1} on a timelock. - /// @dev Only callable by authorized address. /// @dev Fails of adding module exeeds max modules limit. /// @dev Fails if address invalid or address already added as module. /// @param module The module address to add. function _initiateAddModuleWithTimelock(address module) internal - __ModuleManager_onlyAuthorized isNotModule(module) validModule(module) { @@ -262,25 +250,21 @@ abstract contract ModuleManagerBase_v1 is } /// @notice Initiates removing of a module from the {Orchestrator_v1} on a timelock. - /// @dev Only callable by authorized address. /// @dev Fails if address not added as module. /// @param module The module address to remove. function _initiateRemoveModuleWithTimelock(address module) internal - __ModuleManager_onlyAuthorized isModule_(module) { _startModuleUpdateTimelock(module); } /// @notice Executes adding of a module to the {Orchestrator_v1}. - /// @dev Only callable by authorized address. /// @dev Fails if adding of module has not been initiated. /// @dev Fails if timelock has not been expired yet. /// @param module The module address to add. function _executeAddModule(address module) internal - __ModuleManager_onlyAuthorized updatingModuleAlreadyStarted(module) timelockExpired(module) { @@ -291,13 +275,11 @@ abstract contract ModuleManagerBase_v1 is } /// @notice Executes removing of a module from the {Orchestrator_v1}. - /// @dev Only callable by authorized address. /// @dev Fails if removing of module has not been initiated. /// @dev Fails if timelock has not been expired yet. /// @param module The module address to remove. function _executeRemoveModule(address module) internal - __ModuleManager_onlyAuthorized updatingModuleAlreadyStarted(module) timelockExpired(module) { @@ -307,9 +289,6 @@ abstract contract ModuleManagerBase_v1 is _commitRemoveModule(module); } - //-------------------------------------------------------------------------- - // Private Functions - /// @dev Expects `module` to be valid module address. /// @dev Expects `module` to not be enabled module. /// @param module The module address to add. @@ -393,12 +372,13 @@ abstract contract ModuleManagerBase_v1 is ); } + //-------------------------------------------------------------------------- // IERC2771ContextUpgradeable + + /// @inheritdoc IModuleManagerBase_v1 // @dev Because we want to expose the isTrustedForwarder function from the ERC2771ContextUpgradeable // Contract in the IOrchestrator_v1 we have to override it here as the original openzeppelin version // doesnt contain a interface that we could use to expose it. - - /// @inheritdoc IModuleManagerBase_v1 function isTrustedForwarder(address forwarder) public view diff --git a/src/orchestrator/interfaces/IModuleManagerBase_v1.sol b/src/orchestrator/interfaces/IModuleManagerBase_v1.sol index 50daf9a02..ffbe2a9f4 100644 --- a/src/orchestrator/interfaces/IModuleManagerBase_v1.sol +++ b/src/orchestrator/interfaces/IModuleManagerBase_v1.sol @@ -19,9 +19,6 @@ interface IModuleManagerBase_v1 is IERC2771Context { //-------------------------------------------------------------------------- // Errors - /// @notice Function is only callable by authorized address. - error ModuleManagerBase__CallerNotAuthorized(); - /// @notice Function is only callable by modules. error ModuleManagerBase__OnlyCallableByModule(); diff --git a/src/orchestrator/interfaces/IOrchestrator_v1.sol b/src/orchestrator/interfaces/IOrchestrator_v1.sol index 19a56e397..e1560fdaa 100644 --- a/src/orchestrator/interfaces/IOrchestrator_v1.sol +++ b/src/orchestrator/interfaces/IOrchestrator_v1.sol @@ -18,9 +18,7 @@ interface IOrchestrator_v1 is IModuleManagerBase_v1 { // Errors /// @notice Function is only callable by authorized caller. - /// @param role The role of the caller. - /// @param caller The caller address. - error Orchestrator__CallerNotAuthorized(bytes32 role, address caller); + error Orchestrator__NotPermissioned(); /// @notice The given module is not used in the {Orchestrator_v1}. /// @param module The module address. @@ -77,7 +75,32 @@ interface IOrchestrator_v1 is IModuleManagerBase_v1 { ); //-------------------------------------------------------------------------- - // Functions + // Getter Functions + + /// @notice Returns the {Orchestrator_v1}'s id. + /// @dev Unique id set by the {OrchestratorFactory_v1} during initialization. + /// @return The {Orchestrator_v1}'s id. + function orchestratorId() external view returns (uint); + + /// @notice The {IFundingManager_v1} implementation used to hold and distribute Funds. + /// @return The {IFundingManager_v1} implementation. + function fundingManager() external view returns (IFundingManager_v1); + + /// @notice The {IAuthorizer_v1} implementation used to authorize addresses. + /// @return The {IAuthorizer_v1} implementation. + function authorizer() external view returns (IAuthorizer_v1); + + /// @notice The {IPaymentProcessor_v2} implementation used to process module + /// payments. + /// @return The {IPaymentProcessor_v2} implementation. + function paymentProcessor() external view returns (IPaymentProcessor_v2); + + /// @notice The {IGovernor_v1} implementation used for protocol level interactions. + /// @return The {IGovernor_v1} implementation. + function governor() external view returns (IGovernor_v1); + + //-------------------------------------------------------------------------- + // Initialization /// @notice Initialization function. /// @param orchestratorId The id of the {Orchestrator_v1}. @@ -97,39 +120,42 @@ interface IOrchestrator_v1 is IModuleManagerBase_v1 { IGovernor_v1 governor ) external; + //-------------------------------------------------------------------------- + // Mutating Functions + /// @notice Initiates replacing the current authorizer with `_authorizer` on a timelock. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param authorizer_ The address of the new authorizer module. function initiateSetAuthorizerWithTimelock(IAuthorizer_v1 authorizer_) external; /// @notice Initiates replaces the current funding manager with `fundingManager_` on a timelock. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param fundingManager_ The address of the new funding manager module. function initiateSetFundingManagerWithTimelock( IFundingManager_v1 fundingManager_ ) external; /// @notice Initiates replaces the current payment processor with `paymentProcessor_` on a timelock. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param paymentProcessor_ The address of the new payment processor module. function initiateSetPaymentProcessorWithTimelock( IPaymentProcessor_v2 paymentProcessor_ ) external; /// @notice Cancels the replacement of the current authorizer with `authorizer_`. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param authorizer_ The address of the new authorizer module, for which the update is canceled. function cancelAuthorizerUpdate(IAuthorizer_v1 authorizer_) external; /// @notice Cancels the replacement of the current funding manager with `fundingManager_`. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param fundingManager_ The address of the new funding manager module, for which the update is canceled. function cancelFundingManagerUpdate(IFundingManager_v1 fundingManager_) external; /// @notice Cancels the replacement of the current payment processor with `paymentProcessor_`. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param paymentProcessor_ The address of the new payment processro module, for which the update is canceled. function cancelPaymentProcessorUpdate( IPaymentProcessor_v2 paymentProcessor_ @@ -138,14 +164,14 @@ interface IOrchestrator_v1 is IModuleManagerBase_v1 { /// @notice Executes replacing the current authorizer with `_authorizer`. /// @notice !!! IMPORTANT !!! When changing the Authorizer the current set of assigned addresses to Roles are lost. /// Make sure initial owners are set properly. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param authorizer_ The address of the new authorizer module. function executeSetAuthorizer(IAuthorizer_v1 authorizer_) external; /// @notice Executes replaces the current funding manager with `fundingManager_`. /// @notice !!! IMPORTANT !!! When changing the FundingManager the current funds still contained in the module might /// not be retrievable. Make sure to clean the FundingManager properly beforehand. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param fundingManager_ The address of the new funding manager module. function executeSetFundingManager(IFundingManager_v1 fundingManager_) external; @@ -153,13 +179,13 @@ interface IOrchestrator_v1 is IModuleManagerBase_v1 { /// @notice Executes replaces the current payment processor with `paymentProcessor_`. /// @notice !!! IMPORTANT !!! When changing the PaymentProcessor the current ongoing payment orders are lost. /// Make sure to resolve those payments properly beforehand. - /// @dev Only callable by authorized caller. + /// @dev Function access controlled by authorizer. /// @param paymentProcessor_ The address of the new payment processor module. function executeSetPaymentProcessor(IPaymentProcessor_v2 paymentProcessor_) external; /// @notice Initiates the adding of a module to the {Orchestrator_v1} on a timelock. - /// @dev Only callable by authorized address. + /// @dev Function access controlled by authorizer. /// @dev Fails of adding module exeeds max modules limit. /// @dev Fails if address invalid or address already added as module. /// @param module The module address to add. @@ -168,20 +194,20 @@ interface IOrchestrator_v1 is IModuleManagerBase_v1 { /// @notice Initiate the removal of a module from the {Orchestrator_v1} on a timelock. /// @dev Reverts if module to be removed is the current authorizer/fundingManager/paymentProcessor. /// The functions specific to updating these 3 module categories should be used instead. - /// @dev Only callable by authorized address. + /// @dev Function access controlled by authorizer. /// @dev Fails if address not added as module. /// @param module The module address to remove. function initiateRemoveModuleWithTimelock(address module) external; /// @notice Adds address `module` as module. - /// @dev Only callable by authorized address. + /// @dev Function access controlled by authorizer. /// @dev Fails if adding of module has not been initiated. /// @dev Fails if timelock has not been expired yet. /// @param module The module address to add. function executeAddModule(address module) external; /// @notice Removes address `module` as module. - /// @dev Only callable by authorized address. + /// @dev Function access controlled by authorizer. /// @dev Fails if removing of module has not been initiated. /// @dev Fails if timelock has not been expired yet. /// @param module The module address to remove. @@ -189,30 +215,8 @@ interface IOrchestrator_v1 is IModuleManagerBase_v1 { /// @notice Cancels an initiated update for a module. Can be adding or removing a module /// from the {Orchestrator_v1}. - /// @dev Only callable by authorized address. + /// @dev Function access controlled by authorizer. /// @dev Fails if module update has not been initiated. /// @param module The module address to remove. function cancelModuleUpdate(address module) external; - - /// @notice Returns the {Orchestrator_v1}'s id. - /// @dev Unique id set by the {OrchestratorFactory_v1} during initialization. - /// @return The {Orchestrator_v1}'s id. - function orchestratorId() external view returns (uint); - - /// @notice The {IFundingManager_v1} implementation used to hold and distribute Funds. - /// @return The {IFundingManager_v1} implementation. - function fundingManager() external view returns (IFundingManager_v1); - - /// @notice The {IAuthorizer_v1} implementation used to authorize addresses. - /// @return The {IAuthorizer_v1} implementation. - function authorizer() external view returns (IAuthorizer_v1); - - /// @notice The {IPaymentProcessor_v2} implementation used to process module - /// payments. - /// @return The {IPaymentProcessor_v2} implementation. - function paymentProcessor() external view returns (IPaymentProcessor_v2); - - /// @notice The {IGovernor_v1} implementation used for protocol level interactions. - /// @return The {IGovernor_v1} implementation. - function governor() external view returns (IGovernor_v1); } diff --git a/src/templates/modules/ILM_PC_Template_v1.sol b/src/templates/modules/ILM_PC_Template_v1.sol index 8a8899eeb..91b5de239 100644 --- a/src/templates/modules/ILM_PC_Template_v1.sol +++ b/src/templates/modules/ILM_PC_Template_v1.sol @@ -80,10 +80,6 @@ interface ILM_PC_Template_v1 is IERC20PaymentClientBase_v2 { /// @return token_ The address of the payment token. function getPaymentToken() external view returns (address token_); - /// @notice Returns the deposit admin role. - /// @return role_ The address of the deposit admin role. - function getDepositAdminRole() external view returns (bytes32 role_); - /// @notice Returns the maximum deposit amount. /// @return amount_ The maximum deposit amount. function getMaxDepositAmount() external view returns (uint amount_); @@ -96,6 +92,7 @@ interface ILM_PC_Template_v1 is IERC20PaymentClientBase_v2 { function deposit(uint amount_) external; /// @notice Processes a user's deposit. + /// @dev Function access controlled by authorizer. /// @param user_ The address of the user whose deposit to process. /// @param start_ The start timestamp for the payment schedule. /// @param cliff_ The cliff timestamp for the payment schedule. diff --git a/src/templates/modules/LM_PC_Template_v1.sol b/src/templates/modules/LM_PC_Template_v1.sol index 65a4a4a1f..c4ea6cac7 100644 --- a/src/templates/modules/LM_PC_Template_v1.sol +++ b/src/templates/modules/LM_PC_Template_v1.sol @@ -36,24 +36,30 @@ import {ERC165Upgradeable} from * * Key components: * - Inherits from ERC20PaymentClientBase_v2 - * - Uses DEPOSIT_ADMIN_ROLE for authorized payment processing + * - Uses permissioned modifier for authorized payment processing * - Tracks user deposits in _depositedAmounts mapping * - Enforces maximum deposit limit of 100 ether * - Processes payments through Orchestrator's payment processor * - Makes use of payment order flags * - * @custom:setup This module requires the following MANDATORY setup steps: + * @custom:setup This module has the following OPTIONAL setup steps: * - * 1. Configure DEPOSIT_ADMIN_ROLE: + * 1. Configure a role for authorized deposit processing: * - Purpose: Implements access control for processing user - * deposits. Only authorized admins can process + * deposits. Only permissioned role holders can process * deposits into payment orders. - * - How: The OrchestratorAdmin must: - * 1. Retrieve the deposit admin role identifier - * 2. Grant the role to designated admins - * - Example: module.grantModuleRole( - * module.DEPOSIT_ADMIN_ROLE(), - * adminAddress + * - How: The DefaultAdmin must: + * 1. Create a role for the designated admins + * 2. Look up the target function selector + * 3. Add access permission to the role + * - Example: uint roleId = authorizer.createRole( + * "DEPOSIT_ADMIN", + * authorizer.getAdminRole(), + * address[]); + * authorizer.addAccessPermission( + * address(module), + * ILM_PC_Template_v1.processDeposit.selector, + * roleId * ); * * @custom:security-contact security@inverter.network @@ -92,9 +98,6 @@ contract LM_PC_Template_v1 is ILM_PC_Template_v1, ERC20PaymentClientBase_v2 { /// @notice The maximum deposit amount. uint internal constant MAX_DEPOSIT_AMOUNT = 100 ether; - /// @notice The role that allows processing deposits - bytes32 internal constant DEPOSIT_ADMIN_ROLE = "DEPOSIT_ADMIN"; - /// @notice The payment processor flag for the start timestamp. uint8 internal constant FLAG_START = 1; @@ -173,11 +176,6 @@ contract LM_PC_Template_v1 is ILM_PC_Template_v1, ERC20PaymentClientBase_v2 { return address(_paymentToken); } - /// @inheritdoc ILM_PC_Template_v1 - function getDepositAdminRole() external pure returns (bytes32) { - return DEPOSIT_ADMIN_ROLE; - } - /// @inheritdoc ILM_PC_Template_v1 function getMaxDepositAmount() external pure returns (uint) { return MAX_DEPOSIT_AMOUNT; @@ -205,7 +203,7 @@ contract LM_PC_Template_v1 is ILM_PC_Template_v1, ERC20PaymentClientBase_v2 { /// @inheritdoc ILM_PC_Template_v1 function processDeposit(address user_, uint start_, uint cliff_, uint end_) external - onlyModuleRole(DEPOSIT_ADMIN_ROLE) + permissioned { uint amount = _depositedAmounts[user_]; diff --git a/src/templates/tests/unit/FM_Template_v1.t.sol b/src/templates/tests/unit/FM_Template_v1.t.sol index 81531d964..efcb923b6 100644 --- a/src/templates/tests/unit/FM_Template_v1.t.sol +++ b/src/templates/tests/unit/FM_Template_v1.t.sol @@ -101,7 +101,7 @@ contract FM_Template_v1_Test is ModuleTest { } // Test the interface support - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( fundingManager.supportsInterface( type(IFundingManager_v1).interfaceId diff --git a/src/templates/tests/unit/LM_PC_Template_v1.t.sol b/src/templates/tests/unit/LM_PC_Template_v1.t.sol index e61f7f388..b46b2c36e 100644 --- a/src/templates/tests/unit/LM_PC_Template_v1.t.sol +++ b/src/templates/tests/unit/LM_PC_Template_v1.t.sol @@ -72,10 +72,8 @@ contract LM_PC_Template_v1_Test is ModuleTest { _orchestrator, _METADATA, abi.encode(address(paymentToken)) ); - // Give test contract the DEPOSIT_ADMIN_ROLE. - paymentClient.grantModuleRole( - paymentClient.getDepositAdminRole(), address(this) - ); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); } // ------------------------------------------------------------------------- @@ -86,7 +84,7 @@ contract LM_PC_Template_v1_Test is ModuleTest { assertEq(paymentClient.getPaymentToken(), address(paymentToken)); } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( paymentClient.supportsInterface( type(IERC20PaymentClientBase_v2).interfaceId @@ -161,43 +159,28 @@ contract LM_PC_Template_v1_Test is ModuleTest { } /* Test: processDeposit() - ├── Given the caller does not have the DEPOSIT_ADMIN_ROLE + ├── Given the caller is not permissioned │ └── When the function processDeposit() is called │ └── Then it reverts (modifier in place) - └── Given the caller has DEPOSIT_ADMIN_ROLE + └── Given the caller is permissioned └── When the function processDeposit() is called ├── Then the deposit balance clears └── And the payment order processes */ - function testProcessDeposit_revertGivenNotAdmin(address notAdmin_) public { - // Setup - vm.assume(notAdmin_ != address(this) && notAdmin_ != address(0)); - - uint depositAmount = 50 ether; - vm.startPrank(notAdmin_); - paymentToken.mint(notAdmin_, depositAmount); - paymentToken.approve(address(paymentClient), depositAmount); - - uint start = block.timestamp; - uint cliff = block.timestamp + 30 days; - uint end = block.timestamp + 90 days; - paymentClient.deposit(depositAmount); + function testBuyFor_ModifierInPositionChecks() public { + // permissioned - // Test + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _orchestrator.authorizer().generateRoleId( - address(paymentClient), paymentClient.getDepositAdminRole() - ), - notAdmin_ + IModule_v1.Module__CallerNotPermissioned.selector ) ); - paymentClient.processDeposit(notAdmin_, start, cliff, end); - - vm.stopPrank(); + vm.prank(address(0xB0B)); + paymentClient.processDeposit(address(0), 0, 0, 0); } function testProcessDeposit_worksGivenDepositIsProcessed( @@ -264,14 +247,6 @@ contract LM_PC_Template_v1_Test is ModuleTest { assertEq(paymentClient.getPaymentToken(), address(paymentToken)); } - /* Test: getDepositAdminRole() - └── When the function getDepositAdminRole() is called - └── Then it returns the deposit admin role - */ - function testGetDepositAdminRole() public { - assertEq(paymentClient.getDepositAdminRole(), DEPOSIT_ADMIN_ROLE); - } - /* Test: getMaxDepositAmount() └── When the function getMaxDepositAmount() is called └── Then it returns the maximum deposit amount diff --git a/src/templates/tests/unit/PP_Template_v1.t.sol b/src/templates/tests/unit/PP_Template_v1.t.sol index 2013005d7..9df34a2b0 100644 --- a/src/templates/tests/unit/PP_Template_v1.t.sol +++ b/src/templates/tests/unit/PP_Template_v1.t.sol @@ -104,7 +104,7 @@ contract PP_Template_v1_Test is ModuleTest { } // Test the interface support - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( paymentProcessor.supportsInterface( type(IPaymentProcessor_v2).interfaceId diff --git a/test/e2e/authorizer/RoleAuthorizerE2E.t.sol b/test/e2e/authorizer/RoleAuthorizerE2E.t.sol index 29091f255..1c594e8b8 100644 --- a/test/e2e/authorizer/RoleAuthorizerE2E.t.sol +++ b/test/e2e/authorizer/RoleAuthorizerE2E.t.sol @@ -21,12 +21,13 @@ import { IERC20PaymentClientBase_v2 } from "@lm/LM_PC_Bounties_v2.sol"; -contract RoleAuthorizerE2E is E2ETest { +contract RoleAuthorizerE2E1 is E2ETest { // Module Configurations for the current E2E test. Should be filled during setUp() call. IOrchestratorFactory_v1.ModuleConfig[] moduleConfigurations; // E2E Test Variables - address orchestratorAdmin = makeAddr("orchestratorAdmin"); + address initialAdmin = makeAddr("initialAdmin"); + address bountyIssuer = makeAddr("bountyIssuer"); address bountyVerifier = makeAddr("bountyVerifier"); address bountySubmitter = makeAddr("bountySubmitter"); @@ -54,7 +55,8 @@ contract RoleAuthorizerE2E is E2ETest { setUpRoleAuthorizer(); moduleConfigurations.push( IOrchestratorFactory_v1.ModuleConfig( - roleAuthorizerMetadata, abi.encode(address(this)) + roleAuthorizerMetadata, + abi.encode(initialAdmin) //this sets the given address to be the initialAdmin ) ); @@ -75,7 +77,7 @@ contract RoleAuthorizerE2E is E2ETest { ); } - function test_e2e_RoleAuthorizer() public { + function test_e2e_RoleAuthorizer(address caller_) public { //-------------------------------------------------------------------------- // Orchestrator_v1 Initialization //-------------------------------------------------------------------------- @@ -109,34 +111,125 @@ contract RoleAuthorizerE2E is E2ETest { } //-------------------------------------------------------------------------- - // Assign Bounty Manager Roles + // Create Bounty Manager Roles //-------------------------------------------------------------------------- - // we authorize the Admin to create bounties - bountyManager.grantModuleRole( - bountyManager.BOUNTY_ISSUER_ROLE(), address(orchestratorAdmin) - ); - - // we authorize the manager to verify bounty claims - bountyManager.grantModuleRole( - bountyManager.VERIFIER_ROLE(), bountyVerifier - ); - - // we authorize the bountySubmitter to submit bounty claims - bountyManager.grantModuleRole( - bountyManager.CLAIMANT_ROLE(), address(bountySubmitter) - ); - - // Since we deploy the orchestrator, with address(this) as admin, - // we now assign them to two external addresses. In production these will be directly set on deployment. - - // we grant admin role to adminAddress - bytes32 adminRole = authorizer.getAdminRole(); - authorizer.grantRole(adminRole, address(orchestratorAdmin)); - authorizer.renounceRole(adminRole, address(this)); - assertTrue(authorizer.hasRole(adminRole, orchestratorAdmin)); - assertEq(authorizer.getRoleMemberCount(adminRole), 1); - + // This subsection divider is to prevent stack too deep errors + { + // First we define new Roles that we want to have in the system + // and assign initial members to them + + // BOUNTY_ISSUER Role + // Lets start with the Bounty Issuer Role + // For that we define a name + string memory roleName = "BOUNTY_ISSUER"; + // We define which role will be the admin of the new role + // (Admin Roles can add and remove members to that role) + // In this case we will use the initial admin role (number 0) + bytes32 roleAdmin = authorizer.getAdminRole(); // Alternatively bytes32(uint(0)) + + // We define the initial members of the role + address[] memory roleMembers = new address[](1); + roleMembers[0] = bountyIssuer; + + // With the above defined values we can now create the role + // We save the Id of the role in a variable + // The function is initally only callable by the initial admin + // but can be adapted like any other permissioned function (see down below) + vm.prank(initialAdmin); + bytes32 bountyIssuerRoleId = + authorizer.createRole(roleName, roleAdmin, roleMembers); + + // Public Role (Bounty Submitter) + // For the Bounty Submitter we choose the public role + // Which allows anyone to join the role + // We fetch the roleId of the public role from the authorizer + bytes32 bountySubmitterRoleId = authorizer.PUBLIC_ROLE(); // Alternatively bytes32(uint(1)) + + //-------------------------------------------------------------------------- + // Define Role Permissions + //-------------------------------------------------------------------------- + + // Now lets define the permissions for the roles + // Starting with the Bounty Issuer Role + // For this we will use the addAccessPermission function in the authorizer + + // First we define the target and function selector + // The target is the address of the bountyManager + address target = address(bountyManager); + // The function selector is the addBounty function + bytes4 selector = bountyManager.addBounty.selector; + // With the previously created role and respective id + // We can now add the permission to the bounty issuer role + vm.prank(initialAdmin); + authorizer.addAccessPermission(target, selector, bountyIssuerRoleId); + + // Lets check that the role holder is permissioned to call the function + assertTrue(authorizer.hasPermission(bountyIssuer, target, selector)); + + // Now lets add the Permission Bounty Submitter Role + // The selector here is the addClaim function + selector = bountyManager.addClaim.selector; + // As we defined before we want to make the function have public access + // so we use the public role as the given roleId + vm.prank(initialAdmin); + authorizer.addAccessPermission( + target, selector, bountySubmitterRoleId + ); + + // Now we check that the address bountySubmitter, which we never set as + // a member of the BOUNTY_ISSUER role, has the permission to call the function + assertTrue( + authorizer.hasPermission(bountySubmitter, target, selector) + ); + + //-------------------------------------------------------------------------- + // Creating Roles and Defining Role Permissions in the same Call + //-------------------------------------------------------------------------- + + // BOUNTY_VERIFIER Role + // For the creation of the BOUNTY_VERIFIER Role we will use + // the combined function createRoleAndAddAccessPermissions + // Which allows us to create and assign roles and permissions + // at the same time + + // We define the role name + roleName = "BOUNTY_VERIFIER"; + // We define the role admin + roleAdmin = authorizer.getAdminRole(); + // We define the initial members of the role + roleMembers = new address[](1); + roleMembers[0] = bountyVerifier; + + // Now we need to create two arrays + // The first one defines the contracts for which we want to assign permissions + address[] memory targets = new address[](1); + // We take the address of the bountyManager here + targets[0] = address(bountyManager); + // The second one defines the selectors for which we want to assign permissions + // This is a two dimensional array, where the first dimension is defines + // the target contract for which we want to assign permissions and the second + // dimension defines the function selectors of the target contract + // In this case we want only the bountyManager functions, so we define the + // array to be of length 1 + bytes4[][] memory selectors = new bytes4[][](1); + // And we define the single bountyManager function verifyClaim(), so we + // define the array to be of length 1 again + selectors[0] = new bytes4[](1); + // In case we wanted to select multiple functions, we would define the array + // to be the size of the number of functions we want to modify permissions for + // With that we can add the selector + selectors[0][0] = bountyManager.verifyClaim.selector; + + vm.prank(initialAdmin); + bytes32 bountyVerifierRoleId = authorizer + .createRoleAndAddAccessPermissions( + roleName, roleAdmin, roleMembers, targets, selectors + ); + + // Now we check that the role has been created + assertTrue(authorizer.hasRole(bountyVerifierRoleId, bountyVerifier)); + } //-------------------------------------------------------------------------- // Set up seed deposit and initial deposit by users //-------------------------------------------------------------------------- @@ -165,7 +258,7 @@ contract RoleAuthorizerE2E is E2ETest { uint maximumPayoutAmount = 500e18; bytes memory details = "This is a test bounty"; - vm.prank(orchestratorAdmin); + vm.prank(bountyIssuer); uint bountyId = bountyManager.addBounty( minimumPayoutAmount, maximumPayoutAmount, details ); diff --git a/test/e2e/authorizer/TokenGatedRoleAuthorizerE2E.t.sol b/test/e2e/authorizer/TokenGatedRoleAuthorizerE2E.t.sol index 221e6aa1c..d9b04219a 100644 --- a/test/e2e/authorizer/TokenGatedRoleAuthorizerE2E.t.sol +++ b/test/e2e/authorizer/TokenGatedRoleAuthorizerE2E.t.sol @@ -116,39 +116,72 @@ contract TokenGatedRoleAuthorizerE2E is E2ETest { vm.startPrank(orchestratorAdmin); { - // Make the BOUNTY_ADMIN_ROLE token-gated by GATOR token and set the threshold - bytes32 bountyRoleId = authorizer.generateRoleId( - address(bountyManager), bountyManager.BOUNTY_ISSUER_ROLE() + // BOUNTY_ISSUER_ROLE + // Create the role + bytes32 bountyIssuerRoleId = authorizer.createRole( + "BOUNTY_ISSUER_ROLE", + authorizer.DEFAULT_ADMIN_ROLE(), + new address[](0) ); - authorizer.setTokenGated(bountyRoleId, true); - authorizer.setThreshold(bountyRoleId, address(gatingToken), 100); - authorizer.grantRole(bountyRoleId, address(gatingToken)); - // We mint 101 tokens to the orchestrator admin so they can create bounties - gatingToken.mint(orchestratorAdmin, 101); + authorizer.setTokenGated(bountyIssuerRoleId, true); + authorizer.setThreshold( + bountyIssuerRoleId, address(gatingToken), 100 + ); - // Make the VERIFY_ADMIN_ROLE token-gated by GATOR token and set the threshold - bytes32 verifierRoleId = authorizer.generateRoleId( - address(bountyManager), bountyManager.VERIFIER_ROLE() + // Now add the gating Token as a member of the role + // With this any holder of that token with a balance equal or higher than + // 100 will have permission to access the BOUNTY_ISSUER_ROLE functions + // In this case we actually only want the orchestrator admin to be able to call this + // As the default admin is always allowed to call permissioned functions + authorizer.grantRole(bountyIssuerRoleId, address(gatingToken)); + + // VERIFIER_ROLE + // Create the role + bytes32 verifierRoleId = authorizer.createRole( + "VERIFIER_ROLE", + authorizer.DEFAULT_ADMIN_ROLE(), + new address[](0) ); authorizer.setTokenGated(verifierRoleId, true); authorizer.setThreshold(verifierRoleId, address(gatingToken), 50); authorizer.grantRole(verifierRoleId, address(gatingToken)); - // We mint 51 tokens to the orchestrator manager so they can verify bounties - gatingToken.mint(bountyVerifier, 51); + // We mint 50 tokens to the orchestrator manager so they can verify bounties + gatingToken.mint(bountyVerifier, 50); - // Make the CLAIM_ADMIN_ROLE token-gated by GATOR token and set the threshold - bytes32 claimRoleId = authorizer.generateRoleId( - address(bountyManager), bountyManager.CLAIMANT_ROLE() + // CLAIMANT_ROLE + // Create the role + bytes32 claimRoleId = authorizer.createRole( + "CLAIMANT_ROLE", + authorizer.DEFAULT_ADMIN_ROLE(), + new address[](0) ); authorizer.setTokenGated(claimRoleId, true); authorizer.setThreshold(claimRoleId, address(gatingToken), 25); authorizer.grantRole(claimRoleId, address(gatingToken)); - // We mint 26 tokens to the bounty submitter so they can submit bounties - gatingToken.mint(bountySubmitter, 26); + // We mint 25 tokens to the bounty submitter so they can submit bounties + gatingToken.mint(bountySubmitter, 25); + + // Assign the correct permissions to the roles + authorizer.addAccessPermission( + address(bountyManager), + bountyManager.addBounty.selector, + bountyIssuerRoleId + ); + authorizer.addAccessPermission( + address(bountyManager), + bountyManager.addClaim.selector, + claimRoleId + ); + authorizer.addAccessPermission( + address(bountyManager), + bountyManager.verifyClaim.selector, + verifierRoleId + ); } + vm.stopPrank(); //-------------------------------------------------------------------------- diff --git a/test/e2e/authorizer/extensions/VotingRoleManagerE2E.t.sol b/test/e2e/authorizer/extensions/VotingRoleManagerE2E.t.sol index a9af8d70b..cd3ab8806 100644 --- a/test/e2e/authorizer/extensions/VotingRoleManagerE2E.t.sol +++ b/test/e2e/authorizer/extensions/VotingRoleManagerE2E.t.sol @@ -51,13 +51,12 @@ contract VotingRoleManagerE2E is E2ETest { ); // Authorizer - setUpTokenGatedRoleAuthorizer(); + setUpRoleAuthorizer(); moduleConfigurations.push( IOrchestratorFactory_v1.ModuleConfig( - tokenRoleAuthorizerMetadata, abi.encode(address(this)) + roleAuthorizerMetadata, abi.encode(address(this)) ) ); - // PaymentProcessor setUpSimplePaymentProcessor(); moduleConfigurations.push( @@ -125,18 +124,27 @@ contract VotingRoleManagerE2E is E2ETest { } } - // We make the governor the only admin - bytes32 adminRole = authorizer.getAdminRole(); - authorizer.grantRole(adminRole, address(votingRoles)); - - // we authorize governance to create bounties - bountyManager.grantModuleRole( - bountyManager.BOUNTY_ISSUER_ROLE(), address(votingRoles) + // Create and assign role to create bounties for the governance contract + + // Members of the role + address[] memory roleMembers = new address[](1); + roleMembers[0] = address(votingRoles); + // Target contract and function selectors + address[] memory targets = new address[](1); + targets[0] = address(bountyManager); + bytes4[][] memory selectors = new bytes4[][](1); + selectors[0] = new bytes4[](1); + selectors[0][0] = bountyManager.addBounty.selector; + + // Create role and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "BOUNTY_ISSUER", + orchestrator.authorizer().getAdminRole(), + roleMembers, + targets, + selectors ); - // By having address(this) renounce the Admin Role, all changes from now on need to go through the AUT_EXT_VotingRoles_v1 - authorizer.renounceRole(adminRole, address(this)); - //-------------------------------------------------------------------------- // Set up Vote to create Bounty //-------------------------------------------------------------------------- @@ -188,8 +196,8 @@ contract VotingRoleManagerE2E is E2ETest { } function _getMotionExecutionResult( - AUT_EXT_VotingRoles_v1 votingRoles, - bytes32 motionId + AUT_EXT_VotingRoles_v1 votingRoles_, + bytes32 motionId_ ) internal view returns (bool, bytes memory) { ( , // address _addr @@ -203,7 +211,7 @@ contract VotingRoleManagerE2E is E2ETest { , // uint _excAt bool _excRes, bytes memory _excData - ) = votingRoles.motions(motionId); + ) = votingRoles_.getMotion(motionId_); return (_excRes, _excData); } diff --git a/test/e2e/fundingManager/BondingCurveFundingManagerE2E.t.sol b/test/e2e/fundingManager/BondingCurveFundingManagerE2E.t.sol index 2ad65686e..27cbc95da 100644 --- a/test/e2e/fundingManager/BondingCurveFundingManagerE2E.t.sol +++ b/test/e2e/fundingManager/BondingCurveFundingManagerE2E.t.sol @@ -10,6 +10,7 @@ import { IOrchestrator_v1 } from "test/e2e/E2ETest.sol"; +import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; // SuT @@ -112,6 +113,9 @@ contract BondingCurveFundingManagerE2E is E2ETest { IOrchestrator_v1 orchestrator = _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + AUT_Roles_v1 authorizer = + AUT_Roles_v1(address(orchestrator.authorizer())); + FM_BC_Bancor_Redeeming_VirtualSupply_v1 fundingManager = FM_BC_Bancor_Redeeming_VirtualSupply_v1( address(orchestrator.fundingManager()) @@ -119,6 +123,20 @@ contract BondingCurveFundingManagerE2E is E2ETest { issuanceToken.setMinter(address(fundingManager), true); + // Set up Roles + // Make buy public + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.buy.selector, + authorizer.PUBLIC_ROLE() + ); + // Make sell public + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.sell.selector, + authorizer.PUBLIC_ROLE() + ); + // Mint some tokens to alice and bob in order to fund the fundingmanager. // Alice will perform a very big initial buy. diff --git a/test/e2e/fundingManager/BondingCurveTokenRescueE2E.t.sol b/test/e2e/fundingManager/BondingCurveTokenRescueE2E.t.sol index bcb3df7b6..bf6faa919 100644 --- a/test/e2e/fundingManager/BondingCurveTokenRescueE2E.t.sol +++ b/test/e2e/fundingManager/BondingCurveTokenRescueE2E.t.sol @@ -10,6 +10,8 @@ import { IOrchestrator_v1 } from "test/e2e/E2ETest.sol"; +import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; + import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; import {LM_PC_PaymentRouter_v2} from "@lm/LM_PC_PaymentRouter_v2.sol"; import {IFundingManager_v1} from "@fm/IFundingManager_v1.sol"; @@ -104,6 +106,9 @@ contract BondingCurveTokenRescueE2E is E2ETest { IOrchestrator_v1 orchestrator = _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + AUT_Roles_v1 authorizer = + AUT_Roles_v1(address(orchestrator.authorizer())); + FM_BC_Bancor_Redeeming_VirtualSupply_v1 fundingManager = FM_BC_Bancor_Redeeming_VirtualSupply_v1( address(orchestrator.fundingManager()) @@ -111,6 +116,14 @@ contract BondingCurveTokenRescueE2E is E2ETest { issuanceToken.setMinter(address(fundingManager), true); + // Set up Roles + // Make buy public + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.buy.selector, + authorizer.PUBLIC_ROLE() + ); + // Mint some tokens to alice in order to fund the fundingmanager. // Alice will perform a very big initial buy. @@ -177,11 +190,6 @@ contract BondingCurveTokenRescueE2E is E2ETest { // Transfer all collateral to the new BC - LM_PC_PaymentRouter_v2(paymentRouter).grantModuleRole( - LM_PC_PaymentRouter_v2(paymentRouter).PAYMENT_PUSHER_ROLE(), - address(this) - ); - LM_PC_PaymentRouter_v2(paymentRouter).pushPayment( newBondingCurve, // recipient address(token), // token @@ -225,6 +233,14 @@ contract BondingCurveTokenRescueE2E is E2ETest { oldCollateralSupply, fundingManager.getVirtualCollateralSupply() ); + // Assign new Permissions to the new BC + + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.buy.selector, + authorizer.PUBLIC_ROLE() + ); + // Bob performs a buy address bob = address(0x606); uint bobBuyAmount = 5000e18; diff --git a/test/e2e/fundingManager/BondingSurfaceFundingManagerE2E.t.sol b/test/e2e/fundingManager/BondingSurfaceFundingManagerE2E.t.sol index 12c0dcb2b..4a07aafe7 100644 --- a/test/e2e/fundingManager/BondingSurfaceFundingManagerE2E.t.sol +++ b/test/e2e/fundingManager/BondingSurfaceFundingManagerE2E.t.sol @@ -10,6 +10,8 @@ import { IOrchestrator_v1 } from "test/e2e/E2ETest.sol"; +import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; + import {IModule_v1} from "src/modules/base/IModule_v1.sol"; import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; @@ -147,6 +149,9 @@ contract BondingSurfaceFundingManagerE2E is E2ETest { IOrchestrator_v1 orchestrator = _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + AUT_Roles_v1 authorizer = + AUT_Roles_v1(address(orchestrator.authorizer())); + FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 fundingManager = FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1( @@ -176,15 +181,84 @@ contract BondingSurfaceFundingManagerE2E is E2ETest { fundingManager.setTokenVault(address(tokenVault)); // Set Roles - fundingManager.grantModuleRole( - fundingManager.RISK_MANAGER_ROLE(), riskManager - ); - fundingManager.grantModuleRole( - fundingManager.COVER_MANAGER_ROLE(), coverManager - ); - fundingManager.grantModuleRole( - fundingManager.CURVE_INTERACTION_ROLE(), curveUser - ); + { + // Create and assign role for the riskManager + + // Members of the role + address[] memory roleMembers = new address[](1); + roleMembers[0] = riskManager; + // Target contract and function selectors + address[] memory targets = new address[](1); + targets[0] = address(fundingManager); + bytes4[][] memory selectors = new bytes4[][](1); + selectors[0] = new bytes4[](2); + selectors[0][0] = fundingManager.setCapitalRequired.selector; + selectors[0][1] = fundingManager.setBasePriceMultiplier.selector; + + // Create role and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "RISK_MANAGER_ROLE", + authorizer.getAdminRole(), + roleMembers, + targets, + selectors + ); + + // Create and assign role for the coverManager + + // Members of the role + roleMembers = new address[](1); + roleMembers[0] = coverManager; + // Target contract and function selectors + targets = new address[](1); + targets[0] = address(fundingManager); + selectors = new bytes4[][](1); + selectors[0] = new bytes4[](1); + selectors[0][0] = fundingManager.seize.selector; + + // Create role and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "COVER_MANAGER_ROLE", + authorizer.getAdminRole(), + roleMembers, + targets, + selectors + ); + + // Create and assign role for the curveUser + + // Members of the role + roleMembers = new address[](1); + roleMembers[0] = curveUser; + // Target contract and function selectors + targets = new address[](1); + targets[0] = address(fundingManager); + selectors = new bytes4[][](1); + selectors[0] = new bytes4[](2); + selectors[0][0] = fundingManager.buy.selector; + selectors[0][1] = fundingManager.sell.selector; + + // Create role and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "CURVE_USER_ROLE", + authorizer.getAdminRole(), + roleMembers, + targets, + selectors + ); + + // Buy and sell should be public initially + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.buy.selector, + authorizer.PUBLIC_ROLE() + ); + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.sell.selector, + authorizer.PUBLIC_ROLE() + ); + } //-------------------------------------------------------------------------------- // Setup @@ -201,22 +275,38 @@ contract BondingSurfaceFundingManagerE2E is E2ETest { //-------------------------------------------------------------------------------- // Buy and Sell Restrictions - // Check for that buy and sell is not restricted - assertEq(fundingManager.isBuyAndSellRestricted(), false); + // Check for that buy and sell is public initially + assertEq( + authorizer.hasPermission( + address(0), address(fundingManager), fundingManager.buy.selector + ), + true + ); + assertEq( + authorizer.hasPermission( + address(0), + address(fundingManager), + fundingManager.sell.selector + ), + true + ); - // Restrict Buy and Sell - vm.prank(coverManager); - fundingManager.restrictBuyAndSell(); + // Restrict Buy and Sell by removing the public role + authorizer.removeAccessPermission( + address(fundingManager), + fundingManager.buy.selector, + authorizer.PUBLIC_ROLE() + ); + authorizer.removeAccessPermission( + address(fundingManager), + fundingManager.sell.selector, + authorizer.PUBLIC_ROLE() + ); // Check that the buy and sell functionalities dont work anymore for a regular user vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - orchestrator.authorizer().generateRoleId( - address(fundingManager), - fundingManager.CURVE_INTERACTION_ROLE() - ), - alice + IModule_v1.Module__CallerNotPermissioned.selector ) ); vm.prank(alice); @@ -224,12 +314,7 @@ contract BondingSurfaceFundingManagerE2E is E2ETest { vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - orchestrator.authorizer().generateRoleId( - address(fundingManager), - fundingManager.CURVE_INTERACTION_ROLE() - ), - alice + IModule_v1.Module__CallerNotPermissioned.selector ) ); vm.prank(alice); @@ -248,9 +333,17 @@ contract BondingSurfaceFundingManagerE2E is E2ETest { vm.prank(curveUser); fundingManager.sell(curveUserSellAmount, 1); - // Open up functions again - vm.prank(coverManager); - fundingManager.unrestrictBuyAndSell(); + // Open up functions again by making them public again + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.buy.selector, + authorizer.PUBLIC_ROLE() + ); + authorizer.addAccessPermission( + address(fundingManager), + fundingManager.sell.selector, + authorizer.PUBLIC_ROLE() + ); //-------------------------------------------------------------------------------- // Transfer Repayment diff --git a/test/e2e/fundingManager/oracle/OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.sol b/test/e2e/fundingManager/oracle/OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.t.sol similarity index 76% rename from test/e2e/fundingManager/oracle/OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.sol rename to test/e2e/fundingManager/oracle/OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.t.sol index 174701b48..03d1d084a 100644 --- a/test/e2e/fundingManager/oracle/OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.sol +++ b/test/e2e/fundingManager/oracle/OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E.t.sol @@ -214,6 +214,8 @@ contract OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E is orchestrator = _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + authorizer = AUT_Roles_v1(address(orchestrator.authorizer())); + // Get funding manager fundingManager = FM_PC_Oracle_Redeeming_v1(address(orchestrator.fundingManager())); @@ -246,102 +248,156 @@ contract OracleFundingManagerAndManualQueueBasedPaymentProcessorE2E is function _setRoles() internal { //-------------------------------------------------------------------------- - // Set role admins roles in the system + // Set roles and their admin roles in the system - bytes32 roleId; - bytes32 adminRoleId; + { + //Here we create the different roles and set their initial members + // We remember that the role id is just counting up from 1 + // 0 is the default admin role and 1 is the public role + // The create Role function can only be called by permissioned addresses + // In this case only the default admin can call it - // Set role admin for price setter role - roleId = orchestrator.authorizer().generateRoleId( - address(permissionedOracle), permissionedOracle.getPriceSetterRole() - ); - adminRoleId = orchestrator.authorizer().generateRoleId( - address(permissionedOracle), - permissionedOracle.getPriceSetterRoleAdmin() - ); - orchestrator.authorizer().transferAdminRole(roleId, adminRoleId); + string memory roleName; + bytes32 roleAdmin; + address[] memory roleMembers; - // Set role admin for queue operator role - roleId = orchestrator.authorizer().generateRoleId( - address(paymentProcessor), paymentProcessor.getQueueOperatorRole() - ); - adminRoleId = orchestrator.authorizer().generateRoleId( - address(paymentProcessor), - paymentProcessor.getQueueOperatorRoleAdmin() - ); - orchestrator.authorizer().transferAdminRole(roleId, adminRoleId); + //PRICE_SETTER_ROLE_ADMIN + roleName = "PRICE_SETTER_ROLE_ADMIN"; + roleAdmin = bytes32(0); // The default admin + roleMembers = new address[](1); + roleMembers[0] = priceSetterRoleAdmin; - // Set role admin for whitelist role - roleId = orchestrator.authorizer().generateRoleId( - address(fundingManager), fundingManager.getWhitelistRole() - ); - adminRoleId = orchestrator.authorizer().generateRoleId( - address(fundingManager), fundingManager.getWhitelistRoleAdmin() - ); - orchestrator.authorizer().transferAdminRole(roleId, adminRoleId); + authorizer.createRole(roleName, roleAdmin, roleMembers); - // Set role admin for queue executor role - roleId = orchestrator.authorizer().generateRoleId( - address(fundingManager), fundingManager.getQueueExecutorRole() - ); - adminRoleId = orchestrator.authorizer().generateRoleId( - address(fundingManager), fundingManager.getQueueExecutorRoleAdmin() - ); - orchestrator.authorizer().transferAdminRole(roleId, adminRoleId); + //PRICE_SETTER_ROLE + roleName = "PRICE_SETTER_ROLE"; + roleAdmin = bytes32(uint(2)); // The newly created role Price Setter admin + roleMembers = new address[](0); // No initial members as we want for the admins to set them - //-------------------------------------------------------------------------- - // Assign role admin roles + authorizer.createRole(roleName, roleAdmin, roleMembers); - // Assign price setter role admin - permissionedOracle.grantModuleRole( - permissionedOracle.getPriceSetterRoleAdmin(), priceSetterRoleAdmin - ); + //QUEUE_OPERATOR_ROLE_ADMIN + roleName = "QUEUE_OPERATOR_ROLE_ADMIN"; + roleAdmin = bytes32(0); // The default admin + roleMembers = new address[](1); + roleMembers[0] = queueOperatorRoleAdmin; - // Assign queue operator role admin - paymentProcessor.grantModuleRole( - paymentProcessor.getQueueOperatorRoleAdmin(), queueOperatorRoleAdmin - ); + authorizer.createRole(roleName, roleAdmin, roleMembers); - // Assign whitelist role admin - fundingManager.grantModuleRole( - fundingManager.getWhitelistRoleAdmin(), whitelistRoleAdmin - ); + //QUEUE_OPERATOR_ROLE + roleName = "QUEUE_OPERATOR_ROLE"; + roleAdmin = bytes32(uint(4)); // The newly created role Queue Operator admin + roleMembers = new address[](0); // No initial members as we want for the admins to set them - // Assign queue executor role admin - fundingManager.grantModuleRole( - fundingManager.getQueueExecutorRoleAdmin(), queueExecutorRoleAdmin - ); + authorizer.createRole(roleName, roleAdmin, roleMembers); + + //WHITELIST_ROLE_ADMIN + roleName = "WHITELIST_ROLE_ADMIN"; + roleAdmin = bytes32(0); // The default admin + roleMembers = new address[](1); + roleMembers[0] = whitelistRoleAdmin; + + authorizer.createRole(roleName, roleAdmin, roleMembers); + + //WHITELIST_ROLE + roleName = "WHITELIST_ROLE"; + roleAdmin = bytes32(uint(6)); // The newly created role Whitelist admin + roleMembers = new address[](0); // No initial members as we want for the admins to set them + authorizer.createRole(roleName, roleAdmin, roleMembers); + + //QUEUE_EXECUTOR_ROLE_ADMIN + roleName = "QUEUE_EXECUTOR_ROLE_ADMIN"; + roleAdmin = bytes32(0); // The default admin + roleMembers = new address[](1); + roleMembers[0] = queueExecutorRoleAdmin; + + authorizer.createRole(roleName, roleAdmin, roleMembers); + + //QUEUE_EXECUTOR_ROLE + roleName = "QUEUE_EXECUTOR_ROLE"; + roleAdmin = bytes32(uint(8)); // The newly created role Queue Executor admin + roleMembers = new address[](0); // No initial members as we want for the admins to set them + + authorizer.createRole(roleName, roleAdmin, roleMembers); + } + + //-------------------------------------------------------------------------- + // Add Access Permissions to the roles + { + // The addAccessPermission function can only be called by permissioned addresses + // In this case only the default admin can call it + + address target; + bytes4 selector; + bytes32 roleId; + + //PRICE_SETTER_ROLE - setIssuancePrice + target = address(permissionedOracle); + selector = permissionedOracle.setIssuancePrice.selector; + roleId = bytes32(uint(3)); + authorizer.addAccessPermission(target, selector, roleId); + + //PRICE_SETTER_ROLE - setRedemptionPrice + selector = permissionedOracle.setRedemptionPrice.selector; + authorizer.addAccessPermission(target, selector, roleId); + + //PRICE_SETTER_ROLE - setIssuanceAndRedemptionPrice + selector = permissionedOracle.setIssuanceAndRedemptionPrice.selector; + authorizer.addAccessPermission(target, selector, roleId); + + //QUEUE_OPERATOR_ROLE - processPayments + target = address(paymentProcessor); + selector = + paymentProcessor.claimPreviouslyUnclaimableToTreasury.selector; + roleId = bytes32(uint(5)); + authorizer.addAccessPermission(target, selector, roleId); + + //QUEUE_OPERATOR_ROLE - cancelPaymentOrderThroughQueueId + selector = + paymentProcessor.cancelPaymentOrderThroughQueueId.selector; + authorizer.addAccessPermission(target, selector, roleId); + + //WHITELIST_ROLE - buy + target = address(fundingManager); + selector = fundingManager.buy.selector; + authorizer.addAccessPermission(target, selector, bytes32(uint(7))); + + //WHITELIST_ROLE - buyFor + selector = fundingManager.buyFor.selector; + authorizer.addAccessPermission(target, selector, bytes32(uint(7))); + + //WHITELIST_ROLE - sell + selector = fundingManager.sell.selector; + authorizer.addAccessPermission(target, selector, bytes32(uint(7))); + + //WHITELIST_ROLE - sellTo + selector = fundingManager.sellTo.selector; + authorizer.addAccessPermission(target, selector, bytes32(uint(7))); + + //QUEUE_EXECUTOR_ROLE - executeRedemptionQueue + target = address(fundingManager); + selector = fundingManager.executeRedemptionQueue.selector; + authorizer.addAccessPermission(target, selector, bytes32(uint(9))); + } //-------------------------------------------------------------------------- // Assign roles through admins - // Assign price setter role - vm.startPrank(priceSetterRoleAdmin); - permissionedOracle.grantModuleRole( - permissionedOracle.getPriceSetterRole(), priceSetter - ); - vm.stopPrank(); + // Price Setter + vm.prank(priceSetterRoleAdmin); + authorizer.grantRole(bytes32(uint(3)), priceSetter); - // Assign queue operator role - vm.startPrank(queueOperatorRoleAdmin); - paymentProcessor.grantModuleRole( - paymentProcessor.getQueueOperatorRole(), queueOperator - ); - vm.stopPrank(); + // Queue Operator + vm.prank(queueOperatorRoleAdmin); + authorizer.grantRole(bytes32(uint(5)), queueOperator); - // Assign whitelist role - vm.startPrank(whitelistRoleAdmin); - fundingManager.grantModuleRole( - fundingManager.getWhitelistRole(), whitelistedUser - ); - vm.stopPrank(); + // Whitelist + vm.prank(whitelistRoleAdmin); + authorizer.grantRole(bytes32(uint(7)), whitelistedUser); - // Assign queue executor role - vm.startPrank(queueExecutorRoleAdmin); - fundingManager.grantModuleRole( - fundingManager.getQueueExecutorRole(), queueExecutor - ); - vm.stopPrank(); + // Queue Executor + vm.prank(queueExecutorRoleAdmin); + authorizer.grantRole(bytes32(uint(9)), queueExecutor); //-------------------------------------------------------------------------- // Assign other roles in the system diff --git a/test/e2e/logicModule/BountyManagerE2E.t.sol b/test/e2e/logicModule/BountyManagerE2E.t.sol index e511f1568..82fca653a 100644 --- a/test/e2e/logicModule/BountyManagerE2E.t.sol +++ b/test/e2e/logicModule/BountyManagerE2E.t.sol @@ -7,6 +7,7 @@ import { IOrchestratorFactory_v1, IOrchestrator_v1 } from "test/e2e/E2ETest.sol"; +import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; // SuT import { @@ -93,6 +94,9 @@ contract BountyManagerE2E is E2ETest { FM_DepositVault_v1 fundingManager = FM_DepositVault_v1(address(orchestrator.fundingManager())); + AUT_Roles_v1 authorizer = + AUT_Roles_v1(address(orchestrator.authorizer())); + LM_PC_Bounties_v2 bountyManager; address[] memory modulesList = orchestrator.listModules(); @@ -107,10 +111,65 @@ contract BountyManagerE2E is E2ETest { } } - // we authorize the deployer of the orchestrator as the bounty admin - bountyManager.grantModuleRole( - bountyManager.BOUNTY_ISSUER_ROLE(), address(this) + // ========= + // Setting up Roles + + // In the upcoming section we will use different permissioned functions + // For which we need to create roles, add function access and assign the roles + // to the different actors + + // The main functions that we will use are: + // - addBounty + // - addClaim + // - verifyClaim + // For demonstration purposes we will set up the roles in reverse order + + // verifyClaim + // for this function we will set up the VERIFIER role + // First we define the Members of the role in an array + // Verifiers approve claim + + address verifier1 = makeAddr("verifier 1"); + + { + address[] memory roleMembers = new address[](1); + roleMembers[0] = verifier1; + + // Then we select the target contract and function selectors + address[] memory targets = new address[](1); + targets[0] = address(bountyManager); + + bytes4[][] memory selectors = new bytes4[][](1); + selectors[0] = new bytes4[](1); + selectors[0][0] = bountyManager.verifyClaim.selector; + + // Create role, adapt permissions and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "VERIFIER", + authorizer.getAdminRole(), + roleMembers, + targets, + selectors + ); + } + + // addClaim + // Instead of a assigning a role to this function we will make it public + // so that anyone can call it + + authorizer.addAccessPermission( + address(bountyManager), + bountyManager.addClaim.selector, + authorizer.PUBLIC_ROLE() ); + + // addBounty + // addBounty allows the caller to create a new bounty + // This could be a high level admin function so we will only allow the workflow admin to call it + // As the initial admin of the workflow already has access to all permissioned functions + // we wont set up anything for the access to work + // The initial admin in this case is this contract itself, so we dont need to prank the calls + // Funders deposit funds // IMPORTANT @@ -151,11 +210,6 @@ contract BountyManagerE2E is E2ETest { ILM_PC_Bounties_v2.Contributor memory contrib2 = ILM_PC_Bounties_v2.Contributor(address(0xb0b), 150e18); - // auth.setIsAuthorized(address(0xA11CE), true); - bountyManager.grantModuleRole( - bountyManager.CLAIMANT_ROLE(), address(0xA11CE) - ); - ILM_PC_Bounties_v2.Contributor[] memory contribs = new ILM_PC_Bounties_v2.Contributor[](2); contribs[0] = contrib1; @@ -166,13 +220,6 @@ contract BountyManagerE2E is E2ETest { vm.prank(address(0xA11CE)); uint claimId = bountyManager.addClaim(bountyId, contribs, claimDetails); - // Verifiers approve claim - - address verifier1 = makeAddr("verifier 1"); - - // auth.setIsAuthorized(verifier1, true); - bountyManager.grantModuleRole(bountyManager.VERIFIER_ROLE(), verifier1); - vm.prank(verifier1); bountyManager.verifyClaim(claimId, contribs); diff --git a/test/e2e/logicModule/KPIRewarderLifecycle.t.sol b/test/e2e/logicModule/KPIRewarderLifecycle.t.sol index 50195e6db..3828900e3 100644 --- a/test/e2e/logicModule/KPIRewarderLifecycle.t.sol +++ b/test/e2e/logicModule/KPIRewarderLifecycle.t.sol @@ -8,7 +8,7 @@ import "forge-std/console.sol"; import {ModuleTest, IOrchestrator_v1} from "@unitTest/modules/ModuleTest.sol"; import {IModule_v1, ERC165Upgradeable} from "src/modules/base/Module_v1.sol"; import {IOrchestratorFactory_v1} from "src/factories/OrchestratorFactory_v1.sol"; -import {AuthorizerV1Mock} from "@mocks/modules/authorizer/AuthorizerV1Mock.sol"; +import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; // External Libraries import {Clones} from "@oz/proxy/Clones.sol"; @@ -82,6 +82,7 @@ contract LM_PC_KPIRewarder_v2Lifecycle is E2ETest { IOrchestrator_v1 orchestrator; FM_DepositVault_v1 fundingManager; + AUT_Roles_v1 authorizer; LM_PC_KPIRewarder_v2 kpiRewarder; ERC20Mock USDC; @@ -245,6 +246,8 @@ contract LM_PC_KPIRewarder_v2Lifecycle is E2ETest { orchestrator = _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + authorizer = AUT_Roles_v1(address(orchestrator.authorizer())); + fundingManager = FM_DepositVault_v1(address(orchestrator.fundingManager())); @@ -472,9 +475,45 @@ contract LM_PC_KPIRewarder_v2Lifecycle is E2ETest { } function _prepareLM_PC_KPIRewarder_v2() internal { - kpiRewarder.grantModuleRole( - kpiRewarder.ASSERTER_ROLE(), AUTOMATION_SERVICE - ); + { + address[] memory roleMembers = new address[](1); + roleMembers[0] = AUTOMATION_SERVICE; + + // Then we select the target contract and function selectors + address[] memory targets = new address[](1); + targets[0] = address(kpiRewarder); + + bytes4[][] memory selectors = new bytes4[][](1); + selectors[0] = new bytes4[](1); + selectors[0][0] = kpiRewarder.postAssertion.selector; + + // Create role, adapt permissions and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "ASSERTER_ROLE", + authorizer.getAdminRole(), + roleMembers, + targets, + selectors + ); + + // make stake, unstake, claimRewards public + authorizer.addAccessPermission( + address(kpiRewarder), + kpiRewarder.stake.selector, + authorizer.PUBLIC_ROLE() + ); + authorizer.addAccessPermission( + address(kpiRewarder), + kpiRewarder.unstake.selector, + authorizer.PUBLIC_ROLE() + ); + authorizer.addAccessPermission( + address(kpiRewarder), + kpiRewarder.claimRewards.selector, + authorizer.PUBLIC_ROLE() + ); + } + _createDummyContinuousKPI(address(kpiRewarder)); } diff --git a/test/e2e/logicModule/StakingManagerLifecycle.t.sol b/test/e2e/logicModule/StakingManagerLifecycle.t.sol index efa750755..1324af955 100644 --- a/test/e2e/logicModule/StakingManagerLifecycle.t.sol +++ b/test/e2e/logicModule/StakingManagerLifecycle.t.sol @@ -11,7 +11,7 @@ import { IOrchestrator_v1 } from "@unitTest/modules/ModuleTest.sol"; import {IOrchestratorFactory_v1} from "src/factories/OrchestratorFactory_v1.sol"; -import {AuthorizerV1Mock} from "@mocks/modules/authorizer/AuthorizerV1Mock.sol"; +import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; // External Libraries import {Clones} from "@oz/proxy/Clones.sol"; @@ -120,6 +120,9 @@ contract LM_PC_Staking_v2Lifecycle is E2ETest { IOrchestrator_v1 orchestrator = _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + AUT_Roles_v1 authorizer = + AUT_Roles_v1(address(orchestrator.authorizer())); + FM_DepositVault_v1 fundingManager = FM_DepositVault_v1(address(orchestrator.fundingManager())); @@ -137,12 +140,24 @@ contract LM_PC_Staking_v2Lifecycle is E2ETest { } } + // Make stake and unstake public + authorizer.addAccessPermission( + address(stakingManager), + stakingManager.stake.selector, + authorizer.PUBLIC_ROLE() + ); + authorizer.addAccessPermission( + address(stakingManager), + stakingManager.unstake.selector, + authorizer.PUBLIC_ROLE() + ); + // Warp to reasonable time vm.warp(52 weeks); // ---------------- - // 1. deopsit some funds to fundingManager + // 1. deposit some funds to fundingManager uint initialDeposit = amount1 + amount2 + amount3 * 2; rewardToken.mint(address(this), initialDeposit); rewardToken.approve(address(fundingManager), initialDeposit); diff --git a/test/e2e/module/MetaTxAndMulticallE2E.t.sol b/test/e2e/module/MetaTxAndMulticallE2E.t.sol index 4b216d983..6bb1d3547 100644 --- a/test/e2e/module/MetaTxAndMulticallE2E.t.sol +++ b/test/e2e/module/MetaTxAndMulticallE2E.t.sol @@ -49,11 +49,10 @@ contract MetaTxAndMulticallE2E is E2ETest { ); // Authorizer - setUpTokenGatedRoleAuthorizer(); + setUpRoleAuthorizer(); moduleConfigurations.push( IOrchestratorFactory_v1.ModuleConfig( - tokenRoleAuthorizerMetadata, - abi.encode(address(this), address(this)) + roleAuthorizerMetadata, abi.encode(address(this)) ) ); @@ -88,6 +87,9 @@ contract MetaTxAndMulticallE2E is E2ETest { IOrchestrator_v1 orchestrator = _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + AUT_Roles_v1 authorizer = + AUT_Roles_v1(address(orchestrator.authorizer())); + //-------------------------------------------------------------------------- // Module E2E Test //-------------------------------------------------------------------------- @@ -146,32 +148,79 @@ contract MetaTxAndMulticallE2E is E2ETest { FM_DepositVault_v1(fundingManager).token().balanceOf(fundingManager), depositAmount ); + } + + function test_e2e_SendMetaTransaction_WithRole() public { + //-------------------------------------------------------------------------- + // Orchestrator_v1 Initialization + //-------------------------------------------------------------------------- + + IOrchestratorFactory_v1.WorkflowConfig memory workflowConfig = + IOrchestratorFactory_v1.WorkflowConfig({ + independentUpdates: false, + independentUpdateAdmin: address(0) + }); + + IOrchestrator_v1 orchestrator = + _create_E2E_Orchestrator(workflowConfig, moduleConfigurations); + + AUT_Roles_v1 authorizer = + AUT_Roles_v1(address(orchestrator.authorizer())); + + //-------------------------------------------------------------------------- + // Module E2E Test + //-------------------------------------------------------------------------- - //----------------------------------------------------- - // Call Function with role // In this example we're gonna call the bountyManagers createBounty Function // The function needs a role to access it // Lets get the bountyManager address LM_PC_Bounties_v2 bountyManager; - address[] memory modulesList = orchestrator.listModules(); - for (uint i; i < modulesList.length; ++i) { - try ILM_PC_Bounties_v2(modulesList[i]).isExistingBountyId(0) - returns (bool) { - bountyManager = LM_PC_Bounties_v2(modulesList[i]); - break; - } catch { - continue; + { + address[] memory modulesList = orchestrator.listModules(); + for (uint i; i < modulesList.length; ++i) { + try ILM_PC_Bounties_v2(modulesList[i]).isExistingBountyId(0) + returns (bool) { + bountyManager = LM_PC_Bounties_v2(modulesList[i]); + break; + } catch { + continue; + } } } - // Give the signer address the according role - bountyManager.grantModuleRole( - bountyManager.BOUNTY_ISSUER_ROLE(), signer + //----------------------------------------------------- + // Create signer + + uint signerPrivateKey = 0xa11ce; + address signer = vm.addr(signerPrivateKey); + + // Create and assign role for the signer + + // Members of the role + address[] memory roleMembers = new address[](1); + roleMembers[0] = signer; + // Target contract and function selectors + address[] memory targets = new address[](1); + targets[0] = address(bountyManager); + bytes4[][] memory selectors = new bytes4[][](1); + selectors[0] = new bytes4[](1); + selectors[0][0] = bountyManager.addBounty.selector; + + // Create role and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "BOUNTY_ISSUER", + authorizer.getAdminRole(), + roleMembers, + targets, + selectors ); + //----------------------------------------------------- // Then we need to create the ForwardRequest - req = ERC2771ForwarderUpgradeable.ForwardRequestData({ + + ERC2771ForwarderUpgradeable.ForwardRequestData memory req = + ERC2771ForwarderUpgradeable.ForwardRequestData({ from: signer, to: address(bountyManager), value: 0, @@ -189,12 +238,12 @@ contract MetaTxAndMulticallE2E is E2ETest { }); // Create the digest needed to create the signature - digest = forwarder.createDigest(req); + bytes32 digest = forwarder.createDigest(req); // Create Signature with digest (This has to be handled by the frontend) vm.prank(signer); - (v, r, s) = vm.sign(signerPrivateKey, digest); - signature = abi.encodePacked(r, s, v); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); req.signature = signature; @@ -205,7 +254,7 @@ contract MetaTxAndMulticallE2E is E2ETest { assertTrue(bountyManager.isExistingBountyId(1)); } - function test_e2e_SendMulticall() public { + function test_e2e_SendMulticall_WithRole() public { //-------------------------------------------------------------------------- // Orchestrator_v1 Initialization //-------------------------------------------------------------------------- @@ -275,8 +324,26 @@ contract MetaTxAndMulticallE2E is E2ETest { } } - // Give the signer address the according role - bountyManager.grantModuleRole(bountyManager.BOUNTY_ISSUER_ROLE(), user); + // Create and assign role for the user + + // Members of the role + address[] memory roleMembers = new address[](1); + roleMembers[0] = user; + // Target contract and function selectors + address[] memory targets = new address[](1); + targets[0] = address(bountyManager); + bytes4[][] memory selectors = new bytes4[][](1); + selectors[0] = new bytes4[](1); + selectors[0][0] = bountyManager.addBounty.selector; + + // Create role and set members + orchestrator.authorizer().createRoleAndAddAccessPermissions( + "BOUNTY_ISSUER", + orchestrator.authorizer().getAdminRole(), + roleMembers, + targets, + selectors + ); // We create a call struct containing the call we want to make ITransactionForwarder_v1.SingleCall memory call2 = diff --git a/test/e2e/proxies/InverterBeaconE2E.t.sol b/test/e2e/proxies/InverterBeaconE2E.t.sol index 013bce1da..2121f4067 100644 --- a/test/e2e/proxies/InverterBeaconE2E.t.sol +++ b/test/e2e/proxies/InverterBeaconE2E.t.sol @@ -75,10 +75,10 @@ contract InverterBeaconE2E is E2ETest { ); // Authorizer - setUpTokenGatedRoleAuthorizer(); + setUpRoleAuthorizer(); moduleConfigurations.push( IOrchestratorFactory_v1.ModuleConfig( - tokenRoleAuthorizerMetadata, abi.encode(address(this)) + roleAuthorizerMetadata, abi.encode(address(this)) ) ); diff --git a/test/mocks/modules/authorizer/AUT_Roles_v1_Exposed.sol b/test/mocks/modules/authorizer/AUT_Roles_v1_Exposed.sol new file mode 100644 index 000000000..acda46dea --- /dev/null +++ b/test/mocks/modules/authorizer/AUT_Roles_v1_Exposed.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; +// Internal Dependencies + +import {AUT_Roles_v1} from "@aut/role/AUT_Roles_v1.sol"; + +contract AUT_Roles_v1_Exposed is AUT_Roles_v1 { + //========================================================================== + // State Access Functions + + function changeLastAssignedRoleId(uint newLastAssignedRoleIdValue_) + external + { + _lastAssignedRoleId = newLastAssignedRoleIdValue_; + } + + function addAccessPermission_unrestricted( + address target_, + bytes4 selector_, + bytes32 roleId_ + ) external { + _permissions[target_][selector_].push(roleId_); + } + + //========================================================================== + // Modifiers + + function idNotDefaultAdminModifier_exposed(bytes32 roleId_) + public + idNotDefaultAdmin(roleId_) + {} + + function idExistsModifier_exposed(bytes32 roleId_) + public + idExists(roleId_) + {} + + //========================================================================== + // Helper Functions +} diff --git a/test/mocks/modules/authorizer/AUT_TokenGated_Roles_v1_Exposed.sol b/test/mocks/modules/authorizer/AUT_TokenGated_Roles_v1_Exposed.sol new file mode 100644 index 000000000..ca1c489fc --- /dev/null +++ b/test/mocks/modules/authorizer/AUT_TokenGated_Roles_v1_Exposed.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; +// Internal Dependencies + +import {AUT_TokenGated_Roles_v1} from "@aut/role/AUT_TokenGated_Roles_v1.sol"; +import {AccessControlUpgradeable} from + "@oz-up/access/AccessControlUpgradeable.sol"; + +contract AUT_TokenGated_Roles_v1_Exposed is AUT_TokenGated_Roles_v1 { + //========================================================================== + // Modifier + + function onlyEmptyRoleModifier_exposed(bytes32 roleId_) + public + onlyEmptyRole(roleId_) + {} + + function notPublicRoleModifier_exposed(bytes32 roleId_) + public + notPublicRole(roleId_) + {} + + function onlyTokenGatedModifier_exposed(bytes32 roleId_) + public + onlyTokenGated(roleId_) + {} + function validThresholdModifier_exposed(uint threshold_) + public + validThreshold(threshold_) + {} + + //========================================================================== + // Internal Exposed Functions + + function exposed_setThreshold( + bytes32 roleId_, + address token_, + uint threshold_ + ) public { + _setThreshold(roleId_, token_, threshold_); + } + + function exposed_hasTokenRole(bytes32 roleId_, address who_) + public + view + returns (bool hasTokenRole_) + { + return _hasTokenRole(roleId_, who_); + } + + function exposed_AccessControlUpgradeable_hasRole( + bytes32 roleId_, + address who_ + ) public view returns (bool hasRole_) { + return AccessControlUpgradeable.hasRole(roleId_, who_); + } + + //========================================================================== + // Helper Functions + + function setTokenGated_unrestricted(bytes32 roleId_, bool to_) public { + _isTokenGated[roleId_] = to_; + } + + function setThreshold_unrestricted( + bytes32 roleId_, + address token_, + uint threshold_ + ) public { + bytes32 thresholdId = keccak256(abi.encodePacked(roleId_, token_)); + _thresholdMap[thresholdId] = threshold_; + } +} diff --git a/test/mocks/modules/authorizer/AuthorizerV1Mock.sol b/test/mocks/modules/authorizer/AuthorizerV1Mock.sol index 2e0899930..49f81869b 100644 --- a/test/mocks/modules/authorizer/AuthorizerV1Mock.sol +++ b/test/mocks/modules/authorizer/AuthorizerV1Mock.sol @@ -27,7 +27,20 @@ contract AuthorizerV1Mock is IAuthorizer_v1, Module_v1 { mapping(address => bool) private _authorized; mapping(bytes32 => mapping(address => bool)) private _roleAuthorized; + mapping( + address caller + => mapping( + address target + => mapping(bytes4 functionSelector => bool permission) + ) + ) internal _permissions; + bool private _allAuthorized; + address _defaultAdmin; + + function setDefaultAdmin(address who) external { + _defaultAdmin = who; + } function setIsAuthorized(address who, bool to) external { _authorized[who] = to; @@ -53,8 +66,9 @@ contract AuthorizerV1Mock is IAuthorizer_v1, Module_v1 { _authorized[authorized] = true; - _roleAuthorized["0x00"][msg.sender] = true; - _roleAuthorized["0x02"][msg.sender] = true; + _roleAuthorized[0x00][msg.sender] = true; + + _defaultAdmin = authorized; } function mockInit(bytes memory configData) public { @@ -65,53 +79,102 @@ contract AuthorizerV1Mock is IAuthorizer_v1, Module_v1 { _authorized[authorized] = true; } - //-------------------------------------------------------------------------- - // IAuthorizer_v1 Functions + // ======================================================================== + // Mock Overrides - function generateRoleId(address module, bytes32 role) - public - pure - returns (bytes32) + function grantRole(bytes32 role, address who) public { + _roleAuthorized[role][who] = true; + } + + function hasRole(bytes32 role, address who) external view returns (bool) { + return _authorized[who] || _roleAuthorized[role][who] || _allAuthorized; + } + + function checkRoleMembership(bytes32 role, address who) + external + view + returns (bool) { - return keccak256(abi.encodePacked(module, role)); + return _roleAuthorized[role][who]; } - function grantRoleFromModule(bytes32 role, address target) external { - _roleAuthorized[generateRoleId(_msgSender(), role)][target] = true; + function revokeRole(bytes32 role, address who) public { + _roleAuthorized[role][who] = false; } - function grantRoleFromModuleBatched( - bytes32 role, - address[] calldata targets - ) external { - for (uint i = 0; i < targets.length; i++) { - _roleAuthorized[generateRoleId(_msgSender(), role)][targets[i]] = - true; - } + function renounceRole(bytes32, address) external pure { + revert("Not implemented in Authorizer Mock"); } - function revokeRoleFromModule(bytes32 role, address target) external { - _roleAuthorized[generateRoleId(_msgSender(), role)][target] = false; + function getRoleAdmin(bytes32) external pure returns (bytes32) { + return 0x00; // In this mock, all roles have the owner as admin } - function revokeRoleFromModuleBatched( - bytes32 role, - address[] calldata targets - ) external { - for (uint i = 0; i < targets.length; i++) { - _roleAuthorized[generateRoleId(_msgSender(), role)][targets[i]] = - false; + function getRoleMember(bytes32, uint) external pure returns (address) { + revert("Not implemented in Authorizer Mock"); + } + + function getRoleMemberCount(bytes32) external pure returns (uint) { + revert("Not implemented in Authorizer Mock"); + } + + // ======================================================================== + // Public Getter Functions + + // ------------------------------------------------------------------------ + // Getter - Authorization + + function getPermissions(address, bytes4) + external + view + returns (bytes32[] memory) + {} + + function getLastAssignedRoleId() + external + view + returns (uint lastAssignedRoleId_) + {} + + function isRolePermissioned(address, bytes4, bytes32) + external + view + returns (bool) + {} + + function hasPermission( + address caller_, + address target_, + bytes4 functionSelector_ + ) external view returns (bool) { + if (_allAuthorized) { + return true; + } + if (caller_ == _defaultAdmin) { + return true; } + return _permissions[caller_][target_][functionSelector_]; } - function grantRole(bytes32 role, address who) public { - _roleAuthorized[role][who] = true; + function setHasPermission( + address caller_, + address target_, + bytes4 functionSelector_, + bool to + ) external { + _permissions[caller_][target_][functionSelector_] = to; } - function hasRole(bytes32 role, address who) external view returns (bool) { - return _authorized[who] || _roleAuthorized[role][who] || _allAuthorized; + // ------------------------------------------------------------------------ + // Getter - Role Management + + function getAdminRole() external pure returns (bytes32) { + return 0x00; } + // ------------------------------------------------------------------------ + // Getter - Out of Order + function checkForRole(bytes32 role, address who) external view @@ -120,54 +183,75 @@ contract AuthorizerV1Mock is IAuthorizer_v1, Module_v1 { return _authorized[who] || _roleAuthorized[role][who] || _allAuthorized; } - function checkRoleMembership(bytes32 role, address who) - external - view - returns (bool) + function generateRoleId(address module, bytes32 role) + public + pure + returns (bytes32) { - return _roleAuthorized[role][who]; + return keccak256(abi.encodePacked(module, role)); } - function revokeRole(bytes32 role, address who) public { - _roleAuthorized[role][who] = false; - } + // ======================================================================== + // Mutating Functions - function getAdminRole() external pure returns (bytes32) { - return "0x00"; - } + // ------------------------------------------------------------------------ + // Mutating - Authorization + + function addAccessPermission(address, bytes4, bytes32) external {} + + function removeAccessPermission(address, bytes4, bytes32) external {} + + // ------------------------------------------------------------------------ + // Mutating - Role Management - function grantGlobalRole(bytes32 role, address target) external { - bytes32 roleID = generateRoleId(address(orchestrator()), role); - grantRole(roleID, target); + function createRole(string memory, bytes32, address[] memory) + external + returns (bytes32) + {} + + function labelRole(bytes32, string memory) external {} + + function transferAdminRole(bytes32, bytes32) external pure { + revert("Not implemented in Authorizer Mock"); } - function revokeGlobalRole(bytes32 role, address target) external { - bytes32 roleID = generateRoleId(address(orchestrator()), role); - revokeRole(roleID, target); + function burnRoleAdmin(bytes32) external pure { + revert("Not implemented in Authorizer Mock"); } - //-------------------------------------------------------------------------- - // Functions left empty + // ------------------------------------------------------------------------ + // Mutating - Mixed Utility - function grantGlobalRoleBatched(bytes32, address[] calldata) - external - pure - { + function createRoleAndAddAccessPermissions( + string memory, + bytes32, + address[] memory, + address[] memory, + bytes4[][] memory + ) external returns (bytes32) {} + + // ------------------------------------------------------------------------ + // Mutating - Out of Order + + function grantRoleFromModule(bytes32, address) external pure { revert("Not implemented in Authorizer Mock"); } - function revokeGlobalRoleBatched(bytes32, address[] calldata) + function grantRoleFromModuleBatched(bytes32, address[] calldata) external pure { revert("Not implemented in Authorizer Mock"); } - function renounceRole(bytes32, address) external pure { + function revokeRoleFromModule(bytes32, address) external pure { revert("Not implemented in Authorizer Mock"); } - function transferAdminRole(bytes32, bytes32) external pure { + function revokeRoleFromModuleBatched(bytes32, address[] calldata) + external + pure + { revert("Not implemented in Authorizer Mock"); } @@ -175,15 +259,25 @@ contract AuthorizerV1Mock is IAuthorizer_v1, Module_v1 { revert("Not implemented in Authorizer Mock"); } - function getRoleAdmin(bytes32) external pure returns (bytes32) { - return "0x00"; // In this mock, all roles have the owner as admin + function grantGlobalRole(bytes32, address) external pure { + revert("Not implemented in Authorizer Mock"); } - function getRoleMember(bytes32, uint) external pure returns (address) { + function grantGlobalRoleBatched(bytes32, address[] calldata) + external + pure + { revert("Not implemented in Authorizer Mock"); } - function getRoleMemberCount(bytes32) external pure returns (uint) { + function revokeGlobalRole(bytes32, address) external pure { + revert("Not implemented in Authorizer Mock"); + } + + function revokeGlobalRoleBatched(bytes32, address[] calldata) + external + pure + { revert("Not implemented in Authorizer Mock"); } } diff --git a/test/mocks/modules/authorizer/TokenInterfaceMock.sol b/test/mocks/modules/authorizer/TokenInterfaceMock.sol new file mode 100644 index 000000000..af19fc1fe --- /dev/null +++ b/test/mocks/modules/authorizer/TokenInterfaceMock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.23; + +import {TokenInterface} from "@aut/role/AUT_TokenGated_Roles_v1.sol"; + +contract TokenInterfaceMock is TokenInterface { + //========================================================================== + // Storage + mapping(address => uint) public tokenBalances; + + //========================================================================== + // Public Getter Functions + + function balanceOf(address _owner) external view returns (uint balance) { + return tokenBalances[_owner]; + } + + //========================================================================== + // Public Setter Functions + + function setTokenBalance(address _owner, uint _balance) external { + tokenBalances[_owner] = _balance; + } +} diff --git a/test/mocks/modules/base/ModuleV1Mock.sol b/test/mocks/modules/base/ModuleV1Mock.sol index 066d8d364..cf3b6f76a 100644 --- a/test/mocks/modules/base/ModuleV1Mock.sol +++ b/test/mocks/modules/base/ModuleV1Mock.sol @@ -8,6 +8,23 @@ import { } from "src/modules/base/Module_v1.sol"; contract ModuleV1Mock is Module_v1 { + // ======================================================================== + // Modifier Access + + function modifierPermissionedCheck() external view permissioned {} + + // Empty function used to test the modifier `onlyPaymentClient` + function modifierOnlyPaymentClientCheck() external view onlyPaymentClient {} + + function modifierOnlyValidAddressCheck(address to) + external + view + validAddress(to) + {} + + // ======================================================================== + // Initialization + function init( IOrchestrator_v1 orchestrator_, Metadata memory metadata, @@ -25,6 +42,8 @@ contract ModuleV1Mock is Module_v1 { __Module_init(orchestrator_, metadata); } + // ======================================================================== + // Internal Function Access function original_msgSender() external view @@ -43,7 +62,7 @@ contract ModuleV1Mock is Module_v1 { return _msgData(); } - function original_getFeeManagerCollateralFeeData(bytes4 functionSelector) + function _getFeeManagerCollateralFeeData_exposed(bytes4 functionSelector) external view returns (uint, address) @@ -51,7 +70,7 @@ contract ModuleV1Mock is Module_v1 { return _getFeeManagerCollateralFeeData(functionSelector); } - function original_getFeeManagerIssuanceFeeData(bytes4 functionSelector) + function _getFeeManagerIssuanceFeeData_exposed(bytes4 functionSelector) external view returns (uint, address) @@ -59,12 +78,10 @@ contract ModuleV1Mock is Module_v1 { return _getFeeManagerIssuanceFeeData(functionSelector); } - // Empty function used to test the modifier `onlyPaymentClient` - function modifierOnlyPaymentClientCheck() external view onlyPaymentClient {} - - function modifierOnlyValidAddressCheck(address to) + function _checkAuthorization_exposed(address caller_, bytes calldata data_) external view - validAddress(to) - {} + { + _checkAuthorization(caller_, data_); + } } diff --git a/test/mocks/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_SeizableV1_Exposed.sol b/test/mocks/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_SeizableV1_Exposed.sol index 43e04bc52..05657ec9d 100644 --- a/test/mocks/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_SeizableV1_Exposed.sol +++ b/test/mocks/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_SeizableV1_Exposed.sol @@ -32,10 +32,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_SeizableV1_Exposed is // ------------------------------------------------------------------------- // Mock access for internal functions - function exposed_onlyIfNotBuyAndSellRestrictedModifier() external view { - _onlyIfNotBuyAndSellRestrictedModifier(); - } - function exposed_getRepayableAmount() external view diff --git a/test/mocks/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock.sol b/test/mocks/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock.sol deleted file mode 100644 index 235ea4740..000000000 --- a/test/mocks/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; - -import "forge-std/console.sol"; - -// Internal Dependencies -import {IOrchestrator_v1} from - "src/orchestrator/interfaces/IOrchestrator_v1.sol"; - -// SuT -import {FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1} from - "@fm/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.sol"; - -import { - FM_BC_Bancor_Redeeming_VirtualSupply_v1, - IFM_BC_Bancor_Redeeming_VirtualSupply_v1, - FM_BC_Tools -} from "@fm/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; -import {IBancorFormula} from "@fm/bondingCurve/interfaces/IBancorFormula.sol"; -import {Module_v1} from "src/modules/base/Module_v1.sol"; - -contract FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock is - FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1 -{ - // ------------------------------------------------------------------------- - // The FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1 is not abstract, so all the necessary functions are already implemented - // The goal of this mock is to provide direct access to internal functions for testing purposes. - - // ------------------------------------------------------------------------- - // Mock access for internal functions - - function call_BPS() external pure returns (uint) { - return BPS; - } - - function call_PPM() external pure returns (uint32) { - return PPM; - } - - function call_reserveRatioForBuying() external view returns (uint32) { - return reserveRatioForBuying; - } - - function call_reserveRatioForSelling() external view returns (uint32) { - return reserveRatioForSelling; - } - - function call_collateralTokenDecimals() external view returns (uint8) { - return collateralTokenDecimals; - } - - function call_issuanceTokenDecimals() external view returns (uint8) { - return issuanceTokenDecimals; - } - - // Since the init calls are not registered for coverage, we call expose setIssuanceToken to get to 100% test coverage. - function call_setIssuanceToken(address _newIssuanceToken) external { - _setIssuanceToken(_newIssuanceToken); - } - /* - function call_staticPricePPM( - uint _issuanceSupply, - uint _collateralSupply, - uint32 _reserveRatio - ) external pure returns (uint) { - return - _staticPricePPM(_issuanceSupply, _collateralSupply, _reserveRatio); - } - */ - - function call_convertAmountToRequiredDecimal( - uint _amount, - uint8 _tokenDecimals, - uint8 _requiredDecimals - ) external pure returns (uint) { - return FM_BC_Tools._convertAmountToRequiredDecimal( - _amount, _tokenDecimals, _requiredDecimals - ); - } - - // Note: this function returns the virtual token supply in the same format it will be fed to the Bancor formula - function call_getFormulaVirtualIssuanceSupply() - external - view - returns (uint) - { - uint decimalConvertedVirtualIssuanceSupply = FM_BC_Tools - ._convertAmountToRequiredDecimal( - virtualIssuanceSupply, issuanceTokenDecimals, 18 - ); - return decimalConvertedVirtualIssuanceSupply; - } - - function call_setVirtualIssuanceSupply(uint _newSupply) external { - _setVirtualIssuanceSupply(_newSupply); - } - - // Note: this function returns the virtual collateral supply in the same format it will be fed to the Bancor formula - function call_getFormulaVirtualCollateralSupply() - external - view - returns (uint) - { - uint decimalConvertedVirtualCollateralSupply = FM_BC_Tools - ._convertAmountToRequiredDecimal( - virtualCollateralSupply, collateralTokenDecimals, 18 - ); - return decimalConvertedVirtualCollateralSupply; - } - - function call_processCollateralTokensForBuyOperation(uint _amount) - external - { - _processCollateralTokensForBuyOperation(_amount); - } - - function call_handleIssuanceTokensAfterBuy(address _receiver, uint _amount) - external - { - _handleIssuanceTokensAfterBuy(_receiver, _amount); - } - - function call_handleCollateralTokensAfterSell( - address _receiver, - uint _amount - ) external { - _handleCollateralTokensAfterSell(_receiver, _amount); - } - - // ------------------------------------------------------------------------- - // Helper Functions - - function setProjectCollateralFeeCollectedHelper(uint _amount) external { - projectCollateralFeeCollected = _amount; - } -} diff --git a/test/mocks/orchestrator/Orchestrator_v1_Exposed.sol b/test/mocks/orchestrator/Orchestrator_v1_Exposed.sol new file mode 100644 index 000000000..a1b334105 --- /dev/null +++ b/test/mocks/orchestrator/Orchestrator_v1_Exposed.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {Orchestrator_v1} from "src/orchestrator/Orchestrator_v1.sol"; + +import {ModuleManagerBase_v1} from + "src/orchestrator/abstracts/ModuleManagerBase_v1.sol"; + +import {IAuthorizer_v1} from "@aut/IAuthorizer_v1.sol"; + +contract Orchestrator_v1_Exposed is Orchestrator_v1 { + //========================================================================== + // Setup + constructor(address _trustedForwarder) Orchestrator_v1(_trustedForwarder) {} + + function setup_authorizer(address authorizer_) external { + authorizer = IAuthorizer_v1(authorizer_); + } + + //========================================================================== + // Modifier Access + + function modifierPermissionedCheck() external view permissioned {} + + //========================================================================== + // Internal Function Access + + function _checkAuthorization_exposed(address caller_, bytes calldata data_) + external + view + { + _checkAuthorization(caller_, data_); + } +} diff --git a/test/tools/CallIntercepter.sol b/test/tools/CallIntercepter.sol new file mode 100644 index 000000000..b15ab8ed1 --- /dev/null +++ b/test/tools/CallIntercepter.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +contract CallIntercepter is Test { + bool isTrusted; + bool public callShouldBreak; + + constructor() { + isTrusted = true; + } + + // ERC2771Context + // @dev Because we want to expose the isTrustedForwarder function from the ERC2771Context Contract in the IOrchestrator_v1 + // we have to override it here as the original openzeppelin version doesnt contain a interface that we could use to expose it. + function isTrustedForwarder(address) public view virtual returns (bool) { + return isTrusted; + } + + function flipIsTrusted() external { + isTrusted = !isTrusted; + } + + function flipCallShouldBreak() external { + callShouldBreak = !callShouldBreak; + } + + event CallReceived(address intercepterAddress, bytes data, address sender); + + error CallReceivedButBroke( + address intercepterAddress, bytes data, address sender + ); + + fallback(bytes calldata) external virtual returns (bytes memory) { + if (callShouldBreak) { + revert CallReceivedButBroke(address(this), msg.data, msg.sender); + } + emit CallReceived(address(this), msg.data, msg.sender); + return (abi.encode("Call Successful")); + } + + receive() external payable { + revert(); + } +} diff --git a/test/tools/OZErrors.sol b/test/tools/OZErrors.sol new file mode 100644 index 000000000..e922f02e7 --- /dev/null +++ b/test/tools/OZErrors.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +/** + * @dev Library providing error types for OpenZeppelin contracts. + */ +library OZErrors { + // Contract: Initializable + bytes4 public constant Initializable__NotInitializing = + bytes4(keccak256("NotInitializing()")); + bytes4 internal constant Initializable__InvalidInitialization = + bytes4(keccak256("InvalidInitialization()")); + + // Contract: Ownable + bytes4 internal constant Ownable__UnauthorizedAccount = + bytes4(keccak256("OwnableUnauthorizedAccount(address)")); +} diff --git a/test/tools/TypeSanityHelper.sol b/test/tools/TypeSanityHelper.sol new file mode 100644 index 000000000..ea1ff8ff4 --- /dev/null +++ b/test/tools/TypeSanityHelper.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +contract TypeSanityHelper is Test { + address private _self; + + constructor(address self) { + _self = self; + } + + //-------------------------------------------------------------------------- + // Helpers + + function assumeElemNotInSet(address[] memory set, address elem) + public + pure + { + for (uint i; i < set.length; ++i) { + vm.assume(elem != set[i]); + } + } + + //-------------------------------------------------------------------------- + // Types for Orchestrator_v1 + // Contract: Orchestrator_v1.sol + + function assumeValidOrchestratorId(uint id) public pure { + vm.assume(id != 0); + } + + //-------------------------------------------------------------------------- + // Types for Module + // Contract: base/ModuleManagerBase_v1.sol + + uint8 private constant MAX_MODULES = 128; + + mapping(address => bool) moduleCache; + + function assumeValidModules(address[] memory modules) public { + vm.assume(modules.length <= MAX_MODULES); + for (uint i; i < modules.length; ++i) { + assumeValidModule(modules[i]); + + // Assume module unique. + vm.assume(!moduleCache[modules[i]]); + + // Add module to cache. + moduleCache[modules[i]] = true; + } + } + + function assumeValidModule(address module) public view { + address[] memory invalids = createInvalidModules(); + + for (uint i; i < invalids.length; ++i) { + vm.assume(module != invalids[i]); + } + } + + function createInvalidModules() public view returns (address[] memory) { + address[] memory invalids = new address[](3); + + invalids[0] = address(0); + invalids[1] = _self; + + return invalids; + } + + //-------------------------------------------------------------------------- + // Types for Funder + // Contract: base/FunderManager.sol + + function assumeValidFunders(address[] memory funders) public {} +} diff --git a/test/unit/factories/workflow-specific/PIM_WorkflowFactory_v1.t.sol b/test/unit/factories/workflow-specific/PIM_WorkflowFactory_v1.t.sol deleted file mode 100644 index c4cb5514f..000000000 --- a/test/unit/factories/workflow-specific/PIM_WorkflowFactory_v1.t.sol +++ /dev/null @@ -1,572 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -// Internal Dependencies -import {IModule_v1} from "src/modules/base/IModule_v1.sol"; -import {IOrchestrator_v1} from - "src/orchestrator/interfaces/IOrchestrator_v1.sol"; -import {IOrchestratorFactory_v1} from - "src/factories/interfaces/IOrchestratorFactory_v1.sol"; -import {IPIM_WorkflowFactory_v1} from - "src/factories/interfaces/IPIM_WorkflowFactory_v1.sol"; -import {IERC20Issuance_v1} from - "src/external/token/interfaces/IERC20Issuance_v1.sol"; -import {ERC20Issuance_v1} from "src/external/token/ERC20Issuance_v1.sol"; -import {IFM_BC_Bancor_Redeeming_VirtualSupply_v1} from - "@fm/bondingCurve/interfaces/IFM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; -import {PIM_WorkflowFactory_v1} from - "src/factories/workflow-specific/PIM_WorkflowFactory_v1.sol"; -import {E2ETest} from "test/e2e/E2ETest.sol"; -import {Ownable} from "@oz/access/Ownable.sol"; -import {IBondingCurveBase_v1} from - "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; -import {IRedeemingBondingCurveBase_v1} from - "@fm/bondingCurve/interfaces/IRedeemingBondingCurveBase_v1.sol"; - -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {ERC20} from "@oz/token/ERC20/ERC20.sol"; - -contract PIM_WorkflowFactory_v1Test is E2ETest { - // SuT - PIM_WorkflowFactory_v1 factory; - - // Deployment Parameters - IOrchestratorFactory_v1.WorkflowConfig workflowConfig; - IOrchestratorFactory_v1.ModuleConfig authorizerConfig; - IOrchestratorFactory_v1.ModuleConfig paymentProcessorConfig; - IOrchestratorFactory_v1.ModuleConfig[] logicModuleConfigs; - IFM_BC_Bancor_Redeeming_VirtualSupply_v1.BondingCurveProperties bcProperties; - IPIM_WorkflowFactory_v1.IssuanceTokenParams issuanceTokenParams; - - address factoryDeployer = vm.addr(1); - address workflowDeployer = vm.addr(2); - address mockTrustedForwarder = vm.addr(3); - address alice = vm.addr(0xA11CE); - - uint initialIssuuanceSupply = 122_727_272_727_272_727_272_727; - uint initialCollateralSupply = 3_163_408_614_166_851_161; - uint firstCollateralIn = 100_000_000; - uint32 reserveRatio = 160_000; - - event PIMWorkflowCreated(address indexed issuanceToken); - - function setUp() public override { - super.setUp(); - - // deploy new factory - factory = new PIM_WorkflowFactory_v1( - address(orchestratorFactory), factoryDeployer, mockTrustedForwarder - ); - assert(factory.owner() == factoryDeployer); - - // Orchestrator/Workflow config - workflowConfig = IOrchestratorFactory_v1.WorkflowConfig({ - independentUpdates: false, - independentUpdateAdmin: address(0) - }); - - // Authorizer - setUpRoleAuthorizer(); - authorizerConfig = IOrchestratorFactory_v1.ModuleConfig( - roleAuthorizerMetadata, abi.encode(address(factory)) - ); - - // PaymentProcessor - setUpSimplePaymentProcessor(); - paymentProcessorConfig = IOrchestratorFactory_v1.ModuleConfig( - simplePaymentProcessorMetadata, bytes("") - ); - - // Additional Logic Modules: bounty manager - setUpBountyManager(); - logicModuleConfigs.push( - IOrchestratorFactory_v1.ModuleConfig( - bountyManagerMetadata, bytes("") - ) - ); - - // Funding Manager: Bancor Virtual Supply - setUpBancorVirtualSupplyBondingCurveFundingManager(); - bcProperties = IFM_BC_Bancor_Redeeming_VirtualSupply_v1 - .BondingCurveProperties({ - formula: address(formula), - reserveRatioForBuying: reserveRatio, - reserveRatioForSelling: reserveRatio, - buyFee: 0, - sellFee: 0, - buyIsOpen: true, - sellIsOpen: true, - initialIssuanceSupply: initialIssuuanceSupply, - initialCollateralSupply: initialCollateralSupply - }); - - // Deploy Issuance Token - issuanceTokenParams = IPIM_WorkflowFactory_v1.IssuanceTokenParams({ - name: "Bonding Curve Token", - symbol: "BCT", - decimals: 18, - maxSupply: type(uint).max - 1 - }); - - // mint collateral token to deployer and approve to factory - token.mint(address(this), type(uint).max); - token.approve(address(factory), type(uint).max); - } - - /* Test testCreatePIMWorkflow - ├── given the default config - │ └── when called - │ └── then it deploys, cleans up minting rights and makes first purchase - └── given withInitialLiquidity flag is set to true - | └── when called - | │ └── then the curve receives initial collateral supply and initial issuance supply is minted to recipient - └── given withInitialLiquidity flag is set to false - | └── when called - | │ └── then the curve doesn't receive initial collateral supply and burns initial issuance supply - └── given both renounce flags are set - | └── when called - | └── then the issuance token doesn't have owner and factory remains workflow admin - └── given only isRenouncedIssuanceToken flag is set - | └── when called - | └── then issuance token doesn't have owner and admin (from params) is workflow admin - └── given only isRenouncedWorkflow flag is set - | └── when called - | └── then admin (from params) is owner of issuance token and factory is workflow admin - └── given msg.sender hasn't approved collateral token for factory - └── when called - └── then it reverts - */ - - function testCreatePIMWorkflow() public { - // CHECK: event is emitted - vm.expectEmit(false, false, false, false); - emit IPIM_WorkflowFactory_v1.PIMWorkflowCreated( - address(0), address(0), address(0), address(0), true, true - ); - // get default config - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - - (IOrchestrator_v1 orchestrator, ERC20Issuance_v1 issuanceToken) = - factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - // CHECK: factory DOES NOT have minting rights on token anymore - bool isFactoryStillMinter = - issuanceToken.allowedMinters(address(factory)); - assertFalse(isFactoryStillMinter); - // CHECK: bonding curve module HAS minting rights on token - bool isBcMinter = - issuanceToken.allowedMinters(address(orchestrator.fundingManager())); - assertTrue(isBcMinter); - // CHECK: deployer uses firstCollateralIn (amount) to make first purchase - assertTrue(issuanceToken.balanceOf(pimConfig.recipient) > 0); - } - - function testCreatePIMWorkflow_WithInitialLiquidity() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - pimConfig.withInitialLiquidity = true; // just to highlight what is being tested - - (IOrchestrator_v1 orchestrator, ERC20Issuance_v1 issuanceToken) = - factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - address fundingManager = address(orchestrator.fundingManager()); - - // CHECK: curve HAS received initial collateral supply and firstCollateralIn - assertTrue( - token.balanceOf(fundingManager) - == pimConfig.bcProperties.initialCollateralSupply - + pimConfig.firstCollateralIn - ); - // CHECK: recipient receives initialIssuanceSupply plus first purchase amount - assertGt( - issuanceToken.balanceOf(pimConfig.recipient), - bcProperties.initialIssuanceSupply - ); - // CHECK: if recipient SELLS complete stack and BUYS BACK they get same amount of issuanceToken - vm.startPrank(pimConfig.recipient); - // get issuance balance before sale and rebuy - uint balanceBefore = issuanceToken.balanceOf(pimConfig.recipient); - // get static price before sale - uint staticPriceBefore = - IBondingCurveBase_v1(fundingManager).getStaticPriceForBuying(); - // get how many issuance tokens are sold - uint firstPurchaseVolume = - balanceBefore - pimConfig.bcProperties.initialIssuanceSupply; - issuanceToken.approve(fundingManager, firstPurchaseVolume); - uint collateralAmountOut = IRedeemingBondingCurveBase_v1(fundingManager) - .calculateSaleReturn(firstPurchaseVolume); - // sell all tokens from initial purchase (the one that happened atomically in the factory) - IRedeemingBondingCurveBase_v1(fundingManager).sell( - firstPurchaseVolume, collateralAmountOut - ); - // alice only has initial issuance supply left? - assert( - issuanceToken.balanceOf(pimConfig.recipient) - == pimConfig.bcProperties.initialIssuanceSupply - ); - token.approve(fundingManager, collateralAmountOut); - uint issuanceAmountOut = IBondingCurveBase_v1(fundingManager) - .calculatePurchaseReturn(collateralAmountOut); - // now use the collateral from the sale to buy back - IBondingCurveBase_v1(fundingManager).buy( - collateralAmountOut, issuanceAmountOut - ); - // get the static price after the re-buy - uint staticPriceAfter = - IBondingCurveBase_v1(fundingManager).getStaticPriceForBuying(); - // CHECK: if recipient SELLS complete stack and BUYS BACK they end up with same balance of issuanceToken - assertApproxEqRel( - balanceBefore, - issuanceToken.balanceOf(pimConfig.recipient), - 0.00001e18 - ); - // CHECK: if recipient SELLS complete stack and BUYS BACK the static price is the same again - assertEq(staticPriceBefore, staticPriceAfter); - vm.stopPrank(); - } - - function testCreatePIMWorkflow_WithoutInitialLiquidity() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - pimConfig.withInitialLiquidity = false; - - uint preCollateralBalance = token.balanceOf(address(this)); - - (IOrchestrator_v1 orchestrator, ERC20Issuance_v1 issuanceToken) = - factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - uint postCollateralBalance = token.balanceOf(address(this)); - - address fundingManager = address(orchestrator.fundingManager()); - - // CHECK: deployer DID NOT send initial collateral supply to curve, ONLY did first purchase - assertEq( - preCollateralBalance - postCollateralBalance, - pimConfig.firstCollateralIn - ); - // CHECK: initialIssuanceSupply is BURNT (sent to 0xDEAD) - assertEq( - issuanceToken.balanceOf(address(0xDEAD)), - bcProperties.initialIssuanceSupply - ); - // CHECK: recipient receives some amount of issuanceToken (due to first purchase) - assertTrue(issuanceToken.balanceOf(pimConfig.recipient) > 0); - - vm.startPrank(pimConfig.recipient); - // get issuance balance before sale and rebuy - uint balanceBefore = issuanceToken.balanceOf(pimConfig.recipient); - // get static price before sale - uint staticPriceBefore = - IBondingCurveBase_v1(fundingManager).getStaticPriceForBuying(); - issuanceToken.approve(fundingManager, balanceBefore); - uint collateralAmountOut = IRedeemingBondingCurveBase_v1(fundingManager) - .calculateSaleReturn(balanceBefore); - // use complete issuance balance to sell for collateral - IRedeemingBondingCurveBase_v1(fundingManager).sell( - balanceBefore, collateralAmountOut - ); - // alice doesn't have any issuance token left - assert(issuanceToken.balanceOf(pimConfig.recipient) == 0); - token.approve(fundingManager, collateralAmountOut); - uint issuanceAmountOut = IBondingCurveBase_v1(fundingManager) - .calculatePurchaseReturn(collateralAmountOut); - // now use the collateral from the sale to buy back - IBondingCurveBase_v1(fundingManager).buy( - collateralAmountOut, issuanceAmountOut - ); - uint staticPriceAfter = - IBondingCurveBase_v1(fundingManager).getStaticPriceForBuying(); - // CHECK: if recipient SELLS complete stack and BUYS BACK they end up with same balance of issuanceToken - assertApproxEqRel( - balanceBefore, - issuanceToken.balanceOf(pimConfig.recipient), - 0.00001e18 - ); - // CHECK: if recipient SELLS complete stack and BUYS BACK the static price is the same again - assertEq(staticPriceBefore, staticPriceAfter); - vm.stopPrank(); - } - - function testCreatePIMWorkflow_IfFullyRenounced() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - pimConfig.isRenouncedIssuanceToken = true; - pimConfig.isRenouncedWorkflow = true; - - (IOrchestrator_v1 orchestrator, ERC20Issuance_v1 issuanceToken) = - factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - // CHECK: the token DOES NOT have an owner anymore - address owner = issuanceToken.owner(); - assertEq(owner, address(0)); - // CHECK: the deployer DOES NOT get admin rights over workflow - bytes32 adminRole = orchestrator.authorizer().getAdminRole(); - bool isDeployerAdmin = orchestrator.authorizer().hasRole( - adminRole, address(workflowDeployer) - ); - assertFalse(isDeployerAdmin); - // CHECK: the factory HAS admin rights over workflow - bool isFactoryAdmin = - orchestrator.authorizer().hasRole(adminRole, address(factory)); - assertTrue(isFactoryAdmin); - } - - function testCreatePIMWorkflow_IfNotRenounced() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - pimConfig.isRenouncedIssuanceToken = false; - pimConfig.isRenouncedWorkflow = false; - pimConfig.recipient = alice; - pimConfig.admin = workflowDeployer; - - (IOrchestrator_v1 orchestrator, ERC20Issuance_v1 issuanceToken) = - factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - // CHECK: the deployer IS owner of the token - assertEq(issuanceToken.owner(), workflowDeployer); - // CHECK: the deployer IS admin of the workflow - bytes32 adminRole = orchestrator.authorizer().getAdminRole(); - bool isDeployerAdmin = orchestrator.authorizer().hasRole( - adminRole, address(workflowDeployer) - ); - assertTrue(isDeployerAdmin); - } - - function testCreatePIMWorkflow_IfTokenRenounced() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - pimConfig.isRenouncedIssuanceToken = true; - pimConfig.isRenouncedWorkflow = false; - pimConfig.recipient = alice; - pimConfig.admin = workflowDeployer; - - (IOrchestrator_v1 orchestrator, ERC20Issuance_v1 issuanceToken) = - factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - // CHECK: the token DOES NOT have an owner anymore - assertEq(issuanceToken.owner(), address(0)); - // CHECK: the deployer IS admin of the workflow - bytes32 adminRole = orchestrator.authorizer().getAdminRole(); - bool isDeployerAdmin = orchestrator.authorizer().hasRole( - adminRole, address(workflowDeployer) - ); - assertTrue(isDeployerAdmin); - } - - function testCreatePIMWorkflow_IfWorkflowRenounced() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - pimConfig.isRenouncedIssuanceToken = false; - pimConfig.isRenouncedWorkflow = true; - - (IOrchestrator_v1 orchestrator,) = factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - // CHECK: the deployer IS owner of the token - // assertEq(issuanceToken.owner(), workflowDeployer); - // CHECK: the deployer DOES NOT have admin rights over workflow - bytes32 adminRole = orchestrator.authorizer().getAdminRole(); - bool isDeployerAdmin = orchestrator.authorizer().hasRole( - adminRole, address(workflowDeployer) - ); - assertFalse(isDeployerAdmin); - // CHECK: the factory DOES have admin rights over workflow - bool isFactoryAdmin = - orchestrator.authorizer().hasRole(adminRole, address(factory)); - assertTrue(isFactoryAdmin); - } - - function testCreatePIMWorkflow_FailsWithoutCollateralTokenApproval() - public - { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - address deployer = address(0xB0B); - vm.prank(deployer); - - vm.expectRevert(); - - factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - } - - /* Test testWithdrawPimFee - ├── given the msg.sender is the fee recipient - | └── when called - | └── then it emits fee claim events on bc and factory - └── given the msg.sender is NOT the fee recipient - └── when called - └── then it reverts - */ - - function testWithdrawPimFee() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - - (IOrchestrator_v1 orchestrator,) = factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - address fundingManager = address(orchestrator.fundingManager()); - - // CHECK: bonding curve EMITS event for fee withdrawal - vm.expectEmit(true, true, true, false); - emit IBondingCurveBase_v1.ProjectCollateralFeeWithdrawn( - address(this), 0 - ); - vm.expectEmit(true, true, true, true); - emit IPIM_WorkflowFactory_v1.PimFeeClaimed(address(this), 0); - factory.withdrawPimFee(fundingManager, alice); - } - - function testWithdrawPimFee__FailsIfCallerIsNotPimFeeRecipient() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - - (IOrchestrator_v1 orchestrator,) = factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - address fundingManager = address(orchestrator.fundingManager()); - // CHECK: withdrawal REVERTS if caller IS NOT the fee recipient - vm.expectRevert( - abi.encodeWithSelector( - IPIM_WorkflowFactory_v1 - .PIM_WorkflowFactory__OnlyPimFeeRecipient - .selector - ) - ); - vm.prank(alice); - factory.withdrawPimFee(fundingManager, alice); - } - - /* Test testTransferPimFeeEligibility - ├── given the msg.sender is the fee recipient - | └── when called - | └── then it emits an event indicating role update and lets new recipient withdraw fee - └── given the msg.sender is NOT the fee recipient - └── when called - └── then it reverts - */ - - function testTransferPimFeeEligibility() public { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - - (IOrchestrator_v1 orchestrator,) = factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - address fundingManager = address(orchestrator.fundingManager()); - // CHECK: when fee recipient is updated event is emitted - vm.expectEmit(true, true, true, true); - emit IPIM_WorkflowFactory_v1.PimFeeRecipientUpdated( - address(this), alice - ); - factory.transferPimFeeEligibility(fundingManager, alice); - - // CHECK: new recipient (alice) CAN withdraw fee - vm.prank(alice); - vm.expectEmit(true, true, true, false); - emit IBondingCurveBase_v1.ProjectCollateralFeeWithdrawn( - address(this), 0 - ); - factory.withdrawPimFee(fundingManager, alice); - } - - function testTransferPimFeeEligibility_FailsIfCallerIsNotPimFeeRecipient() - public - { - IPIM_WorkflowFactory_v1.PIMConfig memory pimConfig = - getDefaultPIMConfig(); - - (IOrchestrator_v1 orchestrator,) = factory.createPIMWorkflow( - workflowConfig, - paymentProcessorConfig, - logicModuleConfigs, - pimConfig - ); - - address fundingManager = address(orchestrator.fundingManager()); - // CHECK: withdrawal REVERTS if caller IS NOT the fee recipient - vm.expectRevert( - abi.encodeWithSelector( - IPIM_WorkflowFactory_v1 - .PIM_WorkflowFactory__OnlyPimFeeRecipient - .selector - ) - ); - vm.prank(alice); - factory.transferPimFeeEligibility(fundingManager, address(0xB0B)); - } - - // UTILS - function getDefaultPIMConfig() - internal - view - returns (IPIM_WorkflowFactory_v1.PIMConfig memory) - { - return IPIM_WorkflowFactory_v1.PIMConfig({ - fundingManagerMetadata: bancorVirtualSupplyBondingCurveFundingManagerMetadata, - authorizerMetadata: roleAuthorizerMetadata, - bcProperties: bcProperties, - issuanceTokenParams: issuanceTokenParams, - collateralToken: address(token), - firstCollateralIn: firstCollateralIn, - admin: address(this), - recipient: alice, - isRenouncedIssuanceToken: true, - isRenouncedWorkflow: true, - withInitialLiquidity: true - }); - } -} diff --git a/test/unit/modules/ModuleTest.sol b/test/unit/modules/ModuleTest.sol index e6a08338f..d8779f669 100644 --- a/test/unit/modules/ModuleTest.sol +++ b/test/unit/modules/ModuleTest.sol @@ -113,6 +113,47 @@ abstract contract ModuleTest is Test { _fundingManager.setToken(IERC20(address(_token))); } + function _setUpOrchestrator() internal virtual { + // Needs to be a proxy for the notInitialized Check + feeManager = FeeManager_v1( + address( + new TransparentUpgradeableProxy( // based on openzeppelins TransparentUpgradeableProxy + address(new FeeManager_v1()), // Implementation Address + address(this), // Admin + bytes("") // data field that could have been used for calls, but not necessary + ) + ) + ); + feeManager.init(address(this), treasury, 0, 0); + governor.setFeeManager(address(feeManager)); + + address[] memory modules = new address[](0); + + address impl = address(new OrchestratorV1Mock(address(_forwarder))); + _orchestrator = OrchestratorV1Mock(Clones.clone(impl)); + + impl = address(new FundingManagerV1Mock()); + _fundingManager = FundingManagerV1Mock(Clones.clone(impl)); + + impl = address(new AuthorizerV1Mock()); + _authorizer = AuthorizerV1Mock(Clones.clone(impl)); + + _orchestrator.init( + _ORCHESTRATOR_ID, + address(moduleFactory), + modules, + _fundingManager, + _authorizer, + _paymentProcessor, + governor + ); + + _authorizer.init(_orchestrator, _METADATA, abi.encode(address(this))); + + _fundingManager.init(_orchestrator, _METADATA, abi.encode("")); + _fundingManager.setToken(IERC20(address(_token))); + } + //-------------------------------------------------------------------------- // Test: Initialization // @@ -122,6 +163,8 @@ abstract contract ModuleTest is Test { function testReinitFails() public virtual; + function testSupportsInterface() public virtual; + //-------------------------------------------------------------------------- // Assertion Helper Functions // diff --git a/test/unit/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.t.sol b/test/unit/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.t.sol index 37aa652f0..828e29a2c 100644 --- a/test/unit/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.t.sol +++ b/test/unit/modules/authorizer/extensions/AUT_EXT_VotingRoles_v1.t.sol @@ -103,12 +103,11 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { _authorizer.grantRole(adminRole, address(_votingRoles)); // _authorizer.setIsAuthorized(address(_votingRoles), true); - // Initialize the votingRoles with 3 users + // Initialize the votingRoles with 2 users - initialVoters = new address[](3); + initialVoters = new address[](2); initialVoters[0] = ALBA; initialVoters[1] = BOB; - initialVoters[2] = COBIE; uint _startingThreshold = DEFAULT_QUORUM; uint _startingDuration = DEFAULT_DURATION; @@ -121,12 +120,11 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { currentVoters.push(ALBA); currentVoters.push(BOB); - currentVoters.push(COBIE); // validation of the initial state happens in testInit() } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( _votingRoles.supportsInterface( type(IAUT_EXT_VotingRoles_v1).interfaceId @@ -140,8 +138,9 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { public returns (bytes32) { - bytes32 countID = - keccak256(abi.encodePacked(_addr, _msg, _votingRoles.motionCount())); + bytes32 countID = keccak256( + abi.encodePacked(_addr, _msg, _votingRoles.getMotionCount()) + ); vm.prank(callingUser); vm.expectEmit(true, true, true, true); @@ -205,7 +204,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { } // the voting time passes - vm.warp(block.timestamp + _votingRoles.voteDuration() + 1); + vm.warp(block.timestamp + _votingRoles.getVoteDuration() + 1); return _voteID; } @@ -220,7 +219,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { } bytes32 _voteID = createVote(_voters[0], _target, _action); - for (uint i = 1; i < _votingRoles.threshold(); ++i) { + for (uint i = 1; i < _votingRoles.getThreshold(); ++i) { if (i < _voters.length) { vm.expectEmit(true, true, true, true); emit VoteCast(_voteID, _voters[(i - 1)], 0); @@ -229,7 +228,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { } // the voting time passes - vm.warp(block.timestamp + _votingRoles.voteDuration() + 1); + vm.warp(block.timestamp + _votingRoles.getVoteDuration() + 1); return _voteID; } @@ -257,7 +256,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { uint _excAt, bool _excRes, bytes memory _excData - ) = _votingRoles.motions(voteId); + ) = _votingRoles.getMotion(voteId); _bufMotion.target = _addr; _bufMotion.action = _act; @@ -290,13 +289,12 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { assertEq(_authorizer.hasRole(admin, address(_votingRoles)), true); // Admin role assertEq(_votingRoles.isVoter(ALBA), true); assertEq(_votingRoles.isVoter(BOB), true); - assertEq(_votingRoles.isVoter(COBIE), true); assertEq(_authorizer.hasRole(admin, address(this)), true); assertEq(_authorizer.hasRole(admin, address(_orchestrator)), false); assertEq(_votingRoles.isVoter(address(this)), false); assertEq(_votingRoles.isVoter(address(_orchestrator)), false); - assertEq(_votingRoles.voterCount(), 3); + assertEq(_votingRoles.getVoterCount(), 2); } function testInitWithInitialVoters(address[] memory testVoters) public { @@ -328,7 +326,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { assertEq(testAuthorizer.isVoter(testVoters[i]), true); } assertEq(testAuthorizer.isVoter(address(this)), false); - assertEq(testAuthorizer.voterCount(), testVoters.length); + assertEq(testAuthorizer.getVoterCount(), testVoters.length); } function testInitWithDuplicateInitialVotersFails( @@ -440,7 +438,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { ); assertEq(address(testAuthorizer.orchestrator()), address(0)); - assertEq(testAuthorizer.voterCount(), 0); + assertEq(testAuthorizer.getVoterCount(), 0); } //-------------------------------------------------------------------------- @@ -495,11 +493,9 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { for (uint i; i < users.length; ++i) { vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - bytes32("onlySelf"), - users[i] - ) + IAUT_EXT_VotingRoles_v1 + .Module__VotingRoleManager__OnlySelfCallAllowed + .selector ); vm.prank(users[i]); // authorized, but not Module if (i % 3 == 0) { @@ -516,11 +512,9 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { for (uint i; i < users.length; ++i) { vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - bytes32("onlySelf"), - users[i] - ) + IAUT_EXT_VotingRoles_v1 + .Module__VotingRoleManager__OnlySelfCallAllowed + .selector ); vm.prank(users[i]); // authorized, but not Module if (i % 3 == 0) { @@ -911,13 +905,13 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { // 3) The vote gets executed (by anybody) vm.expectEmit(true, true, true, true); - uint _oldDuration = _votingRoles.voteDuration(); + uint _oldDuration = _votingRoles.getVoteDuration(); emit VoteDurationUpdated(_oldDuration, _newDuration); emit MotionExecuted(_voteID); _votingRoles.executeMotion(_voteID); // 4) The module state has changed - assertEq(_votingRoles.voteDuration(), _newDuration); + assertEq(_votingRoles.getVoteDuration(), _newDuration); } // Fail to execute vote that didn't pass @@ -969,7 +963,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { _votingRoles.executeMotion(_voteID); // we wait and try again in the last block of voting time - vm.warp(block.timestamp + _votingRoles.voteDuration()); + vm.warp(block.timestamp + _votingRoles.getVoteDuration()); vm.expectRevert( abi.encodePacked( @@ -995,7 +989,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { _votingRoles.executeMotion(_voteID); // 3) the module state has changed - assertEq(_votingRoles.voteDuration(), _newDuration); + assertEq(_votingRoles.getVoteDuration(), _newDuration); // 4) Now we test that we can't execute again: vm.expectRevert( @@ -1015,7 +1009,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { vm.startPrank(address(_votingRoles)); for (uint i; i < users.length; ++i) { - uint before = _votingRoles.threshold(); + uint before = _votingRoles.getThreshold(); vm.expectEmit(); emit VoterAdded(users[i]); @@ -1025,10 +1019,10 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { vm.expectEmit(); emit ThresholdUpdated(before, before + 1); _votingRoles.addVoterAndUpdateThreshold(users[i], before + 1); - assertEq(_votingRoles.threshold(), before + 1); + assertEq(_votingRoles.getThreshold(), before + 1); } else { _votingRoles.addVoter(users[i]); - assertEq(_votingRoles.threshold(), before); + assertEq(_votingRoles.getThreshold(), before); } } @@ -1047,6 +1041,34 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { vm.stopPrank(); } + function testAddVoters_ValidatesThreshold() public { + // To check the validate threshold function we lower the threshold to 1 + vm.prank(address(_votingRoles)); + _votingRoles.setThreshold(1); + + // Afterwards we remove one of the voters + // As the threshold check is tied to having the specific amount of voters of 2 + // after the addVoter function call + vm.prank(address(_votingRoles)); + _votingRoles.removeVoter(BOB); + + // Now we add two voters again, as the condition only triggers on 3+ voters + vm.prank(address(_votingRoles)); + _votingRoles.addVoter(BOB); + + // Here we expect the revert + vm.expectRevert( + abi.encodeWithSelector( + IAUT_EXT_VotingRoles_v1 + .Module__VotingRoleManager__InvalidThreshold + .selector + ) + ); + + vm.prank(address(_votingRoles)); + _votingRoles.addVoter(address(0xBEEF)); + } + function testRemoveVoter(address[] memory users) public { _validateUserList(users); batchAddAuthorized(users); @@ -1105,14 +1127,14 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { .selector ); _votingRoles.removeVoter(BOB); - assertEq(_votingRoles.threshold(), 2); + assertEq(_votingRoles.getThreshold(), 2); // try to remove again, this time including a reduction // of the threshold to 1 vm.expectEmit(); emit ThresholdUpdated(2, 1); _votingRoles.removeVoterAndUpdateThreshold(BOB, 1); - assertEq(_votingRoles.threshold(), 1); + assertEq(_votingRoles.getThreshold(), 1); // this call would leave a 1 person list with a threshold of 2 vm.expectRevert( @@ -1129,14 +1151,14 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { // TEST: QUORUM // Get correct threshold - function testGetThreshold() public { - assertEq(_votingRoles.threshold(), DEFAULT_QUORUM); + function testGetgetThreshold() public { + assertEq(_votingRoles.getThreshold(), DEFAULT_QUORUM); } // Set a new threshold - function testMotionSetThreshold() public { - uint oldThreshold = _votingRoles.threshold(); - uint newThreshold = 3; + function testMotionSetgetThreshold() public { + uint oldThreshold = _votingRoles.getThreshold(); + uint newThreshold = 2; vm.prank(address(_votingRoles)); @@ -1145,13 +1167,13 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { _votingRoles.setThreshold(newThreshold); - assertEq(_votingRoles.threshold(), newThreshold); + assertEq(_votingRoles.getThreshold(), newThreshold); } // Fail to set a threshold that's too damn high or too damn low function testSetInvalidThreshold(uint newThreshold) public { // Test too high - vm.assume(newThreshold > _votingRoles.voterCount()); + vm.assume(newThreshold > 3); vm.expectRevert( IAUT_EXT_VotingRoles_v1 @@ -1161,23 +1183,28 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { vm.prank(address(_votingRoles)); _votingRoles.setThreshold(newThreshold); - // Test too low + // Test too if amount of voters is less than 3 vm.expectRevert( IAUT_EXT_VotingRoles_v1 .Module__VotingRoleManager__InvalidThreshold .selector ); vm.prank(address(_votingRoles)); - _votingRoles.setThreshold(1); + _votingRoles.setThreshold(0); + + // Should fail if voters are equal or more than 3 and threshold is less than 2 + + // Add voter + vm.prank(address(_votingRoles)); + _votingRoles.addVoter(address(makeAddr("voter"))); - // Test too low with zero vm.expectRevert( IAUT_EXT_VotingRoles_v1 .Module__VotingRoleManager__InvalidThreshold .selector ); vm.prank(address(_votingRoles)); - _votingRoles.setThreshold(0); + _votingRoles.setThreshold(1); } // Fail to change threshold when not the module itself @@ -1188,11 +1215,9 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { uint _newQ = 1; for (uint i; i < users.length; ++i) { vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - bytes32("onlySelf"), - users[i] - ) + IAUT_EXT_VotingRoles_v1 + .Module__VotingRoleManager__OnlySelfCallAllowed + .selector ); vm.prank(users[i]); // authorized, but not orchestrator _votingRoles.setThreshold(_newQ); @@ -1201,7 +1226,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { // Change the threshold by going through governance function testGovernanceThresholdChange() public { - uint _newThreshold = 3; + uint _newThreshold = 2; // 1) Create and approve a vote bytes memory _encodedAction = @@ -1212,7 +1237,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { // 2) The vote gets executed by anybody - uint _oldThreshold = _votingRoles.threshold(); + uint _oldThreshold = _votingRoles.getThreshold(); vm.expectEmit(true, true, true, true); emit ThresholdUpdated(_oldThreshold, _newThreshold); @@ -1221,20 +1246,20 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { _votingRoles.executeMotion(_voteID); // 3) The orchestrator state has changed - assertEq(_votingRoles.threshold(), _newThreshold); + assertEq(_votingRoles.getThreshold(), _newThreshold); } //-------------------------------------------------------------------------- // TEST: VOTE DURATION // Get correct vote duration - function testGetVoteDuration() public { - assertEq(_votingRoles.voteDuration(), DEFAULT_DURATION); + function testGetgetVoteDuration() public { + assertEq(_votingRoles.getVoteDuration(), DEFAULT_DURATION); } // Set new vote duration - function testMotionSetVoteDuration() public { - uint _oldDuration = _votingRoles.voteDuration(); + function testMotionSetgetVoteDuration() public { + uint _oldDuration = _votingRoles.getVoteDuration(); uint _newDuration = 3 days; vm.prank(address(_votingRoles)); @@ -1244,12 +1269,12 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { _votingRoles.setVotingDuration(_newDuration); - assertEq(_votingRoles.voteDuration(), _newDuration); + assertEq(_votingRoles.getVoteDuration(), _newDuration); } // Fail to set vote durations out of bounds - function testMotionSetInvalidVoteDuration() public { - uint _oldDur = _votingRoles.voteDuration(); + function testMotionSetInvalidgetVoteDuration() public { + uint _oldDur = _votingRoles.getVoteDuration(); uint _newDur = 3 weeks; vm.expectRevert( @@ -1274,7 +1299,7 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { vm.prank(address(_votingRoles)); _votingRoles.setVotingDuration(_newDur); - assertEq(_votingRoles.voteDuration(), _oldDur); + assertEq(_votingRoles.getVoteDuration(), _oldDur); } // Set new duration bygoing through governance @@ -1293,11 +1318,9 @@ contract AUT_EXT_VotingRoles_v1Test is ModuleTest { uint _newDuration = 5 days; for (uint i; i < users.length; ++i) { vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - bytes32("onlySelf"), - users[i] - ) + IAUT_EXT_VotingRoles_v1 + .Module__VotingRoleManager__OnlySelfCallAllowed + .selector ); vm.prank(users[i]); // authorized, but not orchestrator _votingRoles.setVotingDuration(_newDuration); diff --git a/test/unit/modules/authorizer/role/AUT_Roles_v1.t.sol b/test/unit/modules/authorizer/role/AUT_Roles_v1.t.sol index 7a70b10c2..ff0a79d69 100644 --- a/test/unit/modules/authorizer/role/AUT_Roles_v1.t.sol +++ b/test/unit/modules/authorizer/role/AUT_Roles_v1.t.sol @@ -1,1147 +1,1181 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.0; -// SuT -import {Test} from "forge-std/Test.sol"; -import "forge-std/console.sol"; +import "forge-std/Test.sol"; -import { - AUT_Roles_v1, - IAuthorizer_v1, - IModule_v1 -} from "@aut/role/AUT_Roles_v1.sol"; // External Libraries import {Clones} from "@oz/proxy/Clones.sol"; -import {IERC165} from "@oz/interfaces/IERC165.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {IAccessControlEnumerable} from - "@oz/access/extensions/IAccessControlEnumerable.sol"; +import {IERC165} from "@oz/utils/introspection/IERC165.sol"; -import {IAccessControl} from "@oz/access/IAccessControl.sol"; // Internal Dependencies -import {Orchestrator_v1} from "src/orchestrator/Orchestrator_v1.sol"; -import {TransactionForwarder_v1} from - "src/external/forwarder/TransactionForwarder_v1.sol"; -// Interfaces +import { + ModuleTest, + IModule_v1, + IOrchestrator_v1 +} from "@unitTest/modules/ModuleTest.sol"; + +// Internal Libraries +import {LibMetadata} from "src/modules/lib/LibMetadata.sol"; + +// Internal Interfaces import {IModule_v1, IOrchestrator_v1} from "src/modules/base/IModule_v1.sol"; + +import {Orchestrator_v1} from "src/orchestrator/Orchestrator_v1.sol"; + +import {IAuthorizer_v1} from "@aut/IAuthorizer_v1.sol"; + +// SuT +import {AUT_Roles_v1_Exposed} from + "@mocks/modules/authorizer/AUT_Roles_v1_Exposed.sol"; + // Mocks -import {ERC20Mock} from "@mocks/external/token/ERC20Mock.sol"; -import {ModuleV1Mock} from "@mocks/modules/base/ModuleV1Mock.sol"; import {FundingManagerV1Mock} from "@mocks/modules/fundingManager/FundingManagerV1Mock.sol"; +import {AuthorizerV1Mock} from "@mocks/modules/authorizer/AuthorizerV1Mock.sol"; import {PaymentProcessorV1Mock} from "@mocks/modules/paymentProcessor/PaymentProcessorV1Mock.sol"; -import {GovernorV1Mock} from "@mocks/external/governance/GovernorV1Mock.sol"; -import {ModuleFactoryV1Mock} from "@mocks/factories/ModuleFactoryV1Mock.sol"; - -contract AUT_RolesV1Test is Test { - // Mocks - AUT_Roles_v1 _authorizer; - Orchestrator_v1 internal _orchestrator = new Orchestrator_v1(address(0)); - ERC20Mock internal _token = new ERC20Mock("Mock Token", "MOCK", 18); - FundingManagerV1Mock _fundingManager = new FundingManagerV1Mock(); - PaymentProcessorV1Mock _paymentProcessor = new PaymentProcessorV1Mock(); - GovernorV1Mock internal _governor = new GovernorV1Mock(); - ModuleFactoryV1Mock internal _moduleFactory = new ModuleFactoryV1Mock(); - TransactionForwarder_v1 _forwarder = new TransactionForwarder_v1(); - address ALBA = address(0xa1ba); // default authorized person - address BOB = address(0xb0b); // example person to add - - bytes32 immutable ROLE_0 = "ROLE_0"; - bytes32 immutable ROLE_1 = "ROLE_1"; - - // Orchestrator_v1 Constants - uint internal constant _ORCHESTRATOR_ID = 1; - // Module Constants - uint constant MAJOR_VERSION = 1; - uint constant MINOR_VERSION = 0; - uint constant PATCH_VERSION = 0; - string constant URL = "https://github.com/organization/module"; - string constant TITLE = "Module"; - - IModule_v1.Metadata _METADATA = IModule_v1.Metadata( - MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION, URL, TITLE - ); - - //-------------------------------------------------------------------------- - // Events - - /** - * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` - * - * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite - * {RoleAdminChanged} not being emitted signaling this. - * - * _Available since v3.1._ - */ - event RoleAdminChanged( - bytes32 indexed role, - bytes32 indexed previousAdminRole, - bytes32 indexed newAdminRole - ); - - /** - * @dev Emitted when `account` is granted `role`. - * - * `sender` is the account that originated the contract call, an admin role - * bearer except when using {AccessControl-_setupRole}. - */ - event RoleGranted( - bytes32 indexed role, address indexed account, address indexed sender - ); - - /** - * @dev Emitted when `account` is revoked `role`. - * - * `sender` is the account that originated the contract call: - * - if using `revokeRole`, it is the admin role bearer - * - if using `renounceRole`, it is the role bearer (i.e. `account`) - */ - event RoleRevoked( - bytes32 indexed role, address indexed account, address indexed sender - ); - - function setUp() public virtual { - address authImpl = address(new AUT_Roles_v1()); - _authorizer = AUT_Roles_v1(Clones.clone(authImpl)); - address propImpl = address(new Orchestrator_v1(address(_forwarder))); - _orchestrator = Orchestrator_v1(Clones.clone(propImpl)); - ModuleV1Mock module = new ModuleV1Mock(); - address[] memory modules = new address[](1); - modules[0] = address(module); - _orchestrator.init( - _ORCHESTRATOR_ID, - address(_moduleFactory), - modules, - _fundingManager, - _authorizer, - _paymentProcessor, - _governor - ); - - address initialAuth = ALBA; +import {ERC20PaymentClientBaseV2Mock} from + "@mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; +import {ERC20Mock} from "@mocks/external/token/ERC20Mock.sol"; - _authorizer.init( - IOrchestrator_v1(_orchestrator), _METADATA, abi.encode(initialAuth) - ); +// Errors +import {OZErrors} from "@testUtilities/OZErrors.sol"; - // console.log(_authorizer.hasRole(_authorizer.getAdminRole(), ALBA)); - assertEq(_authorizer.hasRole(_authorizer.getAdminRole(), ALBA), true); - // console.log(_authorizer.hasRole(_authorizer.getAdminRole(), address(this))); - assertEq( - _authorizer.hasRole(_authorizer.getAdminRole(), address(this)), - false - ); - } +// External Dependencies +import {IAccessControl} from "@oz/access/IAccessControl.sol"; - //-------------------------------------------------------------------------------- - // Tests Initialization +contract AUT_Roles_v1_Test is ModuleTest { + /////////////////////////////////////////////////////////////////////////// + // State - function testSupportsInterface() public { - assertTrue( - _authorizer.supportsInterface(type(IAuthorizer_v1).interfaceId) - ); - } + // SuT + AUT_Roles_v1_Exposed _authSuT; - function testInitWithInitialAdmin(address initialAuth) public { - // Checks that address list gets correctly stored on initialization - // We "reuse" the orchestrator created in the setup, but the orchestrator doesn't know about this new authorizer. + // Constants + address _initialAdmin = makeAddr("initialAdmin"); + address _bob = makeAddr("Bob"); + address _alice = makeAddr("Alice"); - address authImpl = address(new AUT_Roles_v1()); - AUT_Roles_v1 testAuthorizer = AUT_Roles_v1(Clones.clone(authImpl)); + // Addresses - vm.assume(initialAuth != address(0)); - vm.assume(initialAuth != address(this)); - vm.assume(initialAuth != address(_orchestrator)); + // Bob and Alice can Access + bytes4 _selector1 = bytes4(keccak256("selector1()")); + // Alice can access + bytes4 _selector2 = bytes4(keccak256("selector2()")); + // No Permissions + bytes4 _selector3 = bytes4(keccak256("selector3()")); + // Public Role can access + bytes4 _selector4 = bytes4(keccak256("selector4()")); - testAuthorizer.init( - IOrchestrator_v1(_orchestrator), - _METADATA, - abi.encode(initialAuth, address(this)) - ); + /////////////////////////////////////////////////////////////////////////// + // Setup - assertEq( - testAuthorizer.getRoleAdmin(testAuthorizer.BURN_ADMIN_ROLE()), - testAuthorizer.BURN_ADMIN_ROLE() - ); + function setUp() public { + address impl = address(new AUT_Roles_v1_Exposed()); + _authSuT = AUT_Roles_v1_Exposed(Clones.clone(impl)); - assertEq(address(testAuthorizer.orchestrator()), address(_orchestrator)); + // initiate orchestrator without extra Module + _setUpOrchestrator(); - assertEq( - testAuthorizer.hasRole(testAuthorizer.getAdminRole(), initialAuth), - true - ); + _authSuT.init(_orchestrator, _METADATA, abi.encode(_initialAdmin)); - assertEq( - testAuthorizer.hasRole(testAuthorizer.getAdminRole(), address(this)), - false - ); - assertEq( - testAuthorizer.getRoleMemberCount(testAuthorizer.getAdminRole()), 1 + // Change Authorizer of Module Test to SuT + _orchestrator.initiateSetAuthorizerWithTimelock( + IAuthorizer_v1(_authSuT) ); + vm.warp(72 hours + 1); + _orchestrator.executeSetAuthorizer(IAuthorizer_v1(_authSuT)); } - function testInitWithoutInitialAdmins() public { - // Checks that address list gets correctly stored on initialization if there are no admins given - // We "reuse" the orchestrator created in the setup, but the orchestrator doesn't know about this new authorizer. - - address authImpl = address(new AUT_Roles_v1()); - AUT_Roles_v1 testAuthorizer = AUT_Roles_v1(Clones.clone(authImpl)); - - address initialAuth = address(0); - - vm.expectRevert( - IAuthorizer_v1.Module__Authorizer__InvalidInitialAdmin.selector - ); - - testAuthorizer.init( - IOrchestrator_v1(_orchestrator), - _METADATA, - abi.encode(initialAuth, address(this)) - ); - - assertEq( - testAuthorizer.getRoleMemberCount(testAuthorizer.getAdminRole()), 0 - ); + /////////////////////////////////////////////////////////////////////////// + // Test Initialization + + /* + Test: SupportsInterface + └── Given: The interfaceId is IAuthorizer_v1 + └── When: the function supportsInterface is called + └── Then: the function should return true + */ + function testSupportsInterface() public override(ModuleTest) { + assertTrue(_authSuT.supportsInterface(type(IAuthorizer_v1).interfaceId)); } - function testInitWithInitialAdminSameAsDeployer() public { - // Checks that address list gets correctly stored on initialization - // We "reuse" the orchestrator created in the setup, but the orchestrator doesn't know about this new authorizer. - - address authImpl = address(new AUT_Roles_v1()); - AUT_Roles_v1 testAuthorizer = AUT_Roles_v1(Clones.clone(authImpl)); - - address initialAuth = address(this); - - testAuthorizer.init( - IOrchestrator_v1(_orchestrator), - _METADATA, - abi.encode(initialAuth, address(this)) - ); - - assertEq( - testAuthorizer.getRoleAdmin(testAuthorizer.BURN_ADMIN_ROLE()), - testAuthorizer.BURN_ADMIN_ROLE() - ); - - assertEq(address(testAuthorizer.orchestrator()), address(_orchestrator)); - - assertEq( - testAuthorizer.hasRole(testAuthorizer.getAdminRole(), initialAuth), - true - ); - - assertEq( - testAuthorizer.getRoleMemberCount(testAuthorizer.getAdminRole()), 1 - ); + /* + Test: Init + └── When: the function init is called + └── Then: the function should set the initial admin + */ + function testInit() public override { + // Check that the initial Admin is set + assertTrue(_authSuT.hasRole(_authSuT.getAdminRole(), _initialAdmin)); } - function testReinitFails() public { - // Create a mock new orchestrator - Orchestrator_v1 newOrchestrator = Orchestrator_v1( - Clones.clone(address(new Orchestrator_v1(address(0)))) - ); - - address initialAdmin = address(this); - - vm.expectRevert(); - _authorizer.init( - IOrchestrator_v1(newOrchestrator), - _METADATA, - abi.encode(initialAdmin) - ); - assertEq( - _authorizer.hasRole(_authorizer.getAdminRole(), address(this)), - false - ); - assertEq(address(_authorizer.orchestrator()), address(_orchestrator)); - assertEq(_authorizer.hasRole(_authorizer.getAdminRole(), ALBA), true); - assertEq(_authorizer.getRoleMemberCount(_authorizer.getAdminRole()), 1); + /* + Test: ReinitFails + └── When: the function init is called after the contract has been initialized + └── Then: the function should revert + */ + function testReinitFails() public override { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + _authSuT.init(_orchestrator, _METADATA, abi.encode(_initialAdmin)); + } + ///////////////////////////////////////////////////////////////////////////// + // Test Modifier + + //idNotDefaultAdmin + /* + Test: idNotDefaultAdmin Modifier + └── Given: Role ID is the default admin role + └── When: function with idNotDefaultAdmin modifier is called + └── Then: the function should revert + */ + function testIdNotDefaultAdminModifier(bytes32 _givenRoleId) public { + if (_givenRoleId == _authSuT.DEFAULT_ADMIN_ROLE()) { + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1 + .Module__Authorizer__CannotModifyAdminRoleAccess + .selector + ) + ); + } + _authSuT.idNotDefaultAdminModifier_exposed(_givenRoleId); } - // Test Register Roles - - //-------------------------------------------------------------------------------- - // Test manually granting and revoking roles as orchestrator-defined Admin - - function testGrantAdminRole(address[] memory newAuthorized) public { - uint amountAuth = - _authorizer.getRoleMemberCount(_authorizer.getAdminRole()); - - _validateAuthorizedList(newAuthorized); - - vm.startPrank(address(ALBA)); - for (uint i; i < newAuthorized.length; ++i) { - vm.expectEmit(true, true, true, true); - emit RoleGranted( - _authorizer.getAdminRole(), newAuthorized[i], address(ALBA) + /* + Test: idExists Modifier + └── Given: Role ID is not existing and is not Public Role + └── When: function with idExists modifier is called + └── Then: the function should revert + */ + function testidExistsModifier( + uint _lastAssignedRoleIdValue, + bytes32 _givenRoleId + ) public { + _authSuT.changeLastAssignedRoleId(_lastAssignedRoleIdValue); + if ( + _givenRoleId != _authSuT.PUBLIC_ROLE() + && uint(_givenRoleId) > _lastAssignedRoleIdValue + ) { + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1 + .Module__Authorizer__RoleIdNotExisting + .selector + ) ); + } + _authSuT.idExistsModifier_exposed(_givenRoleId); + } - _authorizer.grantRole(_authorizer.getAdminRole(), newAuthorized[i]); + /////////////////////////////////////////////////////////////////////////// + // Test External Functions + + // ======================================================================== + // Public Getter Functions + + // ------------------------------------------------------------------------ + // Getter - Authorization + + /* + Test: getPermissions + └── When: getPermissions is called + └── Then: Return all Role ids that are listed for that function + */ + function testGetPermissions( + address target_, + bytes4 selector_, + bytes32[] memory permissions_ + ) public { + for (uint i = 0; i < permissions_.length; i++) { + _authSuT.addAccessPermission_unrestricted( + target_, selector_, permissions_[i] + ); } - vm.stopPrank(); + bytes32[] memory returnedPermissions = + _authSuT.getPermissions(target_, selector_); + assertEq(returnedPermissions.length, permissions_.length); + for (uint i = 0; i < permissions_.length; i++) { + assertEq(returnedPermissions[i], permissions_[i]); + } + } - for (uint i; i < newAuthorized.length; ++i) { - assertEq( - _authorizer.hasRole( - _authorizer.getAdminRole(), newAuthorized[i] - ), - true + /* + Test: isRolePermissioned + └── When: isRolePermissioned is called + └── Then: Return true if the roleId is listed for that function + */ + function testisRolePermissioned( + bytes32 roleId_, + address target_, + bytes4 selector_, + bool isRolePermissioned_ + ) public { + if (isRolePermissioned_) { + _authSuT.addAccessPermission_unrestricted( + target_, selector_, roleId_ ); } assertEq( - _authorizer.getRoleMemberCount(_authorizer.getAdminRole()), - (amountAuth + newAuthorized.length) + _authSuT.isRolePermissioned(target_, selector_, roleId_), + isRolePermissioned_ ); } - function testGrantAdminRoleFailsIfOrchestratorWillBeAdmin() public { - vm.startPrank(address(ALBA)); + /* + Test: hasPermission + ├── Given: Caller has the Default Admin Role + │ └── When: hasPermission is called + │ └── Then: Return true + ├── Given: There are no roleId permissions for the function + │ └── When: hasPermission is called + │ └── Then: Return false + ├── Given: The permissions contain the public role + │ └── When: hasPermission is called + │ └── Then: Return true + └── Given: The caller inhabits one of the roles that have permission + └── When: hasPermission is called + └── Then: Return true + */ + + function testHasPermission_CallerHasDefaultAdminRole(uint seed_) public { + // Create Setup with predetermined roles and function restrictions + address target = address(uint160(seed_)); + createSetup(target); + + // Select function selector from setup based on seed + bytes4 selector = selectFunctionSelectorBasedOnSeed(seed_); + + // Check that if the caller has the default admin role, they can call any function + assertTrue(_authSuT.hasPermission(_initialAdmin, target, selector)); + } - bytes32 adminRole = _authorizer.getAdminRole(); + function testHasPermission_NoPermissionsForFunction( + uint seed_, + address caller_ + ) public { + // Make sure calle does not have the default admin role + vm.assume(caller_ != _initialAdmin); - vm.expectRevert( - abi.encodeWithSelector( - IAuthorizer_v1 - .Module__Authorizer__OrchestratorCannotHaveAdminRole - .selector - ) - ); + // Create Setup with predetermined roles and function restrictions + address target = address(uint160(seed_)); + createSetup(target); - _authorizer.grantRole(adminRole, address(_orchestrator)); + // Select function selector from setup based on seed + bytes4 selector = selectFunctionSelectorBasedOnSeed(seed_); - vm.stopPrank(); + // Check that the function has no permissions + if (_authSuT.getPermissions(target, selector).length == 0) { + // If the function has no permissions, the caller should not be able to call the function + assertFalse(_authSuT.hasPermission(caller_, target, selector)); + } } - function testRevokeAdminRole() public { - // Add Bob as admin - vm.startPrank(address(ALBA)); - _authorizer.grantRole(_authorizer.getAdminRole(), BOB); // Meet your new Manager - vm.stopPrank(); - assertEq(_authorizer.hasRole(_authorizer.getAdminRole(), BOB), true); - - uint amountAuth = - _authorizer.getRoleMemberCount(_authorizer.getAdminRole()); - - vm.startPrank(address(ALBA)); + function testHasPermission_PermissionIsPublicRole( + uint seed_, + address caller_ + ) public { + // Make sure caller does not have the default admin role + vm.assume(caller_ != _initialAdmin); + + // Create Setup with predetermined roles and function restrictions + address target = address(uint160(seed_)); + createSetup(target); + + // Select function selector from setup based on seed + bytes4 selector = selectFunctionSelectorBasedOnSeed(seed_); + + bytes32[] memory permissions = _authSuT.getPermissions(target, selector); + for (uint i = 0; i < permissions.length; i++) { + if (permissions[i] == _authSuT.PUBLIC_ROLE()) { + assertTrue(_authSuT.hasPermission(caller_, target, selector)); + } + } + } - vm.expectEmit(true, true, true, true); - emit RoleRevoked( - _authorizer.getAdminRole(), address(ALBA), address(ALBA) - ); + function testHasPermission_IsNotPublicRole(uint seed_, uint callerSeed_) + public + { + // Fetch caller from seed + address caller = selectCallerBasedOnSeed(callerSeed_); + // Make sure caller is not the default admin or Bob or Alice + vm.assume(caller != _initialAdmin || caller != _bob || caller != _alice); + + // Create Setup with predetermined roles and function restrictions + address target = address(uint160(seed_)); + createSetup(target); + + // Select function selector from setup based on seed + bytes4 selector = selectFunctionSelectorBasedOnSeed(seed_); + + bytes32[] memory permissions = _authSuT.getPermissions(target, selector); + for (uint i = 0; i < permissions.length; i++) { + // If the caller has the role they should be able to call the function + if (_authSuT.hasRole(permissions[i], caller)) { + assertTrue(_authSuT.hasPermission(caller, target, selector)); + } + } + } - _authorizer.revokeRole(_authorizer.getAdminRole(), ALBA); - vm.stopPrank(); + // ------------------------------------------------------------------------ + // Getter - Role Management - assertEq(_authorizer.hasRole(_authorizer.getAdminRole(), ALBA), false); - assertEq( - _authorizer.getRoleMemberCount(_authorizer.getAdminRole()), - amountAuth - 1 - ); + /* + Test: getAdminRole + └── When: getAdminRole is called + └── Then: Return the Admin Role + */ + function testGetAdminRole() public { + assertEq(_authSuT.getAdminRole(), _authSuT.DEFAULT_ADMIN_ROLE()); } - function testRemoveLastAdminFails() public { - uint amountAuth = - _authorizer.getRoleMemberCount(_authorizer.getAdminRole()); - bytes32 adminRole = _authorizer.getAdminRole(); // To correctly time the vm.expectRevert + // ======================================================================== + // Mutating Functions + + // ------------------------------------------------------------------------ + // Mutating - Authorization + + /* + Test: addAccessPermission + ├── Given: Caller does not inhabit the permissioned Role + │ └── When: addAccessPermission is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is the default admin role + │ └── When: addAccessPermission is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is not existing + │ └── When: addAccessPermission is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is existing and not the default admin role + ├── And: The given roleId has already permission + │ └── When: addAccessPermission is called + │ └── Then: Nothing happens + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is existing and not the default admin role + └── And: The given roleId does not have permission yet + └── When: addAccessPermission is called + └── Then: The roleId gains permission + └── And: An event is emitted + */ + + function testAddAccessPermission_ModifierInPostionChecks() public { + //permissioned + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + _authSuT.addAccessPermission(address(this), bytes4(0), bytes32(uint(0))); + //idNotDefaultAdmin(roleId_) vm.expectRevert( abi.encodeWithSelector( IAuthorizer_v1 - .Module__Authorizer__AdminRoleCannotBeEmpty + .Module__Authorizer__CannotModifyAdminRoleAccess .selector ) ); - vm.prank(address(ALBA)); - _authorizer.revokeRole(adminRole, ALBA); + vm.prank(_initialAdmin); + _authSuT.addAccessPermission( + address(this), + bytes4(0), + bytes32(uint(0)) //Default Admin Id + ); - assertEq(_authorizer.hasRole(adminRole, ALBA), true); - assertEq( - _authorizer.getRoleMemberCount(_authorizer.getAdminRole()), - amountAuth + //idExists(roleId_) + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector + ) ); + vm.prank(_initialAdmin); + _authSuT.addAccessPermission(address(this), bytes4(0), bytes32(uint(2))); } - // Test grantRoleFromModule - // - Should revert if caller is not a module - // - Should not revert if role is already granted, but not emit events either - /// forge-config: default.allow_internal_expect_revert = true - function testGrantRoleFromModule() public { - address newModule = _setupMockSelfManagedModule(); - bytes32 role0_module = _authorizer.generateRoleId(newModule, ROLE_0); + function testAddAccessPermission_PermissionAlreadyExisting(uint seed_) + public + { + // Create All Role Ids + _authSuT.changeLastAssignedRoleId(type(uint).max); - assertEq(_authorizer.hasRole(role0_module, ALBA), false); + // Create a random lock setup + (address target, bytes4 selector) = + createRandomLockRestrictions(seed_, 1); - vm.prank(newModule); + // Fetch permission array for comparison + bytes32[] memory permissions = _authSuT.getPermissions(target, selector); - vm.expectEmit(true, true, true, true); - emit RoleGranted(role0_module, ALBA, newModule); + // Fetch one of the permissions from the lock + bytes32 roleIdPermission = permissions[seed_ % permissions.length]; + + // Try it again + vm.prank(_initialAdmin); + _authSuT.addAccessPermission(target, selector, roleIdPermission); - _authorizer.grantRoleFromModule(ROLE_0, ALBA); + // Fetch permission array for comparison + bytes32[] memory permissionsAfter = + _authSuT.getPermissions(target, selector); - assertEq(_authorizer.hasRole(role0_module, ALBA), true); + // Check that the array length is the same + assertEq(permissions.length, permissionsAfter.length); + // Check that the array stayed the same + for (uint i = 0; i < permissions.length; i++) { + assertEq(permissions[i], permissionsAfter[i]); + } } - function testGrantRoleFromModuleFailsIfCalledByNonModule() public { - address newModule = _setupMockSelfManagedModule(); + function testAddAccessPermission_PermissionNotAlreadyExisting( + uint seed_, + bytes32 roleId_ + ) public { + // Make sure roleId_ is not the default admin role or the Public Role + vm.assume(roleId_ != _authSuT.DEFAULT_ADMIN_ROLE()); + vm.assume(roleId_ != _authSuT.PUBLIC_ROLE()); - vm.prank(address(BOB)); - vm.expectRevert(); - _authorizer.grantRoleFromModule(ROLE_0, ALBA); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), ALBA - ), - false - ); - } + // Create All Role Ids + _authSuT.changeLastAssignedRoleId(type(uint).max); - event hm(uint test); + // Create a random lock setup + (address target, bytes4 selector) = + createRandomLockRestrictions(seed_, 0); - function testGrantRoleFromModuleFailsIfModuleNotInOrchestrator() public { - address newModule = _setupMockSelfManagedModule(); + // Make sure roleId_ is not part of the function lock + vm.assume(!_authSuT.isRolePermissioned(target, selector, roleId_)); - vm.startPrank(ALBA); + // Fetch permission array for comparison + bytes32[] memory permissions = _authSuT.getPermissions(target, selector); - _orchestrator.initiateRemoveModuleWithTimelock(newModule); + // Check that event is emitted + vm.expectEmit(true, true, true, true); + emit IAuthorizer_v1.AccessPermissionAdded(target, selector, roleId_); - vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); - _orchestrator.executeRemoveModule(newModule); + // Add permission to roleId to function lock + vm.prank(_initialAdmin); + _authSuT.addAccessPermission(target, selector, roleId_); - vm.stopPrank(); + // Fetch permission array for comparison + bytes32[] memory permissionsAfter = + _authSuT.getPermissions(target, selector); + + // Check that array length was adapted + assertEq(permissionsAfter.length, permissions.length + 1); - vm.prank(newModule); + // Check that the role is permissioned + assertTrue(_authSuT.isRolePermissioned(target, selector, roleId_)); + } + + /* + Test: removeAccessPermission + ├── Given: Caller does not inhabit the permissioned Role + │ └── When: removeAccessPermission is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId not a permissioned + │ └── When: removeAccessPermission is called + │ └── Then: Nothing happens + ├── Given: Caller inhabits the default admin role + └── And: The given roleId is permissioned + └── When: removeAccessPermission is called + └── Then: The permission is removed + └── And: An event is emitted + + */ + function testRemoveAccessPermission_ModifierInPostionChecks() public { + //permissioned vm.expectRevert( abi.encodeWithSelector( - IAuthorizer_v1.Module__Authorizer__NotActiveModule.selector, - newModule + IModule_v1.Module__CallerNotPermissioned.selector ) ); - _authorizer.grantRoleFromModule(ROLE_0, ALBA); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), ALBA - ), - false + _authSuT.removeAccessPermission( + address(this), bytes4(0), bytes32(uint(0)) ); } - function testGrantRoleFromModuleIdempotence() public { - address newModule = _setupMockSelfManagedModule(); + function testRemoveAccessPermission_PermissionNotExisting( + uint seed_, + bytes32 roleId_ + ) public { + // Create All Role Ids + _authSuT.changeLastAssignedRoleId(type(uint).max); - vm.startPrank(newModule); + // Create a random lock setup + (address target, bytes4 selector) = + createRandomLockRestrictions(seed_, 0); - _authorizer.grantRoleFromModule(ROLE_0, ALBA); - - _authorizer.grantRoleFromModule(ROLE_0, ALBA); - // No reverts happen - - vm.stopPrank(); - - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), ALBA - ), - true - ); - } + // Make sure roleId_ is not part of the function lock + vm.assume(!_authSuT.isRolePermissioned(target, selector, roleId_)); - // Test grantRoleFromModuleBatched - // - Should revert if caller is not a module - // - Should not revert if role is already granted, but not emit events either - // - Should not revert if address list is empty + // Fetch permission array for comparison + bytes32[] memory permissions = _authSuT.getPermissions(target, selector); - function testGrantRoleFromModuleBatched(address[] memory newAuthorized) - public - { - _validateAuthorizedList(newAuthorized); + // Remove any permission from function lock + vm.prank(_initialAdmin); + _authSuT.removeAccessPermission(target, selector, roleId_); - address newModule = _setupMockSelfManagedModule(); - bytes32 role0_module = _authorizer.generateRoleId(newModule, ROLE_0); + // Fetch permission array for comparison + bytes32[] memory permissionsAfter = + _authSuT.getPermissions(target, selector); - for (uint i = 0; i < newAuthorized.length; i++) { - assertEq(_authorizer.hasRole(role0_module, newAuthorized[i]), false); + // Check that array length is the same + assertEq(permissions.length, permissionsAfter.length); - vm.expectEmit(true, true, true, true); - emit RoleGranted(role0_module, newAuthorized[i], newModule); + // Check that values stayed the same + for (uint i = 0; i < permissions.length; i++) { + assertEq(permissions[i], permissionsAfter[i]); } + } - vm.prank(newModule); - _authorizer.grantRoleFromModuleBatched(ROLE_0, newAuthorized); + function testRemoveAccessPermission_PermissionExisting(uint seed_) public { + // Create All Role Ids + _authSuT.changeLastAssignedRoleId(type(uint).max); - for (uint i = 0; i < newAuthorized.length; i++) { - assertEq(_authorizer.hasRole(role0_module, newAuthorized[i]), true); - } - } + // Create a random lock setup + (address target, bytes4 selector) = + createRandomLockRestrictions(seed_, 1); - function testGrantRoleFromModuleBatchedFailsIfCalledByNonModule() public { - address newModule = _setupMockSelfManagedModule(); + // Fetch permission array for comparison + bytes32[] memory permissions = _authSuT.getPermissions(target, selector); - address[] memory targets = new address[](2); - targets[0] = address(ALBA); - targets[1] = address(BOB); + // Fetch one of the permissions from the lock + bytes32 roleIdPermission = permissions[uint(seed_) % permissions.length]; - vm.prank(address(BOB)); - vm.expectRevert(); - _authorizer.grantRoleFromModuleBatched(ROLE_0, targets); - (ROLE_0, ALBA); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), ALBA - ), - false - ); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false + // Check that event is emitted + vm.expectEmit(true, true, true, true); + emit IAuthorizer_v1.AccessPermissionRemoved( + target, selector, roleIdPermission ); - } - function testGrantRoleFromModuleBatchedFailsIfModuleNotInOrchestrator() - public - { - address newModule = _setupMockSelfManagedModule(); + // Remove permission from function lock + vm.prank(_initialAdmin); + _authSuT.removeAccessPermission(target, selector, roleIdPermission); - address[] memory targets = new address[](2); - targets[0] = address(ALBA); - targets[1] = address(BOB); + // Fetch permission array for comparison + bytes32[] memory permissionsAfter = + _authSuT.getPermissions(target, selector); - vm.startPrank(ALBA); - _orchestrator.initiateRemoveModuleWithTimelock(newModule); - vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); - _orchestrator.executeRemoveModule(newModule); - vm.stopPrank(); + // Check that array length is one less + assertEq(permissions.length - 1, permissionsAfter.length); - vm.prank(newModule); + // Check that roleId is not permissioned + assertFalse( + _authSuT.isRolePermissioned(target, selector, roleIdPermission) + ); + } + + // ------------------------------------------------------------------------ + // Mutating - Role Management + + /* + Test: createRole + ├── Given: Caller does not inhabit the permissioned Role + │ └── When: createRole is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is not existing + │ └── When: createRole is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + └── And: The given roleId is existing + └── When: createRole is called + ├── Then: The role id counter is incremented + ├── And: The admin for the role is set + ├── And: An event is emitted + └── And: The intial members of the role are set + */ + + function testCreateRole_ModifierInPostionChecks() public { + //permissioned vm.expectRevert( abi.encodeWithSelector( - IAuthorizer_v1.Module__Authorizer__NotActiveModule.selector, - newModule + IModule_v1.Module__CallerNotPermissioned.selector ) ); - _authorizer.grantRoleFromModuleBatched(ROLE_0, targets); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), ALBA - ), - false - ); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false - ); - } - - function testGrantRoleFromModuleBatchedIdempotenceOnEmptyList() public { - address newModule = _setupMockSelfManagedModule(); - - address[] memory targets = new address[](0); + _authSuT.createRole("RoleName", bytes32(uint(0)), new address[](0)); - vm.prank(newModule); - _authorizer.grantRoleFromModuleBatched(ROLE_0, targets); + //idExists(respectiveAdminRole_) + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector + ) + ); + vm.prank(_initialAdmin); + _authSuT.createRole("RoleName", bytes32(uint(2)), new address[](0)); } - // Test revokeRoleFromModule - // - Should revert if caller is not a module - // - Should revert if role does not exist - // - Should not revert if target doesn't have role. + function testCreateRole_RoleIdIsExisting( + string memory roleName_, + uint seed_, + address[] memory members_ + ) public { + // Check that members_ is reasonably sized + vm.assume(members_.length < 2500); - function testRevokeRoleFromModule() public { - address newModule = _setupMockSelfManagedModule(); - bytes32 role0_module = _authorizer.generateRoleId(newModule, ROLE_0); + // Create random number of permissions between 0 and half uint max + _authSuT.changeLastAssignedRoleId(bound(seed_, 0, type(uint).max / 2)); - assertEq(_authorizer.hasRole(role0_module, BOB), false); - - vm.prank(newModule); - - _authorizer.grantRoleFromModule(ROLE_0, address(BOB)); - - assertEq(_authorizer.hasRole(role0_module, BOB), true); - - vm.prank(newModule); + uint currentLastAssignedRoleId = _authSuT.getLastAssignedRoleId(); + bytes32 expectedRoleId = bytes32(currentLastAssignedRoleId + 1); + // Expect event vm.expectEmit(true, true, true, true); - emit RoleRevoked(role0_module, BOB, newModule); + emit IAuthorizer_v1.RoleCreated(expectedRoleId, roleName_); + + // Create role + vm.prank(_initialAdmin); + bytes32 roleId = _authSuT.createRole( + roleName_, + bytes32(bound(seed_, 0, currentLastAssignedRoleId)), + members_ + ); - _authorizer.revokeRoleFromModule(ROLE_0, address(BOB)); + // Check that roleId is the expected roleId + assertEq(roleId, expectedRoleId); - assertEq(_authorizer.hasRole(role0_module, BOB), false); + // Check that each member has the role + for (uint i = 0; i < members_.length; i++) { + assertTrue(_authSuT.hasRole(roleId, members_[i])); + } } - function testRevokeRoleFromModuleFailsIfCalledByNonModule() public { - address newModule = _setupMockSelfManagedModule(); - - vm.prank(address(BOB)); - vm.expectRevert(); - _authorizer.revokeRoleFromModule(ROLE_0, BOB); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false + /* + Test: labelRole + ├── Given: Caller does not inhabit the default admin role + │ └── When: labelRole is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is not existing + │ └── When: labelRole is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + └── And: The given roleId is existing + └── When: labelRole is called + └── Then: An event is emitted + */ + function testLabelRole_ModifierInPositionChecks() public { + //permissioned + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) ); - } - - function testRevokeRoleFromModuleFailsIfModuleNotInOrchestrator() public { - address newModule = _setupMockSelfManagedModule(); - - vm.startPrank(ALBA); - _orchestrator.initiateRemoveModuleWithTimelock(newModule); - vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); - _orchestrator.executeRemoveModule(newModule); - vm.stopPrank(); + _authSuT.labelRole(bytes32(uint(0)), "RoleName"); - vm.prank(newModule); + //idExists(roleId_) vm.expectRevert( abi.encodeWithSelector( - IAuthorizer_v1.Module__Authorizer__NotActiveModule.selector, - newModule + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector ) ); - _authorizer.revokeRoleFromModule(ROLE_0, BOB); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false - ); + vm.prank(_initialAdmin); + _authSuT.labelRole(bytes32(uint(2)), "RoleName"); } - function testRevokeRoleFromModuleIdempotence() public { - address newModule = _setupMockSelfManagedModule(); - - vm.startPrank(newModule); - - _authorizer.revokeRoleFromModule(ROLE_0, BOB); - - _authorizer.revokeRoleFromModule(ROLE_0, BOB); - // No reverts happen + function testLabelRole_idExists(string memory newRoleName_) public { + // Create Role + vm.prank(_initialAdmin); + bytes32 id = + _authSuT.createRole("RoleName", bytes32(0), new address[](0)); - vm.stopPrank(); + // Check that event is emitted + vm.expectEmit(true, true, true, true); + emit IAuthorizer_v1.RoleLabeled(id, newRoleName_); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false - ); + // Label role + vm.prank(_initialAdmin); + _authSuT.labelRole(id, newRoleName_); } - // Test revokeRoleFromModuleBatched - // - Should revert if caller is not a module - // - Should not revert if target doesn't have role. - // - Should not revert if address list is empty - - function testRevokeRoleFromModuleBatched(address[] memory newAuthorized) - public - { - address newModule = _setupMockSelfManagedModule(); - newAuthorized = _validateAuthorizedList(newAuthorized); - bytes32 role0_module = _authorizer.generateRoleId(newModule, ROLE_0); - - // grant role to the addresses - for (uint i = 0; i < newAuthorized.length; i++) { - vm.prank(newModule); - _authorizer.grantRoleFromModule(ROLE_0, newAuthorized[i]); - assertEq(_authorizer.hasRole(role0_module, newAuthorized[i]), true); - } - - for (uint i = 0; i < newAuthorized.length; i++) { - vm.expectEmit(true, true, true, true); - emit RoleRevoked(role0_module, newAuthorized[i], newModule); - } - - vm.prank(newModule); - _authorizer.revokeRoleFromModuleBatched(ROLE_0, newAuthorized); - - for (uint i = 0; i < newAuthorized.length; i++) { - assertEq(_authorizer.hasRole(role0_module, newAuthorized[i]), false); + /* + Test: transferAdminRole + ├── Given: Caller is not the admin ot the role for which the admin is being transferred + │ └── When: transferAdminRole is called + │ └── Then: The function should revert + ├── Given: Caller is the admin of the role for which the admin is being transferred + ├── And: The given roleId is not existing + │ └── When: transferAdminRole is called + │ └── Then: The function should revert (modifier in position check) + ├── Given: Caller is the admin of the role for which the admin is being transferred + ├── And: The given adminRoleId is not existing + │ └── When: transferAdminRole is called + │ └── Then: The function should revert (modifier in position check) + ├── Given: Caller is the admin of the role for which the admin is being transferred + ├── And: The given roleId is existing + └── When: transferAdminRole is called + └── Then: The Admin should be transferred to the new Admin + */ + + function testTransferAdminRole_OnlyRoleAdmin( + uint seed_, + bytes32 roleId_, + bytes32 roleAdmin_ + ) public { + // Make sure roleId_ is not the default admin or the public role + vm.assume(uint(roleId_) > 1); + // make sure that roleAdmin was created before roleId + vm.assume(uint(roleId_) > uint(roleAdmin_)); + + // Create Setup + // Create random amount of Roles making the next created Role have the given RoleId + _authSuT.changeLastAssignedRoleId(uint(roleId_) - 1); + // Create new Role with given RoleAdmin + vm.prank(_initialAdmin); + _authSuT.createRole("RoleName", roleAdmin_, new address[](0)); + + // randomize if caller has the admin role or not + if (seed_ % 2 == 0) { + vm.prank(_initialAdmin); + _authSuT.grantRole(roleAdmin_, _bob); + } else { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + address(_bob), + roleAdmin_ + ) + ); } + vm.prank(_bob); + _authSuT.transferAdminRole(roleId_, roleAdmin_); } - function testRevokeRoleFromModuleBatchedFailsIfCalledByNonModule() public { - address newModule = _setupMockSelfManagedModule(); - - address[] memory targets = new address[](2); - targets[0] = address(ALBA); - targets[1] = address(BOB); - - vm.prank(address(BOB)); - vm.expectRevert(); - _authorizer.revokeRoleFromModuleBatched(ROLE_0, targets); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false + function testTransferAdminRole_ModifierInPositionChecks() public { + //idExists(roleId_) + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector + ) ); - } + vm.prank(_initialAdmin); + _authSuT.transferAdminRole(bytes32(uint(2)), bytes32(uint(0))); - function testRevokeRoleFromModuleBatchedFailsIfModuleNotInOrchestrator() - public - { - address newModule = _setupMockSelfManagedModule(); - - address[] memory targets = new address[](2); - targets[0] = address(ALBA); - targets[1] = address(BOB); - - vm.startPrank(ALBA); - _orchestrator.initiateRemoveModuleWithTimelock(newModule); - vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); - _orchestrator.executeRemoveModule(newModule); - vm.stopPrank(); - - vm.prank(newModule); + //idExists(newAdminRoleId_) vm.expectRevert( abi.encodeWithSelector( - IAuthorizer_v1.Module__Authorizer__NotActiveModule.selector, - newModule + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector ) ); - _authorizer.revokeRoleFromModuleBatched(ROLE_0, targets); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false - ); + vm.prank(_initialAdmin); + _authSuT.transferAdminRole(bytes32(uint(0)), bytes32(uint(2))); } - function testRevokeRoleFromModuleBatchedIdempotence() public { - address newModule = _setupMockSelfManagedModule(); - - address[] memory targets = new address[](2); - targets[0] = address(ALBA); - targets[1] = address(BOB); - - vm.startPrank(newModule); - - _authorizer.revokeRoleFromModuleBatched(ROLE_0, targets); - _authorizer.revokeRoleFromModuleBatched(ROLE_0, targets); - - // No reverts happen + function testTransferAdminRole_idExists( + bytes32 roleId_, + bytes32 newAdminRoleId_ + ) public { + // make sure that roleAdmin was created before roleId + vm.assume(uint(roleId_) > uint(newAdminRoleId_)); + // Create Setup + // Create random amount of Roles making the next created Role have the given RoleId + _authSuT.changeLastAssignedRoleId(uint(roleId_) - 1); + // Create new Role with given RoleAdmin + vm.prank(_initialAdmin); + _authSuT.createRole("RoleName", bytes32(0), new address[](0)); + + // Call transferAdminRole + vm.prank(_initialAdmin); + _authSuT.transferAdminRole(roleId_, newAdminRoleId_); + + // Check that the new Admin Role is the new Admin + assertEq(_authSuT.getRoleAdmin(roleId_), newAdminRoleId_); + } - vm.stopPrank(); + // burnRoleAdmin + + /* + Test: burnRoleAdmin + ├── Given: Caller is not the admin of the role for which the admin is being burned + │ └── When: burnRoleAdmin is called + │ └── Then: The function should revert + ├── Given: Caller is the admin of the role for which the admin is being burned + ├── And: The given roleId is not existing + │ └── When: burnRoleAdmin is called + │ └── Then: The function should revert (modifier in position check) + ├── Given: Caller is the admin of the role for which the admin is being burned + └── And: The given roleId is existing + └── When: burnRoleAdmin is called + └── Then: The Admin should be burned + */ + + function testBurnRoleAdmin_OnlyRoleAdmin( + uint seed_, + bytes32 roleId_, + bytes32 roleAdmin_ + ) public { + // Make sure roleId_ is not the default admin or the public role + vm.assume(uint(roleId_) > 1); + // make sure that roleAdmin was created before roleId + vm.assume(uint(roleId_) > uint(roleAdmin_)); + + // Create Setup + // Create random amount of Roles making the next created Role have the given RoleId + _authSuT.changeLastAssignedRoleId(uint(roleId_) - 1); + // Create new Role with given RoleAdmin + vm.prank(_initialAdmin); + _authSuT.createRole("RoleName", roleAdmin_, new address[](0)); + + // randomize if caller has the admin role or not + if (seed_ % 2 == 0) { + vm.prank(_initialAdmin); + _authSuT.grantRole(roleAdmin_, _bob); + } else { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + address(_bob), + roleAdmin_ + ) + ); + } + vm.prank(_bob); + _authSuT.burnRoleAdmin(roleId_); + } - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), ALBA - ), - false - ); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false + function testBurnRoleAdmin_ModifierInPositionChecks() public { + //idExists(roleId_) + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector + ) ); + vm.prank(_initialAdmin); + _authSuT.burnRoleAdmin(bytes32(uint(2))); } - // Test grant and revoke global roles - - // Grant global roles - function testGrantGlobalRole() public { - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - vm.prank(ALBA); - + function testBurnRoleAdmin_idExists(bytes32 roleId_, bytes32 adminRoleId_) + public + { + // make sure that roleAdmin was created before roleId + vm.assume(uint(roleId_) > uint(adminRoleId_)); + // Create Setup + // Create random amount of Roles making the next created Role have the given RoleId + _authSuT.changeLastAssignedRoleId(uint(roleId_) - 1); + // Create new Role with given RoleAdmin + vm.prank(_initialAdmin); + _authSuT.createRole("RoleName", adminRoleId_, new address[](0)); + + // Give Caller the Admin Role + vm.prank(_initialAdmin); + _authSuT.grantRole(adminRoleId_, _bob); + + // Expect event vm.expectEmit(true, true, true, true); - emit RoleGranted(globalRole, BOB, ALBA); + emit IAuthorizer_v1.RoleAdminBurned(roleId_); - _authorizer.grantGlobalRole(bytes32("0x03"), BOB); - assertTrue(_authorizer.hasRole(globalRole, BOB)); - } + // Call transferAdminRole + vm.prank(_bob); + _authSuT.burnRoleAdmin(roleId_); - function testGrantGlobalRoleFailsIfNotAdmin() public { - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - vm.prank(BOB); - vm.expectRevert(); - _authorizer.grantGlobalRole(bytes32("0x03"), ALBA); - assertFalse(_authorizer.hasRole(globalRole, ALBA)); + // Check that the new Admin Role is the Burned Admin role + assertEq(_authSuT.getRoleAdmin(roleId_), _authSuT.BURN_ADMIN_ROLE()); } - // Test grantGlobalRoleBatched - // - Should revert if caller is not admin - // - Should not revert if address list is empty - - function testGrantGlobalRoleBatched(address[] memory newAuthorized) + // ------------------------------------------------------------------------ + // Mutating - Mixed Utility + + /* + Test: createRoleAndAddAccessPermissions + ├── Given: Caller does not inhabit the permissioned Role + │ └── When: createRoleAndAddAccessPermissions is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is not existing + │ └── When: createRoleAndAddAccessPermissions is called + │ └── Then: Then it should revert (modifier in position check) + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is existing + ├── And: The given targets array and the selectors array do not have the same length + │ └── When: createRoleAndAddAccessPermissions is called + │ └── Then: Then it should revert + ├── Given: Caller inhabits the default admin role + ├── And: The given roleId is existing + └── And: The given targets array and the selectors array have the same length + └── When: createRoleAndAddAccessPermissions is called + └── Then: The role is created + └── And: The permissions are added to the according function locks + */ + function testCreateRoleAndAddAccessPermissions_ModifierInPositionCheck() public { - _validateAuthorizedList(newAuthorized); - - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - - for (uint i = 0; i < newAuthorized.length; i++) { - assertEq(_authorizer.hasRole(globalRole, newAuthorized[i]), false); - - vm.expectEmit(true, true, true, true); - emit RoleGranted(globalRole, newAuthorized[i], ALBA); - } - - vm.prank(ALBA); - _authorizer.grantGlobalRoleBatched(bytes32("0x03"), newAuthorized); + //permissioned + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + _authSuT.createRoleAndAddAccessPermissions( + "RoleName", + bytes32(uint(0)), + new address[](0), + new address[](0), + new bytes4[][](0) + ); - for (uint i = 0; i < newAuthorized.length; i++) { - assertEq(_authorizer.hasRole(globalRole, newAuthorized[i]), true); - } + //idExists(respectiveAdminRole_) + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector + ) + ); + vm.prank(_initialAdmin); + _authSuT.createRoleAndAddAccessPermissions( + "RoleName", + bytes32(uint(2)), + new address[](0), + new address[](0), + new bytes4[][](0) + ); } - function testGrantGlobalRoleBatchedFailsIfCalledByNonAdmin() public { - address newModule = _setupMockSelfManagedModule(); - - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - - address[] memory targets = new address[](2); - targets[0] = address(ALBA); - targets[1] = address(BOB); + function testCreateRoleAndAddAccessPermissions_RevertWhenArrayLengthsAreDifferent( + address[] memory targets_, + bytes4[][] memory selectors_ + ) public { + // Make sure the arrays have different lengths + vm.assume(targets_.length != selectors_.length); - vm.prank(address(BOB)); - vm.expectRevert(); - _authorizer.grantGlobalRoleBatched(globalRole, targets); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, globalRole), ALBA - ), - false + // Invalid Input Length + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__InvalidInputLength.selector + ) ); - assertEq( - _authorizer.hasRole( - _authorizer.generateRoleId(newModule, ROLE_0), BOB - ), - false + vm.prank(_initialAdmin); + _authSuT.createRoleAndAddAccessPermissions( + "RoleName", bytes32(uint(0)), new address[](0), targets_, selectors_ ); } - function testGrantGlobalRoleBatchedIdempotenceOnEmptyList() public { - _setupMockSelfManagedModule(); - - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - - address[] memory targets = new address[](0); - - vm.prank(ALBA); - _authorizer.grantGlobalRoleBatched(globalRole, targets); - } + //@note This test alone takes up as much time as the others combined. Restricted the number of runs to 20 + /// forge-config: default.fuzz.runs = 20 + function testCreateRoleAndAddAccessPermissions_idExists( + string memory roleName_, + address[] memory initialMembers_, + address[] memory targets_, + bytes4[][] memory selectors_ + ) public { + // Downsize arrays to reasonable size + vm.assume(initialMembers_.length < 800); //800 + vm.assume(targets_.length < 75); //75 + vm.assume(selectors_.length <= targets_.length); + + uint selectorLength = selectors_.length; + for (uint i = 0; i < selectorLength; i++) { + vm.assume(selectors_[i].length < 50); //50 + } - // Revoke global roles - function testRevokeGlobalRole() public { - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - vm.startPrank(ALBA); - _authorizer.grantGlobalRole(bytes32("0x03"), BOB); - assertTrue(_authorizer.hasRole(globalRole, BOB)); + // Make sure that selector length and target length are the same + if (selectorLength < targets_.length) { + address[] memory temp = new address[](selectorLength); + for (uint i = 0; i < selectorLength; i++) { + temp[i] = targets_[i]; + } + targets_ = temp; + } + // Check that the role is created vm.expectEmit(true, true, true, true); - emit RoleRevoked(globalRole, BOB, ALBA); - - _authorizer.revokeGlobalRole(bytes32("0x03"), BOB); - assertEq(_authorizer.hasRole(globalRole, BOB), false); + emit IAuthorizer_v1.RoleCreated(bytes32(uint(2)), roleName_); - vm.stopPrank(); - } - - function testRevokeGlobalRoleFailsIfNotAdmin() public { - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - - vm.prank(ALBA); - _authorizer.grantGlobalRole(bytes32("0x03"), BOB); - assertTrue(_authorizer.hasRole(globalRole, BOB)); - - vm.prank(BOB); - vm.expectRevert(); - _authorizer.revokeGlobalRole(bytes32("0x03"), BOB); - assertTrue(_authorizer.hasRole(globalRole, BOB)); - } - - // Test revokeGlobalRoleBatched - // - Should revert if caller is not admin - // - Should not revert if address list is empty - - function testRevokeGlobalRoleBatched(address[] memory newAuthorized) - public - { - _validateAuthorizedList(newAuthorized); - - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - - vm.startPrank(ALBA); - _authorizer.grantGlobalRoleBatched(bytes32("0x03"), newAuthorized); - - for (uint i = 0; i < newAuthorized.length; i++) { - vm.expectEmit(true, true, true, true); - emit RoleRevoked(globalRole, newAuthorized[i], ALBA); - } - - _authorizer.revokeGlobalRoleBatched(bytes32("0x03"), newAuthorized); + vm.prank(_initialAdmin); + bytes32 roleId = _authSuT.createRoleAndAddAccessPermissions( + roleName_, bytes32(0), initialMembers_, targets_, selectors_ + ); - for (uint i = 0; i < newAuthorized.length; i++) { - assertEq(_authorizer.hasRole(globalRole, newAuthorized[i]), false); + // Check that the permissions are added to the function locks + uint targetLength = targets_.length; + for (uint i = 0; i < targetLength; i++) { + for (uint j = 0; j < selectors_[i].length; j++) { + assertTrue( + _authSuT.isRolePermissioned( + targets_[i], selectors_[i][j], roleId + ) + ); + } } - - vm.stopPrank(); } - function testRevokeGlobalRoleBatchedFailsIfNotAdmin() public { - _setupMockSelfManagedModule(); - - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); - - address[] memory targets = new address[](2); - targets[0] = address(ALBA); - targets[1] = address(BOB); + /////////////////////////////////////////////////////////////////////////// + // Test Internal Functions - vm.prank(ALBA); - _authorizer.grantGlobalRoleBatched(bytes32("0x03"), targets); + // ======================================================================== + // Internal Functions - assertEq(_authorizer.hasRole(globalRole, ALBA), true); - assertEq(_authorizer.hasRole(globalRole, BOB), true); + // ------------------------------------------------------------------------ + // Internal - Upstream Function Implementations - vm.prank(address(BOB)); - vm.expectRevert(); - _authorizer.revokeGlobalRoleBatched(globalRole, targets); + // function _grantRole( - assertEq(_authorizer.hasRole(globalRole, ALBA), true); - assertEq(_authorizer.hasRole(globalRole, BOB), true); + /* + Test: grantRole + └── Given: The role id is not existing + └── When: grantRole is called + └── Then: The call reverts (modifier in position check) + */ + function testGrantRole_ModifierInPositionCheck() public { + // idExists(role) + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector + ) + ); + vm.prank(_initialAdmin); + _authSuT.grantRole(bytes32(uint(2)), _bob); } - function testRevokeGlobalRoleBatchedIdempotenceOnEmptyList() public { - _setupMockSelfManagedModule(); + /////////////////////////////////////////////////////////////////////////// + // Helper Functions - bytes32 globalRole = - _authorizer.generateRoleId(address(_orchestrator), bytes32("0x03")); + // Bob and Alice can Access selctor1 + // Alice can access selector2 + // No permissions in selector3 + // Public Role can access selector46("selector4()")); + function createSetup(address target_) internal { + vm.startPrank(_initialAdmin); - address[] memory targets = new address[](0); + address[] memory targetArray = new address[](1); + targetArray[0] = target_; - vm.prank(ALBA); - _authorizer.revokeGlobalRoleBatched(globalRole, targets); - } + address[] memory selector1Members = new address[](2); + selector1Members[0] = _bob; + selector1Members[1] = _alice; - // ========================================================================= - // Test granting and revoking ADMIN control, and test admin control over module roles + address[] memory selector2Members = new address[](1); + selector2Members[0] = _alice; - function testGrantAdminRole() public { - bytes32 adminRole = _authorizer.DEFAULT_ADMIN_ROLE(); - vm.prank(ALBA); + address[] memory selector3Members = new address[](0); - vm.expectEmit(true, true, true, true); - emit RoleGranted(adminRole, BOB, ALBA); + address[] memory selector4Members = new address[](0); - _authorizer.grantRole(adminRole, BOB); - assertTrue(_authorizer.hasRole(adminRole, BOB)); - } + bytes4[][] memory selectorArray = new bytes4[][](1); + bytes4[] memory selector1Array2D = new bytes4[](1); - function testGrantAdminRoleFailsIfNotAdmin() public { - bytes32 adminRole = _authorizer.DEFAULT_ADMIN_ROLE(); - address COBIE = address(0xC0B1E); + selector1Array2D[0] = _selector1; + selectorArray[0] = selector1Array2D; - vm.prank(BOB); - vm.expectRevert(); - _authorizer.grantRole(adminRole, COBIE); - assertFalse(_authorizer.hasRole(adminRole, COBIE)); - } + _authSuT.createRoleAndAddAccessPermissions( + "Selector1Role", + _authSuT.getAdminRole(), + selector1Members, + targetArray, + selectorArray + ); - // Test that only Admin can change admin - function testChangeRoleAdminOnModuleRole() public { - // First, we make BOB admin - bytes32 adminRole = _authorizer.DEFAULT_ADMIN_ROLE(); - vm.prank(ALBA); - _authorizer.grantRole(adminRole, BOB); - assertTrue(_authorizer.hasRole(adminRole, BOB)); - - // Then we set up a mock module - address newModule = _setupMockSelfManagedModule(); - bytes32 roleId = _authorizer.generateRoleId(newModule, ROLE_0); - - // Now we set the OWNER as Role admin - vm.startPrank(BOB); - _authorizer.transferAdminRole(roleId, _authorizer.getAdminRole()); - vm.stopPrank(); + bytes4[] memory selector2Array2D = new bytes4[](1); - // ALBA can now freely grant and revoke roles - assertEq(_authorizer.hasRole(roleId, BOB), false); - vm.startPrank(ALBA); - _authorizer.grantRole(roleId, BOB); - assertEq(_authorizer.hasRole(roleId, BOB), true); - _authorizer.revokeRole(roleId, BOB); - assertEq(_authorizer.hasRole(roleId, BOB), false); - } + selector2Array2D[0] = _selector2; + selectorArray[0] = selector2Array2D; - function testChangeRoleAdminOnModuleRoleFailsIfNotAdmin() public { - // We set up a mock module - address newModule = _setupMockSelfManagedModule(); + _authSuT.createRoleAndAddAccessPermissions( + "Selector2Role", + _authSuT.getAdminRole(), + selector2Members, + targetArray, + selectorArray + ); - bytes32 roleId = _authorizer.generateRoleId(newModule, ROLE_0); - bytes32 adminRole = _authorizer.getAdminRole(); // Buffer this to time revert + bytes4[] memory selector3Array2D = new bytes4[](1); - // BOB is not allowed to do this - vm.startPrank(BOB); - vm.expectRevert(); - _authorizer.transferAdminRole(roleId, adminRole); - vm.stopPrank(); - } + selector3Array2D[0] = _selector3; + selectorArray[0] = selector3Array2D; - // Test that ADMIN cannot change module roles if admin role was burned - - function testAdminCannotModifyRoleIfAdminBurned() public { - // First, we make BOB admin - bytes32 adminRole = _authorizer.DEFAULT_ADMIN_ROLE(); - vm.prank(ALBA); - _authorizer.grantRole(adminRole, BOB); - assertTrue(_authorizer.hasRole(adminRole, BOB)); - - // Then we set up a mock module and buffer the role with burned admin - address newModule = _setupMockSelfManagedModule(); - bytes32 roleId = _authorizer.generateRoleId(newModule, ROLE_1); - - // BOB can NOT grant and revoke roles even though he's admin - assertEq(_authorizer.hasRole(roleId, BOB), false); - vm.startPrank(BOB); - vm.expectRevert(); - _authorizer.grantRole(roleId, BOB); - assertEq(_authorizer.hasRole(roleId, BOB), false); - vm.expectRevert(); - _authorizer.revokeRole(roleId, BOB); - assertEq(_authorizer.hasRole(roleId, BOB), false); - vm.stopPrank(); - } + _authSuT.createRoleAndAddAccessPermissions( + "Selector3Role", + _authSuT.getAdminRole(), + selector3Members, + targetArray, + selectorArray + ); - // Test the burnAdminFromModuleRole - // -> Test burnAdmin changes state - function testBurnAdminChangesRoleState() public { - // _setupMockSelfManagedModule implicitly test this - } - // -> Test a role with burnt admin cannot be modified by admin - - function testModifyRoleByAdminFailsIfAdminBurned() public { - // First, we make BOB admin - bytes32 adminRole = _authorizer.DEFAULT_ADMIN_ROLE(); - vm.prank(ALBA); - _authorizer.grantRole(adminRole, BOB); - assertTrue(_authorizer.hasRole(adminRole, BOB)); - - // Then we set up a mock module and buffer both roles - address newModule = _setupMockSelfManagedModule(); - bytes32 roleId_0 = _authorizer.generateRoleId(newModule, ROLE_0); - bytes32 roleId_1 = _authorizer.generateRoleId(newModule, ROLE_1); - - vm.startPrank(BOB); - - // BOB can modify role 0 - assertEq(_authorizer.hasRole(roleId_0, ALBA), false); - _authorizer.grantRole(roleId_0, ALBA); - assertEq(_authorizer.hasRole(roleId_0, ALBA), true); - _authorizer.revokeRole(roleId_0, ALBA); - assertEq(_authorizer.hasRole(roleId_0, ALBA), false); - - // But not role 1 - vm.expectRevert(); - _authorizer.grantRole(roleId_1, ALBA); - assertEq(_authorizer.hasRole(roleId_1, ALBA), false); - vm.expectRevert(); - _authorizer.revokeRole(roleId_1, ALBA); - assertEq(_authorizer.hasRole(roleId_1, ALBA), false); - vm.stopPrank(); - } + bytes4[] memory selector4Array2D = new bytes4[](1); - // ========================================================================= - // Test Helper Functions - - // SetUp ModuleWith Roles. - // Creates a Mock module and adds it to the orchestrator with 2 roles: - // - 1 with default Admin - // - 1 with burnt admin - // BOB is member of both roles. - function _setupMockSelfManagedModule() internal returns (address) { - ModuleV1Mock mockModule = new ModuleV1Mock(); - - vm.startPrank(ALBA); // We assume ALBA is admin - _orchestrator.initiateAddModuleWithTimelock(address(mockModule)); - vm.warp(block.timestamp + _orchestrator.MODULE_UPDATE_TIMELOCK()); - emit hm(_orchestrator.MODULE_UPDATE_TIMELOCK()); - _orchestrator.executeAddModule(address(mockModule)); - vm.stopPrank(); - vm.startPrank(address(mockModule)); + selector4Array2D[0] = _selector4; + selectorArray[0] = selector4Array2D; - vm.expectEmit(true, true, true, true); - emit RoleAdminChanged( - _authorizer.generateRoleId(address(mockModule), ROLE_1), - bytes32(0x00), - _authorizer.BURN_ADMIN_ROLE() + _authSuT.createRoleAndAddAccessPermissions( + "Selector4Role", + _authSuT.getAdminRole(), + selector4Members, + targetArray, + selectorArray ); - _authorizer.burnAdminFromModuleRole(ROLE_1); - - vm.stopPrank(); - - bytes32 burntAdmin = _authorizer.getRoleAdmin( - _authorizer.generateRoleId(address(mockModule), ROLE_1) + _authSuT.addAccessPermission( + target_, _selector4, _authSuT.PUBLIC_ROLE() ); - assertTrue(burntAdmin == _authorizer.BURN_ADMIN_ROLE()); - return address(mockModule); + vm.stopPrank(); } - function _validateAuthorizedList(address[] memory auths) + function selectFunctionSelectorBasedOnSeed(uint seed_) internal - returns (address[] memory) + view + returns (bytes4 selector_) { - vm.assume(auths.length != 0); - vm.assume(auths.length < 20); - assumeValidAuths(auths); - - return auths; - } - // Adapted from orchestrator/helper/TypeSanityHelper.sol - - mapping(address => bool) authorizedCache; - - function assumeValidAuths(address[] memory addrs) public { - for (uint i; i < addrs.length; ++i) { - assumeValidAuth(addrs[i]); - - // Assume authorized address unique. - vm.assume(!authorizedCache[addrs[i]]); - - // Add contributor address to cache. - authorizedCache[addrs[i]] = true; + if (seed_ % 5 == 0) { + selector_ = _selector1; + } else if (seed_ % 5 == 1) { + selector_ = _selector2; + } else if (seed_ % 5 == 2) { + selector_ = _selector3; + } else if (seed_ % 5 == 3) { + selector_ = _selector4; + } else { + selector_ = bytes4(bytes32(seed_)); } } - function assumeValidAuth(address a) public view { - address[] memory invalids = createInvalidAuthorized(); - - for (uint i; i < invalids.length; ++i) { - vm.assume(a != invalids[i]); + /// @dev 3 Possible callers + /// Bob, Alice, or a random address + /// Random address can be Bob or Alice or Initial Admin + function selectCallerBasedOnSeed(uint seed_) + internal + view + returns (address caller_) + { + if (seed_ % 3 == 0) { + caller_ = _bob; + } else if (seed_ % 3 == 1) { + caller_ = _alice; + } else { + caller_ = address(uint160(seed_)); } } - function createInvalidAuthorized() public view returns (address[] memory) { - address[] memory invalids = new address[](8); - - invalids[0] = address(0); - invalids[1] = address(_orchestrator); - invalids[2] = address(_authorizer); - invalids[3] = address(_paymentProcessor); - invalids[4] = address(_token); - invalids[5] = address(this); - invalids[6] = ALBA; - invalids[7] = BOB; - - return invalids; + /// @dev needs all permissions to be unlocked via _authSuT.changeLastAssignedRoleId(type(uint).max); + function createRandomLockRestrictions( + uint seed_, + uint minimumPermissionLength_ + ) internal returns (address target_, bytes4 selector_) { + target_ = address(uint160(seed_)); + selector_ = bytes4(bytes32(seed_)); + + uint permissionLength = seed_ % 50; + if (permissionLength <= minimumPermissionLength_) { + permissionLength = minimumPermissionLength_; + } + uint currentPermissionRoleId = seed_; + for (uint i = 0; i < permissionLength; i++) { + // Increment key in a non regular way + unchecked { + currentPermissionRoleId = currentPermissionRoleId + i * i; + } + // Key cannot be Default Admin Role + if (currentPermissionRoleId == 0) { + currentPermissionRoleId = 1; + } + // Key cannot be Public Role + if (currentPermissionRoleId == type(uint).max) { + currentPermissionRoleId = type(uint).max - 1; + } + vm.prank(_initialAdmin); + _authSuT.addAccessPermission(target_, selector_, bytes32(uint(1))); + } } - // ========================================================================= } diff --git a/test/unit/modules/authorizer/role/AUT_TokenGated_Roles_v1.t.sol b/test/unit/modules/authorizer/role/AUT_TokenGated_Roles_v1.t.sol index 60a6c5505..faf142566 100644 --- a/test/unit/modules/authorizer/role/AUT_TokenGated_Roles_v1.t.sol +++ b/test/unit/modules/authorizer/role/AUT_TokenGated_Roles_v1.t.sol @@ -1,312 +1,375 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.0; -// SuT -import {Test} from "forge-std/Test.sol"; - -import {AUT_RolesV1Test} from - "@unitTest/modules/authorizer/role/AUT_Roles_v1.t.sol"; - -// SuT -import { - AUT_TokenGated_Roles_v1, - IAUT_TokenGated_Roles_v1 -} from "@aut/role/AUT_TokenGated_Roles_v1.sol"; +import "forge-std/Test.sol"; -import {AUT_Roles_v1, IAuthorizer_v1} from "@aut/role/AUT_Roles_v1.sol"; -import {IAuthorizer_v1} from "@aut/IAuthorizer_v1.sol"; // External Libraries import {Clones} from "@oz/proxy/Clones.sol"; + +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + import {IERC165} from "@oz/utils/introspection/IERC165.sol"; -import {IAccessControl} from "@oz/access/IAccessControl.sol"; -import {IAccessControlEnumerable} from - "@oz/access/extensions/IAccessControlEnumerable.sol"; // Internal Dependencies -import {Orchestrator_v1} from "src/orchestrator/Orchestrator_v1.sol"; -// Interfaces +import { + ModuleTest, + IModule_v1, + IOrchestrator_v1 +} from "@unitTest/modules/ModuleTest.sol"; + +// Internal Libraries +import {LibMetadata} from "src/modules/lib/LibMetadata.sol"; + +// Internal Interfaces import {IModule_v1, IOrchestrator_v1} from "src/modules/base/IModule_v1.sol"; + +import {Orchestrator_v1} from "src/orchestrator/Orchestrator_v1.sol"; + +import {IAuthorizer_v1} from "@aut/IAuthorizer_v1.sol"; +import {IAUT_TokenGated_Roles_v1} from + "@aut/role/interfaces/IAUT_TokenGated_Roles_v1.sol"; + +import {TokenInterface} from "@aut/role/AUT_TokenGated_Roles_v1.sol"; + +// SuT +import {AUT_TokenGated_Roles_v1_Exposed} from + "@mocks/modules/authorizer/AUT_TokenGated_Roles_v1_Exposed.sol"; + // Mocks -import {ERC20Mock} from "@mocks/external/token/ERC20Mock.sol"; -import {ERC721Mock} from "@mocks/external/token/ERC721Mock.sol"; -import {ModuleV1Mock} from "@mocks/modules/base/ModuleV1Mock.sol"; import {FundingManagerV1Mock} from "@mocks/modules/fundingManager/FundingManagerV1Mock.sol"; +import {AuthorizerV1Mock} from "@mocks/modules/authorizer/AuthorizerV1Mock.sol"; import {PaymentProcessorV1Mock} from "@mocks/modules/paymentProcessor/PaymentProcessorV1Mock.sol"; -import {GovernorV1Mock} from "@mocks/external/governance/GovernorV1Mock.sol"; -import {ModuleFactoryV1Mock} from "@mocks/factories/ModuleFactoryV1Mock.sol"; - -// Run through the AUT_Roles_v1 tests with the AUT_TokenGated_Roles_v1 -contract AUT_TokenGated_RolesV1Test is AUT_RolesV1Test { - function setUp() public override { - //==== We use the AUT_TokenGated_Roles_v1 as a regular AUT_Roles_v1 ===== - address authImpl = address(new AUT_TokenGated_Roles_v1()); - _authorizer = AUT_Roles_v1(Clones.clone(authImpl)); - //========================================================================== - - address propImpl = address(new Orchestrator_v1(address(0))); - _orchestrator = Orchestrator_v1(Clones.clone(propImpl)); - ModuleV1Mock module = new ModuleV1Mock(); - address[] memory modules = new address[](1); - modules[0] = address(module); - _orchestrator.init( - _ORCHESTRATOR_ID, - address(_moduleFactory), - modules, - _fundingManager, - _authorizer, - _paymentProcessor, - _governor - ); - - address initialAuth = ALBA; - - _authorizer.init( - IOrchestrator_v1(_orchestrator), _METADATA, abi.encode(initialAuth) - ); - assertEq(_authorizer.hasRole(_authorizer.getAdminRole(), ALBA), true); - assertEq( - _authorizer.hasRole(_authorizer.getAdminRole(), address(this)), - false - ); - } -} +import {ERC20PaymentClientBaseV2Mock} from + "@mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; +import {TokenInterfaceMock} from + "@mocks/modules/authorizer/TokenInterfaceMock.sol"; -contract TokenGatedAUT_RoleV1Test is Test { - // Mocks - AUT_TokenGated_Roles_v1 _authorizer; - Orchestrator_v1 internal _orchestrator = new Orchestrator_v1(address(0)); - ERC20Mock internal _token = new ERC20Mock("Mock Token", "MOCK", 18); - FundingManagerV1Mock _fundingManager = new FundingManagerV1Mock(); - PaymentProcessorV1Mock _paymentProcessor = new PaymentProcessorV1Mock(); - GovernorV1Mock internal _governor = new GovernorV1Mock(); - ModuleFactoryV1Mock internal _moduleFactory = new ModuleFactoryV1Mock(); - - ModuleV1Mock mockModule = new ModuleV1Mock(); - - address ALBA = address(0xa1ba); // default authorized person - address BOB = address(0xb0b); // example person - address CLOE = address(0xc10e); // example person - - ERC20Mock internal roleToken = - new ERC20Mock("Inverters With Benefits", "IWB", 18); - ERC721Mock internal roleNft = - new ERC721Mock("detrevnI epA thcaY bulC", "EPA"); - - bytes32 immutable ROLE_TOKEN = "ROLE_TOKEN"; - bytes32 immutable ROLE_NFT = "ROLE_NFT"; - - // Orchestrator_v1 Constants - uint internal constant _ORCHESTRATOR_ID = 1; - // Module Constants - uint constant MAJOR_VERSION = 1; - uint constant MINOR_VERSION = 0; - uint constant PATCH_VERSION = 0; - string constant URL = "https://github.com/organization/module"; - string constant TITLE = "Module"; - - IModule_v1.Metadata _METADATA = IModule_v1.Metadata( - MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION, URL, TITLE - ); - - //-------------------------------------------------------------------------- - // Events - - /// @notice Event emitted when the token-gating of a role changes. - /// @param role The role that was modified. - /// @param newValue The new value of the role. - event ChangedTokenGating(bytes32 role, bool newValue); - - /// @notice Event emitted when the threshold of a token-gated role changes. - /// @param role The role that was modified. - /// @param token The token for which the threshold was modified. - /// @param newValue The new value of the threshold. - event ChangedTokenThreshold(bytes32 role, address token, uint newValue); - - /// @notice Event emitted when `account` is revoked of `role`. - /// @param role The role that was revoked. - /// @param account The account that has the role revoked. - /// @param sender The account that performed the revocation. - event RoleRevoked( - bytes32 indexed role, address indexed account, address indexed sender - ); +// Errors +import {OZErrors} from "@testUtilities/OZErrors.sol"; - function setUp() public { - address authImpl = address(new AUT_TokenGated_Roles_v1()); - _authorizer = AUT_TokenGated_Roles_v1(Clones.clone(authImpl)); - address propImpl = address(new Orchestrator_v1(address(0))); - _orchestrator = Orchestrator_v1(Clones.clone(propImpl)); - address[] memory modules = new address[](1); - modules[0] = address(mockModule); - _orchestrator.init( - _ORCHESTRATOR_ID, - address(_moduleFactory), - modules, - _fundingManager, - _authorizer, - _paymentProcessor, - _governor - ); +// External Dependencies +import {IAccessControl} from "@oz/access/IAccessControl.sol"; - address initialAuth = ALBA; +contract AUT_TokenGated_Roles_v1_Test is ModuleTest { + /////////////////////////////////////////////////////////////////////////// + // State - _authorizer.init( - IOrchestrator_v1(_orchestrator), _METADATA, abi.encode(initialAuth) - ); - assertEq(_authorizer.hasRole(_authorizer.getAdminRole(), ALBA), true); - assertEq( - _authorizer.hasRole(_authorizer.getAdminRole(), address(this)), - false - ); + // SuT + AUT_TokenGated_Roles_v1_Exposed _authSuT; - // We mint some tokens: First, two different amounts of ERC20 - roleToken.mint(BOB, 1000); - roleToken.mint(CLOE, 10); + // Constants + address _bob = makeAddr("Bob"); - // Then, a ERC721 for BOB - roleNft.mint(BOB); + // Addresses + + // Bob and Alice can Access + bytes4 _selector1 = bytes4(keccak256("selector1()")); + // Alice can access + bytes4 _selector2 = bytes4(keccak256("selector2()")); + // No Permissions + bytes4 _selector3 = bytes4(keccak256("selector3()")); + // Public Role can access + bytes4 _selector4 = bytes4(keccak256("selector4()")); + + /////////////////////////////////////////////////////////////////////////// + // Setup + + function setUp() public { + address impl = address(new AUT_TokenGated_Roles_v1_Exposed()); + _authSuT = AUT_TokenGated_Roles_v1_Exposed(Clones.clone(impl)); + + // initiate orchestrator without extra Module + _setUpOrchestrator(); + + // Init SuT + // Initial Admin is this contract + _authSuT.init(_orchestrator, _METADATA, abi.encode(address(this))); + + // Change Authorizer of Module Test to SuT + _orchestrator.initiateSetAuthorizerWithTimelock( + IAuthorizer_v1(_authSuT) + ); + vm.warp(72 hours + 1); + _orchestrator.executeSetAuthorizer(IAuthorizer_v1(_authSuT)); } - function testSupportsInterface() public { + /////////////////////////////////////////////////////////////////////////// + // Test Initialization + + /* + Test: SupportsInterface + └── Given: The interfaceId is IAuthorizer_v1 + └── When: the function supportsInterface is called + └── Then: the function should return true + */ + function testSupportsInterface() public override(ModuleTest) { assertTrue( - _authorizer.supportsInterface( + _authSuT.supportsInterface( type(IAUT_TokenGated_Roles_v1).interfaceId ) ); } - //------------------------------------------------- - // Helper Functions - - // function set up tokenGated role with threshold - function setUpTokenGatedRole( - address module, - bytes32 role, - address token, - uint threshold - ) internal returns (bytes32) { - bytes32 roleId = _authorizer.generateRoleId(module, role); - vm.startPrank(module); - - vm.expectEmit(); - emit ChangedTokenGating(roleId, true); - emit ChangedTokenThreshold(roleId, address(token), threshold); - - _authorizer.makeRoleTokenGatedFromModule(role); - _authorizer.grantTokenRoleFromModule(role, address(token), threshold); - vm.stopPrank(); - return roleId; - } - - // function set up nftGated role - function setUpNFTGatedRole(address module, bytes32 role, address nft) - internal - returns (bytes32) - { - bytes32 roleId = _authorizer.generateRoleId(module, role); - vm.startPrank(module); - - _authorizer.makeRoleTokenGatedFromModule(role); - _authorizer.grantTokenRoleFromModule(role, address(nft), 1); - vm.stopPrank(); - return roleId; + /* + Test: Init + └── When: the function init is called + └── Then: the function should set the initial admin + */ + function testInit() public override { + // Check that the initial Admin is set + assertTrue(_authSuT.hasRole(_authSuT.getAdminRole(), address(this))); } - function makeAddressDefaultAdmin(address who) public { - bytes32 adminRole = _authorizer.DEFAULT_ADMIN_ROLE(); - vm.prank(ALBA); - _authorizer.grantRole(adminRole, who); - assertTrue(_authorizer.hasRole(adminRole, who)); + /* + Test: ReinitFails + └── When: the function init is called after the contract has been initialized + └── Then: the function should revert + */ + function testReinitFails() public override { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + _authSuT.init(_orchestrator, _METADATA, abi.encode(address(this))); } + ///////////////////////////////////////////////////////////////////////////// + // Test Modifier + + /* + Test: onlyEmptyRole Modifier + └── Given: Role is not empty + └── When: function with onlyEmptyRole modifier is called + └── Then: the function should revert + */ + function testOnlyEmptyRoleModifier(uint seed_) public { + // Create address array + address[] memory members = new address[](seed_ % 20); + for (uint i; i < members.length; ++i) { + members[i] = address(uint160(i)); + } - // ------------------------------------- - // State change and validation tests - - // test make role token gated + // Create Role + bytes32 roleId = + _authSuT.createRole("Role", _authSuT.DEFAULT_ADMIN_ROLE(), members); + + if (members.length != 0) { + vm.expectRevert( + abi.encodeWithSelector( + IAUT_TokenGated_Roles_v1 + .Module__AUT_TokenGated_Roles__RoleNotEmpty + .selector + ) + ); + } + _authSuT.onlyEmptyRoleModifier_exposed(roleId); + } - function testMakeRoleTokenGated() public { - bytes32 roleId_1 = setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), 500 + /* + Test notPublicRole Modifier + └── Given: Role is public + └── When: function with notPublicRole modifier is called + └── Then: the function should revert + */ + function testNotPublicRoleModifier() public { + bytes32 roleId = _authSuT.PUBLIC_ROLE(); + vm.expectRevert( + abi.encodeWithSelector( + IAUT_TokenGated_Roles_v1 + .Module__AUT_TokenGated_Roles__RoleIsPublic + .selector + ) ); - assertTrue(_authorizer.isTokenGated(roleId_1)); + _authSuT.notPublicRoleModifier_exposed(roleId); + } - bytes32 roleId_2 = - setUpNFTGatedRole(address(mockModule), ROLE_NFT, address(roleNft)); - assertTrue(_authorizer.isTokenGated(roleId_2)); + /* + Test: onlyTokenGated Modifier + └── Given: Role is not token-gated + └── When: function with onlyTokenGated modifier is called + └── Then: the function should revert + */ + function testOnlyTokenGatedModifier(bool isTokenGated_) public { + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); + + // Set token gated + if (isTokenGated_) { + _authSuT.setTokenGated(roleId, true); + } else { + // If not token gated, then the function should revert + vm.expectRevert( + abi.encodeWithSelector( + IAUT_TokenGated_Roles_v1 + .Module__AUT_TokenGated_Roles__RoleNotTokenGated + .selector + ) + ); + } + _authSuT.onlyTokenGatedModifier_exposed(roleId); } - // test admin setTokenGating - function testSetTokenGatingByAdmin() public { - // we set CLOE as admin - makeAddressDefaultAdmin(CLOE); + /* + Test: validThreshold Modifier + └── Given: Threshold is invalid + └── When: function with validThreshold modifier is called + └── Then: the function should revert + */ + function testValidThresholdModifier(uint threshold_) public { + if (threshold_ == 0) { + vm.expectRevert( + abi.encodeWithSelector( + IAUT_TokenGated_Roles_v1 + .Module__AUT_TokenGated_Roles__InvalidThreshold + .selector, + threshold_ + ) + ); + } + _authSuT.validThresholdModifier_exposed(threshold_); + } + + /////////////////////////////////////////////////////////////////////////// + // Test External Functions + + // ======================================================================== + // Public Getter Functions - // we set and unset on an empty role + /* + Test: isTokenGated + └── When: isTokenGated is called + └── Then: Return if the role is token gated + */ + function testIsTokenGated(bool isTokenGated_, bytes32 roleId_) public { + // Public role cannot be token gated + vm.assume(roleId_ != _authSuT.PUBLIC_ROLE()); - bytes32 roleId = _authorizer.generateRoleId(address(mockModule), "0x00"); + // Set token gated + if (isTokenGated_) { + _authSuT.setTokenGated_unrestricted(roleId_, true); + } + assertEq(_authSuT.isTokenGated(roleId_), isTokenGated_); + } - // now we make it tokengated as admin - vm.prank(CLOE); + /* + Test: hasTokenRole + ├── Given: Role is not token gated + │ └── When: hasTokenRole is called + │ └── Then: It should revert (modifier in position check) + └── Given: Role is token gated + └── When: hasTokenRole is called + └── Then: It should call the internal function + */ + function testHasTokenRole_ModifierInPositionChecks() public { + // onlyTokenGated + vm.expectRevert( + abi.encodeWithSelector( + IAUT_TokenGated_Roles_v1 + .Module__AUT_TokenGated_Roles__RoleNotTokenGated + .selector + ) + ); + _authSuT.hasTokenRole(bytes32(uint(0)), address(0)); + } - vm.expectEmit(); - emit ChangedTokenGating(roleId, true); + function testHasTokenRole_TokenGated_CallsInternalFunction( + address who_, + bool hasTokenRole_ + ) public { + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); - _authorizer.setTokenGated(roleId, true); + // Make Role token gated + _authSuT.setTokenGated(roleId, true); - assertTrue(_authorizer.isTokenGated(roleId)); + // Create Token Interface Mock + address token = address(new TokenInterfaceMock()); - // and revert the change - vm.prank(CLOE); + // Set threshold + _authSuT.setThreshold(roleId, token, 1); - vm.expectEmit(); - emit ChangedTokenGating(roleId, false); + // Grant Role + _authSuT.grantRole(roleId, token); - _authorizer.setTokenGated(roleId, false); + if (hasTokenRole_) { + // Give the address some tokens + TokenInterfaceMock(token).setTokenBalance(who_, 1); + } - assertFalse(_authorizer.isTokenGated(roleId)); + // Check that the role is granted + assertEq(_authSuT.hasTokenRole(roleId, who_), hasTokenRole_); } - // test makeTokenGated fails if not empty - function testMakingFunctionTokenGatedFailsIfAlreadyInUse() public { - bytes32 roleId = - _authorizer.generateRoleId(address(mockModule), ROLE_TOKEN); + /* + Test: getThresholdValue + └── When: getThresholdValue is called + └── Then: Return the threshold value + */ + function testGetThresholdValue( + uint threshold_, + bytes32 roleId_, + address token_ + ) public { + // Set threshold + _authSuT.setThreshold_unrestricted(roleId_, token_, threshold_); - // we switch on self-management and whitelist an address - vm.startPrank(address(mockModule)); - _authorizer.grantRoleFromModule(ROLE_TOKEN, CLOE); + assertEq(_authSuT.getThresholdValue(roleId_, token_), threshold_); + } + // ======================================================================== + // Mutating Functions + + // ------------------------------------------------------------------------ + // Mutating - TokenGated Settings + + /* + Test: setTokenGated + ├── Given: Caller is not permissioned + │ └── When: setTokenGated is called + │ └── Then: The call reverts (modifier in position check) + ├── Given: Caller is permissioned + ├── And: Role is not empty + │ └── When: setTokenGated is called + │ └── Then: The call reverts (modifier in position check) + ├── Given: Caller is permissioned + ├── And: Role is empty + ├── And: Role is Public Role + │ └── When: setTokenGated is called + │ └── Then: The call reverts (modifier in position check) + ├── Given: Caller is permissioned + ├── And: Role is empty + └── And: Role is not Public Role + └── When: setTokenGated is called + └── Then: The role becomes token gated + └── And: An event is emitted + */ + function testSetTokenGated_ModifierInPositionChecks() public { + // permissioned vm.expectRevert( abi.encodeWithSelector( - IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__RoleNotEmpty - .selector + IModule_v1.Module__CallerNotPermissioned.selector ) ); - _authorizer.makeRoleTokenGatedFromModule(ROLE_TOKEN); - assertFalse(_authorizer.isTokenGated(roleId)); + vm.prank(_bob); + _authSuT.setTokenGated(bytes32(uint(0)), true); - // we revoke the whitelist - _authorizer.revokeRoleFromModule(ROLE_TOKEN, CLOE); - - // now it works: - _authorizer.makeRoleTokenGatedFromModule(ROLE_TOKEN); - assertTrue(_authorizer.isTokenGated(roleId)); - } - // smae but with admin + //idExists(roleId_) + vm.expectRevert( + abi.encodeWithSelector( + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector + ) + ); + _authSuT.setTokenGated(bytes32(uint(2)), true); - function testSetTokenGatedFailsIfRoleAlreadyInUse() public { - // we set BOB as admin - makeAddressDefaultAdmin(BOB); + // onlyEmptyRole(roleId_) + // Create Role that is not empty + address[] memory members = new address[](1); + members[0] = _bob; bytes32 roleId = - _authorizer.generateRoleId(address(mockModule), ROLE_TOKEN); - - // we switch on self-management and whitelist an address - vm.prank(address(mockModule)); - _authorizer.grantRoleFromModule(ROLE_TOKEN, CLOE); - - vm.startPrank(BOB); - + _authSuT.createRole("Role", _authSuT.DEFAULT_ADMIN_ROLE(), members); vm.expectRevert( abi.encodeWithSelector( IAUT_TokenGated_Roles_v1 @@ -314,474 +377,573 @@ contract TokenGatedAUT_RoleV1Test is Test { .selector ) ); - _authorizer.setTokenGated(roleId, true); + _authSuT.setTokenGated(roleId, true); + // notPublicRole(roleId_) + roleId = _authSuT.PUBLIC_ROLE(); vm.expectRevert( abi.encodeWithSelector( IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__RoleNotEmpty + .Module__AUT_TokenGated_Roles__RoleIsPublic .selector ) ); - _authorizer.setTokenGated(roleId, false); - - // we revoke the whitelist - _authorizer.revokeRole(roleId, CLOE); - - // now it works: - _authorizer.setTokenGated(roleId, true); - assertTrue(_authorizer.isTokenGated(roleId)); - - _authorizer.setTokenGated(roleId, false); - assertFalse(_authorizer.isTokenGated(roleId)); + _authSuT.setTokenGated(roleId, true); } - // test interface enforcement when granting role - // -> yes case - function testCanAddTokenWhenTokenGated() public { - setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), 500 + function testSetTokenGated_Functionality() public { + // Create Role that is empty + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) ); - setUpNFTGatedRole(address(mockModule), ROLE_NFT, address(roleNft)); + + // Expect event + vm.expectEmit(true, true, true, true); + emit IAUT_TokenGated_Roles_v1.ChangedTokenGating(roleId, true); + + // Set token gated + _authSuT.setTokenGated(roleId, true); + assertTrue(_authSuT.isTokenGated(roleId)); } - // -> no case - /// forge-config: default.allow_internal_expect_revert = true - function testCannotAddNonTokenWhenTokenGated() public { - setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), 500 + /* + Test: setThreshold + ├── Given: Caller is not permissioned + │ └── When: setThreshold is called + │ └── Then: The call reverts (modifier in position check) + └── Given: Caller is permissioned + └── When: setThreshold is called + └── Then: The underlying function is called (Check via event) + */ + function testSetThreshold_ModifierInPositionChecks() public { + // permissioned + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) ); + vm.prank(_bob); + _authSuT.setThreshold(bytes32(uint(0)), address(0), 0); - vm.prank(address(mockModule)); - // First, the call to the interface reverts without reason - vm.expectRevert(); - // Then the contract handles the reversion and sends the correct error message + //idExists(roleId_) vm.expectRevert( abi.encodeWithSelector( - IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__InvalidToken - .selector, - CLOE + IAuthorizer_v1.Module__Authorizer__RoleIdNotExisting.selector ) ); - _authorizer.grantRoleFromModule(ROLE_TOKEN, CLOE); + _authSuT.setThreshold(bytes32(uint(2)), address(0), 0); } - /// forge-config: default.allow_internal_expect_revert = true - function testAdminCannotAddNonTokenWhenTokenGated() public { - // we set BOB as admin - makeAddressDefaultAdmin(BOB); + function testSetThreshold_Functionality() public { + // Create Role that is empty + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); - bytes32 roleId = setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), 500 + // Make it token gated + _authSuT.setTokenGated(roleId, true); + + // Expect event + vm.expectEmit(true, true, true, true); + emit IAUT_TokenGated_Roles_v1.ChangedTokenThreshold( + roleId, address(0), 1 ); - vm.prank(BOB); - // First, the call to the interface reverts without reason - vm.expectRevert(); - // Then the contract handles the reversion and sends the correct error message - vm.expectRevert( - abi.encodeWithSelector( - IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__InvalidToken - .selector, - CLOE - ) + // Set threshold + _authSuT.setThreshold(roleId, address(0), 1); + } + + /////////////////////////////////////////////////////////////////////////// + // Test Override Functions + + /* + Test: hasRole + ├── Given: Role is not token gated + ├── And: The given address does not have the role + │ └── When: hasRole is called + │ └── Then: hasRole works like base contract + ├── Given: Role is token gated + ├── And: The given address has the role + │ └── When: hasRole is called + │ └── Then: hasRole works like base contract + └── Given: Role is token gated + └── When: hasRole is called + └── Then: It should use the internal _hasTokenRole function + */ + function testHasRole_NotTokenGated_AddressDoesNotHaveRole(address who_) + public + { + // Check that address is not initial admin + vm.assume(who_ != address(this)); + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) ); - _authorizer.grantRole(roleId, CLOE); + assertFalse(_authSuT.hasRole(roleId, who_)); } - // Check setting the threshold - // yes case - function testSetThreshold() public { - bytes32 roleId = setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), 500 + function testHasRole_NotTokenGated_AddressHasRole(address who_) public { + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) ); - assertEq(_authorizer.getThresholdValue(roleId, address(roleToken)), 500); + // Add address to role + _authSuT.grantRole(roleId, who_); + assertTrue(_authSuT.hasRole(roleId, who_)); } - // invalid threshold from module + function testHasRole_TokenGated_CallsInternalFunction( + address who_, + bool hasTokenRole_ + ) public { + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); - function testSetThresholdFailsIfInvalid() public { - bytes32 role = ROLE_TOKEN; - vm.startPrank(address(mockModule)); - _authorizer.makeRoleTokenGatedFromModule(role); + // Make Role token gated + _authSuT.setTokenGated(roleId, true); - vm.expectRevert( - abi.encodeWithSelector( - IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__InvalidThreshold - .selector, - 0 - ) - ); - _authorizer.grantTokenRoleFromModule(role, address(roleToken), 0); + // Create Token Interface Mock + address token = address(new TokenInterfaceMock()); - vm.stopPrank(); - } - // invalid threshold from admin + // Set threshold + _authSuT.setThreshold(roleId, token, 1); - function testSetThresholdFromAdminFailsIfInvalid() public { - // we set BOB as admin - makeAddressDefaultAdmin(BOB); - // First we set up a valid role - bytes32 roleId = setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), 500 - ); + // Grant Role + _authSuT.grantRole(roleId, token); - // and we try to break it - vm.prank(BOB); - vm.expectRevert( - abi.encodeWithSelector( - IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__InvalidThreshold - .selector, - 0 - ) + if (hasTokenRole_) { + // Give the address some tokens + TokenInterfaceMock(token).setTokenBalance(who_, 1); + } + + // Check that the role is granted + assertEq(_authSuT.hasRole(roleId, who_), hasTokenRole_); + } + /* + Test: grantRole + ├── Given: Role is not token gated + │ └── When: grantRole is called + │ └── Then: Grant Role works like base contract + ├── Given: Role is token gated + ├── And: The given address has code size 0 + │ └── When: grantRole is called + │ └── Then: The function should revert + ├── Given: Role is token gated + ├── And: The given address has code size > 0 + ├── And: The Threshold is 0 for the given address + │ └── When: grantRole is called + │ └── Then: The function should revert + ├── Given: Role is token gated + ├── And: The given address has code size > 0 + ├── And: The Threshold is > 0 for the given address + ├── And: The given address does not implement the TokenInterface + │ └── When: grantRole is called + │ └── Then: The function should revert + ├── Given: Role is token gated + ├── And: the given address has code size > 0 + ├── And: The Threshold is > 0 for the given address + └── And: The given address implements the TokenInterface + └── When: grantRole is called + └── Then: Grant Role works like base contract + */ + + function test_grantRole_NotTokenGated(address who_) public { + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); + + // Grant Role + _authSuT.grantRole(roleId, who_); + + // Check that the role is granted + assertTrue( + _authSuT.exposed_AccessControlUpgradeable_hasRole(roleId, who_) ); - _authorizer.setThreshold(roleId, address(roleToken), 0); } - function testSetThresholdFailsIfNotTokenGated() public { - // we set BOB as admin - makeAddressDefaultAdmin(BOB); + function test_grantRole_CodeSizeZero(address who_) public { + uint32 size; + assembly { + size := extcodesize(who_) + } + vm.assume(size == 0); - vm.prank(address(mockModule)); - // We didn't make the role token-gated beforehand - vm.expectRevert( - abi.encodeWithSelector( - IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__RoleNotTokenGated - .selector - ) - ); - _authorizer.grantTokenRoleFromModule( - ROLE_TOKEN, address(roleToken), 500 + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) ); - // also fails for the admin - bytes32 roleId = - _authorizer.generateRoleId(address(mockModule), ROLE_TOKEN); + // Make Role token gated + _authSuT.setTokenGated(roleId, true); - vm.prank(BOB); + // Grant Role vm.expectRevert( abi.encodeWithSelector( IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__RoleNotTokenGated - .selector + .Module__AUT_TokenGated_Roles__InvalidToken + .selector, + address(who_) ) ); - _authorizer.setThreshold(roleId, address(roleToken), 500); + _authSuT.grantRole(roleId, who_); } - // Test setThresholdFromModule + function test_grantRole_TokenInterfaceThresholdZero() public { + // Create Mock Token Interface + TokenInterfaceMock tokenInterfaceMock = new TokenInterfaceMock(); - function testSetThresholdFromModule() public { - bytes32 roleId = setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), 500 + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) ); - vm.prank(address(mockModule)); - _authorizer.setThresholdFromModule(ROLE_TOKEN, address(roleToken), 1000); - assertEq( - _authorizer.getThresholdValue(roleId, address(roleToken)), 1000 - ); - } - - // invalid threshold from module - - function testSetThresholdFromModuleFailsIfInvalid() public { - bytes32 role = ROLE_TOKEN; - vm.startPrank(address(mockModule)); - _authorizer.makeRoleTokenGatedFromModule(role); - _authorizer.grantTokenRoleFromModule(role, address(roleToken), 100); + // Make Role token gated + _authSuT.setTokenGated(roleId, true); + // Grant Role vm.expectRevert( abi.encodeWithSelector( IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__InvalidThreshold + .Module__AUT_TokenGated_Roles__TokenRoleMustHaveThreshold .selector, - 0 + roleId, + address(tokenInterfaceMock) ) ); - _authorizer.setThresholdFromModule(ROLE_TOKEN, address(roleToken), 0); - vm.stopPrank(); + _authSuT.grantRole(roleId, address(tokenInterfaceMock)); } - function testSetThresholdFromModuleFailsIfNotTokenGated() public { - // we set BOB as admin - makeAddressDefaultAdmin(BOB); + function test_grantRole_TokenInterfaceNotImplemented() public { + // We pick a contract that definetly does not implement the interface + address who_ = address(_orchestrator); + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); - vm.prank(address(mockModule)); - // We didn't make the role token-gated beforehand + // Make Role token gated + _authSuT.setTokenGated(roleId, true); + + // Set threshold + _authSuT.setThreshold(roleId, who_, 1); + + // Grant Role vm.expectRevert( abi.encodeWithSelector( IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__RoleNotTokenGated - .selector + .Module__AUT_TokenGated_Roles__InvalidToken + .selector, + who_ ) ); - _authorizer.setThresholdFromModule(ROLE_TOKEN, address(roleToken), 500); + _authSuT.grantRole(roleId, who_); } - // Threshold state checks: - // Cannot grant role if threshold is set to zero + function test_grantRole_TokenInterfaceImplemented() public { + // Create Mock Token Interface + TokenInterfaceMock tokenInterfaceMock = new TokenInterfaceMock(); - function testGrantTokenRoleFailsIfThresholdWouldBeZero() public { - bytes32 role = ROLE_TOKEN; + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); - // Make the role token-gated, but don't set a token with grantRoleFromModule() - vm.prank(address(mockModule)); - _authorizer.makeRoleTokenGatedFromModule(role); + // Make Role token gated + _authSuT.setTokenGated(roleId, true); - bytes32 storedRoleId = - _authorizer.generateRoleId(address(mockModule), role); + // Set threshold + _authSuT.setThreshold(roleId, address(tokenInterfaceMock), 1); - // Now we make BOB admin of the role - makeAddressDefaultAdmin(BOB); + // Grant Role + _authSuT.grantRole(roleId, address(tokenInterfaceMock)); - vm.startPrank(BOB); - vm.expectRevert( - abi.encodeWithSelector( - IAUT_TokenGated_Roles_v1 - .Module__AUT_TokenGated_Roles__TokenRoleMustHaveThreshold - .selector, - storedRoleId, - address(roleToken) + // Check that the role is granted + assertTrue( + _authSuT.exposed_AccessControlUpgradeable_hasRole( + roleId, address(tokenInterfaceMock) ) ); - _authorizer.grantRole(storedRoleId, address(roleToken)); // BOB tries to circumvent setting a threshold - - vm.stopPrank(); } - // Threshold is zero after revoking role - - function testThresholdStateGetsDeletedOnRevoke() public { - bytes32 role = ROLE_TOKEN; - bytes32 moduleRoleId = - _authorizer.generateRoleId(address(mockModule), role); + /* + Test: _revokeRole + ├── Given: Role is not token gated + │ └── When: revokeRole is called + │ └── Then: Revoke Role works like base contract + └── Given: Role is token gated + └── When: revokeRole is called + └── Then: The Threshold is set to 0 + └── And: An event is emitted + └── And: Revoke Role works like base contract + */ + function test_revokeRole_NotTokenGated(address who_) public { + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); + // Add address to role + _authSuT.grantRole(roleId, who_); + + // Revoke Role + _authSuT.revokeRole(roleId, who_); + + // Check that the role is revoked + assertFalse( + _authSuT.exposed_AccessControlUpgradeable_hasRole(roleId, who_) + ); + } - assertEq( - _authorizer.getThresholdValue(moduleRoleId, address(roleToken)), 0 + function test_revokeRole_TokenGated() public { + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) ); - vm.startPrank(address(mockModule)); + // Make Role token gated + _authSuT.setTokenGated(roleId, true); - // Make the role token-gated with a threshold of 500 - _authorizer.makeRoleTokenGatedFromModule(role); - _authorizer.grantTokenRoleFromModule(role, address(roleToken), 500); + // Create Token Interface Mock + address who = address(new TokenInterfaceMock()); - assertEq( - _authorizer.getThresholdValue(moduleRoleId, address(roleToken)), 500 - ); - assertEq(true, _authorizer.hasRole(moduleRoleId, address(roleToken))); + // Set threshold + _authSuT.setThreshold(roleId, who, 1); - assertEq( - false, _authorizer.hasTokenRole(moduleRoleId, address(roleToken)) - ); - assertEq( - false, _authorizer.checkForRole(moduleRoleId, address(roleToken)) - ); + // Grant Role + _authSuT.grantRole(roleId, who); - _authorizer.revokeRoleFromModule(role, address(roleToken)); + // Expect event + vm.expectEmit(true, true, true, true); + emit IAUT_TokenGated_Roles_v1.ChangedTokenThreshold(roleId, who, 0); - assertEq( - _authorizer.getThresholdValue(moduleRoleId, address(roleToken)), 0 - ); + // Revoke Role + _authSuT.revokeRole(roleId, who); - assertEq(false, _authorizer.hasRole(moduleRoleId, address(roleToken))); + // Check that threshold is set to 0 + assertEq(_authSuT.getThresholdValue(roleId, who), 0); - // Grant the same role again, with different Threshold - _authorizer.grantTokenRoleFromModule(role, address(roleToken), 250); - - assertEq( - _authorizer.getThresholdValue(moduleRoleId, address(roleToken)), 250 + // Check that the role is revoked + assertFalse( + _authSuT.exposed_AccessControlUpgradeable_hasRole(roleId, who) ); - - vm.stopPrank(); } + /////////////////////////////////////////////////////////////////////////// + // Test Internal Functions + + // ------------------------------------------------------------------------ + // Internal - Upstream Function Implementations + + /* + Test: _setThreshold + ├── Given: The given roleId is not token gated + │ └── When: _setThreshold is called + │ └── Then: The call reverts (modifier in position check) + ├── Given: The given roleId is token gated + ├── And: the given threshold is invalid + │ └── When: _setThreshold is called + │ └── Then: The call reverts (modifier in position check) + ├── Given: The given roleId is token gated + └── And: the given threshold is valid + └── When: _setThreshold is called + └── Then: the threshold map is updated + └── And: A event is emitted + */ + function test_setThreshold_ModifierInPositionChecks() public { + // onlyTokenGated(roleId_) + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); - // Test Authorization - - // Test token authorization - // -> yes case - function testFuzzTokenAuthorization( - uint threshold, - address[] calldata callers, - uint[] calldata amounts - ) public { - vm.assume(callers.length <= amounts.length); - vm.assume(threshold != 0); + vm.expectRevert( + abi.encodeWithSelector( + IAUT_TokenGated_Roles_v1 + .Module__AUT_TokenGated_Roles__RoleNotTokenGated + .selector + ) + ); + _authSuT.exposed_setThreshold(roleId, address(0), 0); - // This implcitly confirms ERC20 compatibility + // validThreshold(threshold_) - // We burn the tokens created on setup - roleToken.burn(BOB, 1000); - roleToken.burn(CLOE, 10); + // Make role token gated + _authSuT.setTokenGated(roleId, true); - bytes32 roleId = setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), threshold + vm.expectRevert( + abi.encodeWithSelector( + IAUT_TokenGated_Roles_v1 + .Module__AUT_TokenGated_Roles__InvalidThreshold + .selector, + 0 + ) ); + _authSuT.exposed_setThreshold(roleId, address(0), 0); + } - for (uint i = 0; i < callers.length; i++) { - if (callers[i] == address(0)) { - // cannot mint to 0 address - continue; - } + function test_setThreshold_Functionality(address token_, uint threshold_) + public + { + // Make sure threshold_ is not zero + vm.assume(threshold_ != 0); + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); + // Make it TokenGated + _authSuT.setTokenGated(roleId, true); - roleToken.mint(callers[i], amounts[i]); + vm.expectEmit(true, true, true, true); + emit IAUT_TokenGated_Roles_v1.ChangedTokenThreshold( + roleId, token_, threshold_ + ); + // Set Threshold + _authSuT.setThreshold(roleId, token_, threshold_); - // we ensure both ways to check give the same result - vm.prank(address(mockModule)); - bool result = _authorizer.checkForRole(roleId, callers[i]); - assertEq(result, _authorizer.hasTokenRole(roleId, callers[i])); + assertEq(_authSuT.getThresholdValue(roleId, token_), threshold_); + } - // we verify the result ir correct - if (amounts[i] >= threshold) { - assertTrue(result); - } else { - assertFalse(result); + /* + Test: _hasTokenRole + Invariant: Role can only contain TokenInterface addresses + ├── Given: Role contains TokenInterface mock addresses + ├── And: The token amount is less than the threshold + │ └── When: _hasTokenRole is called + │ └── Then: It should return false + ├── Given: Role contains TokenInterface mock addresses + └── And: The token amount of at least one of them is equal or higher than the threshold + └── When: _hasTokenRole is called + └── Then: It should return true + */ + function test_hasTokenRole_TokenInterfacesWrongThreshold( + uint[] memory thresholdAmounts_, + address who_ + ) public { + // Assume realistic number of token interface Mocks + vm.assume(thresholdAmounts_.length < 50); + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); + // Set token gated + _authSuT.setTokenGated(roleId, true); + + // Create token interface mocks + address[] memory tokenInterfaceMocks = + new address[](thresholdAmounts_.length); + + for (uint i; i < thresholdAmounts_.length; ++i) { + // Should the threshold be zero then set it to 1 + if (thresholdAmounts_[i] == 0) { + thresholdAmounts_[i] = 1; } - - // we burn the minted tokens to avoid overflows - roleToken.burn(callers[i], amounts[i]); + tokenInterfaceMocks[i] = + _createTokenInterfaceMock_SetThresholdAmount_AddToMembers( + roleId, thresholdAmounts_[i] + ); } + + // Should return false as target has no tokens in any of the mocks + assertFalse(_authSuT.exposed_hasTokenRole(roleId, who_)); } - // Test NFT authorization - // -> yes case - // -> no case - function testFuzzNFTAuthorization( - address[] calldata callers, - bool[] calldata hasNFT + function test_hasTokenRole_WhoHasTokens( + uint seed_, + uint[] memory thresholdAmounts_, + uint[] memory tokenAmounts_, + address who_ ) public { - vm.assume(callers.length < 50); - vm.assume(callers.length <= hasNFT.length); - - // This is similar to the function above, but in this case we just do a yes/no check - // This implcitly confirms ERC721 compatibility + // Assume realistic number of token interface Mocks + vm.assume(thresholdAmounts_.length > 0); - // We burn the token created on setup - roleNft.burn(roleNft.idCounter() - 1); + // Cap the array to 50 elements + if (thresholdAmounts_.length > 50) { + uint[] memory cappedArr = new uint[](50); // Create new memory array - bytes32 roleId = - setUpNFTGatedRole(address(mockModule), ROLE_NFT, address(roleNft)); - - for (uint i = 0; i < callers.length; i++) { - if (callers[i] == address(0)) { - // cannot mint to 0 address - continue; + for (uint i = 0; i < 50; i++) { + cappedArr[i] = thresholdAmounts_[i]; // Copy elements into new array } - if (hasNFT[i]) { - roleNft.mint(callers[i]); - } - - // we ensure both ways to check give the same result - vm.prank(address(mockModule)); - bool result = _authorizer.checkForRole(roleId, callers[i]); - assertEq(result, _authorizer.hasTokenRole(roleId, callers[i])); - // we verify the result ir correct - if (hasNFT[i]) { - assertTrue(result); - } else { - assertFalse(result); - } - - // If we minted a token we burn it to guarantee a clean slate in case of address repetition - if (hasNFT[i]) { - roleNft.burn(roleNft.idCounter() - 1); - } + thresholdAmounts_ = cappedArr; } - } - - function testFuzzTokenAuthorizationAndRevoke( - uint threshold, - address[] calldata callers, - uint[] calldata amounts - ) public { - vm.assume(callers.length <= amounts.length); - vm.assume(threshold != 0); - - // This implcitly confirms ERC20 compatibility - // We burn the tokens created on setup - roleToken.burn(BOB, 1000); - roleToken.burn(CLOE, 10); - - bytes32 roleId = setUpTokenGatedRole( - address(mockModule), ROLE_TOKEN, address(roleToken), threshold + vm.assume(tokenAmounts_.length <= thresholdAmounts_.length); + // Create Role + bytes32 roleId = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) ); + // Set token gated + _authSuT.setTokenGated(roleId, true); - assertEq(true, _authorizer.hasRole(roleId, address(roleToken))); // The token has been added to the core authorizer mapping - - assertEq(false, _authorizer.checkForRole(roleId, address(roleToken))); // The token itself does not have the role - assertEq( - _authorizer.checkForRole(roleId, address(roleToken)), - _authorizer.hasTokenRole(roleId, address(roleToken)) - ); // We ensure both ways to check give the same result - - for (uint i = 0; i < callers.length; i++) { - if (callers[i] == address(0)) { - // cannot mint to 0 address - continue; - } - - roleToken.mint(callers[i], amounts[i]); + // Create token interface mocks + address[] memory tokenInterfaceMocks = + new address[](thresholdAmounts_.length); - // we ensure both ways to check give the same result - vm.prank(address(mockModule)); - bool result = _authorizer.checkForRole(roleId, callers[i]); - assertEq(result, _authorizer.hasTokenRole(roleId, callers[i])); - - // we verify the result is correct - if (amounts[i] >= threshold) { - assertTrue(result); - } else { - assertFalse(result); + for (uint i; i < thresholdAmounts_.length; ++i) { + // Should the threshold be zero then set it to 1 + if (thresholdAmounts_[i] == 0) { + thresholdAmounts_[i] = 1; } - - // we burn the minted tokens to avoid overflows - roleToken.burn(callers[i], amounts[i]); + tokenInterfaceMocks[i] = + _createTokenInterfaceMock_SetThresholdAmount_AddToMembers( + roleId, thresholdAmounts_[i] + ); } - // Now we revoke the token from the role - vm.startPrank(address(mockModule)); - vm.expectEmit(); - emit ChangedTokenThreshold(roleId, address(roleToken), 0); - emit RoleRevoked(roleId, address(roleToken), address(mockModule)); - - _authorizer.revokeRoleFromModule(ROLE_TOKEN, address(roleToken)); - vm.stopPrank(); - - assertEq(false, _authorizer.hasRole(roleId, address(roleToken))); // The token has been revoked from the core authorizer mapping + // Give the target some tokens + for (uint i; i < tokenAmounts_.length; ++i) { + TokenInterfaceMock(tokenInterfaceMocks[i]).setTokenBalance( + who_, tokenAmounts_[i] + ); + } - assertEq(false, _authorizer.checkForRole(roleId, address(roleToken))); // The token itsef still does not have the role - assertEq( - _authorizer.checkForRole(roleId, address(roleToken)), - _authorizer.hasTokenRole(roleId, address(roleToken)) - ); // We ensure both ways to check give the same result + // Make sure that the target has at least more or equal tokens in one of the mocks + // Pick a random token interface mock + address randomTokenInterfaceAddress = + tokenInterfaceMocks[seed_ % thresholdAmounts_.length]; + // Pick the respective threshold amount + uint thresholdAmountOfTokenInterfaceMock = + thresholdAmounts_[seed_ % thresholdAmounts_.length]; - for (uint i = 0; i < callers.length; i++) { - if (callers[i] == address(0)) { - // cannot mint to 0 address - continue; - } + // Set the balance of the target to the threshold amount of the token interface mock + TokenInterfaceMock(randomTokenInterfaceAddress).setTokenBalance( + who_, thresholdAmountOfTokenInterfaceMock + ); - roleToken.mint(callers[i], amounts[i]); + // Should return true as target has thethreshold in at least one contract + assertTrue(_authSuT.exposed_hasTokenRole(roleId, who_)); + } - // we ensure both ways to check give the same result - vm.prank(address(mockModule)); - bool result = _authorizer.checkForRole(ROLE_TOKEN, callers[i]); - assertEq(result, _authorizer.hasTokenRole(roleId, callers[i])); + /////////////////////////////////////////////////////////////////////////// + // Helper Functions - // we verify the user is not authorized - assertFalse(result); + /// @notice Creates a role with token gated set to true + /// @return roleId_ The id of the created role + function _createTokenGatedRole() internal returns (bytes32 roleId_) { + // Create Role + roleId_ = _authSuT.createRole( + "Role", _authSuT.DEFAULT_ADMIN_ROLE(), new address[](0) + ); + // Set token gated + _authSuT.setTokenGated(roleId_, true); + } - // we burn the minted tokens to avoid overflows - roleToken.burn(callers[i], amounts[i]); - } + /// @notice Creates a token interface mock, sets the threshold and adds it to the role + /// @param roleId_ The id of the role to add the token interface mock to + /// @param thresholdAmount_ The threshold amount to set + /// @return tokenInterfaceMock_ The address of the token interface mock + function _createTokenInterfaceMock_SetThresholdAmount_AddToMembers( + bytes32 roleId_, + uint thresholdAmount_ + ) internal returns (address tokenInterfaceMock_) { + // Create token interface mock + tokenInterfaceMock_ = address(new TokenInterfaceMock()); + + // Set threshold + _authSuT.setThreshold(roleId_, tokenInterfaceMock_, thresholdAmount_); + + // Add token interface mock to role + _authSuT.grantRole(roleId_, tokenInterfaceMock_); } } diff --git a/test/unit/modules/base/Module_v1.t.sol b/test/unit/modules/base/Module_v1.t.sol index 99cead1bc..5fc653741 100644 --- a/test/unit/modules/base/Module_v1.t.sol +++ b/test/unit/modules/base/Module_v1.t.sol @@ -40,20 +40,16 @@ import {ERC20Mock} from "@mocks/external/token/ERC20Mock.sol"; import {OZErrors} from "@testUtilities/OZErrors.sol"; contract ModuleBaseV1Test is ModuleTest { + /////////////////////////////////////////////////////////////////////////// + // State + // SuT ModuleV1Mock module; bytes _CONFIGDATA = bytes(""); - //-------------------------------------------------------------------------- - // Events - - /// @notice Module has been initialized. - /// @param parentOrchestrator The address of the orchestrator the module is linked to. - /// @param metadata The metadata of the module. - event ModuleInitialized( - address indexed parentOrchestrator, IModule_v1.Metadata metadata - ); + /////////////////////////////////////////////////////////////////////////// + // Setup function setUp() public { address impl = address(new ModuleV1Mock()); @@ -62,15 +58,21 @@ contract ModuleBaseV1Test is ModuleTest { _setUpOrchestrator(module); vm.expectEmit(true, true, true, false); - emit ModuleInitialized(address(_orchestrator), _METADATA); + emit IModule_v1.ModuleInitialized(address(_orchestrator), _METADATA); module.init(_orchestrator, _METADATA, _CONFIGDATA); } - //-------------------------------------------------------------------------- - // Tests: Initialization + /////////////////////////////////////////////////////////////////////////// + // Test Initialization - function testSupportsInterface() public { + /* + Test: SupportsInterface + └── Given: The interfaceId is IModule_v1 + └── When: the function supportsInterface is called + └── Then: the function should return true + */ + function testSupportsInterface() public override(ModuleTest) { assertTrue(module.supportsInterface(type(IModule_v1).interfaceId)); } @@ -143,147 +145,127 @@ contract ModuleBaseV1Test is ModuleTest { ); } - //-------------------------------------------------------------------------- - // Role Functions - - function testGrantModuleRole(bytes32 role, address addr) public { - vm.assume(addr != address(0)); - - vm.startPrank(address(this)); - - module.grantModuleRole(role, addr); - - bytes32 roleId = _authorizer.generateRoleId(address(module), role); - bool isAuthorized = _authorizer.checkRoleMembership(roleId, addr); - assertTrue(isAuthorized); - - vm.stopPrank(); - } + ///////////////////////////////////////////////////////////////////////////// + // Test Modifier - function testGrantModuleRoleBatched(bytes32 role, address[] memory addrs) + /* + Test: permissioned + ├── Given: modifierPermissionedCheck is executed via call with a valid selector, but random data + ├── And: The call sender is randomised + └── And: The Caller is permissioned to call the function + └── When: The function modifierPermissionedCheck is called + └── Then: the function should not revert, because the sender and only the function selector were correctly passed + */ + function testPermissioned_modifier(address caller_, bytes memory data_) public { - vm.startPrank(address(this)); + // Assume that the calldata is at least 4 bytes long + vm.assume(data_.length >= 4); - for (uint i = 0; i < addrs.length; i++) { - vm.assume(addrs[i] != address(0)); - } + bytes4 targetSelector = ModuleV1Mock.modifierPermissionedCheck.selector; - module.grantModuleRoleBatched(role, addrs); + // Proof + _authorizer.setHasPermission( + caller_, address(module), targetSelector, true + ); - for (uint i = 0; i < addrs.length; i++) { - bytes32 roleId = _authorizer.generateRoleId(address(module), role); - bool isAuthorized = - _authorizer.checkRoleMembership(roleId, addrs[i]); - assertTrue(isAuthorized); + // Replace the msg.data function selector with the correct one + for (uint i = 0; i < 4; i++) { + data_[i] = targetSelector[i]; } - vm.stopPrank(); + // Expect no revert + vm.prank(caller_); + address(module).call(data_); } - function testRevokeModuleRole(bytes32 role, address addr) public { - vm.assume(addr != address(0)); - - vm.startPrank(address(this)); - - module.grantModuleRole(role, addr); - - bytes32 roleId = _authorizer.generateRoleId(address(module), role); - bool isAuthorizedBefore = _authorizer.checkRoleMembership(roleId, addr); - assertTrue(isAuthorizedBefore); - - module.revokeModuleRole(role, addr); - - bool isAuthorizedAfter = _authorizer.checkRoleMembership(roleId, addr); - assertFalse(isAuthorizedAfter); + /* + Test modifier onlyPaymentClient + ├── given the caller is not a PaymentClient + │ └── when the function modifierOnlyPaymentClientCheck() gets called + │ └── then it should revert + └── given the caller is a PaymentClient module + └── and the PaymentClient module is not registered in the Orchestrator + └── when the function modifierOnlyPaymentClientCheck() gets called + └── then it should revert + */ - vm.stopPrank(); + function testOnlyPaymentClientModifier_worksGivenCallerIsNotPaymentClient( + address _notPaymentClient + ) public { + vm.prank(address(_notPaymentClient)); + vm.expectRevert(IModule_v1.Module__OnlyCallableByPaymentClient.selector); + module.modifierOnlyPaymentClientCheck(); } - function testRevokeModuleRoleBatched(bytes32 role, address[] memory addrs) - public - { - vm.startPrank(address(this)); - - for (uint i = 0; i < addrs.length; i++) { - vm.assume(addrs[i] != address(0)); - } - - module.grantModuleRoleBatched(role, addrs); - - bytes32 roleId = _authorizer.generateRoleId(address(module), role); - - for (uint i = 0; i < addrs.length; i++) { - bool isAuthorizedBefore = - _authorizer.checkRoleMembership(roleId, addrs[i]); - assertTrue(isAuthorizedBefore); - } - - module.revokeModuleRoleBatched(role, addrs); - - for (uint i = 0; i < addrs.length; i++) { - bool isAuthorizedAfter = - _authorizer.checkRoleMembership(roleId, addrs[i]); - assertFalse(isAuthorizedAfter); - } + function testOnlyPaymentClientModifier_worksGivenCallerIsPaymentClientButNotRegisteredModule( + ) public { + ERC20PaymentClientBaseV2Mock _erc20PaymentClientMock = + new ERC20PaymentClientBaseV2Mock(); - vm.stopPrank(); + vm.prank(address(_erc20PaymentClientMock)); + vm.expectRevert(IModule_v1.Module__OnlyCallableByPaymentClient.selector); + module.modifierOnlyPaymentClientCheck(); } - //-------------------------------------------------------------------------- - // FeeManager - - function testGetFeeManagerCollateralFeeData(bytes4 functionSelector) - public - { - uint setFee = 100; - address treasury = makeAddr("customTreasury"); - - // Set treasury - feeManager.setWorkflowTreasury(address(_orchestrator), treasury); - - // set fee - feeManager.setCollateralWorkflowFee( - address(_orchestrator), - address(module), - functionSelector, - true, - setFee - ); - - (uint returnFee, address returnTreasury) = - module.original_getFeeManagerCollateralFeeData(functionSelector); + /* + Test: validAddress + └── Given: The address is either the zero address or the module address + └── When: validAddress is called + └── Then: The function should revert + */ - assertEq(returnFee, setFee); - assertEq(returnTreasury, treasury); + function testValidAddress(address adr) public { + if (adr == address(0) || adr == address(module)) { + vm.expectRevert(IModule_v1.Module__InvalidAddress.selector); + } + module.modifierOnlyValidAddressCheck(adr); } - function testGetFeeManagerIssuanceFeeData(bytes4 functionSelector) public { - uint setFee = 100; - address treasury = makeAddr("customTreasury"); + /////////////////////////////////////////////////////////////////////////// + // Test External Functions - // Set treasury - feeManager.setWorkflowTreasury(address(_orchestrator), treasury); - - // set fee - feeManager.setIssuanceWorkflowFee( - address(_orchestrator), - address(module), - functionSelector, - true, - setFee - ); + // ======================================================================== + // Public Getter Functions - (uint returnFee, address returnTreasury) = - module.original_getFeeManagerIssuanceFeeData(functionSelector); + // ------------------------------------------------------------------------ + // Getter - Module State - assertEq(returnFee, setFee); - assertEq(returnTreasury, treasury); - } + /* + Test: identifier + └── When: the function identifier is called + └── Then: the function should return the identifier + */ + // Trivial + /* + Test: version + └── When: the function version is called + └── Then: the function should return the version + */ + // Trivial + /* + Test: url + └── When: the function url is called + └── Then: the function should return the url + */ + // Trivial + /* + Test: title + └── When: the function title is called + └── Then: the function should return the title + */ + // Trivial + /* + Test: orchestrator + └── When: the function orchestrator is called + └── Then: the function should return the orchestrator + */ + // Trivial //-------------------------------------------------------------------------- - // ERC2771 + // Getter - ERC2771 Context Upgradeable Overrides + //@todo This test is weird and probably needs to be moved to a different test file function test_msgSender(address signer, address sender, bool fromForwarder) public { @@ -321,6 +303,7 @@ contract ModuleBaseV1Test is ModuleTest { } } + //@todo This test is weird and probably needs to be moved to a different test file function test_msgData(address signer, address sender, bool fromForwarder) public { @@ -358,41 +341,88 @@ contract ModuleBaseV1Test is ModuleTest { } } - //-------------------------------------------------------------------------- - // Modifier + // ======================================================================== + // Internal Functions - /* Test modifier onlyPaymentClient - ├── given the caller is not a PaymentClient - │ └── when the function modifierOnlyPaymentClientCheck() gets called - │ └── then it should revert - └── given the caller is a PaymentClient module - └── and the PaymentClient module is not registered in the Orchestrator - └── when the function modifierOnlyPaymentClientCheck() gets called - └── then it should revert - */ + // ------------------------------------------------------------------------ + // Internal - Fees - function testOnlyPaymentClientModifier_worksGivenCallerIsNotPaymentClient( - address _notPaymentClient - ) public { - vm.prank(address(_notPaymentClient)); - vm.expectRevert(IModule_v1.Module__OnlyCallableByPaymentClient.selector); - module.modifierOnlyPaymentClientCheck(); + function testGetFeeManagerCollateralFeeData(bytes4 functionSelector) + public + { + uint setFee = 100; + address treasury = makeAddr("customTreasury"); + + // Set treasury + feeManager.setWorkflowTreasury(address(_orchestrator), treasury); + + // set fee + feeManager.setCollateralWorkflowFee( + address(_orchestrator), + address(module), + functionSelector, + true, + setFee + ); + + (uint returnFee, address returnTreasury) = + module._getFeeManagerCollateralFeeData_exposed(functionSelector); + + assertEq(returnFee, setFee); + assertEq(returnTreasury, treasury); } - function testOnlyPaymentClientModifier_worksGivenCallerIsPaymentClientButNotRegisteredModule( - ) public { - ERC20PaymentClientBaseV2Mock _erc20PaymentClientMock = - new ERC20PaymentClientBaseV2Mock(); + function testGetFeeManagerIssuanceFeeData(bytes4 functionSelector) public { + uint setFee = 100; + address treasury = makeAddr("customTreasury"); - vm.prank(address(_erc20PaymentClientMock)); - vm.expectRevert(IModule_v1.Module__OnlyCallableByPaymentClient.selector); - module.modifierOnlyPaymentClientCheck(); + // Set treasury + feeManager.setWorkflowTreasury(address(_orchestrator), treasury); + + // set fee + feeManager.setIssuanceWorkflowFee( + address(_orchestrator), + address(module), + functionSelector, + true, + setFee + ); + + (uint returnFee, address returnTreasury) = + module._getFeeManagerIssuanceFeeData_exposed(functionSelector); + + assertEq(returnFee, setFee); + assertEq(returnTreasury, treasury); } - function testValidAddress(address adr) public { - if (adr == address(0) || adr == address(module)) { - vm.expectRevert(IModule_v1.Module__InvalidAddress.selector); + // ------------------------------------------------------------------------ + // Internal - Authorization + + /* + Test: _checkAuthorization_ + └── Given: Authorizer hasPermission() is mocked + ├── When: _checkAuthorization_ is called + └── And: Authorizer hasPermission() returns false + ├── Then: It should forward the function selector properly + └── And: The function should revert + */ + function test_checkAuthorization_hasPermissionMocked( + bool hasPermission_, + address caller_, + bytes calldata data_ + ) public { + vm.assume(data_.length >= 4); + // Assume that caller is not the module as it is the default admin + vm.assume(caller_ != address(this)); + + _authorizer.setHasPermission( + caller_, address(module), bytes4(data_[0:4]), hasPermission_ + ); + + if (!hasPermission_) { + vm.expectRevert(IModule_v1.Module__CallerNotPermissioned.selector); } - module.modifierOnlyValidAddressCheck(adr); + + module._checkAuthorization_exposed(caller_, data_); } } diff --git a/test/unit/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.t.sol b/test/unit/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.t.sol index 6c53fa11c..46934051e 100644 --- a/test/unit/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.t.sol +++ b/test/unit/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.t.sol @@ -15,6 +15,8 @@ import {Clones} from "@oz/proxy/Clones.sol"; import {IERC165} from "@oz/utils/introspection/IERC165.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; // Internal Dependencies @@ -77,39 +79,6 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { address admin_address = address(0xA1BA); address non_admin_address = address(0xB0B); - event Transfer(address indexed from, address indexed to, uint value); - - event TokensBought( - address indexed receiver, - uint depositAmount, - uint receivedAmount, - address buyer - ); - event VirtualCollateralAmountAdded(uint amountAdded, uint newSupply); - event VirtualCollateralAmountSubtracted( - uint amountSubtracted, uint newSupply - ); - event VirtualIssuanceAmountSubtracted( - uint amountSubtracted, uint newSupply - ); - event VirtualIssuanceAmountAdded(uint amountAdded, uint newSupply); - event TokensSold( - address indexed receiver, - uint depositAmount, - uint receivedAmount, - address seller - ); - event BuyReserveRatioSet( - uint32 newBuyReserveRatio, uint32 oldBuyReserveRatio - ); - event SellReserveRatioSet( - uint32 newSellReserveRatio, uint32 oldSellReserveRatio - ); - event VirtualIssuanceSupplySet(uint newSupply, uint oldSupply); - event VirtualCollateralSupplySet(uint newSupply, uint oldSupply); - event TransferOrchestratorToken(address indexed to, uint amount); - event OrchestratorTokenSet(address indexed token, uint8 decimals); - function setUp() public virtual { // Deploy contracts issuanceToken = new ERC20Issuance_v1(NAME, SYMBOL, DECIMALS, MAX_SUPPLY); @@ -138,10 +107,11 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { _setUpOrchestrator(bondingCurveFundingManager); - _authorizer.grantRole(_authorizer.getAdminRole(), admin_address); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); vm.expectEmit(true, true, true, true); - emit OrchestratorTokenSet(address(_token), DECIMALS); + emit IFundingManager_v1.OrchestratorTokenSet(address(_token), DECIMALS); // Init Module bondingCurveFundingManager.init( @@ -158,7 +128,7 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { issuanceToken.setMinter(address(bondingCurveFundingManager), true); } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( bondingCurveFundingManager.supportsInterface( type(IFM_BC_Bancor_Redeeming_VirtualSupply_v1).interfaceId @@ -249,6 +219,104 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { //-------------------------------------------------------------------------- // Public Functions + /* + Test: buyFor Modifier Checks + ├── Given: buyer is not permissioned + │ └── When: buyFor is called + │ └── Then: it should revert (modifier in position check) + ├── Given: buyer is permissioned + ├── And: buying is not enabled + │ └── When: buyFor is called + │ └── Then: it should revert (modifier in position check) + ├── Given: buyer is permissioned + ├── And: buying is enabled + └── And: receiver is invalid + └── When: buyFor is called + └── Then: it should revert (modifier in position check) + */ + + function testBuyFor_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.buyFor(address(0), 0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeBuy(); + + vm.expectRevert( + IBondingCurveBase_v1 + .Module__BondingCurveBase__BuyingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.buyFor(address(0), 0, 0); + + // Open up Buy again + bondingCurveFundingManager.openBuy(); + + // validReceiver + vm.expectRevert( + abi.encodeWithSelector( + IBondingCurveBase_v1 + .Module__BondingCurveBase__InvalidRecipient + .selector + ) + ); + bondingCurveFundingManager.buyFor(address(0), 0, 0); + } + + /* + Test: buy Modifier Checks + ├── Given: buyer is not permissioned + │ └── When: buy is called + │ └── Then: it should revert (modifier in position check) + ├── Given: buyer is permissioned + ├── And: buying is not enabled + └── When: buy is called + └── Then: it should revert (modifier in position check) + */ + + function testBuy_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.buy(0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeBuy(); + + vm.expectRevert( + IBondingCurveBase_v1 + .Module__BondingCurveBase__BuyingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.buy(0, 0); + } + /* Test buy and _virtualSupplyBuyOrder function ├── when the deposit amount is 0 │ └── it should revert @@ -387,23 +455,25 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { // Execution vm.prank(buyer); vm.expectEmit(true, true, true, true, address(_token)); - emit Transfer(buyer, address(bondingCurveFundingManager), amount); + emit IERC20.Transfer(buyer, address(bondingCurveFundingManager), amount); vm.expectEmit(true, true, true, true, address(issuanceToken)); - emit Transfer(address(0), buyer, formulaReturn); + emit IERC20.Transfer(address(0), buyer, formulaReturn); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit TokensBought(buyer, amount, formulaReturn, buyer); + emit IBondingCurveBase_v1.TokensBought( + buyer, amount, formulaReturn, buyer + ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualIssuanceAmountAdded( + emit IVirtualIssuanceSupplyBase_v1.VirtualIssuanceAmountAdded( formulaReturn, (INITIAL_ISSUANCE_SUPPLY + formulaReturn) ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualCollateralAmountAdded( + emit IVirtualCollateralSupplyBase_v1.VirtualCollateralAmountAdded( amount, (INITIAL_COLLATERAL_SUPPLY + amount) ); bondingCurveFundingManager.buy(amount, formulaReturn); @@ -465,23 +535,25 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { // Execution vm.prank(buyer); vm.expectEmit(true, true, true, true, address(_token)); - emit Transfer(buyer, address(bondingCurveFundingManager), amount); + emit IERC20.Transfer(buyer, address(bondingCurveFundingManager), amount); vm.expectEmit(true, true, true, true, address(issuanceToken)); - emit Transfer(address(0), buyer, formulaReturn); + emit IERC20.Transfer(address(0), buyer, formulaReturn); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit TokensBought(buyer, amount, formulaReturn, buyer); + emit IBondingCurveBase_v1.TokensBought( + buyer, amount, formulaReturn, buyer + ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualIssuanceAmountAdded( + emit IVirtualIssuanceSupplyBase_v1.VirtualIssuanceAmountAdded( formulaReturn, (INITIAL_ISSUANCE_SUPPLY + formulaReturn) ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualCollateralAmountAdded( + emit IVirtualCollateralSupplyBase_v1.VirtualCollateralAmountAdded( buyAmountMinusFee, (INITIAL_COLLATERAL_SUPPLY + buyAmountMinusFee) ); bondingCurveFundingManager.buy(amount, formulaReturn); @@ -558,6 +630,105 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { assertEq(issuanceToken.balanceOf(to), formulaReturn); } + /* + Test: sellTo Modifier Checks + ├── Given: seller is not permissioned + │ └── When: sellTo is called + │ └── Then: it should revert (modifier in position check) + ├── Given: seller is permissioned + ├── And: buysellinging is not enabled + │ └── When: sellTo is called + │ └── Then: it should revert (modifier in position check) + ├── Given: seller is permissioned + ├── And: selling is enabled + └── And: receiver is invalid + └── When: sellTo is called + └── Then: it should revert (modifier in position check) + */ + + function testsellTo_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.sellTo(address(0), 0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeSell(); + + vm.expectRevert( + IRedeemingBondingCurveBase_v1 + .Module__RedeemingBondingCurveBase__SellingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.sellTo(address(0), 0, 0); + + // Open up Buy again + bondingCurveFundingManager.openSell(); + + // validReceiver + vm.expectRevert( + abi.encodeWithSelector( + IBondingCurveBase_v1 + .Module__BondingCurveBase__InvalidRecipient + .selector + ) + ); + bondingCurveFundingManager.sellTo(address(0), 0, 0); + } + + /* + Test: sell Modifier Checks + ├── Given: seller is not permissioned + │ └── When: sell is called + │ └── Then: it should revert (modifier in position check) + ├── Given: seller is permissioned + └── And: buysellinging is not enabled + └── When: sell is called + └── Then: it should revert (modifier in position check) + + */ + + function testsell_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.sell(0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeSell(); + + vm.expectRevert( + IRedeemingBondingCurveBase_v1 + .Module__RedeemingBondingCurveBase__SellingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.sell(0, 0); + } + /* Test sell and _virtualSupplySellOrder function ├── when the sell amount is 0 │ └── it should revert @@ -728,7 +899,7 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.startPrank(seller); { vm.expectEmit(true, true, true, true, address(_token)); - emit Transfer( + emit IERC20.Transfer( address(bondingCurveFundingManager), address(seller), normalized_formulaReturn @@ -736,19 +907,20 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit TokensSold( + emit IRedeemingBondingCurveBase_v1.TokensSold( seller, userSellAmount, normalized_formulaReturn, seller ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualIssuanceAmountSubtracted( + emit IVirtualIssuanceSupplyBase_v1.VirtualIssuanceAmountSubtracted( userSellAmount, newVirtualIssuanceSupply - userSellAmount ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualCollateralAmountSubtracted( + emit IVirtualCollateralSupplyBase_v1 + .VirtualCollateralAmountSubtracted( normalized_formulaReturn, newVirtualCollateral - normalized_formulaReturn ); @@ -837,7 +1009,7 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.startPrank(seller); { vm.expectEmit(true, true, true, true, address(_token)); - emit Transfer( + emit IERC20.Transfer( address(bondingCurveFundingManager), address(seller), sellAmountMinusFee @@ -845,17 +1017,20 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit TokensSold(seller, userSellAmount, sellAmountMinusFee, seller); + emit IRedeemingBondingCurveBase_v1.TokensSold( + seller, userSellAmount, sellAmountMinusFee, seller + ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualIssuanceAmountSubtracted( + emit IVirtualIssuanceSupplyBase_v1.VirtualIssuanceAmountSubtracted( userSellAmount, newVirtualIssuanceSupply - userSellAmount ); vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit VirtualCollateralAmountSubtracted( + emit IVirtualCollateralSupplyBase_v1 + .VirtualCollateralAmountSubtracted( normalized_formulaReturn, newVirtualCollateral - normalized_formulaReturn ); @@ -1172,10 +1347,10 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { // OnlyOrchestrator Functions /* Test setVirtualIssuanceSupply and _setVirtualIssuanceSupply function - ├── given caller is not the Orchestrator_v1 admin + ├── given caller is not permissioned │ └── when the function setVirtualIssuanceSupply() is called │ └── then it should revert (test modifier is in place. Modifier test itself is tested in base Module tests) - └── given the caller is the Orchestrator_v1 admin + └── given the caller is permissioned ├── and the buy | sell curve are still open (modifier test) │ └── when the function_setVirtualIssuanceSupply() is called │ └── then it should revert @@ -1191,19 +1366,20 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { └── and it should emit an event */ - function testSetVirtualIssuanceSupply_WorksGivenOnlyOrchestratorAdminModifierInPlace( - uint _newSupply - ) public { - vm.assume(_newSupply != 0); + function testSetVirtualIssuanceSupply_PermissionedModifierInPlace() + public + { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - non_admin_address + IModule_v1.Module__CallerNotPermissioned.selector ) ); - vm.prank(non_admin_address); - bondingCurveFundingManager.setVirtualIssuanceSupply(_newSupply); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setVirtualIssuanceSupply(0); } function testSetVirtualIssuanceSupply_WorksGivenOnlyWhenCurveInteractionsAreClosedModifierInPosition( @@ -1254,7 +1430,9 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.expectEmit( true, true, false, false, address(bondingCurveFundingManager) ); - emit VirtualIssuanceSupplySet(_newSupply, INITIAL_ISSUANCE_SUPPLY); + emit IVirtualIssuanceSupplyBase_v1.VirtualIssuanceSupplySet( + _newSupply, INITIAL_ISSUANCE_SUPPLY + ); bondingCurveFundingManager.call_setVirtualIssuanceSupply(_newSupply); assertEq( bondingCurveFundingManager.getVirtualIssuanceSupply(), _newSupply @@ -1262,10 +1440,10 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { } /* Test setVirtualCollateralSupply and _setVirtualCollateralSupply function - ├── given caller is not the Orchestrator_v1 admin + ├── given caller is not permissioned │ └── when the function setVirtualCollateralSupply() is called │ └── then it should revert (test modifier is in place. Modifier test itself is tested in base Module tests) - └── given the caller is the Orchestrator_v1 admin + └── given the caller is permissioned ├── and the buy | sell curve are still open (modifier test) │ └── when the setVirtualCollateralSupply() is called │ └── then it should revert @@ -1278,19 +1456,20 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { └── and it should emit an event */ - function testSetVirtualCollateralSupply_WorksGivenOnlyOrchestratorAdminModifierInPlace( - uint _newSupply - ) public { - vm.assume(_newSupply != 0); + function testSetVirtualCollateralSupply_permissionedModifierInPlace() + public + { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - non_admin_address + IModule_v1.Module__CallerNotPermissioned.selector ) ); - vm.prank(non_admin_address); - bondingCurveFundingManager.setVirtualCollateralSupply(_newSupply); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setVirtualCollateralSupply(0); } function testSetVirtualCollateralSupply_WorksGivenOnlyWhenCurveInteractionsAreClosedModifierInPosition( @@ -1330,7 +1509,9 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.expectEmit( true, true, false, false, address(bondingCurveFundingManager) ); - emit VirtualCollateralSupplySet(_newSupply, INITIAL_COLLATERAL_SUPPLY); + emit IVirtualCollateralSupplyBase_v1.VirtualCollateralSupplySet( + _newSupply, INITIAL_COLLATERAL_SUPPLY + ); bondingCurveFundingManager.setVirtualCollateralSupply(_newSupply); assertEq( bondingCurveFundingManager.getVirtualCollateralSupply(), _newSupply @@ -1338,8 +1519,8 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { } /* Test setReserveRatioForBuying and _setReserveRatioForBuying function - ├── when caller is not the Orchestrator_v1 admin - │ └── it should revert (tested in base Module tests) + ├── when caller is not permissioned + │ └── it should revert (modifier in position test) └── when caller is the Orchestrator_v1 admin ├── when buy | sell is still open (modifier test) │ └── it should revert @@ -1355,6 +1536,22 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { └── it should revert */ + function testSetReserveRatioForBuying_permissionedModifierInPosition() + public + { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setReserveRatioForBuying(0); + } + function testSetReserveRatioForBuying_WorksGivenOnlyWhenCurveInteractionsAreClosedModifierInPosition( ) public { vm.expectRevert( @@ -1401,7 +1598,9 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.expectEmit( true, true, false, false, address(bondingCurveFundingManager) ); - emit BuyReserveRatioSet(_newRatio, RESERVE_RATIO_FOR_BUYING); + emit IFM_BC_Bancor_Redeeming_VirtualSupply_v1.BuyReserveRatioSet( + _newRatio, RESERVE_RATIO_FOR_BUYING + ); bondingCurveFundingManager.setReserveRatioForBuying(_newRatio); assertEq( bondingCurveFundingManager.call_reserveRatioForBuying(), _newRatio @@ -1412,9 +1611,9 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { // Test reserve ratio changes /* Test setReserveRatioForSelling and _setReserveRatioForSelling function - ├── when caller is not the Orchestrator_v1 admin - │ └── it should revert (tested in base Module tests) - └── when caller is the Orchestrator_v1 admin + ├── when caller is not permissioned + │ └── it should revert (modifier in position) + └── when caller is permissioned ├── when buy | sell is still open (modifier test) │ └── it should revert ├── when reserve ratio is 0% @@ -1429,6 +1628,22 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { └── it should revert */ + function testSetReserveRatioForSelling_permissionedModifierInPosition() + public + { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setReserveRatioForSelling(0); + } + function testSetReserveRatioForSelling_WorksGivenOnlyWhenCurveInteractionsAreClosedModifierInPosition( ) public { vm.expectRevert( @@ -1475,7 +1690,9 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.expectEmit( true, true, false, false, address(bondingCurveFundingManager) ); - emit SellReserveRatioSet(_newRatio, RESERVE_RATIO_FOR_SELLING); + emit IFM_BC_Bancor_Redeeming_VirtualSupply_v1.SellReserveRatioSet( + _newRatio, RESERVE_RATIO_FOR_SELLING + ); bondingCurveFundingManager.setReserveRatioForSelling(_newRatio); assertEq( bondingCurveFundingManager.call_reserveRatioForSelling(), _newRatio @@ -1726,7 +1943,7 @@ contract FM_BC_Bancor_Redeeming_VirtualSupplyV1Test is ModuleTest { vm.startPrank(address(_erc20PaymentClientMock)); { vm.expectEmit(true, true, true, true); - emit TransferOrchestratorToken(to, amount); + emit IFundingManager_v1.TransferOrchestratorToken(to, amount); bondingCurveFundingManager.transferOrchestratorToken(to, amount); } diff --git a/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.t.sol b/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.t.sol index 4c3c931af..e87af5247 100644 --- a/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.t.sol +++ b/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1.t.sol @@ -71,9 +71,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is bool private constant BUY_AND_SELL_IS_RESTRICTED = false; uint32 private constant BPS = 10_000; - bytes32 private constant RISK_MANAGER_ROLE = "RISK_MANAGER"; - bytes32 private constant COVER_MANAGER_ROLE = "COVER_MANAGER"; - uint private MIN_RESERVE = 10 ** _token.decimals(); uint64 private constant MAX_SEIZE = 100; uint64 private constant MAX_SELL_FEE = 100; @@ -92,10 +89,7 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is address buyer = makeAddr("buyer"); address seller = makeAddr("seller"); address burner = makeAddr("burner"); - address coverManager = address(0xa1bc); - address riskManager = address(0xb0b); address tokenVault = makeAddr("tokenVault"); - bytes32 CURVE_INTERACTION_ROLE = "CURVE_USER"; function setUp() public { // Deploy contracts @@ -132,7 +126,9 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is ); _setUpOrchestrator(bondingCurveFundingManager); - _authorizer.setIsAuthorized(address(this), true); + + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); // Set Minter issuanceToken.setMinter(address(bondingCurveFundingManager), true); @@ -155,14 +151,6 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is address(bondingCurveFundingManager), bondingCurveFundingManager.MIN_RESERVE() ); - - address[] memory targets = new address[](2); - targets[0] = buyer; - targets[1] = seller; - - bondingCurveFundingManager.grantModuleRoleBatched( - CURVE_INTERACTION_ROLE, targets - ); } // ------------------------------------------------------------------------- @@ -349,8 +337,8 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is ); } - // ------------------------------------------------------------------------- - // Public Functions + // ======================================================================== + // Init Functions /* Test: Init fails for invalid formula @@ -401,7 +389,7 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is // ------------------------------------------------------------------------- // Tests: Supports Interface - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( bondingCurveFundingManager.supportsInterface( type( @@ -416,183 +404,70 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is ); } - // ------------------------------------------------------------------------- - // Public Functions - - /* Test buy() & buyFor() functions - Please Note: The functions have been extensively tested in the BondingCurveBase_v1.t contract. These - tests only check for the placement of the onlyIfNotBuyAndSellRestricted() modifier - ├── Given the modifier onlyIfNotBuyAndSellRestricted() is in place - tests only check for the placement of the onlyIfNotBuyAndSellRestricted() modifier - ├── Given the modifier onlyIfNotBuyAndSellRestricted() is in place - │ └── And the modifier condition isn't met - │ ├── When the function buy() is called - │ └── Then it should revert - └── Given the modifier onlyIfNotBuyAndSellRestricted() is in place - └── Given the modifier onlyIfNotBuyAndSellRestricted() is in place - └── And the modifier condition isn't met - └── When the function buyFor() is called - └── Then it should revert - */ - - function testBuy_modifierInPlace() public { - // Set buyAndSellIsRestricted to true - bondingCurveFundingManager.restrictBuyAndSell(); - assertEq(bondingCurveFundingManager.isBuyAndSellRestricted(), true); - - // Check for modifier - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.CURVE_INTERACTION_ROLE() - ), - nonAuthorizedBuyer - ) - ); - vm.prank(nonAuthorizedBuyer); - bondingCurveFundingManager.buy(1, 1); - } - - function testBuyFor_modifierInPlace() public { - // Set buyAndSellIsRestricted to true - bondingCurveFundingManager.restrictBuyAndSell(); - assertEq(bondingCurveFundingManager.isBuyAndSellRestricted(), true); + // ======================================================================== + // Public Getter Functions - // Check for modifier - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.CURVE_INTERACTION_ROLE() - ), - nonAuthorizedBuyer - ) - ); - vm.prank(nonAuthorizedBuyer); - bondingCurveFundingManager.buyFor(seller, 1, 1); - } + // ------------------------------------------------------------------------- + // Implementation Specific Public Functions - /* Test unrestrictBuyAndSell() function - ├── Given the onlyModuleRole(COVER_MANAGER_ROLE) modifier is in place - │ └── And the modifier condition isn't met - │ └── When the function unrestrictBuyAndSell() is called - │ └── Then it should revert - └── Given the msg.sender is the COVER_MANAGER_ROLE - └── When the function unrestrictBuyAndSell() is called - └── Then it should set the buyAndSellIsRestricted to false - └── And it should emit an event + /* Test seizable() + └── When: the function seizable() gets called + └── Then: it should return the correct seizable amount */ - function testBuyAndSellIsUnrestricted_modifierInPlace() public { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.COVER_MANAGER_ROLE() - ), - nonAuthorizedBuyer - ) - ); - vm.prank(nonAuthorizedBuyer); - bondingCurveFundingManager.unrestrictBuyAndSell(); - } - - function testBuyAndSellIsUnrestricted_worksGivenCallerHasCoverManagerRole() - public - { + function testSeizable_works(uint tokenBalance_, uint64 seize_) public { + tokenBalance_ = + bound(tokenBalance_, 1, (UINT256_MAX - MIN_RESERVE) / 1000); // to protect agains overflow if max balance * max seize + seize_ = + uint64(bound(seize_, 1, bondingCurveFundingManager.MAX_SEIZE())); // Setup - bondingCurveFundingManager.restrictBuyAndSell(); - assertEq(bondingCurveFundingManager.isBuyAndSellRestricted(), true); - - // Test condition - vm.expectEmit( - true, true, true, true, address(bondingCurveFundingManager) + // Get balance before test + uint tokenBalanceFundingMangerBaseline = + _token.balanceOf(address(bondingCurveFundingManager)); + // mint collateral to funding manager + _mintCollateralTokenToAddressHelper( + address(bondingCurveFundingManager), tokenBalance_ ); - emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .BuyAndSellIsUnrestricted(); - bondingCurveFundingManager.unrestrictBuyAndSell(); - } - - /* Test restrictBuyAndSell() function - ├── Given the onlyModuleRole(COVER_MANAGER_ROLE) modifier is in place - │ └── And the modifier condition isn't met - │ └── When the function restrictBuyAndSell() is called - │ └── Then it should revert - └── Given the msg.sender is the COVER_MANAGER_ROLE - └── When the function restrictBuyAndSell() is called - └── Then it should set the buyAndSellIsRestricted to true - └── And it should emit an event - */ + // set seize in contract + bondingCurveFundingManager.adjustSeize(seize_); - function testRestrictBuyAndSell_modifierInPlace() public { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.COVER_MANAGER_ROLE() - ), - nonAuthorizedBuyer - ) - ); - vm.prank(nonAuthorizedBuyer); - bondingCurveFundingManager.restrictBuyAndSell(); - } + // calculate return value + uint expectedReturnValue = + ((tokenBalance_ + tokenBalanceFundingMangerBaseline) * seize_) / BPS; - function testUnrestrictBuyAndSell_worksGivenCallerHasCoverManagerRole() - public - { - // Setup - bondingCurveFundingManager.unrestrictBuyAndSell(); - assertEq(bondingCurveFundingManager.isBuyAndSellRestricted(), false); + // Execute tx + uint returnValue = bondingCurveFundingManager.getSeizableAmount(); - // Test condition - vm.expectEmit( - true, true, true, true, address(bondingCurveFundingManager) - ); - emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .BuyAndSellIsRestricted(); - bondingCurveFundingManager.restrictBuyAndSell(); + // Assert right return value + assertEq(returnValue, expectedReturnValue); } - /* Test internal _onlyIfNotBuyAndSellRestrictedModifier() function - /* Test internal _onlyIfNotBuyAndSellRestrictedModifier() function - └── Given buy and selling is restricted - └── And the msg.sender does not have the CURVE_INTERACTION_ROLE - └── When the function _onlyIfNotBuyAndSellRestrictedModifier() is called - └── When the function _onlyIfNotBuyAndSellRestrictedModifier() is called - └── Then it should revert + /* Test getRepayableAmount() + └── When: the function getRepayableAmount() gets called + └── Then: it should return the return value of _getRepayableAmount() */ - function testInternalonlyIfNotBuyAndSellRestrictedModifier_revertGivenCallerHasNotCoverManagerRole( - ) public { - // Setup - bondingCurveFundingManager.restrictBuyAndSell(); - assertEq(bondingCurveFundingManager.isBuyAndSellRestricted(), true); - - // Test condition - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.CURVE_INTERACTION_ROLE() - ), - nonAuthorizedBuyer - ) - ); - vm.prank(nonAuthorizedBuyer); - bondingCurveFundingManager.exposed_onlyIfNotBuyAndSellRestrictedModifier( - ); - bondingCurveFundingManager.exposed_onlyIfNotBuyAndSellRestrictedModifier( - ); + function testPublicGetRepayableAmount_works() public { + // get return value from internal function + uint internalFunctionResult = + bondingCurveFundingManager.exposed_getRepayableAmount(); + // Get return value from public function + uint publicFunctionResult = + bondingCurveFundingManager.getRepayableAmount(); + // Assert they are equal + assertEq(internalFunctionResult, publicFunctionResult); } + // ======================================================================== + // Public Mutating Functions + + // ------------------------------------------------------------------------ + // Mutating - Permissioned Functions + /* Test burnIssuanceToken() + ├── Given: caller is not permissioned + │ └── When: the function burnIssuanceToken() gets called + │ └── Then: it should revert (modifier in position) ├── Given: amount_ > msg.sender balance of issuance token │ └── When: the function burnIssuanceToken() gets called │ └── Then: it should revert @@ -600,6 +475,19 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is └── When: the function burnIssuanceToken() gets called └── Then: it should burn amount_ from msg.sender's balance */ + function testBurnIssuanceToken_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.burnIssuanceToken(0); + } function testBurnIssuanceToken_revertGivenAmountBiggerThanMsgSenderBalance( uint amount_ @@ -643,6 +531,9 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is } /* Test burnIssuanceTokenFor() + ├── Given: caller is not permissioned + │ └── When: the function burnIssuanceToken() gets called + │ └── Then: it should revert (modifier in position) ├── Given: _owner != msg.sender │ ├── And: the allowance < amount_ │ │ └── When: the function burnIssuanceTokenFor() gets called @@ -662,6 +553,19 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is └── When: the function burnIssuanceToken() gets called └── Then: it should burn amount_ from _owner's balance */ + function testBurnIssuanceTokenFor_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.burnIssuanceTokenFor(address(0), 0); + } function testBurnIssuanceTokenFor_revertGivenAmountHigherThanOwnerAllowance( uint amount_ @@ -780,62 +684,334 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is assertEq(issuanceToken.balanceOf(burner), burnerTokenBalance - amount_); } - // ------------------------------------------------------------------------- - // Implementation Specific Public Functions - - /* Test seizable() - └── When: the function seizable() gets called - └── Then: it should return the correct seizable amount + /* Test seize() + ├── Given: the calleris not permissioned + │ └── When: the function seize() gets called + │ └── Then: it should revert (modifier in position) + └── Given: the caller_ is permissioned + ├── And: the parameter amount_ > the seizable amount + │ └── When: the function seize() gets called + │ └── Then: it should revert + ├── And: the lastSeizeTimestamp + SEIZE_DELAY > block.timestamp + │ └── When: the function seize() gets called + │ └── Then: it should revert + ├── And: the capital available - amount_ < MIN_RESERVE + │ └── When: the function seize() gets called + │ └── Then: it should transfer the value of capitalAvailable - MIN_RESERVE tokens to the msg.sender + │ ├── And: it should set the current timeStamp to lastSeizeTimestamp + │ └── And: it should emit an event + └── And: the capital available - amount_ > MIN_RESERVE + └── And: the lastSeizeTimestamp + SEIZE_DELAY < block.timestamp + └── When: the function seize() gets called + └── Then: it should transfer the value of amount_ tokens to the msg.sender + ├── And: it should set the current timeStamp to lastSeizeTimestamp + └── And: it should emit an event */ + function testSeize_ModifierInPosition() public { + // permissioned - function testSeizable_works(uint tokenBalance_, uint64 seize_) public { - tokenBalance_ = - bound(tokenBalance_, 1, (UINT256_MAX - MIN_RESERVE) / 1000); // to protect agains overflow if max balance * max seize - seize_ = - uint64(bound(seize_, 1, bondingCurveFundingManager.MAX_SEIZE())); + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.seize(0); + } + + function testSeize_revertGivenAmountBiggerThanSeizableAmount(uint amount_) + public + { + uint currentSeizable = bondingCurveFundingManager.getSeizableAmount(); + vm.assume(amount_ > currentSeizable); + + vm.expectRevert( + abi.encodeWithSelector( + IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__InvalidSeizeAmount + .selector, + currentSeizable + ) + ); + bondingCurveFundingManager.seize(amount_); + } + + function testSeize_revertGivenLastSeizeTimerNotReset() public { + uint seizeAmount = 1 ether; // Setup - // Get balance before test - uint tokenBalanceFundingMangerBaseline = - _token.balanceOf(address(bondingCurveFundingManager)); - // mint collateral to funding manager + // Mint collateral for enough capital available _mintCollateralTokenToAddressHelper( - address(bondingCurveFundingManager), tokenBalance_ + address(bondingCurveFundingManager), seizeAmount * 100 ); - // set seize in contract - bondingCurveFundingManager.adjustSeize(seize_); - - // calculate return value - uint expectedReturnValue = - ((tokenBalance_ + tokenBalanceFundingMangerBaseline) * seize_) / BPS; + // Check Seize timestamp before calling function + uint seizeTimestampBefore = + bondingCurveFundingManager.getLastSeizeTimestamp(); - // Execute tx - uint returnValue = bondingCurveFundingManager.getSeizableAmount(); + // Assert expected fail. block.timestamp == 1 without setting it in vm.warp + assertGt((seizeTimestampBefore + SEIZE_DELAY), block.timestamp); - // Assert right return value - assertEq(returnValue, expectedReturnValue); + // Execute Tx expecting it to revert + vm.expectRevert( + abi.encodeWithSelector( + IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__SeizeTimeout + .selector, + ( + seizeTimestampBefore + + bondingCurveFundingManager.SEIZE_DELAY() + ) + ) + ); + bondingCurveFundingManager.seize(seizeAmount); } - /* Test getRepayableAmount() - └── When: the function getRepayableAmount() gets called - └── Then: it should return the return value of _getRepayableAmount() - */ + function testSeize_worksGivenCapitalAvailableMinusMinReserveIsReturned() + public + { + // Setup + // Set block.timestamp to valid time + vm.warp(SEIZE_DELAY + 1); + // Amount has to be smaller than seizable amount which is (currentBalance * currentSeize) / BPS + // i.e. (1 ether * 200 ) / 10_000 + uint amount = 1e16; // 0.01 ether + // Return value check for emit. Expected return is 0. Capital available - MIN_RESERVE + uint expectedReturnValue = bondingCurveFundingManager + .exposed_getCapitalAvailable() - MIN_RESERVE; + assertEq(expectedReturnValue, 0); - function testPublicGetRepayableAmount_works() public { - // get return value from internal function - uint internalFunctionResult = - bondingCurveFundingManager.exposed_getRepayableAmount(); - // Get return value from public function - uint publicFunctionResult = - bondingCurveFundingManager.getRepayableAmount(); - // Assert they are equal - assertEq(internalFunctionResult, publicFunctionResult); + //Get balance before seize + uint balanceBeforeBuy = _token.balanceOf(address(this)); + + // Execute Tx + vm.expectEmit( + true, true, true, true, address(bondingCurveFundingManager) + ); + emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + .CollateralSeized(expectedReturnValue); + bondingCurveFundingManager.seize(amount); + + // Assert that no tokens have been sent + assertEq(balanceBeforeBuy, balanceBeforeBuy); } - // ------------------------------------------------------------------------- - // onlyLiquidityVaultController Functions + function testSeize_worksGivenCapitalAmountIsReturnd(uint amount_) public { + // Setup + // Set block.timestamp to valid time + vm.warp(SEIZE_DELAY + 1); + // Bound seizable value + amount_ = bound(amount_, 1, type(uint128).max); + // Mint enough surplus so seizing can happen + _mintCollateralTokenToAddressHelper( + address(bondingCurveFundingManager), amount_ * 10_000 + ); + //Get balance before seize + uint balanceBeforeBuy = _token.balanceOf(address(this)); - /* Test transferRepayment() - ├── Given the caller_ is not the liquidityVaultController + // Execute Tx + vm.expectEmit( + true, true, true, true, address(bondingCurveFundingManager) + ); + emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + .CollateralSeized(amount_); + bondingCurveFundingManager.seize(amount_); + + // Get balance after buying + uint balanceAfterBuy = _token.balanceOf(address(this)); + // Assert that no tokens have been sent + assertEq(balanceAfterBuy, balanceBeforeBuy + amount_); + } + + /* Test adjust size + ├── Given: the caller_ is not permissioned + │ └── When: the function adjustSeize() gets called + │ └── Then: it should revert (modifier in position) + └── Given: the caller_ is permissioned + └── When: the function adjustSeize() gets called + └── Then: it should call the internal function and set the state + */ + + function testAdjustSeize_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + + bondingCurveFundingManager.adjustSeize(0); + } + + function testAdjustSeize_permissioned(uint64 seize_) public { + vm.assume(seize_ != bondingCurveFundingManager.getCurrentSeize()); + seize_ = uint64(bound(seize_, 1, MAX_SEIZE)); + + // Execute Tx + bondingCurveFundingManager.adjustSeize(seize_); + + assertEq(bondingCurveFundingManager.getCurrentSeize(), seize_); + } + + /* Test setliquidityVaultControllerContract() + ├── Given: the caller_ is not permissioned + │ └── When: the function setliquidityVaultControllerContract() gets called + │ └── Then: it should revert (modifier in position) + └── Given: the caller_ is permissioned + ├── And: _lp == address(0) + │ └── When: the function setliquidityVaultController() gets called + │ └── Then: it should revert + └── And: _lp != address(0) + └── When: the function setliquidityVaultController() gets called + └── Then: it should set the state liquidityVaultController to _lp + └── And: it should emit an event + */ + function testSetliquidityVaultControllerContract_ModifierInPosition() + public + { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setLiquidityVaultControllerContract( + address(0) + ); + } + + function testSetliquidityVaultControllerContract_revertGivenAddressIsZero() + public + { + address lvc_ = address(0); + + // Expect Revert + vm.expectRevert( + IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__InvalidInputAddress + .selector + ); + bondingCurveFundingManager.setLiquidityVaultControllerContract(lvc_); + } + + function testSetliquidityVaultControllerContract_revertGivenAddressIsEqualToFM( + ) public { + address lvc = address(bondingCurveFundingManager); + + // Expect Revert + vm.expectRevert( + IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__InvalidInputAddress + .selector + ); + bondingCurveFundingManager.setLiquidityVaultControllerContract(lvc); + } + + function testSetliquidityVaultControllerContract_permissioned(address lvc_) + public + { + vm.assume( + lvc_ != address(0) && lvc_ != address(bondingCurveFundingManager) + ); + + vm.expectEmit( + true, true, true, true, address(bondingCurveFundingManager) + ); + emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 + .LiquidityVaultControllerChanged( + address(lvc_), liquidityVaultController + ); + bondingCurveFundingManager.setLiquidityVaultControllerContract(lvc_); + } + + /* Test setRepayableAmount() + ├── Given: the caller_ is not permissioned + │ └── When: the function setRepayableAmount() gets called + │ └── Then: it should revert (modifier in position) + └── Given: the caller_ is permissioned + ├── And: amount_ > either capitalAvailable or capitalRequirements + │ └── When: the function setRepayableAmount() gets called + │ └── Then: it should revert + └── And: amount_ <= either capitalAvailable or capitalRequirements + └── When: the function setRepayableAmount() gets called + └── Then: it should set the state of repayableAmount to amount_ + └── And: it should emit an event + */ + function testSetRepayableAmount_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setRepayableAmount(0); + } + + function testSetRepayableAmount_permissioned(uint amount_) public { + amount_ = bound( + amount_, + bondingCurveFundingManager.exposed_getSmallerCaCr() + 1, + type(uint).max + ); + + // Execute Tx + vm.expectRevert( + abi.encodeWithSelector( + IFM_BC_BondingSurface_Redeeming_v1 + .FM_BC_BondingSurface_Redeeming_v1__InvalidInputAmount + .selector + ) + ); + bondingCurveFundingManager.setRepayableAmount(amount_); + } + + /* Test: setTokenVault() modifier in position + └── Given: the caller_ is permissioned + └── When: the function setTokenVault() gets called + └── Then: it should revert (modifier in position) + └── Given: the caller_ is permissioned + └── When: the function setTokenVault() gets called + └── Then: it should change the _tokenVault address to the given address + */ + function testSetTokenVault_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setTokenVault(address(0)); + } + + function testSetTokenVault_permissioned(address _tokenVault) public { + vm.assume(_tokenVault != address(0)); + vm.assume(_tokenVault != address(bondingCurveFundingManager)); + bondingCurveFundingManager.setTokenVault(_tokenVault); + + assertEq(_tokenVault, bondingCurveFundingManager.getTokenVault()); + } + + // ------------------------------------------------------------------------- + // onlyLiquidityVaultController Functions + + /* Test transferRepayment() + ├── Given the caller_ is not the liquidityVaultController │ └── When the function transferRepayment() is called │ └── Then it should revert ├── Given modifier validReceiver(to_) is in place: Please Note: Modifier test can be found in BondingCurveBase_v1.t @@ -1020,502 +1196,14 @@ contract FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1_Test is } // ------------------------------------------------------------------------- - // OnlyCoverManager Functions - - /* Test seize() - ├── Given: the caller_ has not the COVER_MANAGER_ROLE - │ └── When: the function seize() gets called - │ └── Then: it should revert - └── Given: the caller_ has the COVER_MANAGER_ROLE - ├── And: the parameter amount_ > the seizable amount - │ └── When: the function seize() gets called - │ └── Then: it should revert - ├── And: the lastSeizeTimestamp + SEIZE_DELAY > block.timestamp - │ └── When: the function seize() gets called - │ └── Then: it should revert - ├── And: the capital available - amount_ < MIN_RESERVE - │ └── When: the function seize() gets called - │ └── Then: it should transfer the value of capitalAvailable - MIN_RESERVE tokens to the msg.sender - │ ├── And: it should set the current timeStamp to lastSeizeTimestamp - │ └── And: it should emit an event - └── And: the capital available - amount_ > MIN_RESERVE - └── And: the lastSeizeTimestamp + SEIZE_DELAY < block.timestamp - └── When: the function seize() gets called - └── Then: it should transfer the value of amount_ tokens to the msg.sender - ├── And: it should set the current timeStamp to lastSeizeTimestamp - └── And: it should emit an event - */ - - function testSeize_revertGivenCallerHasNotCoverManagerRole() public { - uint amount_ = 1; - - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.COVER_MANAGER_ROLE() - ), - seller - ) - ); - bondingCurveFundingManager.seize(amount_); - } - } - - function testSeize_revertGivenAmountBiggerThanSeizableAmount(uint amount_) - public - { - uint currentSeizable = bondingCurveFundingManager.getSeizableAmount(); - vm.assume(amount_ > currentSeizable); - - vm.expectRevert( - abi.encodeWithSelector( - IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__InvalidSeizeAmount - .selector, - currentSeizable - ) - ); - bondingCurveFundingManager.seize(amount_); - } - - function testSeize_revertGivenLastSeizeTimerNotReset() public { - uint seizeAmount = 1 ether; - // Setup - // Mint collateral for enough capital available - _mintCollateralTokenToAddressHelper( - address(bondingCurveFundingManager), seizeAmount * 100 - ); - // Check Seize timestamp before calling function - uint seizeTimestampBefore = - bondingCurveFundingManager.getLastSeizeTimestamp(); - - // Assert expected fail. block.timestamp == 1 without setting it in vm.warp - assertGt((seizeTimestampBefore + SEIZE_DELAY), block.timestamp); + // Mutating - Out of Order - // Execute Tx expecting it to revert - vm.expectRevert( - abi.encodeWithSelector( - IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__SeizeTimeout - .selector, - ( - seizeTimestampBefore - + bondingCurveFundingManager.SEIZE_DELAY() - ) - ) - ); - bondingCurveFundingManager.seize(seizeAmount); - } - - function testSeize_worksGivenCapitalAvailableMinusMinReserveIsReturned() - public - { - // Setup - // Set block.timestamp to valid time - vm.warp(SEIZE_DELAY + 1); - // Amount has to be smaller than seizable amount which is (currentBalance * currentSeize) / BPS - // i.e. (1 ether * 200 ) / 10_000 - uint amount = 1e16; // 0.01 ether - // Return value check for emit. Expected return is 0. Capital available - MIN_RESERVE - uint expectedReturnValue = bondingCurveFundingManager - .exposed_getCapitalAvailable() - MIN_RESERVE; - assertEq(expectedReturnValue, 0); + /* Test: withdrawProjectCollateralFee + └── When: the function withdrawProjectCollateralFee() gets called + └── Then: it should revert - //Get balance before seize - uint balanceBeforeBuy = _token.balanceOf(address(this)); - - // Execute Tx - vm.expectEmit( - true, true, true, true, address(bondingCurveFundingManager) - ); - emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .CollateralSeized(expectedReturnValue); - bondingCurveFundingManager.seize(amount); - - // Assert that no tokens have been sent - assertEq(balanceBeforeBuy, balanceBeforeBuy); - } - - function testSeize_worksGivenCapitalAmountIsReturnd(uint amount_) public { - // Setup - // Set block.timestamp to valid time - vm.warp(SEIZE_DELAY + 1); - // Bound seizable value - amount_ = bound(amount_, 1, type(uint128).max); - // Mint enough surplus so seizing can happen - _mintCollateralTokenToAddressHelper( - address(bondingCurveFundingManager), amount_ * 10_000 - ); - //Get balance before seize - uint balanceBeforeBuy = _token.balanceOf(address(this)); - - // Execute Tx - vm.expectEmit( - true, true, true, true, address(bondingCurveFundingManager) - ); - emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .CollateralSeized(amount_); - bondingCurveFundingManager.seize(amount_); - - // Get balance after buying - uint balanceAfterBuy = _token.balanceOf(address(this)); - // Assert that no tokens have been sent - assertEq(balanceAfterBuy, balanceBeforeBuy + amount_); - } - - /* Test adjust - ├── Given: the caller_ has not the COVER_MANAGER_ROLE - │ └── When: the function adjustSeize() gets called - │ └── Then: it should revert - └── Given: the caller_ has the COVER_MANAGER_ROLE - └── When: the function adjustSeize() gets called - └── Then: it should call the internal function and set the state - */ - - function testAdjustSeize_revertGivenCallerHasNotCoverManagerRole() public { - uint64 seize_ = 10_000; - - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.COVER_MANAGER_ROLE() - ), - seller - ) - ); - bondingCurveFundingManager.adjustSeize(seize_); - } - } - - function testAdjustSeize_worksGivenCallerHasCoverManagerRole(uint64 seize_) - public - { - vm.assume(seize_ != bondingCurveFundingManager.getCurrentSeize()); - seize_ = uint64(bound(seize_, 1, MAX_SEIZE)); - - // Execute Tx - bondingCurveFundingManager.adjustSeize(seize_); - - assertEq(bondingCurveFundingManager.getCurrentSeize(), seize_); - } - - /* Test setSellFee() - ├── Given: the caller_ has not the COVER_MANAGER_ROLE - │ └── When: the function setSellFee() gets called - │ └── Then: it should revert - └── Given: the caller_ has the COVER_MANAGER_ROLE - ├── And: fee_ > MAX_SELL_FEE - │ └── When: the function setSellFee() getrs called - │ └── Then: it should revert - └── And: fee_ <= MAX_SELL_FEE - └── When: the function setSellFee() gets called - └── Then: it should set the state of sellFee to fee_ - */ - - function testSetSellFee_revertGivenCallerHasNotCoverManagerRole() public { - uint sellFee = 100; - - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.COVER_MANAGER_ROLE() - ), - seller - ) - ); - bondingCurveFundingManager.setSellFee(sellFee); - } - } - - function testSetSellFee_revertGivenSeizeBiggerThanMaxSeize(uint fee_) - public - { - vm.assume(fee_ > MAX_SELL_FEE); - - // Execute Tx - vm.expectRevert( - abi.encodeWithSelector( - IBondingCurveBase_v1 - .Module__BondingCurveBase__InvalidFeePercentage - .selector - ) - ); - bondingCurveFundingManager.setSellFee(fee_); - } - - function testAdjustSeize_worksGivenCallerHasRoleAndSeizeIsValid(uint fee_) - public - { - vm.assume(fee_ != bondingCurveFundingManager.sellFee()); - fee_ = bound(fee_, 1, MAX_SELL_FEE); - - // Execute Tx - bondingCurveFundingManager.setSellFee(fee_); - - assertEq(bondingCurveFundingManager.sellFee(), fee_); - } - - /* Test setRepayableAmount() - ├── Given: the caller_ has not the COVER_MANAGER_ROLE - │ └── When: the function setRepayableAmount() gets called - │ └── Then: it should revert - └── Given: the caller_ has the COVER_MANAGER_ROLE - ├── And: amount_ > either capitalAvailable or capitalRequirements - │ └── When: the function setRepayableAmount() gets called - │ └── Then: it should revert - └── And: amount_ <= either capitalAvailable or capitalRequirements - └── When: the function setRepayableAmount() gets called - └── Then: it should set the state of repayableAmount to amount_ - └── And: it should emit an event */ - - function testSetRepayableAmount_revertGivenCallerHasNotCoverManagerRole() - public - { - uint amount = 1000; - - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.COVER_MANAGER_ROLE() - ), - seller - ) - ); - bondingCurveFundingManager.setRepayableAmount(amount); - } - } - - function testSetRepayableAmount_revertGivenCallerHasNotCoverManagerRole( - uint amount_ - ) public { - amount_ = bound( - amount_, - bondingCurveFundingManager.exposed_getSmallerCaCr() + 1, - type(uint).max - ); - - // Execute Tx - vm.expectRevert( - abi.encodeWithSelector( - IFM_BC_BondingSurface_Redeeming_v1 - .FM_BC_BondingSurface_Redeeming_v1__InvalidInputAmount - .selector - ) - ); - bondingCurveFundingManager.setRepayableAmount(amount_); - } - - /* Test setliquidityVaultControllerContract() - ├── Given: the caller_ has not the COVER_MANAGER_ROLE - │ └── When: the function setliquidityVaultControllerContract() gets called - │ └── Then: it should revert - └── Given: the caller_ has the COVER_MANAGER_ROLE - ├── And: _lp == address(0) - │ └── When: the function setliquidityVaultController() gets called - │ └── Then: it should revert - └── And: _lp != address(0) - └── When: the function setliquidityVaultController() gets called - └── Then: it should set the state liquidityVaultController to _lp - └── And: it should emit an event - */ - - function testSetliquidityVaultControllerContract_revertGivenCallerHasNotCoverManagerRole( - address lvc_ - ) public { - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.COVER_MANAGER_ROLE() - ), - seller - ) - ); - bondingCurveFundingManager.setLiquidityVaultControllerContract(lvc_); - } - } - - function testSetliquidityVaultControllerContract_revertGivenAddressIsZero() - public - { - address lvc_ = address(0); - - // Expect Revert - vm.expectRevert( - IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__InvalidInputAddress - .selector - ); - bondingCurveFundingManager.setLiquidityVaultControllerContract(lvc_); - } - - function testSetliquidityVaultControllerContract_revertGivenAddressIsEqualToFM( - ) public { - address lvc = address(bondingCurveFundingManager); - - // Expect Revert - vm.expectRevert( - IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .FM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1__InvalidInputAddress - .selector - ); - bondingCurveFundingManager.setLiquidityVaultControllerContract(lvc); - } - - function testSetliquidityVaultControllerContract_worksGivenCallerHasRoleAndAddressValid( - address lvc_ - ) public { - vm.assume( - lvc_ != address(0) && lvc_ != address(bondingCurveFundingManager) - ); - - vm.expectEmit( - true, true, true, true, address(bondingCurveFundingManager) - ); - emit IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 - .LiquidityVaultControllerChanged( - address(lvc_), liquidityVaultController - ); - bondingCurveFundingManager.setLiquidityVaultControllerContract(lvc_); - } - - // ------------------------------------------------------------------------- - // OnlyCoverManager Functions - - /* Test setCapitalRequired() - ├── Given: the caller_ has not the RISK_MANAGER_ROLE - │ └── When: the function setCapitalRequired() is called - │ └── Then: it should revert - */ - function testSetCapitalRequired_modifierInPosition() public { - uint newCapitalRequired = 1 ether; - - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.RISK_MANAGER_ROLE() - ), - seller - ) - ); - bondingCurveFundingManager.setCapitalRequired(newCapitalRequired); - } - } - - /* Test setBaseMultiplier() - ├── Given: the caller_ has not the RISK_MANAGER_ROLE - │ └── When: the function setBaseMultiplier() is called - │ └── Then: it should revert - */ - - function testSetBasePriceMultiplier_modifierInPosition() public { - uint newBaseMultiplier = 1; - - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bondingCurveFundingManager), - bondingCurveFundingManager.RISK_MANAGER_ROLE() - ), - seller - ) - ); - bondingCurveFundingManager.setBasePriceMultiplier(newBaseMultiplier); - } - } - - // ------------------------------------------------------------------------- - // OnlyOrchestratorAdmin Functions - - /* Test: setTokenVault() modifier in position - └── Given: the caller_ is not the OrchestratorAdmin - └── When: the function setTokenVault() gets called - └── Then: it should revert - └── Given: the caller_ is the OrchestratorAdmin - └── When: the function setTokenVault() gets called - └── Then: it should change the _tokenVault address to the given address - */ - function testSetTokenVault_ModifierInPosition() public { - // onlyOrchestratorAdmin - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - address(0) - ) - ); - vm.prank(address(0)); - bondingCurveFundingManager.setTokenVault(address(0)); - } - - function testSetTokenVault_worksGivenCallerIsOrchestratorAdmin( - address _tokenVault - ) public { - vm.assume(_tokenVault != address(0)); - vm.assume(_tokenVault != address(bondingCurveFundingManager)); - bondingCurveFundingManager.setTokenVault(_tokenVault); - - assertEq(_tokenVault, bondingCurveFundingManager.getTokenVault()); - } - - /* Test: withdrawProjectCollateralFee - └── Given: the caller_ is not the OrchestratorAdmin - └── When: the function withdrawProjectCollateralFee() gets called - └── Then: it should revert - └── Given: the caller_ is the OrchestratorAdmin - └── When: the function withdrawProjectCollateralFee() gets called - └── Then: it should revert - */ - - function testWithdrawProjectCollateralFee_ModifierInPosition() public { - // onlyOrchestratorAdmin - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - address(0) - ) - ); - vm.prank(address(0)); - bondingCurveFundingManager.withdrawProjectCollateralFee(address(0), 0); - } - - function testWithdrawProjectCollateralFee_revertsGivenCallerIsOrchestratorAdmin( - ) public { + function testWithdrawProjectCollateralFee_reverts() public { vm.expectRevert( abi.encodeWithSelector( IFM_BC_BondingSurface_Redeeming_Restricted_Repayer_Seizable_v1 diff --git a/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.t.sol b/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.t.sol index 970532e31..f31927f07 100644 --- a/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.t.sol +++ b/test/unit/modules/fundingManager/bondingCurve/FM_BC_BondingSurface_Redeeming_v1.t.sol @@ -104,7 +104,9 @@ contract FM_BC_BondingSurface_Redeeming_v1_Test is ModuleTest { FM_BC_BondingSurface_RedeemingV1_Exposed(Clones.clone(impl)); _setUpOrchestrator(bondingCurveFundingManager); - _authorizer.setIsAuthorized(address(this), true); + + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); // Set Minter issuanceToken.setMinter(address(bondingCurveFundingManager), true); @@ -294,7 +296,7 @@ contract FM_BC_BondingSurface_Redeeming_v1_Test is ModuleTest { // ------------------------------------------------------------------------- // Tests: Supports Interface - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( bondingCurveFundingManager.supportsInterface( type(IFM_BC_BondingSurface_Redeeming_v1).interfaceId @@ -411,35 +413,30 @@ contract FM_BC_BondingSurface_Redeeming_v1_Test is ModuleTest { // OnlyOrchestratorAdmin Functions /* Test setCapitalRequired() - ├── Given: the caller_ is not the OrchestratorAdmin + ├── Given: the caller_ is permissioned │ └── When: the function setCapitalRequired() is called - │ └── Then: it should revert + │ └── Then: it should revert (modifier in position test) ├── Given: the amount is invalid │ └── When: the function setCapitalRequired() is called - │ └── Then: it should revert - └── Given: the caller_ is the OrchestratorAdmin + │ └── Then: it should revert + └── Given: the caller_ is permissioned └── When: the function setCapitalRequired() is called └── Then: it should call the internal function and set the state */ - function testSetCapitalRequired_revertGivenCallerHasNotRiskManagerRole() - public - { - uint newCapitalRequired = 1 ether; + function testSetCapitalRequired_ModifierInPosition() public { + // permissioned - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - seller - ) - ); - bondingCurveFundingManager.setCapitalRequired(newCapitalRequired); - } + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setCapitalRequired(0); } function testSetCapitalRequired_revertGivenAmountIsInvalid() public { @@ -481,31 +478,26 @@ contract FM_BC_BondingSurface_Redeeming_v1_Test is ModuleTest { } /* Test setBaseMultiplier() - ├── Given: the caller_ is not the OrchestratorAdmin + ├── Given: the caller_ is not permissioned │ └── When: the function setBaseMultiplier() is called - │ └── Then: it should revert - └── Given: the caller_ is the OrchestratorAdmin + │ └── Then: it should revert (modifier in position test) + └── Given: the caller_ is permissioned └── When: the function setBaseMultiplier() is called └── Then: it should call the internal function and set the state */ - function testSetBaseMultiplier_revertGivenCallerHasNotRiskManagerRole() - public - { - uint newBaseMultiplier = 1; + function testSetBaseMultiplier_modifierInPosition() public { + // permissioned - // Execute Tx - vm.startPrank(seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - seller - ) - ); - bondingCurveFundingManager.setBasePriceMultiplier(newBaseMultiplier); - } + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setBasePriceMultiplier(0); } function testSetBaseMultiplier_worksGivenCallerHasRiskManagerRole( diff --git a/test/unit/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.t.sol b/test/unit/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.t.sol deleted file mode 100644 index cb24b5209..000000000 --- a/test/unit/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.t.sol +++ /dev/null @@ -1,352 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; - -import "forge-std/console.sol"; - -// SuT -import {FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1} from - "@fm/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupply_v1.sol"; - -import {Clones} from "@oz/proxy/Clones.sol"; - -import {IERC165} from "@oz/utils/introspection/IERC165.sol"; -import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; - -import { - IFM_BC_Bancor_Redeeming_VirtualSupply_v1, - FM_BC_Bancor_Redeeming_VirtualSupply_v1, - IFundingManager_v1 -} from "@fm/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.sol"; -import {BancorFormula} from "@fm/bondingCurve/formulas/BancorFormula.sol"; -import {IBondingCurveBase_v1} from - "@fm/bondingCurve/interfaces/IBondingCurveBase_v1.sol"; -import {ERC20PaymentClientBaseV2Mock} from - "@mocks/modules/paymentClient/ERC20PaymentClientBaseV2Mock.sol"; - -import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol"; - -import { - FM_BC_Bancor_Redeeming_VirtualSupplyV1Test, - ModuleTest, - IModule_v1 -} from - "@unitTest/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupply_v1.t.sol"; -import {FM_BC_Bancor_Redeeming_VirtualSupplyV1Mock} from - "@mocks/modules/fundingManager/bondingCurve/FM_BC_Bancor_Redeeming_VirtualSupplyV1Mock.sol"; -import {FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock} from - "@mocks/modules/fundingManager/bondingCurve/FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock.sol"; -import {OZErrors} from "@testUtilities/OZErrors.sol"; - -contract FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1UpstreamTests is - FM_BC_Bancor_Redeeming_VirtualSupplyV1Test -{ - function setUp() public override { - // Deploy contracts - issuanceToken = new ERC20Issuance_v1(NAME, SYMBOL, DECIMALS, MAX_SUPPLY); - issuanceToken.setMinter(address(this), true); - - BancorFormula bancorFormula = new BancorFormula(); - formula = address(bancorFormula); - - IFM_BC_Bancor_Redeeming_VirtualSupply_v1.BondingCurveProperties memory - bc_properties; - - bc_properties.formula = formula; - bc_properties.reserveRatioForBuying = RESERVE_RATIO_FOR_BUYING; - bc_properties.reserveRatioForSelling = RESERVE_RATIO_FOR_SELLING; - bc_properties.buyFee = BUY_FEE; - bc_properties.sellFee = SELL_FEE; - bc_properties.buyIsOpen = BUY_IS_OPEN; - bc_properties.sellIsOpen = SELL_IS_OPEN; - bc_properties.initialIssuanceSupply = INITIAL_ISSUANCE_SUPPLY; - bc_properties.initialCollateralSupply = INITIAL_COLLATERAL_SUPPLY; - - address impl = - address(new FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock()); - - bondingCurveFundingManager = - FM_BC_Bancor_Redeeming_VirtualSupplyV1Mock(Clones.clone(impl)); - - _setUpOrchestrator(bondingCurveFundingManager); - - _authorizer.grantRole(_authorizer.getAdminRole(), admin_address); - - // Init Module - bondingCurveFundingManager.init( - _orchestrator, - _METADATA, - abi.encode( - address(issuanceToken), - bc_properties, - _token // fetching from ModuleTest.sol (specifically after the _setUpOrchestrator function call) - ) - ); - - // we grant minting rights to the bonding curve - issuanceToken.setMinter(address(bondingCurveFundingManager), true); - - // Grant necessary roles for the Upstream tests to pass - - bytes32 CURVE_INTERACTION_ROLE = "CURVE_USER"; - address buyer = makeAddr("buyer"); - address seller = makeAddr("seller"); - - address[] memory targets = new address[](3); - targets[0] = buyer; - targets[1] = seller; - targets[2] = admin_address; - - bondingCurveFundingManager.grantModuleRoleBatched( - CURVE_INTERACTION_ROLE, targets - ); - } - - function testTransferOrchestratorToken_FailsGivenNotEnoughCollateralInFM( - address to, - uint amount, - uint projectCollateralFeeCollected - ) public override { - // Temp test override, as in dev branch we have already removed the restriction - // to call the transferOrchestratorToken() function - } -} - -contract FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Tests is - ModuleTest -{ - string internal constant NAME = "Bonding Curve Token"; - string internal constant SYMBOL = "BCT"; - uint8 internal constant DECIMALS = 18; - uint internal constant MAX_SUPPLY = type(uint).max; - - uint internal constant INITIAL_ISSUANCE_SUPPLY = 10; - uint internal constant INITIAL_COLLATERAL_SUPPLY = 30; - uint32 internal constant RESERVE_RATIO_FOR_BUYING = 333_333; - uint32 internal constant RESERVE_RATIO_FOR_SELLING = 333_333; - uint internal constant BUY_FEE = 0; - uint internal constant SELL_FEE = 0; - bool internal constant BUY_IS_OPEN = true; - bool internal constant SELL_IS_OPEN = true; - - FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock - bondingCurveFundingManager; - address formula; - - ERC20Issuance_v1 issuanceToken; - - address admin_address = address(0xA1BA); - address non_admin_address = address(0xB0B); - - function setUp() public { - // Deploy contracts - issuanceToken = new ERC20Issuance_v1(NAME, SYMBOL, DECIMALS, MAX_SUPPLY); - issuanceToken.setMinter(address(this), true); - - BancorFormula bancorFormula = new BancorFormula(); - formula = address(bancorFormula); - - IFM_BC_Bancor_Redeeming_VirtualSupply_v1.BondingCurveProperties memory - bc_properties; - - bc_properties.formula = formula; - bc_properties.reserveRatioForBuying = RESERVE_RATIO_FOR_BUYING; - bc_properties.reserveRatioForSelling = RESERVE_RATIO_FOR_SELLING; - bc_properties.buyFee = BUY_FEE; - bc_properties.sellFee = SELL_FEE; - bc_properties.buyIsOpen = BUY_IS_OPEN; - bc_properties.sellIsOpen = SELL_IS_OPEN; - bc_properties.initialIssuanceSupply = INITIAL_ISSUANCE_SUPPLY; - bc_properties.initialCollateralSupply = INITIAL_COLLATERAL_SUPPLY; - - address impl = - address(new FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock()); - - bondingCurveFundingManager = - FM_BC_Restricted_Bancor_Redeeming_VirtualSupplyV1Mock( - Clones.clone(impl) - ); - - _setUpOrchestrator(bondingCurveFundingManager); - - _authorizer.grantRole(_authorizer.getAdminRole(), admin_address); - - // Init Module - bondingCurveFundingManager.init( - _orchestrator, - _METADATA, - abi.encode( - address(issuanceToken), - bc_properties, - _token // fetching from ModuleTest.sol (specifically after the _setUpOrchestrator function call) - ) - ); - - // we grant minting rights to the bonding curve - issuanceToken.setMinter(address(bondingCurveFundingManager), true); - - // Since we tested the success case in the Upstream tests, we now only need to verify revert on unauthorized calls - } - - function testInit() public override { - assertEq( - issuanceToken.name(), - string(abi.encodePacked(NAME)), - "Name has not been set correctly" - ); - assertEq( - issuanceToken.symbol(), - string(abi.encodePacked(SYMBOL)), - "Symbol has not been set correctly" - ); - assertEq( - issuanceToken.decimals(), - DECIMALS, - "Decimals has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.call_collateralTokenDecimals(), - _token.decimals(), - "Collateral token decimals has not been set correctly" - ); - assertEq( - address(bondingCurveFundingManager.formula()), - formula, - "Formula has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.getVirtualIssuanceSupply(), - INITIAL_ISSUANCE_SUPPLY, - "Virtual token supply has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.getVirtualCollateralSupply(), - INITIAL_COLLATERAL_SUPPLY, - "Virtual collateral supply has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.call_reserveRatioForBuying(), - RESERVE_RATIO_FOR_BUYING, - "Reserve ratio for buying has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.call_reserveRatioForSelling(), - RESERVE_RATIO_FOR_SELLING, - "Reserve ratio for selling has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.buyFee(), - BUY_FEE, - "Buy fee has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.buyIsOpen(), - BUY_IS_OPEN, - "Buy-is-open has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.buyFee(), - SELL_FEE, - "Sell fee has not been set correctly" - ); - assertEq( - bondingCurveFundingManager.buyIsOpen(), - SELL_IS_OPEN, - "Sell-is-open has not been set correctly" - ); - } - - function testReinitFails() public override { - vm.expectRevert(OZErrors.Initializable__InvalidInitialization); - bondingCurveFundingManager.init(_orchestrator, _METADATA, abi.encode()); - } - - function testBuyFor_FailsIfCallerNotAuthorized() public { - address _buyer = makeAddr("buyer"); - address _receiver = makeAddr("receiver"); - uint _depositAmount = 1; - - bytes32 _roleId = _authorizer.generateRoleId( - address(bondingCurveFundingManager), "CURVE_USER" - ); - - vm.startPrank(_buyer); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _roleId, - _buyer - ) - ); - bondingCurveFundingManager.buyFor( - _receiver, _depositAmount, _depositAmount - ); - } - } - - function testBuy_FailsIfCallerNotAuthorized() public { - address _buyer = makeAddr("buyer"); - uint _depositAmount = 1; - - bytes32 _roleId = _authorizer.generateRoleId( - address(bondingCurveFundingManager), "CURVE_USER" - ); - - vm.startPrank(_buyer); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _roleId, - _buyer - ) - ); - bondingCurveFundingManager.buy(_depositAmount, _depositAmount); - } - } - - function testsellTo_FailsIfCallerNotAuthorized() public { - address _seller = makeAddr("seller"); - address _receiver = makeAddr("receiver"); - uint _sellAmount = 1; - - bytes32 _roleId = _authorizer.generateRoleId( - address(bondingCurveFundingManager), "CURVE_USER" - ); - - vm.startPrank(_seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _roleId, - _seller - ) - ); - bondingCurveFundingManager.sellTo( - _receiver, _sellAmount, _sellAmount - ); - } - } - - function testSell_FailsIfCallerNotAuthorized() public { - address _seller = makeAddr("seller"); - uint _sellAmount = 1; - - bytes32 _roleId = _authorizer.generateRoleId( - address(bondingCurveFundingManager), "CURVE_USER" - ); - - vm.startPrank(_seller); - { - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _roleId, - _seller - ) - ); - bondingCurveFundingManager.sell(_sellAmount, _sellAmount); - } - } -} diff --git a/test/unit/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.t.sol b/test/unit/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.t.sol index 1bdb3bbd8..a46f6d336 100644 --- a/test/unit/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.t.sol +++ b/test/unit/modules/fundingManager/bondingCurve/abstracts/BondingCurveBase_v1.t.sol @@ -45,22 +45,6 @@ contract BondingCurveBaseV1Test is ModuleTest { address admin_address = makeAddr("alice"); address non_admin_address = makeAddr("bob"); - event BuyingEnabled(); - event BuyingDisabled(); - event BuyFeeUpdated(uint newBuyFee, uint oldBuyFee); - event TokensBought( - address indexed receiver, - uint depositAmount, - uint receivedAmount, - address buyer - ); - event IssuanceTokenSet(address indexed token, uint8 decimals); - - event ProtocolFeeMinted( - address indexed token, address indexed treasury, uint feeAmount - ); - event ProjectCollateralFeeWithdrawn(address receiver, uint amount); - function setUp() public { // Deploy contracts address impl = address(new BondingCurveBaseV1Mock()); @@ -76,7 +60,8 @@ contract BondingCurveBaseV1Test is ModuleTest { issuanceToken.setMinter(address(this), true); _setUpOrchestrator(bondingCurveFundingManager); - _authorizer.grantRole(_authorizer.getAdminRole(), admin_address); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); // Set max fee of feeManager to 100% for testing purposes vm.prank(address(governor)); @@ -90,7 +75,7 @@ contract BondingCurveBaseV1Test is ModuleTest { ); } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( bondingCurveFundingManager.supportsInterface( type(IBondingCurveBase_v1).interfaceId @@ -202,7 +187,105 @@ contract BondingCurveBaseV1Test is ModuleTest { vm.stopPrank(); } - /* Test buy and _buyOrder function + /* + Test: buyFor Modifier Checks + ├── Given: buyer is not permissioned + │ └── When: buyFor is called + │ └── Then: it should revert (modifier in position check) + ├── Given: buyer is permissioned + ├── And: buying is not enabled + │ └── When: buyFor is called + │ └── Then: it should revert (modifier in position check) + ├── Given: buyer is permissioned + ├── And: buying is enabled + └── And: receiver is invalid + └── When: buyFor is called + └── Then: it should revert (modifier in position check) + */ + + function testBuyFor_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.buyFor(address(0), 0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeBuy(); + + vm.expectRevert( + IBondingCurveBase_v1 + .Module__BondingCurveBase__BuyingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.buyFor(address(0), 0, 0); + + // Open up Buy again + bondingCurveFundingManager.openBuy(); + + // validReceiver + vm.expectRevert( + abi.encodeWithSelector( + IBondingCurveBase_v1 + .Module__BondingCurveBase__InvalidRecipient + .selector + ) + ); + bondingCurveFundingManager.buyFor(address(0), 0, 0); + } + + /* + Test: buy Modifier Checks + ├── Given: buyer is not permissioned + │ └── When: buy is called + │ └── Then: it should revert (modifier in position check) + ├── Given: buyer is permissioned + ├── And: buying is not enabled + └── When: buy is called + └── Then: it should revert (modifier in position check) + */ + + function testBuy_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.buy(0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeBuy(); + + vm.expectRevert( + IBondingCurveBase_v1 + .Module__BondingCurveBase__BuyingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.buy(0, 0); + } + + /* Test _buyOrder function ├── when the deposit amount is 0 │ └── it should revert └── when the deposit amount is not 0 @@ -268,7 +351,7 @@ contract BondingCurveBaseV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit TokensBought(buyer, amount, amount, buyer); + emit IBondingCurveBase_v1.TokensBought(buyer, amount, amount, buyer); // Execution vm.prank(buyer); @@ -371,7 +454,9 @@ contract BondingCurveBaseV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit TokensBought(buyer, amount, finalAmount, buyer); // since the fee gets taken before interacting with the bonding curve, we expect the event to already have the fee substracted + emit IBondingCurveBase_v1.TokensBought( + buyer, amount, finalAmount, buyer + ); // since the fee gets taken before interacting with the bonding curve, we expect the event to already have the fee substracted // Execution vm.prank(buyer); @@ -512,7 +597,9 @@ contract BondingCurveBaseV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit ProtocolFeeMinted(address(issuanceToken), treasury, _feeAmount); + emit IBondingCurveBase_v1.ProtocolFeeMinted( + address(issuanceToken), treasury, _feeAmount + ); // Function call bondingCurveFundingManager.call_processProtocolFeeViaMinting( treasury, _feeAmount @@ -759,8 +846,8 @@ contract BondingCurveBaseV1Test is ModuleTest { } /* Test openBuy and _openBuy function - ├── when caller is not the Orchestrator_v1 admin - │ └── it should revert (tested in base Module modifier tests) + ├── when caller is not permissioned + │ └── it should revert (modifier in position) └── when caller is the Orchestrator_v1 admin └── when buy functionality is already open │ └── it should stay as is @@ -769,18 +856,33 @@ contract BondingCurveBaseV1Test is ModuleTest { └── it should open the buy functionality └── it should emit an event */ - function testOpenBuy_Idempotence() public callerIsOrchestratorAdmin { + + function testOpenBuy_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.openBuy(); + } + + function testOpenBuy_Idempotence() public { assertEq(bondingCurveFundingManager.buyIsOpen(), true); vm.expectEmit(address(bondingCurveFundingManager)); - emit BuyingEnabled(); + emit IBondingCurveBase_v1.BuyingEnabled(); bondingCurveFundingManager.openBuy(); assertEq(bondingCurveFundingManager.buyIsOpen(), true); } - function testOpenBuy() public callerIsOrchestratorAdmin { + function testOpenBuy() public { assertEq(bondingCurveFundingManager.buyIsOpen(), true); bondingCurveFundingManager.closeBuy(); @@ -788,7 +890,7 @@ contract BondingCurveBaseV1Test is ModuleTest { assertEq(bondingCurveFundingManager.buyIsOpen(), false); vm.expectEmit(address(bondingCurveFundingManager)); - emit BuyingEnabled(); + emit IBondingCurveBase_v1.BuyingEnabled(); bondingCurveFundingManager.openBuy(); @@ -796,9 +898,9 @@ contract BondingCurveBaseV1Test is ModuleTest { } /* Test closeBuy and _closeBuy function - ├── when caller is not the Orchestrator_v1 admin - │ └── it should revert (tested in base Module tests) - └── when caller is the Orchestrator_v1 admin + ├── when caller is not permissioned + │ └── it should revert (modifier in position check) + └── when caller is permissioned └── when buy functionality is already closed │ └── it should stay as is │ └── it should emit an event @@ -806,30 +908,41 @@ contract BondingCurveBaseV1Test is ModuleTest { ├── it should close the buy functionality └── it should emit an event */ - function testCloseBuy_FailsIfAlreadyClosed() - public - callerIsOrchestratorAdmin - { + function testCloseBuy_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.closeBuy(); + } + + function testCloseBuy_FailsIfAlreadyClosed() public { vm.expectEmit(address(bondingCurveFundingManager)); - emit BuyingDisabled(); + emit IBondingCurveBase_v1.BuyingDisabled(); bondingCurveFundingManager.closeBuy(); assertEq(bondingCurveFundingManager.buyIsOpen(), false); vm.expectEmit(address(bondingCurveFundingManager)); - emit BuyingDisabled(); + emit IBondingCurveBase_v1.BuyingDisabled(); bondingCurveFundingManager.closeBuy(); assertEq(bondingCurveFundingManager.buyIsOpen(), false); } - function testCloseBuy() public callerIsOrchestratorAdmin { + function testCloseBuy() public { assertEq(bondingCurveFundingManager.buyIsOpen(), true); vm.expectEmit(address(bondingCurveFundingManager)); - emit BuyingDisabled(); + emit IBondingCurveBase_v1.BuyingDisabled(); bondingCurveFundingManager.closeBuy(); @@ -837,9 +950,9 @@ contract BondingCurveBaseV1Test is ModuleTest { } /* Test setBuyFee and _setBuyFee function - ├── when caller is not the Orchestrator_v1 admin - │ └── it should revert (tested in base Module tests) - └── when caller is the Orchestrator_v1 admin + ├── when caller is not permissioned + │ └── it should revert (modifier in postion) + └── when caller is permissioned └── when fee is over 100% │ └── it should revert ├── when fee is 100% @@ -848,10 +961,21 @@ contract BondingCurveBaseV1Test is ModuleTest { ├── it should set the new fee └── it should emit an event */ - function testSetBuyFee_FailsIfFee100PercentOrMore(uint _fee) - public - callerIsOrchestratorAdmin - { + function testSetBuyFee_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setBuyFee(0); + } + + function testSetBuyFee_FailsIfFee100PercentOrMore(uint _fee) public { vm.assume(_fee > bondingCurveFundingManager.call_BPS()); vm.expectRevert( IBondingCurveBase_v1 @@ -861,13 +985,13 @@ contract BondingCurveBaseV1Test is ModuleTest { bondingCurveFundingManager.setBuyFee(_fee); } - function testSetBuyFee(uint newFee) public callerIsOrchestratorAdmin { + function testSetBuyFee(uint newFee) public { vm.assume(newFee < bondingCurveFundingManager.call_BPS()); vm.expectEmit( true, true, false, false, address(bondingCurveFundingManager) ); - emit BuyFeeUpdated(newFee, BUY_FEE); + emit IBondingCurveBase_v1.BuyFeeUpdated(newFee, BUY_FEE); bondingCurveFundingManager.setBuyFee(newFee); @@ -903,7 +1027,9 @@ contract BondingCurveBaseV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit IssuanceTokenSet(address(newIssuanceToken), _newDecimals); + emit IBondingCurveBase_v1.IssuanceTokenSet( + address(newIssuanceToken), _newDecimals + ); bondingCurveFundingManager.call_setIssuanceToken( address(newIssuanceToken) ); @@ -1028,35 +1154,40 @@ contract BondingCurveBaseV1Test is ModuleTest { assertEq(internalFunctionReturnValue, functionReturnValue); } - /* Test withdrawProjectCollateralFee function - └── Given the receiver is address zero or equal to bonding curve address - └── When the function withdrawProjectCollateralFee function gets called - └── Then it should revert with invalid receiver + /* + Test: withdrawProjectCollateralFee modifier in postion + ├── Given: buyer is not permissioned + │ └── When: withdrawProjectCollateralFee is called + │ └── Then: it should revert (modifier in position check) + ├── Given: the receiver is address zero or equal to bonding curve address + └── And: buyer is permissioned + └── When: withdrawProjectCollateralFee is called + └── Then: it should revert (modifier in position check) */ - function testWithdrawProjectCollateralFee_revertGivenInvalidReceiver( - uint _amount - ) public { - address receiver = address(0); + function testWithdrawProjectCollateralFee_ModifierInPostion() public { + // permissioned + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( - IBondingCurveBase_v1 - .Module__BondingCurveBase__InvalidRecipient - .selector - ); - bondingCurveFundingManager.withdrawProjectCollateralFee( - receiver, _amount + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.withdrawProjectCollateralFee(address(0), 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); - receiver = address(bondingCurveFundingManager); + // validReceiver vm.expectRevert( IBondingCurveBase_v1 .Module__BondingCurveBase__InvalidRecipient .selector ); - bondingCurveFundingManager.withdrawProjectCollateralFee( - receiver, _amount - ); + bondingCurveFundingManager.withdrawProjectCollateralFee(address(0), 0); } /* Test internal _withdrawProjectCollateralFee function @@ -1118,7 +1249,9 @@ contract BondingCurveBaseV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit ProjectCollateralFeeWithdrawn(receiver, _amount); + emit IBondingCurveBase_v1.ProjectCollateralFeeWithdrawn( + receiver, _amount + ); // Execute function bondingCurveFundingManager.call_withdrawProjectCollateralFee( receiver, _amount @@ -1211,13 +1344,6 @@ contract BondingCurveBaseV1Test is ModuleTest { //-------------------------------------------------------------------------- // Helper functions - // Modifier to ensure the caller has the admin role - modifier callerIsOrchestratorAdmin() { - _authorizer.grantRole(_authorizer.getAdminRole(), admin_address); - vm.startPrank(admin_address); - _; - } - // Helper function that mints enough collateral tokens to a buyer and approves the bonding curve to spend them function _prepareBuyConditions(address buyer, uint amount) internal { _token.mint(buyer, amount); diff --git a/test/unit/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.t.sol b/test/unit/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.t.sol index c412b5189..1fc709070 100644 --- a/test/unit/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.t.sol +++ b/test/unit/modules/fundingManager/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.t.sol @@ -51,16 +51,6 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { address admin_address = address(0xA1BA); address non_admin_address = address(0xB0B); - event SellingEnabled(); - event SellingDisabled(); - event SellFeeUpdated(uint newSellFee, uint oldSellFee); - event TokensSold( - address indexed receiver, - uint depositAmount, - uint receivedAmount, - address seller - ); - function setUp() public { // Deploy contracts address impl = address(new RedeemingBondingCurveBaseV1Mock()); @@ -78,7 +68,8 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { _setUpOrchestrator(bondingCurveFundingManager); - _authorizer.grantRole(_authorizer.getAdminRole(), admin_address); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); // Set max fee of feeManager to 100% for testing purposes vm.prank(address(governor)); @@ -98,7 +89,7 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { ); } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( bondingCurveFundingManager.supportsInterface( type(IRedeemingBondingCurveBase_v1).interfaceId @@ -214,6 +205,105 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { assertEq(_token.balanceOf(seller), 0); } + /* + Test: sellTo Modifier Checks + ├── Given: seller is not permissioned + │ └── When: sellTo is called + │ └── Then: it should revert (modifier in position check) + ├── Given: seller is permissioned + ├── And: buysellinging is not enabled + │ └── When: sellTo is called + │ └── Then: it should revert (modifier in position check) + ├── Given: seller is permissioned + ├── And: selling is enabled + └── And: receiver is invalid + └── When: sellTo is called + └── Then: it should revert (modifier in position check) + */ + + function testsellTo_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.sellTo(address(0), 0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeSell(); + + vm.expectRevert( + IRedeemingBondingCurveBase_v1 + .Module__RedeemingBondingCurveBase__SellingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.sellTo(address(0), 0, 0); + + // Open up Buy again + bondingCurveFundingManager.openSell(); + + // validReceiver + vm.expectRevert( + abi.encodeWithSelector( + IBondingCurveBase_v1 + .Module__BondingCurveBase__InvalidRecipient + .selector + ) + ); + bondingCurveFundingManager.sellTo(address(0), 0, 0); + } + + /* + Test: sell Modifier Checks + ├── Given: seller is not permissioned + │ └── When: sell is called + │ └── Then: it should revert (modifier in position check) + ├── Given: seller is permissioned + └── And: buysellinging is not enabled + └── When: sell is called + └── Then: it should revert (modifier in position check) + + */ + + function testsell_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.sell(0, 0); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // buyingIsEnabled + + // Close buy to check for + bondingCurveFundingManager.closeSell(); + + vm.expectRevert( + IRedeemingBondingCurveBase_v1 + .Module__RedeemingBondingCurveBase__SellingFunctionaltiesClosed + .selector + ); + bondingCurveFundingManager.sell(0, 0); + } + /* Test sell and _sellOrder function ├── when the sell amount is 0 │ └── it should revert @@ -377,7 +467,9 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { vm.expectEmit( true, true, true, true, address(bondingCurveFundingManager) ); - emit TokensSold(seller, amount, finalAmount, seller); + emit IRedeemingBondingCurveBase_v1.TokensSold( + seller, amount, finalAmount, seller + ); // Execution vm.prank(seller); @@ -405,8 +497,8 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { } /* Test openSell function - ├── when caller is not the Orchestrator admin - │ └── it should revert (tested in base Module modifier tests) + ├── when caller is not permissioned + │ └── it should revert (modifier in position check) └── when caller is the Orchestrator admin └── when sell functionality is already open │ └── it should stay as is @@ -415,10 +507,25 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { ├── it should open the sell functionality └── it should emit an event */ + + function testOpenSell_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.openSell(); + } + function testOpenSell_Idempotence() public callerIsOrchestratorAdmin { assertEq(bondingCurveFundingManager.sellIsOpen(), true); vm.expectEmit(address(bondingCurveFundingManager)); - emit SellingEnabled(); + emit IRedeemingBondingCurveBase_v1.SellingEnabled(); bondingCurveFundingManager.openSell(); } @@ -431,7 +538,7 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { assertEq(bondingCurveFundingManager.sellIsOpen(), false); vm.expectEmit(address(bondingCurveFundingManager)); - emit SellingEnabled(); + emit IRedeemingBondingCurveBase_v1.SellingEnabled(); bondingCurveFundingManager.openSell(); @@ -439,8 +546,8 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { } /* Test closeSell function - ├── when caller is not the Orchestrator admin - │ └── it should revert (tested in base Module tests) + ├── when caller is permissioned + │ └── it should revert (modifier in position check) └── when caller is the Orchestrator admin └── when sell functionality is already closed │ └── it should stay as is @@ -449,6 +556,19 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { ├── it should close the sell functionality └── it should emit an event */ + function testCloseSell_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.closeSell(); + } function testCloseSell_FailsIfAlreadyClosed() public @@ -457,13 +577,13 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { assertEq(bondingCurveFundingManager.sellIsOpen(), true); vm.expectEmit(address(bondingCurveFundingManager)); - emit SellingDisabled(); + emit IRedeemingBondingCurveBase_v1.SellingDisabled(); bondingCurveFundingManager.closeSell(); assertEq(bondingCurveFundingManager.sellIsOpen(), false); vm.expectEmit(address(bondingCurveFundingManager)); - emit SellingDisabled(); + emit IRedeemingBondingCurveBase_v1.SellingDisabled(); bondingCurveFundingManager.closeSell(); assertEq(bondingCurveFundingManager.sellIsOpen(), false); @@ -473,15 +593,15 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { assertEq(bondingCurveFundingManager.sellIsOpen(), true); vm.expectEmit(address(bondingCurveFundingManager)); - emit SellingDisabled(); + emit IRedeemingBondingCurveBase_v1.SellingDisabled(); bondingCurveFundingManager.closeSell(); assertEq(bondingCurveFundingManager.sellIsOpen(), false); } /* Test setSellFee and _setSellFee function - ├── when caller is not the Orchestrator admin - │ └── it should revert (tested in base Module tests) + ├── when caller is not permissioned + │ └── it should revert (modifier in position check) └── when caller is the Orchestrator admin └── when fee is over 100% │ └── it should revert @@ -493,6 +613,20 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { └── it should emit an event? */ + function testSetSellFee_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bondingCurveFundingManager.setSellFee(0); + } + function testSetSellFee_FailsIfFeeIsOver100Percent(uint _fee) public callerIsOrchestratorAdmin @@ -514,7 +648,7 @@ contract RedeemingBondingCurveBaseV1Test is ModuleTest { vm.expectEmit( true, true, false, false, address(bondingCurveFundingManager) ); - emit SellFeeUpdated(_fee, oldSellFee); + emit IRedeemingBondingCurveBase_v1.SellFeeUpdated(_fee, oldSellFee); bondingCurveFundingManager.setSellFee(_fee); assertEq(bondingCurveFundingManager.sellFee(), _fee); diff --git a/test/unit/modules/fundingManager/depositVault/FM_DepositVault_v1.t.sol b/test/unit/modules/fundingManager/depositVault/FM_DepositVault_v1.t.sol index 4e5b32849..144d85d6d 100644 --- a/test/unit/modules/fundingManager/depositVault/FM_DepositVault_v1.t.sol +++ b/test/unit/modules/fundingManager/depositVault/FM_DepositVault_v1.t.sol @@ -51,7 +51,7 @@ contract FM_DepositVaultV1Test is ModuleTest { feeManager.setMaxFee(feeManager.BPS()); } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( vault.supportsInterface(type(IFM_DepositVault_v1).interfaceId) ); diff --git a/test/unit/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.t.sol b/test/unit/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.t.sol index 56acbc049..422463c9a 100644 --- a/test/unit/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.t.sol +++ b/test/unit/modules/fundingManager/extensions/FM_EXT_TokenVault_v1.t.sol @@ -40,11 +40,13 @@ contract FM_EXT_TokenVault_v1_Test is ModuleTest { vault = FM_EXT_TokenVault_v1_Exposed(Clones.clone(impl)); _setUpOrchestrator(vault); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); vault.init(_orchestrator, _METADATA, bytes("")); } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( vault.supportsInterface(type(IFM_EXT_TokenVault_v1).interfaceId) ); @@ -62,7 +64,7 @@ contract FM_EXT_TokenVault_v1_Test is ModuleTest { // Modifiers /* Test withdraw() function modifiers in place - ├── Given the caller is not the Orchestrator Admin + ├── Given the caller is not permissioned │ └── And the modifier onlyOrchestratorAdmin is in position │ └── When the function withdraw() is called │ └── Then it should revert @@ -80,15 +82,17 @@ contract FM_EXT_TokenVault_v1_Test is ModuleTest { └── Then it should revert */ - function testWithdraw_onlyOrchestratorAdminModifierInPosition() public { + function testWithdraw_permissionedModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - address(0) + IModule_v1.Module__CallerNotPermissioned.selector ) ); - vm.prank(address(0)); + vm.prank(address(0xB0B)); vault.withdraw(address(0), 0, address(0)); } diff --git a/test/unit/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.t.sol b/test/unit/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.t.sol index bfa0f6d4a..eb27a488c 100644 --- a/test/unit/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.t.sol +++ b/test/unit/modules/fundingManager/oracle/FM_PC_Oracle_Redeeming_v1.t.sol @@ -127,14 +127,8 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { // set oracle address in FM fundingManager.setOracleAddress(address(oracle)); - // Grant whitelist role to test contract - fundingManager.grantModuleRole( - fundingManager.getWhitelistRole(), address(this) - ); - // Grant queue executor role to test contract - fundingManager.grantModuleRole( - fundingManager.getQueueExecutorRole(), address(this) - ); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); // Open buy and sell fundingManager.openBuy(); @@ -218,7 +212,7 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { ├── Then it should return true for supported interfaces └── Then it should return false for unsupported interfaces */ - function testSupportsInterface_worksGivenDifferentInterfaces() public { + function testSupportsInterface() public override(ModuleTest) { // Test - Verify supported interfaces assertTrue( fundingManager.supportsInterface( @@ -308,71 +302,6 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { ); } - /* Test: Function getWhitelistRole() - └── Given we want to get the whitelist role - └── When the function getWhitelistRole() is called - └── Then it should return the correct whitelist role identifier - */ - function testGetWhitelistRole_worksGivenWhitelistRoleRetrieved() public { - // Test - Verify whitelist role - bytes32 expectedRole = bytes32("WHITELIST_ROLE"); - assertEq( - fundingManager.getWhitelistRole(), - expectedRole, - "Incorrect whitelist role identifier" - ); - } - - /* Test: Function: getWhitelistRoleAdmin() - └── Given we want to get the whitelist role admin - └── When the function getWhitelistRoleAdmin() is called - └── Then it should return the correct whitelist role admin identifier - */ - function testGetWhitelistRoleAdmin_worksGivenWhitelistRoleAdminRetrieved() - public - { - // Test - Verify whitelist role admin - bytes32 expectedRole = bytes32("WHITELIST_ROLE_ADMIN"); - assertEq( - fundingManager.getWhitelistRoleAdmin(), - expectedRole, - "Incorrect whitelist role admin identifier" - ); - } - - /* Test: Function getQueueExecutorRole() - └── Given we want to get the queue executor role - └── When the function getQueueExecutorRole() is called - └── Then it should return the correct queue executor role identifier - */ - function testGetQueueExecutorRole_worksGivenQueueExecutorRoleRetrieved() - public - { - // Test - Verify queue executor role - bytes32 expectedRole = bytes32("QUEUE_EXECUTOR_ROLE"); - assertEq( - fundingManager.getQueueExecutorRole(), - expectedRole, - "Incorrect queue executor role identifier" - ); - } - - /* Test: Function getQueueExecutorRoleAdmin() - └── Given we want to get the queue executor role admin - └── When the function getQueueExecutorRoleAdmin() is called - └── Then it should return the correct queue executor role admin identifier - */ - function testGetQueueExecutorRoleAdmin_worksGivenQueueExecutorRoleAdminRetrieved( - ) public { - // Test - Verify queue executor role admin - bytes32 expectedRole = bytes32("QUEUE_EXECUTOR_ROLE_ADMIN"); - assertEq( - fundingManager.getQueueExecutorRoleAdmin(), - expectedRole, - "Incorrect queue executor role admin identifier" - ); - } - /* Test: Function getStaticPriceForBuying() ├── Given we want to get the static price for buying └── When the function getStaticPriceForBuying() is called @@ -559,240 +488,34 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { fundingManager.depositReserve(0); } - /* Test: Function buy() - └── Given a user with WHITELIST_ROLE and buying is open - └── When buy() is called with valid minAmountOut - └── Then it should execute successfully - */ - function testBuy_worksGivenWhitelistedUser() public { - // Setup - uint amount_ = 1e18; - _prepareBuyOrSellConditions( - address(_token), amount_, address(this), address(fundingManager) - ); - - // Setup - Calculate minimum amount out - uint minAmountOut_ = fundingManager.calculatePurchaseReturn(amount_); - - // Test - Should not revert - fundingManager.buy(amount_, minAmountOut_); - } - - /* Test: Function buy() - └── Given a user without WHITELIST_ROLE but buying is open - └── When buy() is called with valid minAmountOut - └── Then it should revert with Module__CallerNotAuthorized error - */ - function testBuy_revertGivenNonWhitelistedUser() public { - // Setup - address nonWhitelisted_ = makeAddr("nonWhitelisted"); - uint amount_ = 1e18; - // Get role for revert - bytes32 roleId = _authorizer.generateRoleId( - address(fundingManager), fundingManager.getWhitelistRole() - ); - - // Test - Switch to non-whitelisted user and expect revert - vm.prank(nonWhitelisted_); - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - nonWhitelisted_ - ) - ); - fundingManager.buy(amount_, amount_); - } - - /* Test: Function buyFor() - └── Given a user with WHITELIST_ROLE and Third Party Operations (TPO) enabled - └── When buyFor() is called - └── Then it should execute successfully - */ - function testBuyFor_worksGivenWhitelistedUserAndTPOEnabled() public { - // Setup - address receiver_ = makeAddr("receiver"); - uint amount_ = 1e18; - _prepareBuyOrSellConditions( - address(_token), amount_, address(this), address(fundingManager) - ); - - fundingManager.exposed_setIsDirectOperationsOnly(false); - - // Setup - Calculate minimum amount out - uint minAmountOut_ = fundingManager.calculatePurchaseReturn(amount_); - - // Test - Should not revert - fundingManager.buyFor(receiver_, amount_, minAmountOut_); - } - - /* Test: Function buyFor() - └── Given a user without WHITELIST_ROLE but Third Party Operations (TPO) enabled - └── When buyFor() is called - └── Then it should revert - */ - function testBuyFor_revertGivenNonWhitelistedUserAndTPOEnabled() public { - // Setup - address nonWhitelisted_ = makeAddr("nonWhitelisted"); - address receiver_ = makeAddr("receiver"); - uint amount_ = 1e18; - _prepareBuyOrSellConditions( - address(_token), amount_, nonWhitelisted_, address(fundingManager) - ); - - fundingManager.exposed_setIsDirectOperationsOnly(false); - - // Setup - Calculate minimum amount out - uint minAmountOut_ = fundingManager.calculatePurchaseReturn(amount_); - - // Test - Switch to non-whitelisted user and expect revert - vm.startPrank(nonWhitelisted_); - vm.expectRevert(); - fundingManager.buyFor(receiver_, amount_, minAmountOut_); - vm.stopPrank(); - } - /* Test: Function buyFor() - └── Given a whitelisted user but Third Party Operations (TPO) disabled + └── Given Third Party Operations (TPO) disabled └── When buyFor() is called - └── Then it should revert + └── Then it should revert (Modifier in place test) */ function testBuyFor_revertGivenTPODisabled() public { - // Setup - address receiver_ = makeAddr("receiver"); - uint amount_ = 1e18; - _prepareBuyOrSellConditions( - address(_token), amount_, address(this), address(fundingManager) - ); - - // Setup - Calculate minimum amount out - uint minAmountOut_ = fundingManager.calculatePurchaseReturn(amount_); - // Test - Should revert as TPO is disabled - vm.expectRevert(); - fundingManager.buyFor(receiver_, amount_, minAmountOut_); - } - - /* Test: Function sell() - └── Given a user with WHITELIST_ROLE and selling is open - └── When sell() is called - └── Then it should execute successfully - */ - function testSell_worksGivenWhitelistedUser() public { - // Setup - uint amount_ = 1e18; - _prepareBuyOrSellConditions( - address(_token), amount_, address(this), address(fundingManager) - ); - - // Setup - Calculate minimum amount out - uint minBuyAmountOut_ = fundingManager.calculatePurchaseReturn(amount_); - - // Test - Should not revert - fundingManager.buy(amount_, minBuyAmountOut_); - - assertEq( - issuanceToken.balanceOf(address(this)), - minBuyAmountOut_, - "Sender balance not decreased correctly" - ); - - uint minSellAmountOut_ = - fundingManager.calculateSaleReturn(minBuyAmountOut_); - - // Test - Should not revert - sell the tokens we received from buying - fundingManager.sell(minBuyAmountOut_, minSellAmountOut_); - - // Test - Verify balances - assertEq(issuanceToken.balanceOf(address(fundingManager)), 0); - } - - /* Test: Function sell() - └── Given a user without WHITELIST_ROLE but selling is open - └── When sell() is called - └── Then it should revert (modifier in place test) - */ - function testSell_revertGivenNonWhitelistedUser() public { - // Setup - address nonWhitelisted_ = makeAddr("nonWhitelisted"); - uint amount_ = 1e18; - // Get role for revert - bytes32 roleId = _authorizer.generateRoleId( - address(fundingManager), fundingManager.getWhitelistRole() - ); - - // Setup - Calculate minimum amount out - uint minAmountOut_ = fundingManager.calculateSaleReturn(amount_); - - // Test - Switch to non-whitelisted user and expect revert - vm.prank(nonWhitelisted_); vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - nonWhitelisted_ - ) + IFM_PC_Oracle_Redeeming_v1 + .Module__FM_PC_ExternalPrice_Redeeming_ThirdPartyOperationsDisabled + .selector ); - fundingManager.sell(amount_, minAmountOut_); - } - /* Test: Function sellTo() - └── Given selling is open - └── And Third Party Operations (TPO) enabled - └── And the caller has WHITELIST_ROLE - └── When sellTo() is called - └── Then it should execute successfully - */ - - function testSellTo_worksGivenWhitelistedUser() public { - // Setup - address receiver_ = makeAddr("receiver"); - uint amount_ = 1e18; - _prepareBuyOrSellConditions( - address(issuanceToken), - amount_, - address(this), - address(fundingManager) - ); - fundingManager.exposed_setIsDirectOperationsOnly(false); - - uint minSellAmountOut_ = fundingManager.calculateSaleReturn(amount_); - - // Test - Should not revert - sell the tokens we received from buying - fundingManager.sellTo(receiver_, amount_, minSellAmountOut_); - - // Test - Verify balances - assertEq(issuanceToken.balanceOf(address(fundingManager)), 0); + fundingManager.buyFor(address(0), 0, 0); } /* Test: Function sellTo() - └── Given selling is open - └── And Third Party Operations (TPO) enabled - └── And the caller has no WHITELIST_ROLE - └── When sellTo() is called - └── Then it should revert (modifier in place test) + └── Given Third Party Operations (TPO) disabled + └── When sellTo() is called + └── Then it should revert (Modifier in place test) */ - function testSellTo_revertGivenNonWhitelistedUser() public { - // Setup - address nonWhitelisted_ = makeAddr("nonWhitelisted"); - address receiver_ = makeAddr("receiver"); - uint amount_ = 1e18; - // Get role for revert - bytes32 roleId = _authorizer.generateRoleId( - address(fundingManager), fundingManager.getWhitelistRole() - ); - // Enable TPO - fundingManager.exposed_setIsDirectOperationsOnly(false); - - // Test - Switch to non-whitelisted user and expect revert - vm.prank(nonWhitelisted_); + function testSellTo_revertGivenTPODisabled() public { + // Test - Should revert as TPO is disabled vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - nonWhitelisted_ - ) + IFM_PC_Oracle_Redeeming_v1 + .Module__FM_PC_ExternalPrice_Redeeming_ThirdPartyOperationsDisabled + .selector ); - fundingManager.sellTo(receiver_, amount_, amount_); + fundingManager.sellTo(address(0), 0, 0); } /* Test: Function transferOrchestratorToken() @@ -939,10 +662,28 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { } /* Test: Function setProjectTreasury() - ├── Given the project treasury is a valid address - │ └── When the function setProjectTreasury() is called - │ └── Then it should set the project treasury correctly + ├── Given: Caller is not permissioned + | └── When the function setProjectTreasury() is called + | └── Then it should revert (modifier in place test) + ├── Given: Caller is permissioned + ├── And: the project treasury is a valid address + └── When the function setProjectTreasury() is called + └── Then it should set the project treasury correctly */ + function testSetProjectTreasury_modifierInPlace() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + fundingManager.setProjectTreasury(address(0)); + } + function testSetProjectTreasury_worksGivenValidAddress( address projectTreasury_ ) public { @@ -957,13 +698,33 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { } /* Test: Function setOracleAddress() - ├── Given the oracle supports the IOraclePrice_v1 interface - │ └── When the function _setOracleAddress() is called - │ └── Then it should set the oracle address correctly - └── Given the oracle does not support the IOraclePrice_v1 interface - └── When the function _setOracleAddress() is called + ├── Given: Caller is not permissioned + | └── When the function setOracleAddress() is called + | └── Then it should revert (modifier in place test) + ├── Given: Caller is permissioned + ├── And: the oracle supports the IOraclePrice_v1 interface + | └── When the function setOracleAddress() is called + | └── Then it should set the oracle address correctly + ├── Given: Caller is permissioned + ├── And: the oracle does not support the IOraclePrice_v1 interface + └── When the function setOracleAddress() is called └── Then it should revert */ + + function testSetOracleAddress_modifierInPlace() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + fundingManager.setOracleAddress(address(0)); + } + function testSetOracleAddress_worksGivenValidOracle(address _oracle) public { @@ -984,10 +745,29 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { } /* Test: Function setIsDirectOperationsOnly() - └── Given a valid value + ├── Given: Caller is not permissioned + | └── When the function setIsDirectOperationsOnly() is called + | └── Then it should revert (modifier in place test) + ├── Given: Caller is permissioned + └── And: Called with a valid value └── When the function exposed_setIsDirectOperationsOnly() is called └── Then the value should be set correctly */ + + function testSetIsDirectOperationsOnly_modifierInPlace() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + fundingManager.setIsDirectOperationsOnly(false); + } + function testSetIsDirectOperationsOnly_worksGivenValidValue( bool _isDirectOperationsOnly ) public { @@ -1003,34 +783,26 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { } /* Test: Function executeRedemptionQueue() - └── Given caller does not have QUEUE_EXECUTOR_ROLE + └── Given caller is not permissioned └── When executeRedemptionQueue() is called └── Then it should revert (modifier in place test) */ - function testExecuteRedemptionQueue_revertGivenCallerDoesNotHaveQueueExecutorRole( - ) public { - // Setup - address nonExecutorRole = makeAddr("nonExecutorRole"); - bytes32 roleId = _authorizer.generateRoleId( - address(fundingManager), fundingManager.getQueueExecutorRole() - ); + function testExecuteRedemptionQueue_modifierInPlace() public { + // permissioned - // Test - Switch to non-executor role user and expect revert - vm.prank(nonExecutorRole); + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - nonExecutorRole + IModule_v1.Module__CallerNotPermissioned.selector ) ); - - // Test + vm.prank(address(0xB0B)); fundingManager.executeRedemptionQueue(); } /* Test: Function executeRedemptionQueue() - ├── Given caller has QUEUE_EXECUTOR_ROLE + ├── Given caller is permissioned └── And the Payment Processor does not have the correct interface └── When executeRedemptionQueue() is called └── Then it should revert @@ -1053,7 +825,7 @@ contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest { } /* Test: Function executeRedemptionQueue() - ├── Given caller has QUEUE_EXECUTOR_ROLE + ├── Given caller is permissioned ├── And there are redemption orders in the queue └── And the payment processor has the correct interface └── When executeRedemptionQueue() is called diff --git a/test/unit/modules/logicModule/LM_Oracle_Permissioned_v1.t.sol b/test/unit/modules/logicModule/LM_Oracle_Permissioned_v1.t.sol index 96a8fd70e..a379536e4 100644 --- a/test/unit/modules/logicModule/LM_Oracle_Permissioned_v1.t.sol +++ b/test/unit/modules/logicModule/LM_Oracle_Permissioned_v1.t.sol @@ -59,13 +59,8 @@ contract LM_Oracle_Permissioned_v1_Test is ModuleTest { bytes memory configData = abi.encode(address(collateralToken)); manualExternalPriceSetter.init(_orchestrator, _METADATA, configData); - // Grant PRICE_SETTER_ROLE and PRICE_SETTER_ROLE_ADMIN to the test contract - manualExternalPriceSetter.grantModuleRole( - manualExternalPriceSetter.getPriceSetterRole(), address(this) - ); - manualExternalPriceSetter.grantModuleRole( - manualExternalPriceSetter.getPriceSetterRoleAdmin(), address(this) - ); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); } // ================================================================================ @@ -87,7 +82,7 @@ contract LM_Oracle_Permissioned_v1_Test is ModuleTest { ); } - function testSupportsInterface_GivenValidInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( manualExternalPriceSetter.supportsInterface( type(ILM_Oracle_Permissioned_v1).interfaceId @@ -99,36 +94,26 @@ contract LM_Oracle_Permissioned_v1_Test is ModuleTest { // Test External (public + external) /* Test: Function SetIssuancePrice() - ├── Given the caller has not PRICE_SETTER_ROLE + ├── Given the caller is not permissioned │ └── When the function setIssuancePrice() is called │ └── Then the function should revert (Modifier in place test) - └── Given the caller has PRICE_SETTER_ROLE + └── Given the caller is permissioned └── When the function setIssuancePrice() is called └── Then the price should be set correctly (redirects to internal func) */ - function testSetIssuancePrice_worksGivenModifierInPlace( - address unauthorized_, - uint price_ - ) public { - // Setup - vm.assume(unauthorized_ != address(this)); - vm.assume(price_ > 0); - bytes32 roleId = _authorizer.generateRoleId( - address(manualExternalPriceSetter), - manualExternalPriceSetter.getPriceSetterRole() - ); + function testSetIssuancePrice_ModifierInPlace() public { + // permissioned - // Test - vm.startPrank(unauthorized_); + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - unauthorized_ + IModule_v1.Module__CallerNotPermissioned.selector ) ); - manualExternalPriceSetter.setIssuancePrice(price_); + vm.prank(address(0xB0B)); + manualExternalPriceSetter.setIssuancePrice(0); } function testSetIssuancePrice_worksGivenPriceIsSet( @@ -159,36 +144,25 @@ contract LM_Oracle_Permissioned_v1_Test is ModuleTest { } /* Test: Function: SetRedemptionPrice() - ├── Given the caller has not PRICE_SETTER_ROLE + ├── Given the caller is not permissioned │ └── When the function setRedemptionPrice() is called │ └── Then the function should revert (Modifier in place test) - └── Given the caller has PRICE_SETTER_ROLE + └── Given the caller is permissioned └── When the function setRedemptionPrice() is called └── Then the price should be set correctly (redirects to internal func) */ - function testSetRedemptionPrice_worksGivenModifierInPlace( - address unauthorized_, - uint price_ - ) public { - // Setup - vm.assume(unauthorized_ != address(this)); - vm.assume(price_ > 0); - bytes32 roleId = _authorizer.generateRoleId( - address(manualExternalPriceSetter), - manualExternalPriceSetter.getPriceSetterRole() - ); - - // Test - vm.startPrank(unauthorized_); + function testSetRedemptionPrice_ModifierInPlace() public { + // permissioned + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - unauthorized_ + IModule_v1.Module__CallerNotPermissioned.selector ) ); - manualExternalPriceSetter.setRedemptionPrice(price_); + vm.prank(address(0xB0B)); + manualExternalPriceSetter.setRedemptionPrice(0); } function testSetRedemptionPrice_worksGivenPriceIsSet( @@ -219,39 +193,26 @@ contract LM_Oracle_Permissioned_v1_Test is ModuleTest { } /* Test: Function: SetIssuanceAndRedemptionPrice() - ├── Given the caller has not PRICE_SETTER_ROLE + ├── Given the caller is not permissioned │ └── When the function setIssuanceAndRedemptionPrice() is called │ └── Then the function should revert (Modifier in place test) - └── Given the caller has PRICE_SETTER_ROLE + └── Given the caller is permissioned └── When the function setIssuanceAndRedemptionPrice() is called └── Then the price should be set correctly (redirects to internal funcs) */ - function testSetIssuanceAndRedemptionPrice_worksGivenModifierInPlace( - address unauthorized_, - uint issuancePrice_, - uint redemptionPrice_ - ) public { - // Setup - vm.assume(unauthorized_ != address(this)); - vm.assume(issuancePrice_ > 0 && redemptionPrice_ > 0); - bytes32 roleId = _authorizer.generateRoleId( - address(manualExternalPriceSetter), - manualExternalPriceSetter.getPriceSetterRole() - ); + function testSetIssuanceAndRedemptionPrice_ModifierInPlace() public { + // permissioned - // Test - vm.startPrank(unauthorized_); + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - unauthorized_ + IModule_v1.Module__CallerNotPermissioned.selector ) ); - manualExternalPriceSetter.setIssuanceAndRedemptionPrice( - issuancePrice_, redemptionPrice_ - ); + vm.prank(address(0xB0B)); + manualExternalPriceSetter.setIssuanceAndRedemptionPrice(0, 0); } function testSetIssuanceAndRedemptionPrice_worksGivenPricesAreSet( diff --git a/test/unit/modules/logicModule/LM_PC_Bounties_v2.t.sol b/test/unit/modules/logicModule/LM_PC_Bounties_v2.t.sol index 1316ec034..9dc7d2266 100644 --- a/test/unit/modules/logicModule/LM_PC_Bounties_v2.t.sol +++ b/test/unit/modules/logicModule/LM_PC_Bounties_v2.t.sol @@ -43,32 +43,6 @@ contract LM_PC_BountiesV1Test is ModuleTest { ILM_PC_Bounties_v2.Contributor[] DEFAULT_CONTRIBUTORS; ILM_PC_Bounties_v2.Contributor[] INVALID_CONTRIBUTORS; - event BountyAdded( - uint indexed bountyId, - uint minimumPayoutAmount, - uint maximumPayoutAmount, - bytes details - ); - - event BountyUpdated(uint indexed bountyId, bytes details); - - event BountyLocked(uint indexed bountyId); - - event ClaimAdded( - uint indexed claimId, - uint indexed bountyId, - ILM_PC_Bounties_v2.Contributor[] contributors, - bytes details - ); - - event ClaimContributorsUpdated( - uint indexed claimId, ILM_PC_Bounties_v2.Contributor[] contributors - ); - - event ClaimDetailsUpdated(uint indexed claimId, bytes details); - - event ClaimVerified(uint indexed claimId); - function setUp() public { // Add Module to Mock Orchestrator_v1 address impl = address(new LM_PC_Bounties_v2_Exposed()); @@ -76,7 +50,8 @@ contract LM_PC_BountiesV1Test is ModuleTest { _setUpOrchestrator(bountyManager); - _authorizer.setIsAuthorized(address(this), true); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); DEFAULT_CONTRIBUTORS.push(ALICE); DEFAULT_CONTRIBUTORS.push(BOB); @@ -89,7 +64,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { //-------------------------------------------------------------------------- // Test: Initialization - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( bountyManager.supportsInterface( type(ILM_PC_Bounties_v2).interfaceId @@ -451,7 +426,9 @@ contract LM_PC_BountiesV1Test is ModuleTest { //Check that internal function is in position vm.expectEmit(true, true, true, true); - emit BountyAdded(1, minimumPayoutAmount, maximumPayoutAmount, details); + emit ILM_PC_Bounties_v2.BountyAdded( + 1, minimumPayoutAmount, maximumPayoutAmount, details + ); bountyManager.addBounty( minimumPayoutAmount, maximumPayoutAmount, details @@ -459,26 +436,26 @@ contract LM_PC_BountiesV1Test is ModuleTest { } function testAddBountyModifierInPosition() public { - // validPayoutAmounts + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( - ILM_PC_Bounties_v2 - .Module__LM_PC_Bounty__InvalidPayoutAmounts - .selector + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) ); + vm.prank(address(0xB0B)); bountyManager.addBounty(0, 0, bytes("")); - // Set this address to not authorized to test the roles correctly - _authorizer.setIsAuthorized(address(this), false); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); - // onlyBountyAdmin + // validPayoutAmounts vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bountyManager), bountyManager.BOUNTY_ISSUER_ROLE() - ), - address(this) - ) + ILM_PC_Bounties_v2 + .Module__LM_PC_Bounty__InvalidPayoutAmounts + .selector ); bountyManager.addBounty(0, 0, bytes("")); } @@ -508,7 +485,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { for (uint i = 0; i < batchSize; i++) { vm.expectEmit(true, true, true, true); - emit BountyAdded( + emit ILM_PC_Bounties_v2.BountyAdded( 1 + i, minimumPayoutAmount, maximumPayoutAmount, details ); } @@ -531,6 +508,23 @@ contract LM_PC_BountiesV1Test is ModuleTest { bytes[] memory details = new bytes[](1); details[0] = bytes(""); + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bountyManager.addBountyBatch( + new uint[](0), maximumPayoutAmounts, details + ); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + // validArrayLengths vm.expectRevert( ILM_PC_Bounties_v2 @@ -550,26 +544,102 @@ contract LM_PC_BountiesV1Test is ModuleTest { bountyManager.addBountyBatch( minimumPayoutAmounts, maximumPayoutAmounts, details ); + } + + //----------------------------------------- + // UpdateBounty + + function testUpdateBounty(bytes calldata details) public { + uint id = bountyManager.addBounty(1, 1, bytes("")); + + vm.expectEmit(true, true, true, true); + emit ILM_PC_Bounties_v2.BountyUpdated(1, details); + + bountyManager.updateBounty(id, details); + + assertEqualBounty(id, 1, 1, details, false); + } + + function testUpdateBountyModifierInPosition() public { + bountyManager.addBounty(1, 1, bytes("")); + + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bountyManager.updateBounty(1, bytes("")); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // validBountyId + vm.expectRevert( + ILM_PC_Bounties_v2.Module__LM_PC_Bounty__InvalidBountyId.selector + ); + bountyManager.updateBounty(0, bytes("")); // Set this address to not authorized to test the roles correctly _authorizer.setIsAuthorized(address(this), false); - // Set maximumPayoutAmounts[0] correctly - maximumPayoutAmounts[0] = 2; + // notLocked + bountyManager.lockBounty(1); + + vm.expectRevert( + ILM_PC_Bounties_v2.Module__LM_PC_Bounty__BountyLocked.selector + ); + bountyManager.updateBounty(1, bytes("")); + } + + //----------------------------------------- + // LockBounty + + function testLockBounty() public { + uint id = bountyManager.addBounty(1, 1, bytes("")); + + vm.expectEmit(true, true, true, true); + emit ILM_PC_Bounties_v2.BountyLocked(1); + + bountyManager.lockBounty(1); + + assertEqualBounty(id, 1, 1, bytes(""), true); + } + + function testLockBountyModifierInPosition() public { + bountyManager.addBounty(1, 1, bytes("")); - // onlyBountyAdmin + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bountyManager), bountyManager.BOUNTY_ISSUER_ROLE() - ), - address(this) + IModule_v1.Module__CallerNotPermissioned.selector ) ); - bountyManager.addBountyBatch( - minimumPayoutAmounts, maximumPayoutAmounts, details + vm.prank(address(0xB0B)); + bountyManager.lockBounty(1); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + + // validBountyId + vm.expectRevert( + ILM_PC_Bounties_v2.Module__LM_PC_Bounty__InvalidBountyId.selector ); + bountyManager.lockBounty(0); + + // NotLocked + bountyManager.lockBounty(1); + vm.expectRevert( + ILM_PC_Bounties_v2.Module__LM_PC_Bounty__BountyLocked.selector + ); + bountyManager.lockBounty(1); } //----------------------------------------- @@ -600,7 +670,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { for (uint i = 0; i < times; i++) { vm.expectEmit(true, true, true, true); // id starts at 2 because the id counter starts at 1 and addBounty increases it by 1 again - emit ClaimAdded(i + 2, 1, contribs, details); + emit ILM_PC_Bounties_v2.ClaimAdded(i + 2, 1, contribs, details); id = bountyManager.addClaim(1, contribs, details); assertEqualClaim(id, 1, contribs, details, false); @@ -615,83 +685,34 @@ contract LM_PC_BountiesV1Test is ModuleTest { function testAddClaimModifierInPosition() public { bountyManager.addBounty(1, 1, bytes("")); - // validBountyId - vm.expectRevert( - ILM_PC_Bounties_v2.Module__LM_PC_Bounty__InvalidBountyId.selector - ); - bountyManager.addClaim(0, DEFAULT_CONTRIBUTORS, bytes("")); - - // _validContributorsForBounty - vm.expectRevert( - ILM_PC_Bounties_v2 - .Module__LM_PC_Bounty__InvalidContributorAmount - .selector - ); - bountyManager.addClaim(1, INVALID_CONTRIBUTORS, bytes("")); - - // notLocked - bountyManager.lockBounty(1); + // permissioned - vm.expectRevert( - ILM_PC_Bounties_v2.Module__LM_PC_Bounty__BountyLocked.selector - ); - bountyManager.addClaim(1, DEFAULT_CONTRIBUTORS, bytes("")); - - // Set this address to not authorized to test the roles correctly - _authorizer.setIsAuthorized(address(this), false); - - // onlyClaimAdmin + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bountyManager), bountyManager.CLAIMANT_ROLE() - ), - address(this) + IModule_v1.Module__CallerNotPermissioned.selector ) ); + vm.prank(address(0xB0B)); bountyManager.addClaim(0, DEFAULT_CONTRIBUTORS, bytes("")); - } - - //----------------------------------------- - // UpdateBounty - - function testUpdateBounty(bytes calldata details) public { - uint id = bountyManager.addBounty(1, 1, bytes("")); - - vm.expectEmit(true, true, true, true); - emit BountyUpdated(1, details); - - bountyManager.updateBounty(id, details); - - assertEqualBounty(id, 1, 1, details, false); - } - function testUpdateBountyModifierInPosition() public { - bountyManager.addBounty(1, 1, bytes("")); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); // validBountyId vm.expectRevert( ILM_PC_Bounties_v2.Module__LM_PC_Bounty__InvalidBountyId.selector ); - bountyManager.updateBounty(0, bytes("")); - - // Set this address to not authorized to test the roles correctly - _authorizer.setIsAuthorized(address(this), false); + bountyManager.addClaim(0, DEFAULT_CONTRIBUTORS, bytes("")); - // onlyBountyAdmin + // _validContributorsForBounty vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bountyManager), bountyManager.BOUNTY_ISSUER_ROLE() - ), - address(this) - ) + ILM_PC_Bounties_v2 + .Module__LM_PC_Bounty__InvalidContributorAmount + .selector ); - bountyManager.updateBounty(1, bytes("")); - // Reset this address to authorized - _authorizer.setIsAuthorized(address(this), true); + bountyManager.addClaim(1, INVALID_CONTRIBUTORS, bytes("")); // notLocked bountyManager.lockBounty(1); @@ -699,53 +720,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { vm.expectRevert( ILM_PC_Bounties_v2.Module__LM_PC_Bounty__BountyLocked.selector ); - bountyManager.updateBounty(1, bytes("")); - } - - //----------------------------------------- - // UpdateBounty - - function testLockBounty() public { - uint id = bountyManager.addBounty(1, 1, bytes("")); - - vm.expectEmit(true, true, true, true); - emit BountyLocked(1); - - bountyManager.lockBounty(1); - - assertEqualBounty(id, 1, 1, bytes(""), true); - } - - function testLockBountyModifierInPosition() public { - bountyManager.addBounty(1, 1, bytes("")); - - // validBountyId - vm.expectRevert( - ILM_PC_Bounties_v2.Module__LM_PC_Bounty__InvalidBountyId.selector - ); - bountyManager.lockBounty(0); - - // NotLocked - bountyManager.lockBounty(1); - vm.expectRevert( - ILM_PC_Bounties_v2.Module__LM_PC_Bounty__BountyLocked.selector - ); - bountyManager.lockBounty(1); - - // Set this address to not authorized to test the roles correctly - _authorizer.setIsAuthorized(address(this), false); - - // onlyBountyAdmin - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bountyManager), bountyManager.BOUNTY_ISSUER_ROLE() - ), - address(this) - ) - ); - bountyManager.lockBounty(0); + bountyManager.addClaim(1, DEFAULT_CONTRIBUTORS, bytes("")); } //----------------------------------------- @@ -771,7 +746,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { uint id = bountyManager.addClaim(1, DEFAULT_CONTRIBUTORS, bytes("")); vm.expectEmit(true, true, true, true); - emit ClaimContributorsUpdated(id, contribs); + emit ILM_PC_Bounties_v2.ClaimContributorsUpdated(id, contribs); bountyManager.updateClaimContributors(id, contribs); @@ -803,6 +778,21 @@ contract LM_PC_BountiesV1Test is ModuleTest { bountyManager.addBounty(1, 100_000_000, bytes("")); // Id 3 bountyManager.addClaim(3, DEFAULT_CONTRIBUTORS, bytes("")); // Id 4 + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + bountyManager.updateClaimContributors(2, DEFAULT_CONTRIBUTORS); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + // validClaimId vm.expectRevert( ILM_PC_Bounties_v2.Module__LM_PC_Bounty__InvalidClaimId.selector @@ -817,24 +807,6 @@ contract LM_PC_BountiesV1Test is ModuleTest { ); bountyManager.updateClaimContributors(2, INVALID_CONTRIBUTORS); - // onlyClaimAdmin - _authorizer.setIsAuthorized(address(this), false); // No access address - vm.expectRevert( - abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bountyManager), bountyManager.CLAIMANT_ROLE() - ), - address(this) - ) - ); - bountyManager.updateClaimContributors(2, DEFAULT_CONTRIBUTORS); - // Reset this address to authorized - _authorizer.setIsAuthorized(address(this), true); - - // Reset this address to be authorized to test correctly - _authorizer.setIsAuthorized(address(this), true); - bountyManager.lockBounty(1); // notLocked @@ -862,7 +834,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { bountyManager.addClaim(1, DEFAULT_CONTRIBUTORS, bytes("")); vm.expectEmit(true, true, true, true); - emit ClaimDetailsUpdated(2, details); + emit ILM_PC_Bounties_v2.ClaimDetailsUpdated(2, details); vm.prank(DEFAULT_CONTRIBUTORS[0].addr); bountyManager.updateClaimDetails(2, details); @@ -935,7 +907,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { uint claimId = bountyManager.addClaim(bountyId, contribs, details); vm.expectEmit(true, true, true, true); - emit ClaimVerified(claimId); + emit ILM_PC_Bounties_v2.ClaimVerified(claimId); bountyManager.verifyClaim(claimId, contribs); @@ -980,23 +952,20 @@ contract LM_PC_BountiesV1Test is ModuleTest { bountyManager.addBounty(1, 100_000_000, bytes("")); // Id 3 bountyManager.addClaim(3, DEFAULT_CONTRIBUTORS, bytes("")); // Id 4 - // Set this address to not authorized to test the roles correctly - _authorizer.setIsAuthorized(address(this), false); + // permissioned - // onlyVerifyAdmin + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(bountyManager), bountyManager.VERIFIER_ROLE() - ), - address(this) + IModule_v1.Module__CallerNotPermissioned.selector ) ); + vm.prank(address(0xB0B)); bountyManager.verifyClaim(0, DEFAULT_CONTRIBUTORS); - // Reset this address to authorized - _authorizer.setIsAuthorized(address(this), true); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); // validClaimId vm.expectRevert( @@ -1068,7 +1037,7 @@ contract LM_PC_BountiesV1Test is ModuleTest { uint id; for (uint i; i < testAmount; i++) { vm.expectEmit(true, true, true, true); - emit BountyAdded( + emit ILM_PC_Bounties_v2.BountyAdded( i + 1, minimumPayoutAmount, maximumPayoutAmount, details ); diff --git a/test/unit/modules/logicModule/LM_PC_KPIRewarder_v2.t.sol b/test/unit/modules/logicModule/LM_PC_KPIRewarder_v2.t.sol index 1f32b2e02..6c61b4f55 100644 --- a/test/unit/modules/logicModule/LM_PC_KPIRewarder_v2.t.sol +++ b/test/unit/modules/logicModule/LM_PC_KPIRewarder_v2.t.sol @@ -61,55 +61,6 @@ contract LM_PC_KPIRewarder_v2Test is ModuleTest { ERC20Mock feeToken = new ERC20Mock("OOV3 Fee Mock Token", "FEE MOCK", 18); uint feeTokenBond; - //========================================================================================= - // Events for emission testing - - event Staked(address indexed user, uint amount); - event DataAsserted( - bytes32 indexed dataId, - bytes32 data, - address indexed asserter, - bytes32 indexed assertionId - ); - event DataAssertionResolved( - bool assertedTruthfully, - bytes32 indexed dataId, - bytes32 data, - address indexed asserter, - bytes32 indexed assertionId - ); - event RewardSet( - uint amount, uint duration, uint rewardRate, uint rewardsEnd - ); - - event KPICreated( - uint indexed KPI_Id, - uint numOfTranches, - uint totalKPIRewards, - bool continuous, - uint[] trancheValues, - uint[] trancheRewards - ); - - event RewardRoundConfigured( - bytes32 indexed assertionId, - uint creationTime, - uint assertedValue, - uint indexed KpiToUse - ); - - event PaymentOrderAdded( - address indexed recipient, - address indexed token, - uint amount, - uint originChainId, - uint targetChainId, - bytes32 flags, - bytes32[] data - ); - - event DeletedStuckAssertion(bytes32 indexed assertionId); - //========================================================================================= // Setup @@ -125,7 +76,8 @@ contract LM_PC_KPIRewarder_v2Test is ModuleTest { _setUpOrchestrator(kpiManager); - _authorizer.setIsAuthorized(address(this), true); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); bytes memory configData = abi.encode( address(stakingToken), @@ -231,16 +183,26 @@ contract LM_PC_KPIRewarder_v2Test is ModuleTest { kpiManager.init(_orchestrator, _METADATA, bytes("")); } - function test_InterfaceInheritanceTree() public view { - kpiManager.supportsInterface(type(ILM_PC_KPIRewarder_v2).interfaceId); - kpiManager.supportsInterface(type(ILM_PC_Staking_v2).interfaceId); - kpiManager.supportsInterface( - type(IOptimisticOracleIntegrator).interfaceId + function testSupportsInterface() public override(ModuleTest) { + assertTrue( + kpiManager.supportsInterface( + type(ILM_PC_KPIRewarder_v2).interfaceId + ) + ); + assertTrue( + kpiManager.supportsInterface(type(ILM_PC_Staking_v2).interfaceId) ); - kpiManager.supportsInterface( - type(OptimisticOracleV3CallbackRecipientInterface).interfaceId + assertTrue( + kpiManager.supportsInterface( + type(IOptimisticOracleIntegrator).interfaceId + ) + ); + assertTrue( + kpiManager.supportsInterface( + type(OptimisticOracleV3CallbackRecipientInterface).interfaceId + ) ); - kpiManager.supportsInterface(type(IModule_v1).interfaceId); + assertTrue(kpiManager.supportsInterface(type(IModule_v1).interfaceId)); } // Creates dummy incontinuous KPI with 3 tranches, a max value of 300 and 300e18 tokens for rewards @@ -313,7 +275,7 @@ contract LM_PC_KPIRewarder_v2Test is ModuleTest { vm.startPrank(cappedUsers[i]); stakingToken.approve(address(kpiManager), cappedAmounts[i]); vm.expectEmit(true, true, true, true, address(kpiManager)); - emit Staked(cappedUsers[i], cappedAmounts[i]); + emit ILM_PC_Staking_v2.Staked(cappedUsers[i], cappedAmounts[i]); kpiManager.stake(cappedAmounts[i]); totalUserFunds += cappedAmounts[i]; vm.stopPrank(); @@ -344,10 +306,6 @@ contract LM_PC_KPIRewarder_v2Test is ModuleTest { else createDummyIncontinuousKPI(); // prepare bond and asserter authorization - kpiManager.grantModuleRole( - kpiManager.ASSERTER_ROLE(), MOCK_ASSERTER_ADDRESS - ); - feeToken.mint( address(MOCK_ASSERTER_ADDRESS), ooV3.getMinimumBond(address(feeToken)) @@ -362,7 +320,7 @@ contract LM_PC_KPIRewarder_v2Test is ModuleTest { // SuT vm.expectEmit(true, false, false, false, address(kpiManager)); - emit DataAsserted( + emit IOptimisticOracleIntegrator.DataAsserted( MOCK_ASSERTION_DATA_ID, bytes32(valueToAssert), MOCK_ASSERTER_ADDRESS, @@ -380,6 +338,8 @@ contract LM_PC_KPIRewarder_v2Test is ModuleTest { /* postAssertionTest +├── when the caller is not permissioned +│ └── it should revert (modifier in position check) ├── when the Asserter is the Module itself │ └── when the default currency is the same as the staking token │ └── it should revert @@ -398,6 +358,22 @@ postAssertionTest */ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { + function test_ModifierInPositionCheck() external { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + kpiManager.postAssertion( + MOCK_ASSERTION_DATA_ID, 100, MOCK_ASSERTER_ADDRESS, 0 + ); + } + function test_RevertWhen_TheBondConfigurationIsInvalid() external { // Since the setup has a correct KPI MAnager, we create a new one with stakingToken == FeeToken @@ -458,9 +434,6 @@ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { createDummyIncontinuousKPI(); // prepare bond and asserter authorization - kpiManager.grantModuleRole( - kpiManager.ASSERTER_ROLE(), MOCK_ASSERTER_ADDRESS - ); feeToken.mint( address(MOCK_ASSERTER_ADDRESS), ooV3.getMinimumBond(address(feeToken)) @@ -473,7 +446,7 @@ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { // SuT vm.expectEmit(true, false, false, false, address(kpiManager)); - emit DataAsserted( + emit IOptimisticOracleIntegrator.DataAsserted( MOCK_ASSERTION_DATA_ID, bytes32(MOCK_ASSERTED_VALUE), MOCK_ASSERTER_ADDRESS, @@ -520,9 +493,6 @@ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { createDummyIncontinuousKPI(); // prepare bond and asserter authorization - kpiManager.grantModuleRole( - kpiManager.ASSERTER_ROLE(), MOCK_ASSERTER_ADDRESS - ); feeToken.mint( address(MOCK_ASSERTER_ADDRESS), ooV3.getMinimumBond(address(feeToken)) @@ -534,7 +504,7 @@ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { // SuT vm.expectEmit(true, false, false, false, address(kpiManager)); - emit DataAsserted( + emit IOptimisticOracleIntegrator.DataAsserted( MOCK_ASSERTION_DATA_ID, bytes32(MOCK_ASSERTED_VALUE), MOCK_ASSERTER_ADDRESS, @@ -542,7 +512,9 @@ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { ); // we don't know the last one vm.expectEmit(false, true, true, true, address(kpiManager)); - emit RewardRoundConfigured(0x0, block.timestamp, 100, 0); // we don't know the generated ID + emit ILM_PC_KPIRewarder_v2.RewardRoundConfigured( + 0x0, block.timestamp, 100, 0 + ); // we don't know the generated ID bytes32 assertionId = kpiManager.postAssertion( MOCK_ASSERTION_DATA_ID, 100, MOCK_ASSERTER_ADDRESS, 0 @@ -585,9 +557,6 @@ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { createDummyIncontinuousKPI(); // prepare bond and asserter authorization - kpiManager.grantModuleRole( - kpiManager.ASSERTER_ROLE(), MOCK_ASSERTER_ADDRESS - ); feeToken.mint( address(MOCK_ASSERTER_ADDRESS), ooV3.getMinimumBond(address(feeToken)) - 1 @@ -615,6 +584,8 @@ contract LM_PC_KPIRewarder_v2_postAssertionTest is LM_PC_KPIRewarder_v2Test { /* createKPITest +├── when the caller is not permissioned +│ └── it should revert (modifier in position check) ├── when the number of tranches is 0 │ └── it should revert ├── when the number of tranches is bigger than 20 @@ -630,6 +601,20 @@ createKPITest */ contract LM_PC_KPIRewarder_v2_createKPITest is LM_PC_KPIRewarder_v2Test { + function test_ModifierInPositionCheck() external { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + kpiManager.createKPI(true, new uint[](0), new uint[](0)); + } + function test_RevertWhen_TheNumberOfTranchesIs0() external { // it should revert @@ -737,7 +722,7 @@ contract LM_PC_KPIRewarder_v2_createKPITest is LM_PC_KPIRewarder_v2Test { } vm.expectEmit(true, true, true, true, address(kpiManager)); - emit KPICreated( + emit ILM_PC_KPIRewarder_v2.KPICreated( 0, numOfTranches, totalRewards, @@ -765,6 +750,8 @@ contract LM_PC_KPIRewarder_v2_createKPITest is LM_PC_KPIRewarder_v2Test { /* stakeTest +├── when the caller is not permissioned +│ └── it should revert (modifier in position check) ├── when the staked amount is 0 │ └── it should revert ├── when there is an unresolved assertion live @@ -776,6 +763,20 @@ stakeTest └── it should stake the funds */ contract LM_PC_KPIRewarder_v2_stakeTest is LM_PC_KPIRewarder_v2Test { + function test_ModifierInPositionCheck() external { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + kpiManager.stake(0); + } + function test_RevertWhen_TheStakedAmountIs0() external { // it should revert @@ -802,9 +803,6 @@ contract LM_PC_KPIRewarder_v2_stakeTest is LM_PC_KPIRewarder_v2Test { createDummyIncontinuousKPI(); // prepare bond and asserter authorization - kpiManager.grantModuleRole( - kpiManager.ASSERTER_ROLE(), MOCK_ASSERTER_ADDRESS - ); feeToken.mint( address(MOCK_ASSERTER_ADDRESS), ooV3.getMinimumBond(address(feeToken)) @@ -817,7 +815,7 @@ contract LM_PC_KPIRewarder_v2_stakeTest is LM_PC_KPIRewarder_v2Test { // SuT vm.expectEmit(true, false, false, false, address(kpiManager)); - emit DataAsserted( + emit IOptimisticOracleIntegrator.DataAsserted( MOCK_ASSERTION_DATA_ID, bytes32(MOCK_ASSERTED_VALUE), MOCK_ASSERTER_ADDRESS, @@ -920,7 +918,7 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is vm.expectEmit(true, true, true, true, address(kpiManager)); // vm.expectEmit(false, false, false, false); - emit DataAssertionResolved( + emit IOptimisticOracleIntegrator.DataAssertionResolved( false, MOCK_ASSERTION_DATA_ID, bytes32(assertedIntermediateValue), @@ -964,7 +962,7 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is vm.startPrank(address(ooV3)); vm.expectEmit(true, true, true, true, address(kpiManager)); - emit DataAssertionResolved( + emit IOptimisticOracleIntegrator.DataAssertionResolved( true, MOCK_ASSERTION_DATA_ID, bytes32(assertedIntermediateValue), @@ -973,7 +971,7 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is ); vm.expectEmit(true, true, true, true, address(kpiManager)); - emit RewardSet(250e32, 1, 250e32, block.timestamp + 1); + emit ILM_PC_Staking_v2.RewardSet(250e32, 1, 250e32, block.timestamp + 1); kpiManager.assertionResolvedCallback(createdID, true); vm.stopPrank(); @@ -1015,7 +1013,7 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is if (earnedReward > 0) { vm.expectEmit(true, true, true, true, address(kpiManager)); - emit PaymentOrderAdded( + emit IERC20PaymentClientBase_v2.PaymentOrderAdded( users[i], address(_token), earnedReward, @@ -1056,7 +1054,7 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is vm.startPrank(address(ooV3)); vm.expectEmit(true, true, true, true, address(kpiManager)); - emit DataAssertionResolved( + emit IOptimisticOracleIntegrator.DataAssertionResolved( true, MOCK_ASSERTION_DATA_ID, bytes32(assertedIntermediateValue), @@ -1065,7 +1063,7 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is ); vm.expectEmit(true, true, true, true, address(kpiManager)); - emit RewardSet(200e32, 1, 200e32, block.timestamp + 1); + emit ILM_PC_Staking_v2.RewardSet(200e32, 1, 200e32, block.timestamp + 1); kpiManager.assertionResolvedCallback(createdID, true); vm.stopPrank(); @@ -1106,7 +1104,7 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is if (earnedReward > 0) { vm.expectEmit(true, true, true, true, address(kpiManager)); - emit PaymentOrderAdded( + emit IERC20PaymentClientBase_v2.PaymentOrderAdded( users[i], address(_token), earnedReward, @@ -1210,6 +1208,8 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is /* testDeleteStuckAssertion + ├── When the caller is not permissioned + │ └── It should revert (modifier in position check) ├── When the assertion isn't stored locally │ └── It should revert |── When the assertion hasn't expired yet @@ -1226,6 +1226,20 @@ contract LM_PC_KPIRewarder_v2_assertionresolvedCallbackTest is contract LM_PC_KPIRewarder_v2_deleteStuckAssertionTest is LM_PC_KPIRewarder_v2Test { + function test_ModifierInPositionCheck() external { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + kpiManager.deleteStuckAssertion(0); + } + function test_RevertWhen_TheAssertionIsntStoredLocally(bytes32 assertionId) external { @@ -1328,7 +1342,7 @@ contract LM_PC_KPIRewarder_v2_deleteStuckAssertionTest is feeToken.burn(address(ooV3), ooV3.getMinimumBond(address(feeToken))); vm.expectEmit(true, true, true, true, address(kpiManager)); - emit DeletedStuckAssertion(createdID); + emit ILM_PC_KPIRewarder_v2.DeletedStuckAssertion(createdID); kpiManager.deleteStuckAssertion(createdID); // Check assertion data is deleted diff --git a/test/unit/modules/logicModule/LM_PC_PaymentRouter_v2.t.sol b/test/unit/modules/logicModule/LM_PC_PaymentRouter_v2.t.sol index 364f7aa23..fc307966f 100644 --- a/test/unit/modules/logicModule/LM_PC_PaymentRouter_v2.t.sol +++ b/test/unit/modules/logicModule/LM_PC_PaymentRouter_v2.t.sol @@ -74,27 +74,11 @@ contract LM_PC_PaymentRouter_v2_Test is ModuleTest { paymentRouter.init(_orchestrator, _METADATA, bytes("")); - bytes32 roleId = _authorizer.generateRoleId( - address(paymentRouter), paymentRouter.PAYMENT_PUSHER_ROLE() - ); - - _authorizer.grantRole(roleId, paymentPusher_user); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); } function testInit() public override(ModuleTest) { - bytes32 roleId = _authorizer.generateRoleId( - address(paymentRouter), paymentRouter.PAYMENT_PUSHER_ROLE() - ); - - assertEq(_authorizer.hasRole(roleId, paymentPusher_user), true); - assertEq( - _authorizer.checkRoleMembership(roleId, paymentPusher_user), true - ); - - vm.startPrank(address(paymentRouter)); - assertEq(_authorizer.checkForRole(roleId, paymentPusher_user), true); - vm.stopPrank(); - assertEq(paymentRouter.getFlagCount(), 3); bytes32 _START_END_CLIFF_FLAG = 0x000000000000000000000000000000000000000000000000000000000000000e; @@ -105,12 +89,20 @@ contract LM_PC_PaymentRouter_v2_Test is ModuleTest { vm.expectRevert(OZErrors.Initializable__InvalidInitialization); paymentRouter.init(_orchestrator, _METADATA, bytes("")); } + + function testSupportsInterface() public override(ModuleTest) { + assertTrue( + paymentRouter.supportsInterface( + type(ILM_PC_PaymentRouter_v2).interfaceId + ) + ); + } } /* test_pushPayment - ├── When the caller doesn't have the PAYMENT_PUSHER_ROLE - │ └── It should revert + ├── When the caller is not permissioned + │ └── It should revert (modifier in position check) └── When the caller has the PAYMENT_PUSHER_ROLE ├── When the Payment Order is incorrect │ ├── When the recipient is incorrect @@ -128,31 +120,33 @@ contract LM_PC_PaymentRouter_v2_Test is ModuleTest { contract LM_PC_PaymentRouter_v2_Test_pushPayment is LM_PC_PaymentRouter_v2_Test { - function test_WhenTheCallerDoesntHaveThePAYMENT_PUSHER_ROLE(address caller) - external - { - // It should revert - _assumeValidAddress(caller); - vm.assume(caller != paymentPusher_user); - vm.startPrank(caller); + function test_ModifierInPositionCheck() external { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(paymentRouter), paymentRouter.PAYMENT_PUSHER_ROLE() - ), - caller + IModule_v1.Module__CallerNotPermissioned.selector ) ); + vm.prank(address(0xB0B)); paymentRouter.pushPayment(address(0), address(0), 0, 0, 0, 0); - vm.stopPrank(); } } +/* @todo Where is this? @0xNuggan + └── When the Payment Order is correct + ├── It should add the Payment Order to the array of Payment Orders + ├── It should emit an event + ├── It should call processPayments + └── It should emit an event +*/ + /* pushPaymentBatched ├── When the caller doesn't have the PAYMENT_PUSHER_ROLE - │ └── It should revert + │ └── It should revert (modifier in position check) └── When the caller has the PAYMENT_PUSHER_ROLE ├── When the Payment Order arrays are incorrect │ ├── When the array lengths are mismatched @@ -192,37 +186,23 @@ contract LM_PC_PaymentRouter_v2_Test_pushPaymentBatched is ends[1] = po_end + 500; } - modifier whenTheCallerHasThePAYMENT_PUSHER_ROLE() { - vm.startPrank(paymentPusher_user); - _; - } + function test_ModifierInPositionCheck() external { + // permissioned - function test_WhenTheCallerDoesntHaveThePAYMENT_PUSHER_ROLE(address caller) - external - { - // It should revert - _assumeValidAddress(caller); - vm.assume(caller != paymentPusher_user); - vm.startPrank(caller); + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.generateRoleId( - address(paymentRouter), paymentRouter.PAYMENT_PUSHER_ROLE() - ), - caller + IModule_v1.Module__CallerNotPermissioned.selector ) ); + vm.prank(address(0xB0B)); paymentRouter.pushPaymentBatched( 0, new address[](0), new address[](0), new uint[](0), 0, 0, 0 ); - vm.stopPrank(); } - function test_WhenTheArrayLengthsAreMismatched() - external - whenTheCallerHasThePAYMENT_PUSHER_ROLE - { + function test_WhenTheArrayLengthsAreMismatched() external { // It should revert with the corresponding error message vm.expectRevert( @@ -284,15 +264,11 @@ contract LM_PC_PaymentRouter_v2_Test_pushPaymentBatched is function test_WhenTheParamatersOfASpecificPaymentOrderAreIncorrect() external - whenTheCallerHasThePAYMENT_PUSHER_ROLE { // It was tested upstream } - function test_WhenThePaymentOrdersAreCorrect(uint8 _numOfOrders) - external - whenTheCallerHasThePAYMENT_PUSHER_ROLE - { + function test_WhenThePaymentOrdersAreCorrect(uint8 _numOfOrders) external { vm.assume(_numOfOrders < 20); // It should add all Payment Orders // It should emit an event for each Payment Order diff --git a/test/unit/modules/logicModule/LM_PC_RecurringPayments_v2.t.sol b/test/unit/modules/logicModule/LM_PC_RecurringPayments_v2.t.sol index f83e7c65e..02a063fa5 100644 --- a/test/unit/modules/logicModule/LM_PC_RecurringPayments_v2.t.sol +++ b/test/unit/modules/logicModule/LM_PC_RecurringPayments_v2.t.sol @@ -33,30 +33,20 @@ contract LM_PC_RecurringV1Test is ModuleTest { bytes32 private constant _FLAGS_SET = 0x000000000000000000000000000000000000000000000000000000000000000a; - event RecurringPaymentAdded( - uint indexed recurringPaymentId, - uint amount, - uint startEpoch, - uint lastTriggeredEpoch, - address recipient - ); - event RecurringPaymentRemoved(uint indexed recurringPaymentId); - event RecurringPaymentsTriggered(uint indexed currentEpoch); - event EpochLengthSet(uint epochLength); - function setUp() public { // Add Module to Mock Orchestrator_v1 address impl = address(new LM_PC_RecurringPayments_v2()); recurringPaymentManager = LM_PC_RecurringPayments_v2(Clones.clone(impl)); _setUpOrchestrator(recurringPaymentManager); - _authorizer.setIsAuthorized(address(this), true); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); } //-------------------------------------------------------------------------- // Test: Initialization - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( recurringPaymentManager.supportsInterface( type(ILM_PC_RecurringPayments_v2).interfaceId @@ -73,7 +63,7 @@ contract LM_PC_RecurringV1Test is ModuleTest { ); vm.expectEmit(true, true, true, true); - emit EpochLengthSet(1 weeks); + emit ILM_PC_RecurringPayments_v2.EpochLengthSet(1 weeks); // Init Module wrongly recurringPaymentManager.init( @@ -206,7 +196,7 @@ contract LM_PC_RecurringV1Test is ModuleTest { startEpoch = bound(startEpoch, currentEpoch, type(uint).max); vm.expectEmit(true, true, true, true); - emit RecurringPaymentAdded( + emit ILM_PC_RecurringPayments_v2.RecurringPaymentAdded( 1, // Id starts at 1 amount, startEpoch, @@ -226,7 +216,7 @@ contract LM_PC_RecurringV1Test is ModuleTest { uint length = bound(amount, 1, 30); // Reasonable amount for (uint i = 2; i < length + 2; i++) { vm.expectEmit(true, true, true, true); - emit RecurringPaymentAdded( + emit ILM_PC_RecurringPayments_v2.RecurringPaymentAdded( i, // Id starts at 1 1, currentEpoch, @@ -252,17 +242,21 @@ contract LM_PC_RecurringV1Test is ModuleTest { // Warp to a reasonable time vm.warp(2 weeks); - // onlyOrchestratorAdmin + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - address(0xBEEF) + IModule_v1.Module__CallerNotPermissioned.selector ) ); - vm.prank(address(0xBEEF)); // Not Authorized + vm.prank(address(0xB0B)); recurringPaymentManager.addRecurringPayment(1, 2 weeks, address(0xBEEF)); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + // validAmount vm.expectRevert( IERC20PaymentClientBase_v2 @@ -315,7 +309,7 @@ contract LM_PC_RecurringV1Test is ModuleTest { uint id = i + 1; // Note that id's start at 1. vm.expectEmit(true, true, true, true); - emit RecurringPaymentRemoved(id); + emit ILM_PC_RecurringPayments_v2.RecurringPaymentRemoved(id); recurringPaymentManager.removeRecurringPayment(_SENTINEL, id); assertEq( @@ -357,10 +351,12 @@ contract LM_PC_RecurringV1Test is ModuleTest { // Check if trigger was called vm.expectEmit(true, true, true, true); - emit RecurringPaymentsTriggered(currentEpoch); + emit ILM_PC_RecurringPayments_v2.RecurringPaymentsTriggered( + currentEpoch + ); vm.expectEmit(true, true, true, true); - emit RecurringPaymentRemoved(id); + emit ILM_PC_RecurringPayments_v2.RecurringPaymentRemoved(id); recurringPaymentManager.removeRecurringPayment(prevId, id); assertEq( @@ -379,15 +375,16 @@ contract LM_PC_RecurringV1Test is ModuleTest { _orchestrator, _METADATA, abi.encode(1 weeks) ); - // onlyOrchestratorAdmin + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - address(0xBEEF) + IModule_v1.Module__CallerNotPermissioned.selector ) ); - vm.prank(address(0xBEEF)); // Not Authorized + vm.prank(address(0xB0B)); recurringPaymentManager.removeRecurringPayment(0, 1); } @@ -420,7 +417,9 @@ contract LM_PC_RecurringV1Test is ModuleTest { // Payout created Payments via trigger vm.expectEmit(true, true, true, true); - emit RecurringPaymentsTriggered(currentEpoch); + emit ILM_PC_RecurringPayments_v2.RecurringPaymentsTriggered( + currentEpoch + ); recurringPaymentManager.trigger(); ILM_PC_RecurringPayments_v2.RecurringPayment[] memory @@ -455,7 +454,9 @@ contract LM_PC_RecurringV1Test is ModuleTest { ); currentEpoch = recurringPaymentManager.getCurrentEpoch(); vm.expectEmit(true, true, true, true); - emit RecurringPaymentsTriggered(currentEpoch); + emit ILM_PC_RecurringPayments_v2.RecurringPaymentsTriggered( + currentEpoch + ); recurringPaymentManager.trigger(); currentRecurringPayments = fetchRecurringPayments(); @@ -518,7 +519,9 @@ contract LM_PC_RecurringV1Test is ModuleTest { uint currentEpoch = recurringPaymentManager.getCurrentEpoch(); vm.expectEmit(true, true, true, true); - emit RecurringPaymentsTriggered(currentEpoch); + emit ILM_PC_RecurringPayments_v2.RecurringPaymentsTriggered( + currentEpoch + ); recurringPaymentManager.triggerFor(startId, endId); // Get currentPayments and filter them diff --git a/test/unit/modules/logicModule/LM_PC_Staking_v2.t.sol b/test/unit/modules/logicModule/LM_PC_Staking_v2.t.sol index 8e4fbd706..d8d951da1 100644 --- a/test/unit/modules/logicModule/LM_PC_Staking_v2.t.sol +++ b/test/unit/modules/logicModule/LM_PC_Staking_v2.t.sol @@ -41,31 +41,17 @@ contract LM_PC_Staking_v2Test is ModuleTest { uint internal initialStakerMaxAmount = 100; uint internal tokenMultiplicator = 1e18; - // Events - - event RewardSet( - uint rewardAmount, uint duration, uint newRewardRate, uint newRewardsEnd - ); - event Staked(address indexed user, uint amount); - event Unstaked(address indexed user, uint amount); - event RewardsDistributed(address indexed user, uint amount); - event Updated( - address indexed triggerAddress, - uint rewardValue, - uint lastUpdate, - uint earnedRewards - ); - event StakingTokenSet(address indexed token); - function setUp() public { // Add Module to Mock Orchestrator address impl = address(new LM_PC_Staking_v2_Exposed()); stakingManager = LM_PC_Staking_v2_Exposed(Clones.clone(impl)); _setUpOrchestrator(stakingManager); - _authorizer.setIsAuthorized(address(this), true); + // Every caller has permission for every permissioned function + _authorizer.setAllAuthorized(true); + vm.expectEmit(true, true, true, true); - emit StakingTokenSet(address(stakingToken)); + emit ILM_PC_Staking_v2.StakingTokenSet(address(stakingToken)); stakingManager.init( _orchestrator, _METADATA, abi.encode(address(stakingToken)) ); @@ -102,6 +88,14 @@ contract LM_PC_Staking_v2Test is ModuleTest { ); } + function testSupportsInterface() public override(ModuleTest) { + assertTrue( + stakingManager.supportsInterface( + type(ILM_PC_Staking_v2).interfaceId + ) + ); + } + //-------------------------------------------------------------------------- // Modifier @@ -248,7 +242,7 @@ contract LM_PC_Staking_v2Test is ModuleTest { uint expectedEarnings = stakingManager.getEarned(staker); vm.expectEmit(true, true, true, true); - emit Staked(staker, stakeAmount); + emit ILM_PC_Staking_v2.Staked(staker, stakeAmount); vm.prank(staker); stakingManager.stake(stakeAmount); @@ -276,6 +270,21 @@ contract LM_PC_Staking_v2Test is ModuleTest { stakingManager.stake(0); + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + stakingManager.stake(1); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + // Check for reentrancy // Set it so that the stakingToken does a reentrancy call on the stakingManager @@ -345,7 +354,7 @@ contract LM_PC_Staking_v2Test is ModuleTest { uint expectedEarnings = stakingManager.getEarned(staker); vm.expectEmit(true, true, true, true); - emit Unstaked(staker, unstakeAmount); + emit ILM_PC_Staking_v2.Unstaked(staker, unstakeAmount); // Withdraw vm.prank(staker); @@ -376,6 +385,21 @@ contract LM_PC_Staking_v2Test is ModuleTest { stakingManager.unstake(0); + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + stakingManager.unstake(1); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); + // Check for reentrancy // Set it so that the stakingToken does a reentrancy call on the stakingManager @@ -454,7 +478,9 @@ contract LM_PC_Staking_v2Test is ModuleTest { } vm.expectEmit(true, true, true, true); - emit RewardSet(amount, duration, expectedRewardRate, expectedRewardsEnd); + emit ILM_PC_Staking_v2.RewardSet( + amount, duration, expectedRewardRate, expectedRewardsEnd + ); stakingManager.setRewards(amount, duration); @@ -483,7 +509,7 @@ contract LM_PC_Staking_v2Test is ModuleTest { } vm.expectEmit(true, true, true, true); - emit RewardSet( + emit ILM_PC_Staking_v2.RewardSet( secondAmount, secondDuration, expectedRewardRate, expectedRewardsEnd ); @@ -494,17 +520,17 @@ contract LM_PC_Staking_v2Test is ModuleTest { } function testSetRewardsModifierInPosition() public { - // onlyOrchestratorAdmin + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - _authorizer.getAdminRole(), - address(0xBEEF) + IModule_v1.Module__CallerNotPermissioned.selector ) ); - - vm.prank(address(0xBEEF)); - stakingManager.setRewards(1, 1); + vm.prank(address(0xB0B)); + stakingManager.stake(1); // validAmount vm.expectRevert( @@ -544,7 +570,7 @@ contract LM_PC_Staking_v2Test is ModuleTest { stakingManager.direct_calculateRewardValue(); } vm.expectEmit(true, true, true, false); - emit Updated( + emit ILM_PC_Staking_v2.Updated( trigger, expectedRewards, stakingManager.getLastUpdate(), 0 ); @@ -641,7 +667,7 @@ contract LM_PC_Staking_v2Test is ModuleTest { stakingManager.direct_update(user); vm.expectEmit(true, true, true, true); - emit RewardsDistributed(user, expectedPayout); + emit ILM_PC_Staking_v2.RewardsDistributed(user, expectedPayout); stakingManager.direct_distributeRewards(user); diff --git a/test/unit/modules/logicModule/abstract/ERCPaymentClientBase_v2.t.sol b/test/unit/modules/logicModule/abstract/ERCPaymentClientBase_v2.t.sol index b4f561003..780a22251 100644 --- a/test/unit/modules/logicModule/abstract/ERCPaymentClientBase_v2.t.sol +++ b/test/unit/modules/logicModule/abstract/ERCPaymentClientBase_v2.t.sol @@ -75,7 +75,7 @@ contract ERC20PaymentClientBaseV2Test is ModuleTest { function testReinitFails() public override {} - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( paymentClient.supportsInterface( type(IERC20PaymentClientBase_v2).interfaceId diff --git a/test/unit/modules/logicModule/abstract/oracle/OptimisticOracleIntegrator.t.sol b/test/unit/modules/logicModule/abstract/oracle/OptimisticOracleIntegrator.t.sol index 711deebf8..6d1d8d656 100644 --- a/test/unit/modules/logicModule/abstract/oracle/OptimisticOracleIntegrator.t.sol +++ b/test/unit/modules/logicModule/abstract/oracle/OptimisticOracleIntegrator.t.sol @@ -7,6 +7,9 @@ import "forge-std/console.sol"; import {OptimisticOracleIntegratorMock} from "@mocks/modules/logicModule/oracle/OptimisiticOracleIntegratorMock.sol"; +import {OptimisticOracleV3CallbackRecipientInterface} from + "@lm/abstracts/oracleIntegrations/UMA_OptimisticOracleV3/optimistic-oracle-v3/interfaces/OptimisticOracleV3CallbackRecipientInterface.sol"; + import {OptimisticOracleV3Mock} from "@mocks/modules/logicModule/oracle/OptimisiticOracleV3Mock.sol"; @@ -120,6 +123,19 @@ contract OptimisticOracleIntegratorTest is ModuleTest { ooIntegrator.init(_orchestrator, _METADATA, bytes("")); } + function testSupportsInterface() public override(ModuleTest) { + assertTrue( + ooIntegrator.supportsInterface( + type(IOptimisticOracleIntegrator).interfaceId + ) + ); + assertTrue( + ooIntegrator.supportsInterface( + type(OptimisticOracleV3CallbackRecipientInterface).interfaceId + ) + ); + } + // Tests /* @@ -161,8 +177,9 @@ contract OptimisticOracleIntegratorTest is ModuleTest { // Setter Functions /* - When the caller is not the admin - reverts (tested in module tests) + Test: setDefaultCurrencyAndBond + When the caller is not permissioned + It should revert (modifier in position check) When the caller is the admin when the address is 0 reverts @@ -177,6 +194,20 @@ contract OptimisticOracleIntegratorTest is ModuleTest { */ + function testSetDefaultCurrencyAndBond_modifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + ooIntegrator.setDefaultCurrencyAndBond(address(0), 0); + } + function testsetDefaultCurrencyAndBondFails_whenNewCurrencyIsZero() public { @@ -221,8 +252,9 @@ contract OptimisticOracleIntegratorTest is ModuleTest { } /* - When the caller is not the admin - reverts (tested in module tests) + Test: setOptimisticOracle + When the caller is not permissioned + It should revert (modifier in position check) When the caller is the admin when the address is 0 reverts @@ -232,6 +264,21 @@ contract OptimisticOracleIntegratorTest is ModuleTest { sets the new address as optimistic oracle emits an event */ + + function testSetOptimisticOracle_modifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + ooIntegrator.setOptimisticOracle(address(0)); + } + function testSetOptimisticOracleFails_WhenNewOracleIsZero() public { vm.expectRevert( IOptimisticOracleIntegrator @@ -259,8 +306,9 @@ contract OptimisticOracleIntegratorTest is ModuleTest { } /* - When the caller is not the admin - reverts (tested in module tests) + Test: setDefaultAssertionLiveness + When the caller is not permissioned + It should revert (modifier in position check) When the caller is the admin when the liveness is below 6 hours reverts @@ -268,6 +316,19 @@ contract OptimisticOracleIntegratorTest is ModuleTest { sets the new liveness emits an event */ + function testSetDefaultAssertionLiveness_modifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + ooIntegrator.setDefaultAssertionLiveness(0); + } function testSetDefaultAssertionLivenessFails_whenLivenessLessThanSixHours( uint64 newLiveness @@ -288,8 +349,9 @@ contract OptimisticOracleIntegratorTest is ModuleTest { } /* - When the caller does not have asserter role - reverts + Test: assertDataFor + When the caller is not permissioned + It should revert (modifier in position check) when the caller has the asserter role when the asserter address is 0 it uses msgSender as asserter address @@ -311,21 +373,17 @@ contract OptimisticOracleIntegratorTest is ModuleTest { */ - function testAssertDataForFails_whenCallerDoesNotHaveAsserterRole() - public - { - bytes32 roleId = _authorizer.generateRoleId( - address(ooIntegrator), ooIntegrator.ASSERTER_ROLE() - ); + function testAssertDataFor_modifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions _authorizer.setAllAuthorized(false); - vm.prank(address(0xBEEF)); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - address(0xBEEF) + IModule_v1.Module__CallerNotPermissioned.selector ) ); + vm.prank(address(0xB0B)); ooIntegrator.assertDataFor( MOCK_ASSERTION_DATA_ID, MOCK_ASSERTION_DATA, MOCK_ASSERTER_ADDRESS ); diff --git a/test/unit/modules/paymentProcessor/PP_Queue_ManualExecution_v1.t.sol b/test/unit/modules/paymentProcessor/PP_Queue_ManualExecution_v1.t.sol index 5887081a4..3ff9038f8 100644 --- a/test/unit/modules/paymentProcessor/PP_Queue_ManualExecution_v1.t.sol +++ b/test/unit/modules/paymentProcessor/PP_Queue_ManualExecution_v1.t.sol @@ -47,7 +47,6 @@ contract PP_Queue_ManualExecution_v1_Test is PP_Queue_v1_Test { // Setup orchestrator once _setUpOrchestrator(queueManualExecution); - _authorizer.setIsAuthorized(address(this), true); // Initialize queue manual execution queueManualExecution.init( @@ -68,6 +67,9 @@ contract PP_Queue_ManualExecution_v1_Test is PP_Queue_v1_Test { paymentClient.init(_orchestrator, _METADATA, bytes("")); paymentClient.setIsAuthorized(address(queueManualExecution), true); paymentClient.setToken(_token); + + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); } /* Test testPublicProcessPayments_succeedsGivenValidSetupAndPaymentOrder() function diff --git a/test/unit/modules/paymentProcessor/PP_Queue_v1.t.sol b/test/unit/modules/paymentProcessor/PP_Queue_v1.t.sol index ef8ae2441..ca58ccb25 100644 --- a/test/unit/modules/paymentProcessor/PP_Queue_v1.t.sol +++ b/test/unit/modules/paymentProcessor/PP_Queue_v1.t.sol @@ -99,8 +99,8 @@ contract PP_Queue_v1_Test is ModuleTest { paymentClient.setIsAuthorized(address(queue), true); paymentClient.setToken(_token); - // Authorize test contract - _authorizer.setIsAuthorized(address(this), true); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); } // ================================================================================ @@ -121,7 +121,7 @@ contract PP_Queue_v1_Test is ModuleTest { ); } - function testSupportsInterface() public { + function testSupportsInterface() public override(ModuleTest) { assertTrue( queue.supportsInterface(type(IPaymentProcessor_v2).interfaceId) ); @@ -453,28 +453,22 @@ contract PP_Queue_v1_Test is ModuleTest { // Test Set Max Orders Per Execution /* Test: Function setMaxOrdersPerExecution() - └── Given the caller does not have the QUEUE_OPERATOR_ROLE_ADMIN role - └── When the function setMaxOrdersPerExecution is called - └── Then it should revert + └── Given: Caller is not permissioned + └── When: the function setMaxOrdersPerExecution() is called + └── Then: it should revert (modifier in place test) */ - function testSetMaxOrdersPerExecution_revertGivenNonQueueOperator( - address nonQueueOperator_ - ) public { - // Setup - vm.assume(nonQueueOperator_ != address(this)); - bytes32 roleId = _authorizer.generateRoleId( - address(queue), queue.getQueueOperatorRole() - ); + function testSetMaxOrdersPerExecution_ModifierInPlace() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( abi.encodeWithSelector( - IModule_v1.Module__CallerNotAuthorized.selector, - roleId, - nonQueueOperator_ + IModule_v1.Module__CallerNotPermissioned.selector ) ); - // Test - vm.prank(nonQueueOperator_); + vm.prank(address(0xB0B)); queue.setMaxOrdersPerExecution(100); } @@ -1547,20 +1541,6 @@ contract PP_Queue_v1_Test is ModuleTest { ); } - /* Test testGetQueueOperatorRole_GivenValidRole() - └── Given a queue operator role - └── When getting the role - └── Then it should return correct role hash. - */ - function testGetQueueOperatorRole_GivenValidRole() public { - bytes32 expectedRole_ = bytes32("QUEUE_OPERATOR_ROLE"); - assertEq( - queue.getQueueOperatorRole(), - expectedRole_, - "Role hash should match." - ); - } - // ================================================================================ // Test Process Next Order @@ -2934,6 +2914,28 @@ contract PP_Queue_v1_Test is ModuleTest { ); } + /* + Test: cancelPaymentOrderThroughQueueId + └── Given: Caller is not permissioned + └── When the function cancelPaymentOrderThroughQueueId() is called + └── Then it should revert (modifier in place test) + */ + function testCancelPaymentOrderThroughQueueId_ModifierInPlace() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + queue.cancelPaymentOrderThroughQueueId( + 0, IERC20PaymentClientBase_v2(address(0)) + ); + } + /* Test: function cancelPaymentOrderThroughQueueId() └── Given the canceledOrdeTreasury address is blacklisted └── When the function cancelPaymentOrderThroughQueueId() is called @@ -3144,53 +3146,33 @@ contract PP_Queue_v1_Test is ModuleTest { ); } - /* Test testPublicClaimPreviouslyUnclaimableToTreasury_revertsGivenUnauthorizedCaller() function - ├── Given an unclaimable payment order has been added - │ └── And tokens have been transferred to the queue - │ └── When claimPreviouslyUnclaimableToTreasury is called by an unauthorized caller - │ └── Then the transaction should revert with "Module__CallerNotAuthorized" + /* Test claimPreviouslyUnclaimableToTreasury + └── Given caller is not permissioned + └── When claimPreviouslyUnclaimableToTreasury is called + └── Then it should revert (modifier in place test) */ - function testPublicClaimPreviouslyUnclaimableToTreasury_revertsGivenUnauthorizedCaller( - ) public { - paymentClient.exposed_addToOutstandingTokenAmounts(address(_token), 200); - - address recipient_ = makeAddr("recipient"); - uint96 amount_ = 100; - - IERC20PaymentClientBase_v2.PaymentOrder memory order = - helper_createTestPaymentOrder(recipient_, amount_, 1, address(_token)); - - _token.mint(address(paymentClient), amount_ * 2); - - vm.startPrank(address(paymentClient)); - _token.approve(address(queue), amount_ * 2); - queue.exposed_addUnclaimableOrder(order, address(paymentClient)); - _token.transfer(address(queue), amount_); - vm.stopPrank(); - - vm.prank(address(queue)); - _token.approve(address(queue), amount_); - - //@todo => calculate role - // bytes32 role = _authorizer.generateRoleId(address(this), QUEUE_OPERATOR_ROLE); + function testClaimPreviouslyUnclaimableToTreasury_modifierInPlace() + public + { + // permissioned - vm.prank(address(paymentClient)); + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); vm.expectRevert( - abi.encodeWithSignature( - "Module__CallerNotAuthorized(bytes32,address)", - 0x5d97d4d42ba6fe9c9c1a1451385e8b1e735e94d33a05d1e7fae480933f0db4e0, - address(paymentClient) + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector ) ); + vm.prank(address(0xB0B)); queue.claimPreviouslyUnclaimableToTreasury( - address(paymentClient), address(_token), recipient_ + address(0), address(0), address(0) ); } /* Test testPublicClaimPreviouslyUnclaimableToTreasury_succeedsGivenValidConditions() function ├── Given an unclaimable payment order has been added │ └── And tokens have been transferred to the queue - │ └── When claimPreviouslyUnclaimableToTreasury is called by an authorized caller + │ └── When claimPreviouslyUnclaimableToTreasury is called by an permissioned caller │ └── Then the unclaimable amount should be zero │ └── And the tokens should be transferred to the failed orders treasury │ └── And the canceled orders treasury should not receive any tokens @@ -3242,14 +3224,29 @@ contract PP_Queue_v1_Test is ModuleTest { ); } - /* Test testPublicSetCanceledOrdersTreasury_succeedsGivenAuthorizedCaller() function + /* Test testPublicSetCanceledOrdersTreasury ├── Given a new treasury address │ └── And the current treasury address is different - │ └── When setCanceledOrdersTreasury is called by an unauthorized caller - │ └── Then it should revert with "Module__CallerNotAuthorized" - │ └── And when called by an authorized caller + │ └── When setCanceledOrdersTreasury is called by non permissioned caller + │ └── Then it should revert (Modifier in place test) + │ └── And when called by a permissioned caller │ └── Then the treasury address should be updated */ + + function testPublicSetCanceledOrdersTreasury_ModifierInPosition() public { + //permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + queue.setCanceledOrdersTreasury(address(0)); + } + function testPublicSetCanceledOrdersTreasury_succeedsGivenAuthorizedCaller() public { @@ -3261,20 +3258,6 @@ contract PP_Queue_v1_Test is ModuleTest { "New treasury should be different from current" ); - bytes32 ORCHESTRATOR_ADMIN_ROLE = - 0x3078303000000000000000000000000000000000000000000000000000000000; - address unauthorized = makeAddr("unauthorized"); - vm.prank(unauthorized); - vm.expectRevert( - abi.encodeWithSignature( - "Module__CallerNotAuthorized(bytes32,address)", - ORCHESTRATOR_ADMIN_ROLE, - unauthorized - ) - ); - queue.setCanceledOrdersTreasury(newTreasury); - - vm.prank(address(this)); queue.setCanceledOrdersTreasury(newTreasury); assertEq( @@ -3284,14 +3267,29 @@ contract PP_Queue_v1_Test is ModuleTest { ); } - /* Test testPublicSetFailedOrdersTreasury_succeedsGivenAuthorizedCaller() function + /* Test testPublicSetFailedOrdersTreasury ├── Given a new failed orders treasury address │ └── And the current failed orders treasury address is different - │ └── When setFailedOrdersTreasury is called by an unauthorized caller - │ └── Then it should revert with "Module__CallerNotAuthorized" - │ └── And when called by an authorized caller + │ └── When setFailedOrdersTreasury is called by a permissioned caller + │ └── Then it should revert (Modifier in place test) + │ └── And when called by a permissioned caller │ └── Then the failed orders treasury address should be updated */ + + function testPublicSetFailedOrdersTreasury_ModifierInPosition() public { + //permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + queue.setFailedOrdersTreasury(address(0)); + } + function testPublicSetFailedOrdersTreasury_succeedsGivenAuthorizedCaller() public { @@ -3303,19 +3301,6 @@ contract PP_Queue_v1_Test is ModuleTest { "New treasury should be different from current" ); - bytes32 ORCHESTRATOR_ADMIN_ROLE = - 0x3078303000000000000000000000000000000000000000000000000000000000; - address unauthorized = makeAddr("unauthorized"); - vm.prank(unauthorized); - vm.expectRevert( - abi.encodeWithSignature( - "Module__CallerNotAuthorized(bytes32,address)", - ORCHESTRATOR_ADMIN_ROLE, - unauthorized - ) - ); - queue.setFailedOrdersTreasury(newTreasury); - vm.prank(address(this)); queue.setFailedOrdersTreasury(newTreasury); @@ -3374,20 +3359,6 @@ contract PP_Queue_v1_Test is ModuleTest { ); } - /* Test testPublicGetQueueOperatorRoleAdmin_succeedsGivenCorrectAdmin() function - ├── When getQueueOperatorRoleAdmin is called - │ └── Then it should return "QUEUE_OPERATOR_ROLE_ADMIN" - */ - function testPublicGetQueueOperatorRoleAdmin_succeedsGivenCorrectAdmin() - public - { - bytes32 operatorRoleAdmin_ = queue.getQueueOperatorRoleAdmin(); - assertTrue( - operatorRoleAdmin_ == "QUEUE_OPERATOR_ROLE_ADMIN", - "Queue operator role admin should be the queue address" - ); - } - /* Test: Function _getProtocolFeeDetails() └── Given valid protocol fee amount ├── And the collectProtocolFee flag is true diff --git a/test/unit/modules/paymentProcessor/PP_Simple_v2.t.sol b/test/unit/modules/paymentProcessor/PP_Simple_v2.t.sol index 3a30044db..ea5914555 100644 --- a/test/unit/modules/paymentProcessor/PP_Simple_v2.t.sol +++ b/test/unit/modules/paymentProcessor/PP_Simple_v2.t.sol @@ -83,7 +83,12 @@ contract PP_SimpleV2Test is ModuleTest { ); } - function testSupportsInterface() public { + function testReinitFails() public override(ModuleTest) { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + paymentProcessor.init(_orchestrator, _METADATA, bytes("")); + } + + function testSupportsInterface() public override(ModuleTest) { assertTrue( paymentProcessor.supportsInterface( type(IPaymentProcessor_v2).interfaceId @@ -91,11 +96,6 @@ contract PP_SimpleV2Test is ModuleTest { ); } - function testReinitFails() public override(ModuleTest) { - vm.expectRevert(OZErrors.Initializable__InvalidInitialization); - paymentProcessor.init(_orchestrator, _METADATA, bytes("")); - } - //-------------------------------------------------------------------------- // Test: Payment Processing diff --git a/test/unit/modules/paymentProcessor/PP_Streaming_v2.t.sol b/test/unit/modules/paymentProcessor/PP_Streaming_v2.t.sol index 57e65b8b3..8e20258be 100644 --- a/test/unit/modules/paymentProcessor/PP_Streaming_v2.t.sol +++ b/test/unit/modules/paymentProcessor/PP_Streaming_v2.t.sol @@ -100,7 +100,8 @@ contract PP_StreamingV1Test is ModuleTest { paymentProcessor.init(_orchestrator, _METADATA, configData); - _authorizer.setIsAuthorized(address(this), true); + // Turn on all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(true); // Set up PaymentClient Correctöy impl = address(new ERC20PaymentClientBaseV2Mock()); @@ -124,7 +125,12 @@ contract PP_StreamingV1Test is ModuleTest { ); } - function testSupportsInterface() public { + function testReinitFails() public override(ModuleTest) { + vm.expectRevert(OZErrors.Initializable__InvalidInitialization); + paymentProcessor.init(_orchestrator, _METADATA, bytes("")); + } + + function testSupportsInterface() public override(ModuleTest) { assertTrue( paymentProcessor.supportsInterface( type(IPP_Streaming_v2).interfaceId @@ -132,11 +138,6 @@ contract PP_StreamingV1Test is ModuleTest { ); } - function testReinitFails() public override(ModuleTest) { - vm.expectRevert(OZErrors.Initializable__InvalidInitialization); - paymentProcessor.init(_orchestrator, _METADATA, bytes("")); - } - //-------------------------------------------------------------------------- // Test: Payment Processing @@ -807,6 +808,22 @@ contract PP_StreamingV1Test is ModuleTest { ); } + function testRemoveAllPaymentReceiverPayments_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + paymentProcessor.removeAllPaymentReceiverPayments( + address(0), address(0) + ); + } + uint initialNumWallets; uint initialPaymentReceiverBalance; uint initialStreamIdAtIndex1; @@ -1073,6 +1090,22 @@ contract PP_StreamingV1Test is ModuleTest { ); } + function testRemovePaymentFromSpecificStream_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + paymentProcessor.removePaymentForSpecificStream( + address(0), address(0), 0 + ); + } + function test_processPayments_failsWhenCalledByNonModule(address nonModule) public { @@ -2206,6 +2239,20 @@ contract PP_StreamingV1Test is ModuleTest { ); } + function testSetStreamingDefaults_ModifierInPosition() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + _authorizer.setAllAuthorized(false); + vm.expectRevert( + abi.encodeWithSelector( + IModule_v1.Module__CallerNotPermissioned.selector + ) + ); + vm.prank(address(0xB0B)); + paymentProcessor.setStreamingDefaults(0, 0, 0); + } + function test_getProcessorFlags() public { bytes32 flags = paymentProcessor.getProcessorFlags(); assertEq(flags, _START_END_CLIFF_FLAG); diff --git a/test/unit/orchestrator/Orchestrator_v1.t.sol b/test/unit/orchestrator/Orchestrator_v1.t.sol index 47add9f34..ede7fa025 100644 --- a/test/unit/orchestrator/Orchestrator_v1.t.sol +++ b/test/unit/orchestrator/Orchestrator_v1.t.sol @@ -10,7 +10,8 @@ import {Clones} from "@oz/proxy/Clones.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; // Internal Dependencies -import {Orchestrator_v1} from "src/orchestrator/Orchestrator_v1.sol"; +import {Orchestrator_v1_Exposed} from + "@mocks/orchestrator/Orchestrator_v1_Exposed.sol"; import {IModule_v1} from "src/modules/base/IModule_v1.sol"; // Internal Interfaces @@ -44,7 +45,7 @@ import {TypeSanityHelper} from "@testUtilities/TypeSanityHelper.sol"; contract OrchestratorV1Test is Test { // SuT - Orchestrator_v1 orchestrator; + Orchestrator_v1_Exposed orchestrator; // Helper TypeSanityHelper types; @@ -58,18 +59,6 @@ contract OrchestratorV1Test is Test { ERC20Mock token; TransactionForwarder_v1 forwarder; - event AuthorizerUpdated(address indexed _address); - event FundingManagerUpdated(address indexed _address); - event PaymentProcessorUpdated(address indexed _address); - event OrchestratorInitialized( - uint indexed orchestratorId_, - address fundingManager, - address authorizer, - address paymentProcessor, - address[] modules, - address governor - ); - function setUp() public { fundingManager = new FundingManagerV1Mock(); authorizer = new AuthorizerV1Mock(); @@ -79,10 +68,19 @@ contract OrchestratorV1Test is Test { forwarder = new TransactionForwarder_v1(); token = new ERC20Mock("TestToken", "TST", 18); - address impl = address(new Orchestrator_v1(address(forwarder))); - orchestrator = Orchestrator_v1(Clones.clone(impl)); + address impl = address(new Orchestrator_v1_Exposed(address(forwarder))); + orchestrator = Orchestrator_v1_Exposed(Clones.clone(impl)); types = new TypeSanityHelper(address(orchestrator)); + + // Actually link the Authorizer to the Orchestrator + orchestrator.setup_authorizer(address(authorizer)); + + // Set the default admin to this contract + authorizer.setDefaultAdmin(address(this)); + + // Every caller has permission for every permissioned function + authorizer.setAllAuthorized(true); } //-------------------------------------------------------------------------- @@ -146,7 +144,7 @@ contract OrchestratorV1Test is Test { // Now we test correct initialization vm.expectEmit(true, true, true, false); - emit OrchestratorInitialized( + emit IOrchestrator_v1.OrchestratorInitialized( orchestratorId, address(fundingManager), address(authorizer), @@ -205,10 +203,99 @@ contract OrchestratorV1Test is Test { ); } + //-------------------------------------------------------------------------- + // Tests: Modifiers + + /* + Test: permissioned + ├── Given: modifierPermissionedCheck is executed via call with a valid selector, but random data + ├── And: The call sender is randomised + └── And: The Caller is permissioned to call the function + └── When: The function modifierPermissionedCheck is called + └── Then: the function should not revert, because the sender and only the function selector were correctly passed + */ + function testPermissioned_modifier(address caller_, bytes memory data_) + public + { + // Assume that the calldata is at least 4 bytes long + vm.assume(data_.length >= 4); + + bytes4 targetSelector = + Orchestrator_v1_Exposed.modifierPermissionedCheck.selector; + + // Proof + authorizer.setHasPermission( + caller_, address(orchestrator), targetSelector, true + ); + + // Replace the msg.data function selector with the correct one + for (uint i = 0; i < 4; i++) { + data_[i] = targetSelector[i]; + } + + // Expect no revert + vm.prank(caller_); + address(orchestrator).call(data_); + } + //-------------------------------------------------------------------------- // Tests: Replacing the three base modules: authorizer, funding manager, // payment processor + /* + Test: initiateSetAuthorizerWithTimelock Modifier Checks + └── Given: caller is not permissioned + └── When: initiateSetAuthorizerWithTimelock is called + └── Then: it should revert (modifier in position check) + */ + function testInitiateSetAuthorizerWithTimelock_ModifierInPositionChecks() + public + { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + + vm.prank(address(0xB0B)); + orchestrator.initiateSetAuthorizerWithTimelock( + IAuthorizer_v1(address(0)) + ); + } + + /* + Test: executeSetAuthorizer Modifier Checks + └── Given: caller is not permissioned + └── When: executeSetAuthorizer is called + └── Then: it should revert (modifier in position check) + */ + function testExecuteSetAuthorizer_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + vm.prank(address(0xB0B)); + orchestrator.executeSetAuthorizer(IAuthorizer_v1(address(0))); + } + + /* + Test: cancelAuthorizerUpdate Modifier Checks + └── Given: caller is not permissioned + └── When: cancelAuthorizerUpdate is called + └── Then: it should revert (modifier in position check) + */ + function testCancelAuthorizerUpdate_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + vm.prank(address(0xB0B)); + orchestrator.cancelAuthorizerUpdate(IAuthorizer_v1(address(0))); + } + function testInitiateAndExecuteSetAuthorizer( uint orchestratorId, uint moduleAmount @@ -226,8 +313,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); - // Create new authorizer module AuthorizerV1Mock newAuthorizer = new AuthorizerV1Mock(); @@ -238,7 +323,7 @@ contract OrchestratorV1Test is Test { // set the new authorizer module vm.expectEmit(true, true, true, true); - emit AuthorizerUpdated(address(newAuthorizer)); + emit IOrchestrator_v1.AuthorizerUpdated(address(newAuthorizer)); orchestrator.executeSetAuthorizer(newAuthorizer); assertTrue(orchestrator.authorizer() == newAuthorizer); @@ -270,8 +355,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); - // Create new authorizer module address newAuthorizer = address(0x8888); @@ -305,8 +388,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); - // Create new authorizer module address newAuthorizer = address(0x8888); @@ -324,6 +405,59 @@ contract OrchestratorV1Test is Test { assertTrue(orchestrator.authorizer() == authorizer); } + /* + Test: initiateSetFundingManagerWithTimelock Modifier Checks + └── Given: caller is not permissioned + └── When: initiateSetFundingManagerWithTimelock is called + └── Then: it should revert (modifier in position check) + */ + function testInitiateSetFundingManagerWithTimelock_ModifierInPositionChecks( + ) public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + + vm.prank(address(0xB0B)); + orchestrator.initiateSetFundingManagerWithTimelock( + IFundingManager_v1(address(0)) + ); + } + + /* + Test: executeSetFundingManager Modifier Checks + └── Given: caller is not permissioned + └── When: executeSetFundingManager is called + └── Then: it should revert (modifier in position check) + */ + function testExecuteSetFundingManager_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + vm.prank(address(0xB0B)); + orchestrator.executeSetFundingManager(IFundingManager_v1(address(0))); + } + + /* + Test: cancelFundingManagerUpdate Modifier Checks + └── Given: caller is not permissioned + └── When: cancelFundingManagerUpdate is called + └── Then: it should revert (modifier in position check) + */ + function testCancelFundingManagerUpdate_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + vm.prank(address(0xB0B)); + orchestrator.cancelFundingManagerUpdate(IFundingManager_v1(address(0))); + } + function testInitiateAndExecuteSetFundingManager( uint orchestratorId, uint moduleAmount @@ -341,7 +475,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); FundingManagerV1Mock(address(orchestrator.fundingManager())).setToken( IERC20(address(0xA11CE)) ); @@ -355,7 +488,7 @@ contract OrchestratorV1Test is Test { // set the new funding manager module vm.expectEmit(true, true, true, true); - emit FundingManagerUpdated(address(newFundingManager)); + emit IOrchestrator_v1.FundingManagerUpdated(address(newFundingManager)); orchestrator.executeSetFundingManager(newFundingManager); assertTrue(orchestrator.fundingManager() == newFundingManager); assertTrue( @@ -380,7 +513,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); FundingManagerV1Mock(address(orchestrator.fundingManager())).setToken( IERC20(address(0xA11CE)) ); @@ -418,7 +550,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); FundingManagerV1Mock(address(orchestrator.fundingManager())).setToken( IERC20(address(0xA11CE)) ); @@ -455,7 +586,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); FundingManagerV1Mock(address(orchestrator.fundingManager())).setToken( IERC20(address(0xA11CE)) ); @@ -476,6 +606,65 @@ contract OrchestratorV1Test is Test { orchestrator.initiateSetFundingManagerWithTimelock(newFundingManager); } + /* + Test: initiateSetPaymentProcessorWithTimelock Modifier Checks + └── Given: caller is not permissioned + └── When: initiateSetPaymentProcessorWithTimelock is called + └── Then: it should revert (modifier in position check) + */ + function testInitiateSetPaymentProcessorWithTimelock_ModifierInPositionChecks( + ) public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + + vm.prank(address(0xB0B)); + orchestrator.initiateSetPaymentProcessorWithTimelock( + IPaymentProcessor_v2(address(0)) + ); + } + + /* + Test: executeSetPaymentProcessor Modifier Checks + └── Given: caller is not permissioned + └── When: executeSetPaymentProcessor is called + └── Then: it should revert (modifier in position check) + */ + function testExecuteSetPaymentProcessor_ModifierInPositionChecks() public { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + vm.prank(address(0xB0B)); + orchestrator.executeSetPaymentProcessor( + IPaymentProcessor_v2(address(0)) + ); + } + + /* + Test: cancelPaymentProcessorUpdate Modifier Checks + └── Given: caller is not permissioned + └── When: cancelPaymentProcessorUpdate is called + └── Then: it should revert (modifier in position check) + */ + function testCancelPaymentProcessorUpdate_ModifierInPositionChecks() + public + { + // permissioned + + // Turn off all adresses are permissioned to call all functions + authorizer.setAllAuthorized(false); + vm.expectRevert(IOrchestrator_v1.Orchestrator__NotPermissioned.selector); + vm.prank(address(0xB0B)); + orchestrator.cancelPaymentProcessorUpdate( + IPaymentProcessor_v2(address(0)) + ); + } + function testInitiateAndExecuteSetPaymentProcessor( uint orchestratorId, uint moduleAmount @@ -492,8 +681,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); - // Create new payment processor module PaymentProcessorV1Mock newPaymentProcessor = new PaymentProcessorV1Mock(); @@ -505,7 +692,9 @@ contract OrchestratorV1Test is Test { // set the new payment processor module vm.expectEmit(true, true, true, true); - emit PaymentProcessorUpdated(address(newPaymentProcessor)); + emit IOrchestrator_v1.PaymentProcessorUpdated( + address(newPaymentProcessor) + ); orchestrator.executeSetPaymentProcessor(newPaymentProcessor); assertTrue(orchestrator.paymentProcessor() == newPaymentProcessor); } @@ -527,8 +716,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); - // Create new payment processor module address newPaymentProcessor = address(0x8888); @@ -564,8 +751,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); - // Create new payment processor module address newPaymentProcessor = address(0x8888); @@ -608,7 +793,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); address currentAuthorizer = address(orchestrator.authorizer()); vm.expectRevert( @@ -683,7 +867,6 @@ contract OrchestratorV1Test is Test { governor ); - authorizer.setIsAuthorized(address(this), true); address currentAuthorizer = address(orchestrator.authorizer()); vm.expectRevert( @@ -735,6 +918,42 @@ contract OrchestratorV1Test is Test { orchestrator.executeRemoveModule(currentPaymentProcessor); } + // ------------------------------------------------------------------------ + // Internal - Authorization + + /* + Test: _checkAuthorization_ + └── Given: Authorizer hasPermission() is mocked + ├── When: _checkAuthorization_ is called + └── And: Authorizer hasPermission() returns false + ├── Then: It should forward the function selector properly + └── And: The function should revert + */ + function test_checkAuthorization_hasPermissionMocked( + bool hasPermission_, + address caller_, + bytes calldata data_ + ) public { + vm.assume(data_.length >= 4); + // Assume that caller is not the module as it is the default admin + vm.assume(caller_ != address(this)); + + // Turn off that every caller has permission for every permissioned function + authorizer.setAllAuthorized(false); + + authorizer.setHasPermission( + caller_, address(orchestrator), bytes4(data_[0:4]), hasPermission_ + ); + + if (!hasPermission_) { + vm.expectRevert( + IOrchestrator_v1.Orchestrator__NotPermissioned.selector + ); + } + + orchestrator._checkAuthorization_exposed(caller_, data_); + } + //-------------------------------------------------------------------------- // Helper Functions diff --git a/test/unit/orchestrator/abstracts/ModuleManagerBase_v1.t.sol b/test/unit/orchestrator/abstracts/ModuleManagerBase_v1.t.sol index 3283808cb..446e98309 100644 --- a/test/unit/orchestrator/abstracts/ModuleManagerBase_v1.t.sol +++ b/test/unit/orchestrator/abstracts/ModuleManagerBase_v1.t.sol @@ -242,35 +242,6 @@ contract ModuleManagerBaseV1Test is Test { moduleManager.call_executeAddModule(module); } - function testInitiateAddModuleWithTimelock_FailsIfCallerNotAuthorized() - public - { - address module = address(new ModuleV1Mock()); - - moduleManager.__ModuleManager_setIsAuthorized(address(this), false); - - vm.expectRevert( - IModuleManagerBase_v1 - .ModuleManagerBase__CallerNotAuthorized - .selector - ); - moduleManager.call_initiateAddModuleWithTimelock(module); - } - - function testExecuteAddModule_FailsIfCallerNotAuthorized() public { - address module = address(new ModuleV1Mock()); - moduleManager.call_initiateAddModuleWithTimelock(module); - - moduleManager.__ModuleManager_setIsAuthorized(address(this), false); - - vm.expectRevert( - IModuleManagerBase_v1 - .ModuleManagerBase__CallerNotAuthorized - .selector - ); - moduleManager.call_executeAddModule(module); - } - function testExecuteAddModule_FailsIfModuleLimitIsExceeded() public { uint modulesUntilLimit = MAX_MODULES - moduleManager.modulesSize(); address[] memory modules = new address[](modulesUntilLimit + 1); @@ -464,42 +435,6 @@ contract ModuleManagerBaseV1Test is Test { assertEq(moduleManager.listModules().length, 0); } - function testInitiateRemoveModuleWithTimelock_FailsIfCallerNotAuthorized() - public - { - address module = address(new ModuleV1Mock()); - - moduleManager.call_initiateAddModuleWithTimelock(module); - vm.warp(block.timestamp + timelock); - moduleManager.call_executeAddModule(module); - - moduleManager.__ModuleManager_setIsAuthorized(address(this), false); - - vm.expectRevert( - IModuleManagerBase_v1 - .ModuleManagerBase__CallerNotAuthorized - .selector - ); - moduleManager.call_initiateRemoveModuleWithTimelock(module); - } - - function testExecuteRemoveModule_FailsIfCallerNotAuthorized() public { - address module = address(new ModuleV1Mock()); - - moduleManager.call_initiateAddModuleWithTimelock(module); - vm.warp(block.timestamp + timelock); - moduleManager.call_executeAddModule(module); - - moduleManager.__ModuleManager_setIsAuthorized(address(this), false); - - vm.expectRevert( - IModuleManagerBase_v1 - .ModuleManagerBase__CallerNotAuthorized - .selector - ); - moduleManager.call_initiateRemoveModuleWithTimelock(module); - } - function testInitiateRemoveModuleWithTimelock_FailsIfNotModule() public { address module = address(new ModuleV1Mock()); @@ -513,28 +448,14 @@ contract ModuleManagerBaseV1Test is Test { // Tests: cancelModuleUpdate() /* Test cancelModuleUpdate() function - ├── Given the caller of the function is not authorized - │ └── When the function cancelModuleUpdate() gets called - │ └── Then it should revert ├── Given no update has been initated for the module │ └── When the function cancelModuleUpdate() gets called │ └── Then it should revert - └── Given caller is authorized & module update has been initiated + └── Given module update has been initiated └── When the function cancelModuleUpdate() gets called └── Then it should cancel the update └── And it should emit an event */ - function testCancelModuleUpdate_failsGivenCallerNotAuthorized() public { - address module = address(new ModuleV1Mock()); - moduleManager.__ModuleManager_setIsAuthorized(address(this), false); - - vm.expectRevert( - IModuleManagerBase_v1 - .ModuleManagerBase__CallerNotAuthorized - .selector - ); - moduleManager.call_cancelModuleUpdate(module); - } function testCancelModuleUpdate_failsGivenModuleUpdateNotInitated() public diff --git a/test/unit/proxies/InverterProxyAdmin.sol b/test/unit/proxies/InverterProxyAdmin.t.sol similarity index 100% rename from test/unit/proxies/InverterProxyAdmin.sol rename to test/unit/proxies/InverterProxyAdmin.t.sol From 8ee380fad4a3a993df303cca45e1c69374a93631 Mon Sep 17 00:00:00 2001 From: FHieser Date: Tue, 10 Jun 2025 13:36:39 +0200 Subject: [PATCH 02/67] Create AUT_Roles_v1.md --- .../modules/authorizer/role/AUT_Roles_v1.md | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 docs/src/modules/authorizer/role/AUT_Roles_v1.md diff --git a/docs/src/modules/authorizer/role/AUT_Roles_v1.md b/docs/src/modules/authorizer/role/AUT_Roles_v1.md new file mode 100644 index 000000000..0025d02e1 --- /dev/null +++ b/docs/src/modules/authorizer/role/AUT_Roles_v1.md @@ -0,0 +1,253 @@ +# AUT_Roles_v2 + +## Purpose of Contract + +This contract provides the access control mechanism for managing roles and permissions across different modules within the Inverter Network, ensuring secure and controlled access to critical functionalities. + +This contract has the following key features: + +- **Role creation and management**: This includes the ability to create roles, revoke roles, assigning and revoking role admins, which can add and remove role members. +- **Role-based access control**: This includes the ability to grant roles access to functions that implement the permissioned modifier. Functions can also be set to public access by adding the public role to the function permissions. + +## Glossary + +To understand the functionalities of the following contract, it is important to be familiar with the following definitions. + +### Roles + +A role has the following properties: + +- A unique identifier (ID) +- A label +- A list of members +- A associated admin role + +The id is a value assigned by the authorizer module and is used to reference the role in the different functions of the authorizer module. + +The label is a string that is emitted as an event when the role is created. It is used to make the role human readable in the frontend and has no practical use in the +onchain live setup. + +The members are the addresses that inhabit the role. The admin role is the role that can add and remove new members to the role. + +### Native Roles + +There are three native roles that are created with the authorizer module: + +- The default admin role +- The public role +- The burn admin role + +The default admin role holds the highest admin privileges and can access every permissioned function at all times. The public role is a role that can be added to a function's permission list and is used to make functions be publicly accessible. The burn admin role is a placeholder role that indicates that the admin role of a role has been burned and is no longer usable. + +### Permissioned Modifier + +Most of the state altering functions in a workflow are so called permissioned functions. This means that only roles that have been granted the according function permission can call the function. The permissioned status is enforced by the `permissioned` modifier. + +Some of the native roles have special rights in this system. The default admin role can access every permissioned function regardless of wether the default admin role was granted the permission or not. If the public role is granted the permission to a function, then every caller can access the function, regardless of +wether they inhabit a already added role or not. + +Note: As a workflow is intialized without any native permissions, some functions that could be perceived as "this should be publicly accessible" are not. Examples for this could be the "buy" and "sell" functions of some funding manager modules or the stake and unstake functions of the staking logic module. For these functions, the public role has to be added to the access of the respective function. + +## Inheritance + +### Class Diagramm + +```mermaid +classDiagram + + Module <|-- AUT_Roles_v1 + AccessControlEnumerableUpgradeable <|-- AUT_Roles_v1 + + note for Module "Base contract for every module implementation" + class Module{ + -IOrchestrator_v1 __Module_orchestrator + + permissioned + } + + note for AccessControlEnumerableUpgradeable "OpenZeppelin Authorization System" + class AccessControlEnumerableUpgradeable { + - mapping(bytes32 role => EnumerableSet.AddressSet) _roleMembers + + getRoleMember() + + hasRole() + + getRoleAdmin() + + grantRole() + + revokeRole() + } + + class AUT_Roles_v1{ + - mapping(address target => mapping(bytes4 selector => bytes32[] roleIds)) _permissions; + } + +``` + +### Base Contracts + +This contract is based on the following contracts and inherits their functionalities: + +- [IAuthorizer_v1](../IAuthorizer_v1.md): Implementation interface. +- [Module_v1](../../base/Module_v1.md): Inverter network base module functionality. +- [AccessControlEnumerableUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/extensions/AccessControlEnumerableUpgradeable.sol): Access control functionality. + +Functions that have been overridden to adapt functionalities are outlined below. + +### Key Changes to the Base Contracts + +_The purpose of this section is to highlight which functions of the base contract have been overridden and why._ + +- `grantRole`: This function has been overridden to allow only the distribution of roles that have already been created by the authorizer module. + +## Key Functionalities + +In this section the key functionalities of the authorizer module are described. + +### Role Management + +This section describes the functionalities of the authorizer module regarding role management. + +#### Role Creation + +A role can be created by calling the createRole function. The function takes the following parameters: + +- The role name +- The role id of the role that will become the admin of the new role. +- The addresses of the initial members of the new role. + +The rolename in this context refers to the label of the role and is therefor not referenceable onchain. The function can only be called by a permissioned address (See [permissioned](#permissioned-modifier)). + +#### Role Labeling + +The label of a role can be overwritten by calling the labelRole function. With this a new event is emitted, that signals the frontend that the label has been updated. The function can only be called by a permissioned address.(See [permissioned](#permissioned-modifier)). + +#### Role granting + +A role can be granted to a address by calling the grantRole function. The function takes the following parameters: + +- The role id of the role to grant +- The address to grant the role to + +The grantRole function can only be called by according admin of the role. + +#### Role revoking + +A role can be revoked from an address by calling therevokeRole function. The function takes the following parameters: + +- The role id of the role to revoke +- The address to revoke the role from + +The revokeRole function can only be called by according admin of the role. + +#### Transferal of Admin Roles + +The admin role of a role can be transferred by calling the transferAdminRole function. The function takes the following parameters: + +- The role id of the role to transfer the admin from +- The role id of the role to transfer the admin to + +The transferAdminRole function can only be called by according admin of the role. + +#### Burning of Admin Roles: + +The admin role can be burned by calling the burnRoleAdmin function. The function takes the following parameters: + +- The role id of the role to burn the admin from + +If the admin role is burned, then no members can be added or removed from a role anymore. Remember: This step is irreversible. + +### Role Based Access Control + +This section describes the functionalities of the authorizer module regarding role based access control. + +#### Adding access permissions + +Adding access permissions is done by calling the `addAccessPermission` function. This function takes the following parameters: + +- The contract for which the permission is added +- The function selector of the target function +- The role ID of the role that will receive the permission + +Example: Adding the role "BOUNTY_MANAGER" to the "createBounty" function of the "bountyManager" contract would look like this: + +```solidity +authorizer.addAccessPermission( + address(bountyManager), + bountyManager.createBounty.selector, + bountyManagerId +); +``` + +The function can only be called by a permissioned address.(See [permissioned](#permissioned-modifier)). + +#### Making a function public + +A function can be made public by calling the `addAccessPermission` function with the public role as target role. + +Example: Making the "buy" function of the funding manager contract public would look like this: + +```solidity +authorizer.addAccessPermission( + address(fundingManager), + fundingManager.buy.selector, + authorizer.PUBLIC_ROLE() +);` +``` + +The public role can be removed in the same way as any other role. + +### Mixed Utility - createRoleAndAddAccessPermissions() + +This function is a convenience function that combines the creation of a new role and the adding of access permissions. It takes the following parameters: + +- The name of the role +- The role id of the role that will become the admin of the new role. +- The addresses of the initial members of the new role. +- The addresses of the targets contracts. +- The selectors of the functions. + +Note: The selectors of the functions are linked to the respective target contracts. As the selectors are passedas a 2 Dimensional array, the first dimension is coupled to target contract and the second one contains the actual selectors for that target contract. + +Example: The target contracts are the fundingManager at position 0 in the array and the logic module at position 1. The selectors therefor contain two arrays, one for position 0 and one for position 1. A call would look like this: + +```solidity +'authorizer.createRoleAndAddAccessPermissions( + "newRole", + authorizer.DEFAULT_ADMIN_ROLE(), + [ + initialMember1, + initialMember2 + ], + [ + fundingManager.address, + logicModule.address + ], + [ + [ + fundingManager.buy.selector, + fundingManager.sell.selector + ], + [ + logicModule.execute.selector + ] + ] +) +``` + +The function can only be called by a permissioned address.(See [permissioned](#permissioned-modifier)). + +## Deployment + +### Deployment Parameters + +The list of deployment parameters can be found in the _Technical Reference_ section of the documentation under the `init()` function ([https://docs.inverter.network/contracts/technical-reference/modules/authorizer/role/aut_roles_v2.sol]()). + +### Deployment + +Deployment should be done using one of the methods provided below: + +- **Manual deployment:** Through Inverter Network's [Control Room application](https://beta.controlroom.inverter.network/). +- **SDK deployment:** Through Inverter Network's [TypeScript SDK](https://docs.inverter.network/sdk/typescript-sdk/guides/deploy-a-workflow) or [React SDK](https://docs.inverter.network/sdk/react-sdk/guides/deploy-a-workflow). + +### Setup Steps + +#### Optional Setup Steps + +Because a workflow and its authorizer module are deployed without any native permissions (except the initial admin role [here](#native-roles)), it might be necessary for some modules to modify the access of their functions. For this the sections [Mixed Utility](#mixed-utility---createroleandaddaccesspermissions), [Role Management](#role-management) and [Role Based Access Control](#role-based-access-control) can be referenced. From bbe3bf5740ceede4d58f3463ccb2b07ad36c4728 Mon Sep 17 00:00:00 2001 From: FHieser Date: Tue, 10 Jun 2025 13:36:54 +0200 Subject: [PATCH 03/67] Adapt the natspec header sections --- src/modules/authorizer/IAuthorizer_v1.sol | 183 +----------------- src/modules/authorizer/role/AUT_Roles_v1.sol | 186 +------------------ 2 files changed, 3 insertions(+), 366 deletions(-) diff --git a/src/modules/authorizer/IAuthorizer_v1.sol b/src/modules/authorizer/IAuthorizer_v1.sol index d5d5e35a1..a15a63585 100644 --- a/src/modules/authorizer/IAuthorizer_v1.sol +++ b/src/modules/authorizer/IAuthorizer_v1.sol @@ -27,188 +27,7 @@ import {IAccessControlEnumerable} from * modifier. Functions can also be set to public access by * adding the public role to the function permissions. * - * @custom:guide - * The following guide explains in detail how to use the key features - * of this module: - * - * - ROLE MANAGEMENT: - * - Roles: - * A role has the following properties: - * - A unique identifier (ID) - * - A label - * - A list of members - * - A associated admin role - * The id is a value assigned by the authorizer module and - * is used to reference the role in the different functions - * of the authorizer module. - * The label is a string that is emitted as an event when - * the role is created. It is used to make the role human - * readable in the frontend and has no practical use in the - * onchain live setup. - * The members are the addresses that inhabit the role. - * The admin role is the role that can add and remove new - * members to the role. - * - * - Native Roles: - * There are three native roles that are created with the - * authorizer module: - * - The default admin role - * - The public role - * - The burn admin role - * The default admin role is the role that is - * - * - Role Creation: - * A role can be created by calling the createRole function. - * The function takes the following parameters: - * - The role name - * - The role id of the role that will become the admin of - * the new role. - * - The addresses of the initial members of the new role. - * The rolename in this context refers to the label of the - * role and is therefor not referenceable onchain. - * The function can only be called by a permissioned address - * (See permissioned section below). - * - * - Role Labeling: - * The label of a role can be overwritten by calling the - * labelRole function. With this a new event is emitted, - * that signals the frontend that the label has been - * updated. - * The function can only be called by a permissioned address - * (See permissioned section below). - * - * - Role granting and revoking: - * A role can be granted to a address by calling the - * grantRole function. The function takes the following - * parameters: - * - The role id of the role to grant - * - The address to grant the role to - * The grantRole function can only be called by according - * admin of the role. - * A role can be revoked from an address by calling the - * revokeRole function. The function takes the following - * parameters: - * - The role id of the role to revoke - * - The address to revoke the role from - * The revokeRole function can only be called by according - * admin of the role. - * - * - Transferal and Burning of Admin Roles: - * The admin role of a role can be transferred by calling - * the transferAdminRole function. The function takes the - * following parameters: - * - The role id of the role to transfer the admin from - * - The role id of the role to transfer the admin to - * The transferAdminRole function can only be called by - * according admin of the role. - * The admin role can be burned by calling the - * burnRoleAdmin function. - * The function takes the following parameters: - * - The role id of the role to burn the admin from - * If the admin role is burned, then no members can be added - * or removed from a role anymore. - * Remmeber: This step is irreversible. - * - * - ROLE BASED ACCESS CONTROL: - * - Permissioned - * Most of the state altering functions in a workflow are - * permissioned functions. This means that only roles that - * have been granted the according function permission can - * call the function. The permissioned status is enforced - * by the `permissioned` modifier. - * Some of the native roles have special rights in this - * system. The default admin role can access every - * permissioned function regardless of wether the default - * admin role was granted the permission or not. If the - * public role is granted the permission to a function, then - * every caller can access the function, regardless of - * wether they inhabit a already added role or not. - * Note: As a workflow is intialized without any native - * permissions, some functions that could be perceived as - * "this should be publicly accessible" are not. Examples - * for this could be the "buy" and "sell" functions of some - * funding manager modules or the stake and unstake - * functions of the staking logic module. For these - * functions, the public role has to be added to the access - * of the respective function. - * - * - Adding access permissions - * Adding access permissions is done by calling the - * `addAccessPermission` function. This function takes the - * following parameters: - * - The contract for which the permission is added - * - The function selector of the target function - * - The role ID of the role that will receive the - * permission - * Example: Adding the role "BOUNTY_MANAGER" to the - * "createBounty" function of the "bountyManager" contract - * would look like this: - * authorizer.addAccessPermission( - * address(bountyManager), - * bountyManager.createBounty.selector, - * bountyManagerId); - * - * - Removing access permissions - * Removing access permissions is done by calling the - * `removeAccessPermission` function. This function takes - * the following parameters: - * - The contract for which the permission is removed - * - The function selector of the target function - * - The role ID of the role that will lose the permission - * Example: Removing the role "BOUNTY_MANAGER" from the - * "createBounty" function of the "bountyManager" contract - * would look like this: - * authorizer.removeAccessPermission( - * address(bountyManager), - * bountyManager.createBounty.selector, - * bountyManagerId); - * - * - Making a function public - * A function can be made public by calling the - * `addAccessPermission` function with the public role as - * target role. - * Example: Making the "buy" function of the funding manager - * contract public would look like this: - * authorizer.addAccessPermission( - * address(fundingManager), - * fundingManager.buy.selector, - * authorizer.PUBLIC_ROLE()); - * The public role can be removed in the same way as any - * other role. - * - * - MIXED UTILITY: - * - The createRoleAndAddAccessPermissions function - * This function is a convenience function that combines - * the creation of a new role and the adding of access - * permissions. It takes the following parameters: - * - The name of the role - * - The role id of the role that will become the admin of - * the new role. - * - The addresses of the initial members of the new role. - * - The addresses of the targets contracts. - * - The selectors of the functions. - * Note: The selectors of the functions are linked to the - * respective target contracts. As the selectors are passed - * as a 2 Dimensional array, the first dimension is coupled - * to target contract and the second one contains the - * actual selectors for that target contract. - * Example: The target contracts are the fundingManager at - * position 0 in the array and the logic module at position - * 1. The selectors therefor contain two arrays, one for - * position 0 and one for position 1. - * Example: authorizer.createRoleAndAddAccessPermissions( - * "newRole", - * authorizer.DEFAULT_ADMIN_ROLE(), - * [initialMember1, initialMember2], - * [fundingManager.address, logicModule.address], - * [ - * [ - * fundingManager.buy.selector, - * fundingManager.sell.selector - * ], - * [logicModule.execute.selector] - * ] - * ) + * @custom:documentation: See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_Roles_v1.sol * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to diff --git a/src/modules/authorizer/role/AUT_Roles_v1.sol b/src/modules/authorizer/role/AUT_Roles_v1.sol index d42d76d9a..8037d5615 100644 --- a/src/modules/authorizer/role/AUT_Roles_v1.sol +++ b/src/modules/authorizer/role/AUT_Roles_v1.sol @@ -39,191 +39,9 @@ import {AccessControlEnumerableUpgradeable} from * - Role-based access control. This includes the ability to grant * roles access to functions that implement the permissioned * modifier. Functions can also be set to public access by - * adding the public role to the function permissions. - * - * @custom:guide - * The following guide explains in detail how to use the key features - * of this module: - * - * - ROLE MANAGEMENT: - * - Roles: - * A role has the following properties: - * - A unique identifier (ID) - * - A label - * - A list of members - * - A associated admin role - * The id is a value assigned by the authorizer module and - * is used to reference the role in the different functions - * of the authorizer module. - * The label is a string that is emitted as an event when - * the role is created. It is used to make the role human - * readable in the frontend and has no practical use in the - * onchain live setup. - * The members are the addresses that inhabit the role. - * The admin role is the role that can add and remove new - * members to the role. - * - * - Native Roles: - * There are three native roles that are created with the - * authorizer module: - * - The default admin role - * - The public role - * - The burn admin role - * The default admin role is the role that is - * - * - Role Creation: - * A role can be created by calling the createRole function. - * The function takes the following parameters: - * - The role name - * - The role id of the role that will become the admin of - * the new role. - * - The addresses of the initial members of the new role. - * The rolename in this context refers to the label of the - * role and is therefor not referenceable onchain. - * The function can only be called by a permissioned address - * (See permissioned section below). - * - * - Role Labeling: - * The label of a role can be overwritten by calling the - * labelRole function. With this a new event is emitted, - * that signals the frontend that the label has been - * updated. - * The function can only be called by a permissioned address - * (See permissioned section below). - * - * - Role granting and revoking: - * A role can be granted to a address by calling the - * grantRole function. The function takes the following - * parameters: - * - The role id of the role to grant - * - The address to grant the role to - * The grantRole function can only be called by according - * admin of the role. - * A role can be revoked from an address by calling the - * revokeRole function. The function takes the following - * parameters: - * - The role id of the role to revoke - * - The address to revoke the role from - * The revokeRole function can only be called by according - * admin of the role. - * - * - Transferal and Burning of Admin Roles: - * The admin role of a role can be transferred by calling - * the transferAdminRole function. The function takes the - * following parameters: - * - The role id of the role to transfer the admin from - * - The role id of the role to transfer the admin to - * The transferAdminRole function can only be called by - * according admin of the role. - * The admin role can be burned by calling the - * burnRoleAdmin function. - * The function takes the following parameters: - * - The role id of the role to burn the admin from - * If the admin role is burned, then no members can be added - * or removed from a role anymore. - * Remmeber: This step is irreversible. - * - * - ROLE BASED ACCESS CONTROL: - * - Permissioned - * Most of the state altering functions in a workflow are - * permissioned functions. This means that only roles that - * have been granted the according function permission can - * call the function. The permissioned status is enforced - * by the `permissioned` modifier. - * Some of the native roles have special rights in this - * system. The default admin role can access every - * permissioned function regardless of wether the default - * admin role was granted the permission or not. If the - * public role is granted the permission to a function, then - * every caller can access the function, regardless of - * wether they inhabit a already added role or not. - * Note: As a workflow is intialized without any native - * permissions, some functions that could be perceived as - * "this should be publicly accessible" are not. Examples - * for this could be the "buy" and "sell" functions of some - * funding manager modules or the stake and unstake - * functions of the staking logic module. For these - * functions, the public role has to be added to the access - * of the respective function. - * - * - Adding access permissions - * Adding access permissions is done by calling the - * `addAccessPermission` function. This function takes the - * following parameters: - * - The contract for which the permission is added - * - The function selector of the target function - * - The role ID of the role that will receive the - * permission - * Example: Adding the role "BOUNTY_MANAGER" to the - * "createBounty" function of the "bountyManager" contract - * would look like this: - * authorizer.addAccessPermission( - * address(bountyManager), - * bountyManager.createBounty.selector, - * bountyManagerId); - * - * - Removing access permissions - * Removing access permissions is done by calling the - * `removeAccessPermission` function. This function takes - * the following parameters: - * - The contract for which the permission is removed - * - The function selector of the target function - * - The role ID of the role that will lose the permission - * Example: Removing the role "BOUNTY_MANAGER" from the - * "createBounty" function of the "bountyManager" contract - * would look like this: - * authorizer.removeAccessPermission( - * address(bountyManager), - * bountyManager.createBounty.selector, - * bountyManagerId); - * - * - Making a function public - * A function can be made public by calling the - * `addAccessPermission` function with the public role as - * target role. - * Example: Making the "buy" function of the funding manager - * contract public would look like this: - * authorizer.addAccessPermission( - * address(fundingManager), - * fundingManager.buy.selector, - * authorizer.PUBLIC_ROLE()); - * The public role can be removed in the same way as any - * other role. - * - * - MIXED UTILITY: - * - The createRoleAndAddAccessPermissions function - * This function is a convenience function that combines - * the creation of a new role and the adding of access - * permissions. It takes the following parameters: - * - The name of the role - * - The role id of the role that will become the admin of - * the new role. - * - The addresses of the initial members of the new role. - * - The addresses of the targets contracts. - * - The selectors of the functions. - * Note: The selectors of the functions are linked to the - * respective target contracts. As the selectors are passed - * as a 2 Dimensional array, the first dimension is coupled - * to target contract and the second one contains the - * actual selectors for that target contract. - * Example: The target contracts are the fundingManager at - * position 0 in the array and the logic module at position - * 1. The selectors therefor contain two arrays, one for - * position 0 and one for position 1. - * Example: authorizer.createRoleAndAddAccessPermissions( - * "newRole", - * authorizer.DEFAULT_ADMIN_ROLE(), - * [initialMember1, initialMember2], - * [fundingManager.address, logicModule.address], - * [ - * [ - * fundingManager.buy.selector, - * fundingManager.sell.selector - * ], - * [logicModule.execute.selector] - * ] - * ) + * adding the public role to the function permissions. * * + * @custom:documentation: See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_Roles_v1.sol * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to From 4167b488bc08a04f0f00be4a7ec89a20a1594566 Mon Sep 17 00:00:00 2001 From: FHieser Date: Wed, 11 Jun 2025 09:32:51 +0200 Subject: [PATCH 04/67] Fix documentation --- src/modules/authorizer/IAuthorizer_v1.sol | 2 +- src/modules/authorizer/role/AUT_Roles_v1.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/authorizer/IAuthorizer_v1.sol b/src/modules/authorizer/IAuthorizer_v1.sol index a15a63585..e076a3c21 100644 --- a/src/modules/authorizer/IAuthorizer_v1.sol +++ b/src/modules/authorizer/IAuthorizer_v1.sol @@ -27,7 +27,7 @@ import {IAccessControlEnumerable} from * modifier. Functions can also be set to public access by * adding the public role to the function permissions. * - * @custom:documentation: See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_Roles_v1.sol + * @custom:documentation See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_Roles_v1.sol * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to diff --git a/src/modules/authorizer/role/AUT_Roles_v1.sol b/src/modules/authorizer/role/AUT_Roles_v1.sol index 8037d5615..cd9612a9d 100644 --- a/src/modules/authorizer/role/AUT_Roles_v1.sol +++ b/src/modules/authorizer/role/AUT_Roles_v1.sol @@ -41,7 +41,7 @@ import {AccessControlEnumerableUpgradeable} from * modifier. Functions can also be set to public access by * adding the public role to the function permissions. * * - * @custom:documentation: See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_Roles_v1.sol + * @custom:documentation See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_Roles_v1.sol * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to From b584027de3356d128109a289faf786b41a37319f Mon Sep 17 00:00:00 2001 From: FHieser Date: Wed, 11 Jun 2025 09:57:55 +0200 Subject: [PATCH 05/67] Update AUT_Roles_v1.md --- docs/src/modules/authorizer/role/AUT_Roles_v1.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/src/modules/authorizer/role/AUT_Roles_v1.md b/docs/src/modules/authorizer/role/AUT_Roles_v1.md index 0025d02e1..da90ec9fc 100644 --- a/docs/src/modules/authorizer/role/AUT_Roles_v1.md +++ b/docs/src/modules/authorizer/role/AUT_Roles_v1.md @@ -76,6 +76,16 @@ classDiagram class AUT_Roles_v1{ - mapping(address target => mapping(bytes4 selector => bytes32[] roleIds)) _permissions; + + getPermissions() + + isRolePermissioned() + + hasPermission() + + createRole() + + labelRole() + + transferAdminRole() + + burnRoleAdmin() + + addAccessPermission() + + removeAccessPermission() + + createRoleAndAddAccessPermissions() } ``` From 7776e7906aea137078b937ecebf1e4506c30087a Mon Sep 17 00:00:00 2001 From: FHieser Date: Wed, 11 Jun 2025 09:57:59 +0200 Subject: [PATCH 06/67] Create AUT_TokenGated_Roles_v1.md --- .../role/AUT_TokenGated_Roles_v1.md | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 docs/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.md diff --git a/docs/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.md b/docs/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.md new file mode 100644 index 000000000..de2d865f5 --- /dev/null +++ b/docs/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.md @@ -0,0 +1,221 @@ +# AUT_Roles_v2 + +## Purpose of Contract + +This contract extends the Inverter's role-based access control to include token gating, enabling roles to be conditionally assigned based on token ownership. This mechanism allows for dynamic permissioning tied to specific token holdings. + +This contract has the following key features: + +- Token-based access checks before role assignment. +- Supports both {ERC20} and {ERC721} tokens. + +## Glossary + +To understand the functionalities of the following contract, it is important to be familiar with the following definitions. + +### Token Gated Role + +With this contract it is possible to extend the base functionality of the [AUT_Roles_v1](./AUT_Roles_v1.md) contract to make a role token gated. A token gated role behaves in all respects like a regular role, but handles the membership of that role differently. A member of a token gated role is only allowed to access the role functionalities if they hold a certain amount of a token. + +The implementation of this contract uses a few tricks to achieve this. Without going into too much detail, this is the main part that is needed to understand the basic mechanism: + +In the contract, token gating is implemented by storing the token address in the role’s members property, instead of directly listing user addresses. This setup allows the contract to check the token balance of a user against a defined threshold when access is requested. + +Example: We want to restrict a role to users who hold a certain amount of Token A. Therefore, we configure the role to be token-gated and set a required token amount the user needs to hold as threshold. When we then call `grantRole` with the address of Token A, the role becomes accessible only to users whose wallet holds at least the specified amount of that token. + +## Inheritance + +### Class Diagramm + +```mermaid +classDiagram + + note for AUT_Roles_v1 "OpenZeppelin Authorization System" + AUT_Roles_v1 <|-- AUT_TokenGated_Roles_v1 + + + + + class AUT_Roles_v1{ + - mapping(address target => mapping(bytes4 selector => bytes32[] roleIds)) _permissions; + + getPermissions() + + isRolePermissioned() + + hasPermission() + + createRole() + + labelRole() + + transferAdminRole() + + burnRoleAdmin() + + addAccessPermission() + + removeAccessPermission() + + createRoleAndAddAccessPermissions() + } + + class AUT_TokenGated_Roles_v1{ + - mapping(bytes32 => bool) _isTokenGated + - mapping(bytes32 => uint) _thresholdMap + + isTokenGated() + + hasTokenRole() + + getThresholdValue() + + setTokenGated() + + setThreshold() + } + +``` + +### Base Contracts + +This contract is based on the following contracts and inherits their functionalities: + +- [IAUT_TokenGated_Roles_v1](./interfaces/IAUT_TokenGated_Roles_v1.md): Implementation interface. +- [Module_v1](../../base/Module_v1.md): Inverter network base module functionality. +- [AccessControlEnumerableUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/extensions/AccessControlEnumerableUpgradeable.sol): Access control functionality. +- [AUT_Roles_v1](./AUT_Roles_v1.md): Base contract for the role-based access control. + +Functions that have been overridden to adapt functionalities are outlined below. + +### Key Changes to the Base Contracts + +_The purpose of this section is to highlight which functions of the base contract have been overridden and why._ + +- `hasRole`: This function has been overridden to allow token gating. +- `grantRole`: This function has been overridden to allow token gating. +- `revokeRole`: This function has been overridden to allow token gating. + +## Key Functionalities + +In this section the key functionalities of the authorizer module are described. + +### Token based Access Control + +This section describes the functionalities of the authorizer module regarding token based access control. + +#### Making a role token gated + +Making a role token gated is done by calling the `setTokenGated` function. This function takes the following parameters: + +- The role id of the role that we change the token gated status of. +- The boolean that indicates if the role should be token gated or not. + +When calling this function, the role can not contain any members, when it is switched to and from token gated. + +Example: Making the role "Whitelisted" token gated would look like this: + +```solidity +authorizer.setTokenGated( + whitelistedRoleId, + true +); +``` + +The function can only be called by a permissioned address (See [permissioned](#permissioned-modifier)). + +#### Setting the token threshold + +Setting the token threshold needed to pass the token gate is done by calling the `setTokenThreshold` function. This function takes the following parameters: + +- The role id of the role to set the threshold for. +- The address of the token to set the threshold for. +- The threshold value to set. + +Example: Setting the threshold for the token "USDC" to 100 would look like this: + +```solidity +authorizer.setTokenThreshold( + whitelistedRoleId, + USDC, + 100 +); +``` + +The function can only be called by a permissioned address (See [permissioned](#permissioned-modifier)). + +#### Adding a token to the token gate + +Adding a token to the token gate is done by calling the `grantRole` function. This function takes the following parameters: + +- The role id of the role to grant. +- The address of the token to grant the role to. + +The grantRole function can only be called by according admin of the role. +Example: Adding a token gate to the token gated role "Whitelisted" would look like this: + +```solidity +authorizer.grantRole( + whitelistedRoleId, + address(USDC) +); +``` + +#### Removing a token from the token gate + +Remove a token from the token gate is done by calling the `revokeRole` function. This function behaves like the regular revokeRole function, except that it sets the threshold for the role and token combination to 0 as well. +Example: Removing the token from the role "Whitelisted" would look like this: + +```solidity +authorizer.revokeRole( + whitelistedRoleId, + address(USDC) +); +``` + +#### Reversing a token gate + +In case the token gated status of a role needs to be reverted, the `setTokenGated` function can be used. The same restrictions as for the `setTokenGated` function apply here as well (see above). + +Example: Reversing the token gated status of the role "Whitelisted" would look like this: + +```solidity +authorizer.setTokenGated( + whitelistedRoleId, + false +); +``` + +### Full process of making a role token gated + +To make a role token gated, the following steps need to be taken in order: + +```mermaid +flowchart LR + + 1{ + Make role + token gated + } + 2{ + Set token + threshold + } + 3{ + Add token to the + token gate + } + + 1 --> 2 + 2 --> 3 +``` + +The first step is to make the role token gated. This is done by calling the `setTokenGated` function (see [here](#making-a-role-token-gated)). Remember that this function can only be called by a permissioned address and can not contain any members, when it is switched to and from token gated. + +The second step is to set the threshold for the role and token combination. This is done by calling the `setTokenThreshold` function (see [here](#setting-the-token-threshold)). This function is also permissioned. + +The third step is to actually add the token to the token gate. This is done by calling the `grantRole` function (see [here](#adding-a-token-to-the-token-gate)). This function can only be called by according admin of the role. + +## Deployment + +### Deployment Parameters + +The list of deployment parameters can be found in the _Technical Reference_ section of the documentation under the `init()` function ([https://docs.inverter.network/contracts/technical-reference/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol]()). + +### Deployment + +Deployment should be done using one of the methods provided below: + +- **Manual deployment:** Through Inverter Network's [Control Room application](https://beta.controlroom.inverter.network/). +- **SDK deployment:** Through Inverter Network's [TypeScript SDK](https://docs.inverter.network/sdk/typescript-sdk/guides/deploy-a-workflow) or [React SDK](https://docs.inverter.network/sdk/react-sdk/guides/deploy-a-workflow). + +### Setup Steps + +#### Optional Setup Steps + +Because a workflow and its authorizer module are deployed without any native permissions (except the initial admin role [here](#native-roles)), it might be necessary for some modules to modify the access of their functions. For this look up the sections `Mixed Utility`,`Role Management` and `Role Based Access Control` from the [AUT_Roles_v1](./AUT_Roles_v1.md) as well as the section [Full process of making a role token gated](#full-process-of-making-a-role-token-gated). From 7c908c9662efba6156dccea9931740b884f1b09b Mon Sep 17 00:00:00 2001 From: FHieser Date: Wed, 11 Jun 2025 09:58:28 +0200 Subject: [PATCH 07/67] Remove according natspec from contract headers --- .../role/AUT_TokenGated_Roles_v1.sol | 99 +------------------ .../interfaces/IAUT_TokenGated_Roles_v1.sol | 99 +------------------ 2 files changed, 3 insertions(+), 195 deletions(-) diff --git a/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol b/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol index c15fa3bd7..6dcf91ad8 100644 --- a/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol +++ b/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.sol @@ -57,106 +57,11 @@ interface TokenInterface { * - {IAUT_TokenGated_Roles_v1}: Implementation interface. * - {AUT_Roles_v1}: Inverter's role-based access control. * - * Key feeatures: + * Key features: * - Token-based access checks before role assignment. * - Supports both {ERC20} and {ERC721} tokens. * - * @custom:guide - * The following guide explains in detail how to use the key features - * of this module: - * - * - TOKEN BASED ACCESS CONTROL: - * - Token Gated Role: - * With this contract it is possible to extend the base - * functionality of the {AUT_Roles_v1} contract to make a - * role token gated. A token gated role behaves in all - * respects like a regular role, but handles the membership - * of that role differently. A member of a token gated role - * is only allowed to access the role functionalities if - * they hold a certain amount of a token. - * The implementation of this contract uses a few tricks to - * achieve this. Without going into too much detail, this - * is the main part that is needed to understand the basic - * mechanism: - * In the contract, token gating is implemented by storing - * the token address in the role’s members property, instead - * of directly listing user addresses. This setup allows the - * contract to check the token balance of a user against a - * defined threshold when access is requested. - * Example: We want to restrict a role to users who hold a - * certain amount of Token A. Therefore, we configure the - * role to be token-gated and set a required token amount - * the user needs to hold as threshold. When we then call - * 'grantRole' with the address of Token A, the role becomes - * accessible only to users whose wallet holds at least the - * specified amount of that token. - * - * - Making a role token gated: - * Making a role token gated is done by calling the - * `setTokenGated` function. This function takes the - * following parameters: - * - The role id of the role that we change the token gated - * status of. - * - The boolean that indicates if the role should be token - * gated or not. - * This function can only be called by a permissioned address - * (See permissioned section in the {AUT_Roles_v1} contract). - * Also the role can not contain any members, when it is - * switched to and from token gated. - * Example: Making the role "Whitelisted" token gated would - * look like this: - * authorizer.setTokenGated(whitelistedRoleId, true); - * - * - Setting the token threshold: - * Setting the token threshold needed to pass the token gate - * is done by calling the setTokenThreshold function. This - * function takes the following parameters: - * - The role id of the role to set the threshold for. - * - The address of the token to set the threshold for. - * - The threshold value to set. - * This function can only be called by a permissioned address - * (See permissioned section in the {AUT_Roles_v1} contract). - * This function can be called anytime, even if the role is - * not token gated yet. - * Example: Setting the threshold for the token "USDC" to - * 100 would look like this: - * authorizer.setTokenThreshold( - * whitelistedRoleId, USDC, 100); - * - * - Adding a token to the token gate: - * Adding a token to the token gate is done by calling the - * grantRole function. This function takes the following - * parameters: - * - The role id of the role to grant. - * - The address of the token to grant the role to. - * The grantRole function can only be called by according - * admin of the role. In addition, the given address needs - * to be a contract, already have a threshold set and - * contain the balanceOf function. - * If the role is not token gated then grantRole will - * behave like the regular grantRole function. - * Example: Adding a token gate to the token gated role - * "Whitelisted" would look like this: - * authorizer.grantRole(whitelistedRoleId, address(USDC)); - * - * - Removing a token from the token gate: - * Removing a token from the token gate is done by calling - * the revokeRole function. This function behaves like the - * regular revokeRole function, except that it sets the - * threshold for the role and token combination to 0 as - * well. - * Example: Removing the token from the role "Whitelisted" - * would look like this: - * authorizer.revokeRole(whitelistedRoleId, address(USDC)); - * - * - Reversing a token gate: - * In case the token gated status of a role needs to be - * reverted, the setTokenGated function can be used. The - * same restrictions as for the setTokenGated function apply - * here as well (see above). - * Example: Reversing the token gated status of the role - * "Whitelisted" would look like this: - * authorizer.setTokenGated(whitelistedRoleId, false); + * @custom:documentation See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.md * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to diff --git a/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol b/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol index 4374616c1..8cc5e4aca 100644 --- a/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol +++ b/src/modules/authorizer/role/interfaces/IAUT_TokenGated_Roles_v1.sol @@ -19,104 +19,7 @@ import {IAuthorizer_v1} from "@aut/IAuthorizer_v1.sol"; * - Token-based access checks before role assignment. * - Supports both {ERC20} and {ERC721} tokens. * - * @custom:guide - * The following guide explains in detail how to use the key features - * of this module: - * - * - TOKEN BASED ACCESS CONTROL: - * - Token Gated Role: - * With this contract it is possible to extend the base - * functionality of the {AUT_Roles_v1} contract to make a - * role token gated. A token gated role behaves in all - * respects like a regular role, but handles the membership - * of that role differently. A member of a token gated role - * is only allowed to access the role functionalities if - * they hold a certain amount of a token. - * The implementation of this contract uses a few tricks to - * achieve this. Without going into too much detail, this - * is the main part that is needed to understand the basic - * mechanism: - * In the contract the token gating is done by instead of - * saving the members of the role directly in the members - * property of the role, the contract saves the token - * address that will gate the role in the property. That way - * the token address can be looked up and the threshold - * amount compared to the token balance of incoming users. - * Example: We want to adapt a role so that it can only be - * accessed by users that hold a certain amount of token A. - * So we make the role token gated and set a threshold of - * how many tokens a address needs to hold to be able to - * access the role. The moment we use the grantRole function - * to add the address of token A to the members property of - * the role, the role will only be accessible by users that - * hold the threshold amount of token A. - * - * - Making a role token gated: - * Making a role token gated is done by calling the - * `setTokenGated` function. This function takes the - * following parameters: - * - The role id of the role that we change the token gated - * status of. - * - The boolean that indicates if the role should be token - * gated or not. - * This function can only be called by a permissioned address - * (See permissioned section in the {AUT_Roles_v1} contract). - * Also the role can not contain any members, when it is - * switched to and from token gated. - * Example: Making the role "Whitelisted" token gated would - * look like this: - * authorizer.setTokenGated(whitelistedRoleId, true); - * - * - Setting the token threshold: - * Setting the token threshold needed to pass the token gate - * is done by calling the setTokenThreshold function. This - * function takes the following parameters: - * - The role id of the role to set the threshold for. - * - The address of the token to set the threshold for. - * - The threshold value to set. - * This function can only be called by a permissioned address - * (See permissioned section in the {AUT_Roles_v1} contract). - * This function can be called anytime, even if the role is - * not token gated yet. - * Example: Setting the threshold for the token "USDC" to - * 100 would look like this: - * authorizer.setTokenThreshold( - * whitelistedRoleId, USDC, 100); - * - * - Adding a token to the token gate: - * Adding a token to the token gate is done by calling the - * grantRole function. This function takes the following - * parameters: - * - The role id of the role to grant. - * - The address of the token to grant the role to. - * The grantRole function can only be called by according - * admin of the role. In addition, the given address needs - * to be a contract, already have a threshold set and - * contain the balanceOf function. - * If the role is not token gated then grantRole will - * behave like the regular grantRole function. - * Example: Adding a token gate to the token gated role - * "Whitelisted" would look like this: - * authorizer.grantRole(whitelistedRoleId, address(USDC)); - * - * - Removing a token from the token gate: - * Removing a token from the token gate is done by calling - * the revokeRole function. This function behaves like the - * regular revokeRole function, except that it sets the - * threshold for the role and token combination to 0 as - * well. - * Example: Removing the token from the role "Whitelisted" - * would look like this: - * authorizer.revokeRole(whitelistedRoleId, address(USDC)); - * - * - Reversing a token gate: - * In case the token gated status of a role needs to be - * reverted, the setTokenGated function can be used. The - * same restrictions as for the setTokenGated function apply - * here as well (see above). - * Example: Reversing the token gated status of the role - * "Whitelisted" would look like this: - * authorizer.setTokenGated(whitelistedRoleId, false); + * @custom:documentation See https://github.com/InverterNetwork/contracts/tree/dev/docs/src/modules/authorizer/role/AUT_TokenGated_Roles_v1.md * * @custom:security-contact security@inverter.network * In case of any concerns or findings, please refer to From 6ca52254976279d9c6923381d2167f28dc8c9889 Mon Sep 17 00:00:00 2001 From: FHieser Date: Wed, 11 Jun 2025 10:00:24 +0200 Subject: [PATCH 08/67] Create 2025-06-05-team-omega-authorizer-update.pdf --- ...2025-06-05-team-omega-authorizer-update.pdf | Bin 0 -> 263659 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 audits/2025-06-05-team-omega-authorizer-update.pdf diff --git a/audits/2025-06-05-team-omega-authorizer-update.pdf b/audits/2025-06-05-team-omega-authorizer-update.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5135209738be2f5ea604ac9a3306180298c333be GIT binary patch literal 263659 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 H@qBdGD*JLUW?XKK?_3e~4Lm zF|R=xe~zSc;ps7Ndmd>KoxnEh!Ou3Ub3rGPmRP@#r_XrzFvh*;2U+*_4>CxqE{YwG zO$o_V|P74Wt&4kcHZp3`M>J#czGi{ z>@j5Tja&-&hjG6V)xgSe55{u+neeC_@QG)gf_|G%l5|#;H$jdWVc#eHDCt?rGvq{Zi zVi-^OeVH6TkBm0BWEur$d+~fscPhqeLw(Ni7rSO<9`=LcNg(4GK2~|8__Y~sRjkU| z5*CzWs8(<|O@F8=rI*P%V)8F-=|ckPWsAVIo~v?DtgQ>g}QI5xy6ys<%e z@Zo;WS(kR8Z!1gW#q<@p%>MKq7^9!~(-{MKii(diZ~9Y;1uV5Z-70+TtWbUoXfIc6 z@8@LUJ_vqw+z_BYX8TGWlyL1`g6G16tm-E}N`YP!haa0nux~cXbC92pxc?@*5R4a+ z@hV^h`eHQ6T#Os~Zx7Sb>;6YO_tX&B6l<^6!aVR!8FfWbn?!o(}n z=f9*{GBK;R-+zr=C$;HoM4u;0*PU>3SdC@rsI%m2UU2y(-kP%>w-ia{+*Lh=!|>SL zlVu-5O*_8PS1q0V>xfn92y6aR)=G!)$idfudl4Vbow}fT@|Ou3P%qWP z&C7yD4pO5cqd7?W2FIunbt2?gNMG7sy~9J76TZno#U)>s_Z5*Xqx*ezb>Jx+)Eb-7 z4J-P3`ChA4Tg&@^ksAFcJ&3UeiK90~5=e_Uj{s6m$M(*F$xZ9^DcF=yN{X)ElUvn; zr9AxPabCux_Vpz@zQ*DAL&+;)J1WZ{uT4KU`uw~0YAsWa-*~RXL|O5+{H0OpxaPhH z^SB_pa)saWf_hT+tveq4BUB%)TeP%r#(Z2qFy2`eyLEMpF1#_a5~VQ^k@gSnfE!%L zSMSR=c*JPGR3#(&K|Jyx#?rq88N~b*|6Y)=my)vI2U)-VTWZ*m#m)+|gGikGK9}wr0?3~Im-1bhg%#S^BtgG@m`n3RB#+xz=<$(c zwWhh<$Zwhe<(H@JcJz_Z91otFLpy_(#TY4itX==J3Wa#x5L;wg*cRk?2Vu^u(SE8> zIgEG!sl3E=#DaeG2%)gkh&c~w<)v=a0!GW=-SElaTw6)B(lS_7v%SQ_xxx-TCsYp0 zF{#m{rHG~!>IOq;T*0~AJ$a~`LIV&S6!uoxCEul`v5Z&{Nlru1X7$<33b(Q z--VTirBq{yRXAH<(;mf8n{SF2gWSIv#;z(?tlevBj}d;nNB%tj5~ZocoFemi=E@ru zUpW^JCrSx%QDRX#rTDArL4}*vhp!+l-2yH5-iPhD)O$) zcdU%CsSG~hod&q{Qq6sFrouFKNO%&_DMXSH274n`jZB_DtJXsf~d$Li4a&EO{7cH)5%wco#}$1tR(6U^o(s7o|`e?C_mp4*C5=p4$8jT#K~5Woe+^?t#)nVx5jK^=!%`c)X9yQJEq)DfDB-Wl3tpSHl0O#%;esQm#ekvg_V-a9O@ z-RUa277-g=&&)GT7k`t(?jf1C=P5&td>cn!g~dxW*SE*3bL=R+?6pnL)YCQ+8Cb2ia3lYQ zY&TYS8)sBydkrRF&f~7mAx2iIw68#2hHW-#JU)71iPpW7*=eu-X0u)XSRGw#>sgJb zfQO2r^3~pkNSML;Mxdpm`pHY*o%SUwTGzJw5td*QwOfum-b44l(73=!N)n9qxaQw@rs^X=K;>XW=-XMqeE>36fK~ZX zf{xg9Bcl=#H@-h01M>2FjGCPT7luK6wS$d{OS9|G52(oEr6S_?(C`rgFyJKwc1_B~I{{brF#(U1MldOnQW!n`y5&f&