diff --git a/src/AssetsLib.sol b/src/AssetsLib.sol new file mode 100644 index 0000000..79cae82 --- /dev/null +++ b/src/AssetsLib.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +/// @title Assets Library +/// @notice A library for handling asset types and conversions in a blockchain system +/// @dev Provides utilities for managing different asset types and converting between asset IDs and addresses +library Assets { + /// @dev Represents different types of assets that can be used in the system + enum Kind { + /// @notice Custom Asset Id + Custom, + /// @notice Standard ERC20 token asset type + Erc20 + } + + /// @dev Represents an asset with its type and associated data + struct Asset { + /// @notice The kind/type of the asset (Custom or ERC20) + Kind kind; + /// @notice The data associated with the asset, encoded as bytes32 + /// @dev The data is encoded as follows: + /// - For Custom assets: The asset ID is stored as uint256 + /// - For ERC20 assets: The token address is stored as address + bytes32 data; + } + + /// @dev The asset kind is not supported by the library. + error UnsupportedAssetKind(uint256 kind); + + /// @dev The supplied address is not a valid asset address, it does not start with 0xFFFFFFFF. + error InvalidAssetId(address assetAddress); + + /// @notice Converts a given asset to its corresponding address representation. + /// @dev The conversion follows the pattern: 0xFFFFFFFF followed by the 16-byte asset ID or if the asset is an ERC20 + /// token, + /// the address of the token contract. + /// + /// @param asset The asset be converted. + /// @return The address representation of the asset. + function toAddress(Asset memory asset) internal pure returns (address) { + if (isErc20(asset)) { + return address(uint160(uint256(asset.data))); + } else if (isCustom(asset)) { + return toAddress(asset.data); + } else { + revert UnsupportedAssetKind(uint256(asset.kind)); + } + } + + /// @notice Converts a given asset ID to its corresponding address representation. + /// @dev The conversion follows the pattern: 0xFFFFFFFF followed by the 16-byte asset ID. + /// + /// @param assetId The bytes32 asset ID to be converted. + /// @return The address representation of the asset ID. + function toAddress(bytes32 assetId) internal pure returns (address) { + // Construct the address by combining the prefix 0xFFFFFFFF00000000000000000000000000000000 + // with the lower 16 bytes of the assetId. + // This ensures the address follows the designated asset address format. + return address(uint160(uint256(0xFFFFFFFF << 128) | uint256(assetId))); + } + + /// @notice Converts an asset address back to its original asset ID. + /// @dev Validates that the address starts with the prefix 0xFFFFFFFF and extracts the 16-byte asset ID. + /// + /// @param assetAddress The address to be converted back to an asset ID. + /// @return The bytes32 representation of the original asset ID. + function toAssetId(address assetAddress) internal pure returns (bytes32) { + // Convert the address to a uint256 for bit manipulation. + uint256 addr = uint256(uint160(assetAddress)); + + // Ensure the upper 128 bits match the expected prefix 0xFFFFFFFF. + if (!isAssetIdCompatible(assetAddress)) { + revert InvalidAssetId(assetAddress); + } + + // Extract the lower 128 bits which represent the original asset ID. + uint128 assetIdUint = uint128(addr); + + // Convert the uint128 asset ID back to bytes32 format. + return bytes32(uint256(assetIdUint)); + } + + /// @notice Converts an asset address to an asset representation. + /// @dev Converts the asset address to an asset ID and constructs an asset object. + /// @param assetAddress The address of the asset to convert. + /// @return Asset Returns the asset representation of the provided address. + function toAsset(address assetAddress) internal pure returns (Asset memory) { + if (isAssetIdCompatible(assetAddress)) { + return Asset(Kind.Custom, toAssetId(assetAddress)); + } else { + return Asset(Kind.Erc20, bytes32(uint256(uint160(assetAddress)))); + } + } + + /// @notice Determines if the provided asset is an ERC20 token. + /// @dev Checks if the asset kind matches the ERC20 enum value. + /// @param asset The asset to check. + /// @return bool Returns true if the asset is an ERC20 token, false otherwise. + function isErc20(Asset memory asset) internal pure returns (bool) { + return asset.kind == Kind.Erc20; + } + + /// @notice Checks i the given asset is a Cstom type. + /// @dev Verifies if the asset's kid property mathes he Custom type. + /// @param asset The asset to check, defned by its kind and data. + /// @return bool Returns true if the asset is Custom, false otherwise. + function isCustom(Asset memory asset) internal pure returns (bool) { + return asset.kind == Kind.Custom; + } + + /// @notice Checks if the given asset address is compatible by verifying it starts with the prefix 0xFFFFFFFF. + /// @dev This function converts the asset address to a uint256 and ensures the upper 128 bits match 0xFFFFFFFF. + /// @param assetAddress The address of the asset to check for compatibility. + /// @return bool Returns true if the asset address is compatible, false otherwise. + function isAssetIdCompatible(address assetAddress) internal pure returns (bool) { + // Convert the address to a uint256 for bit manipulation. + uint256 addr = uint256(uint160(assetAddress)); + + // Ensure the upper 128 bits match the expected prefix 0xFFFFFFFF. + if ((addr >> 128) != 0xFFFFFFFF) { + return false; + } + + return true; + } + + /// @notice Determines if the provided asset is a native asset. + /// @dev This function checks the asset kind and verifies if the asset address or ID corresponds to a native asset. + /// @param asset The asset to be checked, defined by its kind and data. + /// @return bool Returns true if the asset is native, false otherwise. + function isNative(Asset memory asset) internal pure returns (bool) { + if (isErc20(asset)) { + address assetAddress = address(uint160(uint256(asset.data))); + return (assetAddress == address(0)); + } else if (isCustom(asset)) { + uint256 assetId = uint256(asset.data); + return (assetId == 0); + } else { + return false; + } + } +} diff --git a/src/BlueprintServiceManagerBase.sol b/src/BlueprintServiceManagerBase.sol index 717a756..7a1007e 100644 --- a/src/BlueprintServiceManagerBase.sol +++ b/src/BlueprintServiceManagerBase.sol @@ -16,6 +16,9 @@ import "src/IBlueprintServiceManager.sol"; /// of these functions interrupts the process flow. contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabled { using EnumerableSet for EnumerableSet.AddressSet; + using Assets for Assets.Asset; + using Assets for address; + using Assets for bytes32; /// @dev The Current Blueprint Id uint256 public currentBlueprintId; @@ -28,9 +31,6 @@ contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabl /// @notice This mapping is used to store the permitted payment assets for each service. mapping(uint64 => EnumerableSet.AddressSet) private _permittedPaymentAssets; - /// @dev The supplied address is not a valid asset address, it does not start with 0xFFFFFFFF. - error InvalidAssetId(address assetAddress); - /// @inheritdoc IBlueprintServiceManager function onBlueprintCreated(uint64 blueprintId, address owner, address mbsm) external virtual onlyFromRootChain { currentBlueprintId = blueprintId; @@ -176,7 +176,7 @@ contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabl /// @inheritdoc IBlueprintServiceManager function queryIsPaymentAssetAllowed( uint64 serviceId, - ServiceOperators.Asset calldata asset + Assets.Asset calldata asset ) external view @@ -192,25 +192,10 @@ contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabl * @param serviceId The ID of the service for which the asset is being permitted. * @param asset The asset to be permitted, defined by its kind and data. */ - function _permitAsset( - uint64 serviceId, - ServiceOperators.Asset calldata asset - ) - internal - virtual - returns (bool added) - { - if (asset.kind == ServiceOperators.AssetKind.Erc20) { - address assetAddress = address(uint160(uint256(asset.data))); - bool _added = _permittedPaymentAssets[serviceId].add(assetAddress); - return _added; - } else if (asset.kind == ServiceOperators.AssetKind.Custom) { - address assetAddress = _assetIdToAddress(asset.data); - bool _added = _permittedPaymentAssets[serviceId].add(assetAddress); - return _added; - } else { - return false; - } + function _permitAsset(uint64 serviceId, Assets.Asset calldata asset) internal virtual returns (bool added) { + address assetAddress = asset.toAddress(); + bool _added = _permittedPaymentAssets[serviceId].add(assetAddress); + return _added; } /** @@ -219,25 +204,10 @@ contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabl * @param serviceId The ID of the service for which the asset is being revoked. * @param asset The asset to be revoked, defined by its kind and data. */ - function _revokeAsset( - uint64 serviceId, - ServiceOperators.Asset calldata asset - ) - internal - virtual - returns (bool removed) - { - if (asset.kind == ServiceOperators.AssetKind.Erc20) { - address assetAddress = address(uint160(uint256(asset.data))); - bool _removed = _permittedPaymentAssets[serviceId].remove(assetAddress); - return _removed; - } else if (asset.kind == ServiceOperators.AssetKind.Custom) { - address assetAddress = _assetIdToAddress(asset.data); - bool _removed = _permittedPaymentAssets[serviceId].remove(assetAddress); - return _removed; - } else { - return false; - } + function _revokeAsset(uint64 serviceId, Assets.Asset calldata asset) internal virtual returns (bool removed) { + address assetAddress = asset.toAddress(); + bool _removed = _permittedPaymentAssets[serviceId].remove(assetAddress); + return _removed; } /** @@ -279,24 +249,15 @@ contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabl * @param serviceId The ID of the service for which permitted assets are being retrieved. * @return assets An array of ServiceOperators.Asset structs representing the permitted assets. */ - function _getPermittedAssets(uint64 serviceId) internal view virtual returns (ServiceOperators.Asset[] memory) { + function _getPermittedAssets(uint64 serviceId) internal view virtual returns (Assets.Asset[] memory) { EnumerableSet.AddressSet storage permittedAssets = _permittedPaymentAssets[serviceId]; - ServiceOperators.Asset[] memory assets = new ServiceOperators.Asset[](permittedAssets.length()); + Assets.Asset[] memory assets = new Assets.Asset[](permittedAssets.length()); for (uint256 i = 0; i < permittedAssets.length(); i++) { address assetAddress = permittedAssets.at(i); if (assetAddress == address(0)) { continue; } - ServiceOperators.AssetKind kind; - bytes32 data; - if (_checkAddressIsAssetIdCompatible(assetAddress)) { - kind = ServiceOperators.AssetKind.Custom; - data = _addressToAssetId(assetAddress); - } else { - kind = ServiceOperators.AssetKind.Erc20; - data = bytes32(uint256(uint160(assetAddress))); - } - assets[i] = ServiceOperators.Asset(kind, data); + assets[i] = assetAddress.toAsset(); } return assets; } @@ -308,99 +269,13 @@ contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabl * @param asset The asset to check, defined by its kind and data. * @return isAllowed Boolean indicating whether the asset is permitted. */ - function _isAssetPermitted( - uint64 serviceId, - ServiceOperators.Asset calldata asset - ) - internal - view - virtual - returns (bool) - { + function _isAssetPermitted(uint64 serviceId, Assets.Asset calldata asset) internal view virtual returns (bool) { // Native assets are always permitted. - if (_isNativeAsset(asset)) { + if (asset.isNative()) { return true; - } else if (asset.kind == ServiceOperators.AssetKind.Erc20) { - address assetAddress = address(uint160(uint256(asset.data))); - return _permittedPaymentAssets[serviceId].contains(assetAddress); - } else if (asset.kind == ServiceOperators.AssetKind.Custom) { - address assetAddress = _assetIdToAddress(asset.data); - return _permittedPaymentAssets[serviceId].contains(assetAddress); } else { - return false; - } - } - - /** - * @notice Converts a given asset ID to its corresponding address representation. - * @dev The conversion follows the pattern: 0xFFFFFFFF followed by the 16-byte asset ID. - * - * @param assetId The bytes32 asset ID to be converted. - * @return The address representation of the asset ID. - */ - function _assetIdToAddress(bytes32 assetId) internal pure returns (address) { - // Construct the address by combining the prefix 0xFFFFFFFF00000000000000000000000000000000 - // with the lower 16 bytes of the assetId. - // This ensures the address follows the designated asset address format. - return address(uint160(uint256(0xFFFFFFFF << 128) | uint256(assetId))); - } - - /** - * @notice Converts an asset address back to its original asset ID. - * @dev Validates that the address starts with the prefix 0xFFFFFFFF and extracts the 16-byte asset ID. - * - * @param assetAddress The address to be converted back to an asset ID. - * @return The bytes32 representation of the original asset ID. - */ - function _addressToAssetId(address assetAddress) internal pure returns (bytes32) { - // Convert the address to a uint256 for bit manipulation. - uint256 addr = uint256(uint160(assetAddress)); - - // Ensure the upper 128 bits match the expected prefix 0xFFFFFFFF. - if (!_checkAddressIsAssetIdCompatible(assetAddress)) { - revert InvalidAssetId(assetAddress); - } - - // Extract the lower 128 bits which represent the original asset ID. - uint128 assetIdUint = uint128(addr); - - // Convert the uint128 asset ID back to bytes32 format. - return bytes32(uint256(assetIdUint)); - } - - /** - * @notice Checks if the given asset address is compatible by verifying it starts with the prefix 0xFFFFFFFF. - * @dev This function converts the asset address to a uint256 and ensures the upper 128 bits match 0xFFFFFFFF. - * @param assetAddress The address of the asset to check for compatibility. - * @return bool Returns true if the asset address is compatible, false otherwise. - */ - function _checkAddressIsAssetIdCompatible(address assetAddress) internal pure returns (bool) { - // Convert the address to a uint256 for bit manipulation. - uint256 addr = uint256(uint160(assetAddress)); - - // Ensure the upper 128 bits match the expected prefix 0xFFFFFFFF. - if ((addr >> 128) != 0xFFFFFFFF) { - return false; - } - - return true; - } - - /** - * @notice Determines if the provided asset is a native asset. - * @dev This function checks the asset kind and verifies if the asset address or ID corresponds to a native asset. - * @param asset The asset to be checked, defined by its kind and data. - * @return bool Returns true if the asset is native, false otherwise. - */ - function _isNativeAsset(ServiceOperators.Asset calldata asset) internal pure returns (bool) { - if (asset.kind == ServiceOperators.AssetKind.Erc20) { - address assetAddress = address(uint160(uint256(asset.data))); - return (assetAddress == address(0)); - } else if (asset.kind == ServiceOperators.AssetKind.Custom) { - uint256 assetId = uint256(asset.data); - return (assetId == 0); - } else { - return false; + address assetAddress = asset.toAddress(); + return _permittedPaymentAssets[serviceId].contains(assetAddress); } } } diff --git a/src/IBlueprintServiceManager.sol b/src/IBlueprintServiceManager.sol index 0a45c00..6c5bf42 100644 --- a/src/IBlueprintServiceManager.sol +++ b/src/IBlueprintServiceManager.sol @@ -1,72 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; -/// @title Service Operators Library -/// @author Tangle Network Team -/// @dev Contains structs and enums related to service operators. -/// @notice Holds different structs and enums related to service operators. -library ServiceOperators { - /// @dev Represents the preferences of an operator, including their ECDSA public key and price targets. - struct OperatorPreferences { - /// @notice The ECDSA public key of the operator. - bytes ecdsaPublicKey; - /// @notice The price targets associated with the operator. - PriceTargets priceTargets; - } - - /// @dev Defines the pricing targets for various resources such as CPU, memory, and different types of storage. - struct PriceTargets { - /// @notice The CPU price target. - uint64 cpu; - /// @notice The memory price target. - uint64 mem; - /// @notice The HDD storage price target. - uint64 storage_hdd; - /// @notice The SSD storage price target. - uint64 storage_ssd; - /// @notice The NVMe storage price target. - uint64 storage_nvme; - } - - /// @dev Represents different types of assets that can be used in the system - enum AssetKind { - /// @notice Custom Asset Id - Custom, - /// @notice Standard ERC20 token asset type - Erc20 - } - - /// @dev Represents an asset with its type and associated data - struct Asset { - /// @notice The kind/type of the asset (Custom or ERC20) - AssetKind kind; - /// @notice The data associated with the asset, encoded as bytes32 - /// @dev The data is encoded as follows: - /// - For Custom assets: The asset ID is stored as uint256 - /// - For ERC20 assets: The token address is stored as address - bytes32 data; - } - - /// @dev Represents parameters for a service request - struct RequestParams { - /// @notice Unique identifier for the request - uint64 requestId; - /// @notice Address of the requester - address requester; - /// @notice Array of operator preferences containing public keys and price targets - OperatorPreferences[] operators; - /// @notice Input parameters for the request encoded as bytes - bytes requestInputs; - /// @notice Array of addresses that are permitted to call the service - address[] permittedCallers; - /// @notice Time-to-live value indicating how long the service is valid - uint64 ttl; - /// @notice Asset to be used for payment - Asset paymentAsset; - /// @notice Amount of payment asset to be used - uint256 amount; - } -} +import "./ServiceOperatorsLib.sol"; /// @title IBlueprintServiceManager /// @dev Interface for the BlueprintServiceManager contract, which acts as a manager for the lifecycle of a Blueprint @@ -231,7 +166,7 @@ interface IBlueprintServiceManager { /// @return isAllowed Returns true if the asset is allowed, false otherwise. function queryIsPaymentAssetAllowed( uint64 serviceId, - ServiceOperators.Asset calldata asset + Assets.Asset calldata asset ) external view diff --git a/src/MasterBlueprintServiceManager.sol b/src/MasterBlueprintServiceManager.sol index 468f6d7..8c19159 100644 --- a/src/MasterBlueprintServiceManager.sol +++ b/src/MasterBlueprintServiceManager.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "src/Permissions.sol"; import "src/IBlueprintServiceManager.sol"; @@ -13,6 +15,20 @@ import "src/IBlueprintServiceManager.sol"; /// @dev This contract acts as an interceptor between the root chain and blueprint service manager contracts. contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausable { using EnumerableMap for EnumerableMap.UintToAddressMap; + using Assets for Assets.Asset; + using Assets for address; + using Assets for bytes32; + using SafeERC20 for IERC20; + + // ===== Constants ===== + + /// @dev The role that allows the contract to pause and unpause the contract. + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + /// @dev The role that allows the change of tranches and their percentages. + bytes32 public constant TRENCH_UPDATE_ROLE = keccak256("TRENCH_UPDATE_ROLE"); + + /// @dev The base percentage value. + uint16 public constant BASE_PERCENT = 10_000; /// @title Blueprint Structs /// @dev Defines the Blueprint and related data structures for the service. @@ -39,6 +55,34 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa string license; // Empty string represents None } + /// @dev Defines the different tranches for the blueprint. + /// @notice Use this enum to define the different tranches for the blueprint. + enum TrancheKind { + /// @notice Blueprint Developer tranche + /// @dev The developer tranche is for the developer of the blueprint. + Developer, + /// @notice Blueprint Protocol tranche + /// @dev The protocol tranche is for the protocol. + Protocol, + /// @notice Blueprint Operators tranche + /// @dev The operators tranche is for the operators of the blueprint. + Operators, + /// @notice Blueprint Restakers tranche + /// @dev The restakers tranche is for the restakers of the blueprint. + Restakers + } + + /// @dev Defines the tranche with the percentage. + /// @notice Use this struct to define the tranche with the percentage. + struct Tranche { + /// @dev The kind of the tranche. + TrancheKind kind; + /// @dev The percentage of the tranche in the scale of `10000`. + /// where `10000` is equal to `100%` and `1` is equal to `0.01%`. + /// Any value between `1` and `10000` is valid for the percentage, inclusive. + uint16 percent; + } + // ============ Events ============ /// @dev Emitted when a new blueprint is created. @@ -75,7 +119,7 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa uint64 indexed requestId, address indexed requester, uint64 ttl, - ServiceOperators.Asset asset, + Assets.Asset asset, uint256 amount ); @@ -156,6 +200,12 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa uint64 indexed blueprintId, uint64 indexed serviceId, bytes offender, uint8 slashPercent, uint256 totalPayout ); + // =========== Errors ============ + + /// @dev Error when the tranches are invalid and does not sum up to 100%. + /// @notice The tranches should always sum up to 10000 (100%). + error InvalidTranches(); + // ============ Storage ============ /// @dev Mapping that store the blueprint service manager contracts. @@ -170,6 +220,33 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa /// blueprintId => owner EnumerableMap.UintToAddressMap private blueprintOwners; + /// @dev Mapping that stores the Service requests for a blueprint. + /// @notice Contains the service requests for a blueprint. + /// + /// requestId => request + mapping(uint64 => ServiceOperators.RequestParams) private serviceRequests; + + /// @dev An array of tranches and their percentages. + /// always sum up to 10000 (100%). + Tranche[] public tranches; + + /// @dev The address of the protocol fees receiver. + /// @notice The address that receives the protocol fees. + address payable public protocolFeesReceiver; + + // ============ Constructor ============ + + constructor(address payable _protocolFeesReceiver) { + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _grantRole(PAUSER_ROLE, _msgSender()); + _grantRole(TRENCH_UPDATE_ROLE, _msgSender()); + + _grantRole(DEFAULT_ADMIN_ROLE, ROOT_CHAIN); + + _intializeDefaultTranches(); + protocolFeesReceiver = _protocolFeesReceiver; + } + // ======== Functions ========= /// @dev Hook to handle blueprint creation. Gets called by the root chain when a new blueprint is created. @@ -257,6 +334,7 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa { address manager = blueprints.get(blueprintId); IBlueprintServiceManager(manager).onRequest(params); + serviceRequests[params.requestId] = params; emit ServiceRequested( blueprintId, params.requestId, params.requester, params.ttl, params.paymentAsset, params.amount ); @@ -298,6 +376,7 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa { address manager = blueprints.get(blueprintId); IBlueprintServiceManager(manager).onReject(operator, requestId); + delete serviceRequests[requestId]; emit RequestRejected(blueprintId, requestId, operator); } @@ -317,11 +396,14 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa uint64 ttl ) public + payable onlyFromRootChain whenNotPaused { - address manager = blueprints.get(blueprintId); - IBlueprintServiceManager(manager).onServiceInitialized(requestId, serviceId, owner, permittedCallers, ttl); + IBlueprintServiceManager manager = IBlueprintServiceManager(blueprints.get(blueprintId)); + ServiceOperators.RequestParams memory request = serviceRequests[requestId]; + _splitFunds(manager, serviceId, request); + manager.onServiceInitialized(requestId, serviceId, owner, permittedCallers, ttl); emit ServiceInitialized(blueprintId, requestId, serviceId, owner, ttl); } @@ -454,4 +536,113 @@ contract MasterBlueprintServiceManager is RootChainEnabled, AccessControl, Pausa address manager = blueprints.get(blueprintId); return IBlueprintServiceManager(manager).queryDisputeOrigin(serviceId); } + + /// @dev Pause the contract. + /// @notice Only the pauser can pause the contract. + function pause() public onlyRole(PAUSER_ROLE) whenNotPaused { + _pause(); + } + + /// @dev Unpause the contract. + /// @notice Only the pauser can unpause the contract. + function unpause() public onlyRole(PAUSER_ROLE) whenPaused { + _unpause(); + } + + /// @dev Update the tranches and their percentages. + /// @param _tranches The array of tranches and their percentages. + /// @notice Only the tranche updater can update the tranches. + function setTranches(Tranche[] calldata _tranches) public onlyRole(TRENCH_UPDATE_ROLE) { + _setTranches(_tranches); + } + + /// @dev Initialize the default tranches for the blueprint. + function _intializeDefaultTranches() internal { + Tranche[] memory _tranches = new Tranche[](4); + _tranches[0] = Tranche(TrancheKind.Developer, 5000); // 50% + _tranches[1] = Tranche(TrancheKind.Protocol, 2000); // 20% + _tranches[2] = Tranche(TrancheKind.Operators, 1000); // 10% + _tranches[3] = Tranche(TrancheKind.Restakers, 2000); // 20% + _verifyTranches(_tranches); + // Clear the default tranches and set the new ones. + for (uint256 i = 0; i < _tranches.length; i++) { + tranches.push(_tranches[i]); + } + } + + /// @dev Set the tranches and their percentages. + /// @param _tranches The array of tranches and their percentages. + /// @notice the tranches should always sum up to 10000 (100%). + function _setTranches(Tranche[] calldata _tranches) internal { + _verifyTranches(_tranches); + delete tranches; + for (uint256 i = 0; i < _tranches.length; i++) { + tranches.push(_tranches[i]); + } + } + + /// @dev Verify the tranches and their percentages. + function _verifyTranches(Tranche[] memory _tranches) internal pure { + uint16 sum = 0; + for (uint256 i = 0; i < _tranches.length; i++) { + sum += _tranches[i].percent; + } + if (sum != BASE_PERCENT) { + revert InvalidTranches(); + } + } + + /// @dev an internal function to split the funds between the tranches. + /// @param manager The Blueprint Service Manager contract. + /// @param serviceId The ID of the service. + /// @param request The request parameters. + function _splitFunds( + IBlueprintServiceManager manager, + uint64 serviceId, + ServiceOperators.RequestParams memory request + ) + internal + { + uint256 totalAmount = request.amount; + if (totalAmount == 0) { + return; + } + uint256 developerAmount = 0; + uint256 protocolAmount = 0; + uint256 operatorAmount = 0; + uint256 restakerAmount = 0; + + for (uint256 i = 0; i < tranches.length; i++) { + Tranche memory tranche = tranches[i]; + uint256 trancheAmount = (totalAmount * tranche.percent) / BASE_PERCENT; + if (tranche.kind == TrancheKind.Developer) { + developerAmount = trancheAmount; + } else if (tranche.kind == TrancheKind.Protocol) { + protocolAmount = trancheAmount; + } else if (tranche.kind == TrancheKind.Operators) { + operatorAmount = trancheAmount; + } else if (tranche.kind == TrancheKind.Restakers) { + restakerAmount = trancheAmount; + } + } + + uint256 toRewardsPallet = operatorAmount + restakerAmount; + + address payable developer = manager.queryDeveloperPaymentAddress(serviceId); + if (request.paymentAsset.isNative()) { + // Native asset + developer.transfer(developerAmount); + protocolFeesReceiver.transfer(protocolAmount); + // TODO: call the rewards pallet precompile here with the funds. + // for now, we will transfer the funds to the rewards pallet. + payable(address(REWARDS_PALLET)).transfer(toRewardsPallet); + } else { + // ERC20 + address token = request.paymentAsset.toAddress(); + IERC20(token).safeTransfer(developer, developerAmount); + IERC20(token).safeTransfer(protocolFeesReceiver, protocolAmount); + IERC20(token).safeTransfer(REWARDS_PALLET, toRewardsPallet); + // TODO: call the rewards pallet precompile here. + } + } } diff --git a/src/Permissions.sol b/src/Permissions.sol index 07ccf24..1ab4849 100644 --- a/src/Permissions.sol +++ b/src/Permissions.sol @@ -7,6 +7,8 @@ pragma solidity ^0.8.20; contract RootChainEnabled { /// @notice The address of the root chain address public constant ROOT_CHAIN = 0x1111111111111111111111111111111111111111; + /// @notice The address of the rewards pallet + address public constant REWARDS_PALLET = address(0x7e87d5); address public masterBlueprintServiceManager; @@ -22,6 +24,12 @@ contract RootChainEnabled { return ROOT_CHAIN; } + /// @dev Get the rewards pallet address + /// @return rewardsPalletAddress The address of the rewards pallet + function rewardsPallet() external pure returns (address rewardsPalletAddress) { + return REWARDS_PALLET; + } + /// @dev Get the master blueprint service manager address /// @return mbsm The address of the master blueprint service manager function masterBlueprintServiceManagerAddress() external view returns (address mbsm) { diff --git a/src/ServiceOperatorsLib.sol b/src/ServiceOperatorsLib.sol new file mode 100644 index 0000000..6132958 --- /dev/null +++ b/src/ServiceOperatorsLib.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { Assets } from "./AssetsLib.sol"; + +/// @title Service Operators Library +/// @author Tangle Network Team +/// @dev Contains structs and enums related to service operators. +/// @notice Holds different structs and enums related to service operators. +library ServiceOperators { + /// @dev Represents the preferences of an operator, including their ECDSA public key and price targets. + struct OperatorPreferences { + /// @notice The ECDSA public key of the operator. + bytes ecdsaPublicKey; + /// @notice The price targets associated with the operator. + PriceTargets priceTargets; + } + + /// @dev Defines the pricing targets for various resources such as CPU, memory, and different types of storage. + struct PriceTargets { + /// @notice The CPU price target. + uint64 cpu; + /// @notice The memory price target. + uint64 mem; + /// @notice The HDD storage price target. + uint64 storage_hdd; + /// @notice The SSD storage price target. + uint64 storage_ssd; + /// @notice The NVMe storage price target. + uint64 storage_nvme; + } + + /// @dev Represents parameters for a service request + struct RequestParams { + /// @notice Unique identifier for the request + uint64 requestId; + /// @notice Address of the requester + address requester; + /// @notice Array of operator preferences containing public keys and price targets + OperatorPreferences[] operators; + /// @notice Input parameters for the request encoded as bytes + bytes requestInputs; + /// @notice Array of addresses that are permitted to call the service + address[] permittedCallers; + /// @notice Time-to-live value indicating how long the service is valid + uint64 ttl; + /// @notice Asset to be used for payment + Assets.Asset paymentAsset; + /// @notice Amount of payment asset to be used + uint256 amount; + } + + /// @dev Converts a public key to an operator address. + /// @param publicKey The uncompressed public key to convert. + /// @return operator address The operator address. + function asOperatorAddress(bytes calldata publicKey) internal pure returns (address operator) { + return address(uint160(uint256(keccak256(publicKey)))); + } +} diff --git a/test/BlueprintServiceManagerBase.t.sol b/test/BlueprintServiceManagerBase.t.sol index 8b7817b..77e4a40 100644 --- a/test/BlueprintServiceManagerBase.t.sol +++ b/test/BlueprintServiceManagerBase.t.sol @@ -8,6 +8,10 @@ import "./MockERC20.sol"; import "./MockBlueprintServiceManager.sol"; contract BlueprintServiceManagerBaseTest is Test { + using Assets for Assets.Asset; + using Assets for address; + using Assets for bytes32; + MockBlueprintServiceManager manager; address rootChain = 0x1111111111111111111111111111111111111111; address masterBlueprintServiceManager = address(0x2222222222222222222222222222222222222222); @@ -179,10 +183,7 @@ contract BlueprintServiceManagerBaseTest is Test { requestInputs: "input data", permittedCallers: new address[](0), ttl: 1000, - paymentAsset: ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }), + paymentAsset: Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }), amount: 1000 }); @@ -198,10 +199,7 @@ contract BlueprintServiceManagerBaseTest is Test { requestInputs: "input data", permittedCallers: new address[](0), ttl: 1000, - paymentAsset: ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }), + paymentAsset: Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }), amount: 1000 }); @@ -526,10 +524,8 @@ contract BlueprintServiceManagerBaseTest is Test { // Test queryIsPaymentAssetAllowed function test_QueryIsPaymentAssetAllowed_Erc20AssetAllowed() public onlyMaster { uint64 serviceId = 109; - ServiceOperators.Asset memory asset = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }); + Assets.Asset memory asset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }); // Permit the asset manager.permitAsset(serviceId, asset); @@ -540,10 +536,8 @@ contract BlueprintServiceManagerBaseTest is Test { function test_QueryIsPaymentAssetAllowed_Erc20AssetNotAllowed() public view { uint64 serviceId = 110; - ServiceOperators.Asset memory asset = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }); + Assets.Asset memory asset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }); bool isAllowed = manager.queryIsPaymentAssetAllowed(serviceId, asset); assertFalse(isAllowed, "ERC20 asset should not be allowed initially"); @@ -552,8 +546,7 @@ contract BlueprintServiceManagerBaseTest is Test { function test_QueryIsPaymentAssetAllowed_CustomAssetAllowed() public onlyMaster { uint64 serviceId = 111; bytes32 assetId = bytes32(uint256(123_456)); - ServiceOperators.Asset memory asset = - ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: assetId }); + Assets.Asset memory asset = Assets.Asset({ kind: Assets.Kind.Custom, data: assetId }); // Permit the asset manager.permitAsset(serviceId, asset); @@ -565,8 +558,7 @@ contract BlueprintServiceManagerBaseTest is Test { function test_QueryIsPaymentAssetAllowed_CustomAssetNotAllowed() public view { uint64 serviceId = 112; bytes32 assetId = bytes32(uint256(654_321)); - ServiceOperators.Asset memory asset = - ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: assetId }); + Assets.Asset memory asset = Assets.Asset({ kind: Assets.Kind.Custom, data: assetId }); bool isAllowed = manager.queryIsPaymentAssetAllowed(serviceId, asset); assertFalse(isAllowed, "Custom asset should not be allowed initially"); @@ -575,13 +567,10 @@ contract BlueprintServiceManagerBaseTest is Test { // Test _permitAsset and _revokeAsset function test_PermitAndRevokeAsset() public onlyMaster { uint64 serviceId = 113; - ServiceOperators.Asset memory erc20Asset = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }); + Assets.Asset memory erc20Asset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }); bytes32 customAssetId = bytes32(uint256(789_012)); - ServiceOperators.Asset memory customAsset = - ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: customAssetId }); + Assets.Asset memory customAsset = Assets.Asset({ kind: Assets.Kind.Custom, data: customAssetId }); // Permit both assets manager.permitAsset(serviceId, erc20Asset); @@ -604,13 +593,10 @@ contract BlueprintServiceManagerBaseTest is Test { // Test _clearPermittedAssets function test_ClearPermittedAssets() public onlyMaster { uint64 serviceId = 114; - ServiceOperators.Asset memory erc20Asset = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }); + Assets.Asset memory erc20Asset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }); bytes32 customAssetId = bytes32(uint256(890_123)); - ServiceOperators.Asset memory customAsset = - ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: customAssetId }); + Assets.Asset memory customAsset = Assets.Asset({ kind: Assets.Kind.Custom, data: customAssetId }); // Permit both assets manager.permitAsset(serviceId, erc20Asset); @@ -631,10 +617,8 @@ contract BlueprintServiceManagerBaseTest is Test { // Test _getPermittedAssetsAsAddresses function test_GetPermittedAssetsAsAddresses() public onlyMaster { uint64 serviceId = 115; - ServiceOperators.Asset memory erc20Asset = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }); + Assets.Asset memory erc20Asset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }); manager.permitAsset(serviceId, erc20Asset); @@ -646,81 +630,70 @@ contract BlueprintServiceManagerBaseTest is Test { // Test _getPermittedAssets function test_GetPermittedAssets() public onlyMaster { uint64 serviceId = 116; - ServiceOperators.Asset memory erc20Asset = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }); + Assets.Asset memory erc20Asset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }); bytes32 customAssetId = bytes32(uint256(345_678)); - ServiceOperators.Asset memory customAsset = - ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: customAssetId }); + Assets.Asset memory customAsset = Assets.Asset({ kind: Assets.Kind.Custom, data: customAssetId }); manager.permitAsset(serviceId, erc20Asset); manager.permitAsset(serviceId, customAsset); - ServiceOperators.Asset[] memory permitted = manager.getPermittedAssets(serviceId); + Assets.Asset[] memory permitted = manager.getPermittedAssets(serviceId); assertEq(permitted.length, 2, "Should have two permitted assets"); // Verify ERC20 asset - assertTrue(permitted[0].kind == ServiceOperators.AssetKind.Erc20, "First asset should be ERC20"); + assertTrue(permitted[0].kind == Assets.Kind.Erc20, "First asset should be ERC20"); assertEq(address(uint160(uint256(permitted[0].data))), address(mockToken), "ERC20 asset data mismatch"); // Verify Custom asset - assertTrue(permitted[1].kind == ServiceOperators.AssetKind.Custom, "Second asset should be Custom"); + assertTrue(permitted[1].kind == Assets.Kind.Custom, "Second asset should be Custom"); assertEq(permitted[1].data, customAssetId, "Custom asset data mismatch"); } // Test asset ID to address and back function test_AssetIdConversion() public onlyMaster { bytes32 assetId = bytes32(uint256(567_890)); - address assetAddress = manager.assetIdToAddress(assetId); - bytes32 convertedId = manager.addressToAssetId(assetAddress); + address assetAddress = assetId.toAddress(); + bytes32 convertedId = assetAddress.toAssetId(); assertEq(assetId, convertedId, "Asset ID should match after conversion"); } // Test invalid asset address conversion function test_AddressToAssetId_InvalidAddress() public { address invalidAssetAddress = address(0xABCDEF); - vm.expectRevert( - abi.encodeWithSelector(BlueprintServiceManagerBase.InvalidAssetId.selector, invalidAssetAddress) - ); + vm.expectRevert(abi.encodeWithSelector(Assets.InvalidAssetId.selector, invalidAssetAddress)); - manager.addressToAssetId(invalidAssetAddress); + invalidAssetAddress.toAssetId(); } // Test native asset check function test_IsNativeAsset_Erc20NonNative() public view { - ServiceOperators.Asset memory erc20Asset = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(mockToken)))) - }); + Assets.Asset memory erc20Asset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(mockToken)))) }); - bool isNative = manager.isNativeAsset(erc20Asset); + bool isNative = erc20Asset.isNative(); assertFalse(isNative, "ERC20 asset should not be native"); } - function test_IsNativeAsset_CustomNonNative() public view { - ServiceOperators.Asset memory customAsset = - ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: bytes32(uint256(678_901)) }); + function test_IsNativeAsset_CustomNonNative() public pure { + Assets.Asset memory customAsset = Assets.Asset({ kind: Assets.Kind.Custom, data: bytes32(uint256(678_901)) }); - bool isNative = manager.isNativeAsset(customAsset); + bool isNative = customAsset.isNative(); assertFalse(isNative, "Custom asset should not be native"); } - function test_IsNativeAsset_NativeErc20() public view { - ServiceOperators.Asset memory nativeErc20 = ServiceOperators.Asset({ - kind: ServiceOperators.AssetKind.Erc20, - data: bytes32(uint256(uint160(address(0)))) - }); + function test_IsNativeAsset_NativeErc20() public pure { + Assets.Asset memory nativeErc20 = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(0)))) }); - bool isNative = manager.isNativeAsset(nativeErc20); + bool isNative = nativeErc20.isNative(); assertTrue(isNative, "Erc20 with address 0 should be native"); } - function test_IsNativeAsset_NativeCustom() public view { - ServiceOperators.Asset memory nativeCustom = - ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: bytes32(uint256(0)) }); + function test_IsNativeAsset_NativeCustom() public pure { + Assets.Asset memory nativeCustom = Assets.Asset({ kind: Assets.Kind.Custom, data: bytes32(uint256(0)) }); - bool isNative = manager.isNativeAsset(nativeCustom); + bool isNative = nativeCustom.isNative(); assertTrue(isNative, "Custom asset with ID 0 should be native"); } } diff --git a/test/Dummy.t.sol b/test/Dummy.t.sol deleted file mode 100644 index 9e6138c..0000000 --- a/test/Dummy.t.sol +++ /dev/null @@ -1,19 +0,0 @@ -pragma solidity ^0.8.20; - -import { Test } from "forge-std/Test.sol"; - -contract DummyTest is Test { - uint256 testNumber; - - function setUp() public { - testNumber = 42; - } - - function test_NumberIs42() public view { - assertEq(testNumber, 42); - } - - function testFail_Subtract43() public { - testNumber -= 43; - } -} diff --git a/test/MasterBlueprintServiceManager.t.sol b/test/MasterBlueprintServiceManager.t.sol new file mode 100644 index 0000000..adc61f8 --- /dev/null +++ b/test/MasterBlueprintServiceManager.t.sol @@ -0,0 +1,1298 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { Test } from "forge-std/Test.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import { MasterBlueprintServiceManager } from "src/MasterBlueprintServiceManager.sol"; +import "./MockERC20.sol"; +import "src/IBlueprintServiceManager.sol"; +import "src/AssetsLib.sol"; +import "src/ServiceOperatorsLib.sol"; +import "./MockBlueprintServiceManager.sol"; + +contract MasterBlueprintServiceManagerTest is Test { + using Assets for address; + using Assets for Assets.Asset; + + MasterBlueprintServiceManager masterManager; + MockBlueprintServiceManager mockManager; + MockERC20 erc20Token; + address payable protocolFeesReceiver = payable(address(0xdead)); + address payable blueprintOwner = payable(address(0xbeef)); + + // Set up roles + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant TRENCH_UPDATE_ROLE = keccak256("TRENCH_UPDATE_ROLE"); + + address public ROOT_CHAIN = address(0x1111111111111111111111111111111111111111); + + uint16 public constant BASE_PERCENT = 10_000; + + event BlueprintCreated( + address indexed owner, uint64 indexed blueprintId, MasterBlueprintServiceManager.Blueprint blueprint + ); + event OperatorRegistered(uint64 indexed blueprintId, ServiceOperators.OperatorPreferences operator); + event OperatorUnregistered(uint64 indexed blueprintId, ServiceOperators.OperatorPreferences operator); + event PriceTargetsUpdated(uint64 indexed blueprintId, ServiceOperators.OperatorPreferences operator); + event ServiceRequested( + uint64 indexed blueprintId, + uint64 indexed requestId, + address indexed requester, + uint64 ttl, + Assets.Asset asset, + uint256 amount + ); + event RequestApproved( + uint64 indexed blueprintId, + uint64 indexed requestId, + ServiceOperators.OperatorPreferences operator, + uint8 restakingPercent + ); + event RequestRejected( + uint64 indexed blueprintId, uint64 indexed requestId, ServiceOperators.OperatorPreferences operator + ); + event ServiceInitialized( + uint64 indexed blueprintId, uint64 indexed requestId, uint64 indexed serviceId, address owner, uint64 ttl + ); + event JobCalled(uint64 indexed blueprintId, uint64 indexed serviceId, uint8 job, uint64 jobCallId); + event JobResultReceived( + uint64 indexed blueprintId, + uint64 indexed serviceId, + uint8 job, + uint64 jobCallId, + ServiceOperators.OperatorPreferences operator + ); + event ServiceTerminated(uint64 indexed blueprintId, uint64 indexed serviceId, address owner); + event UnappliedSlash( + uint64 indexed blueprintId, uint64 indexed serviceId, bytes offender, uint8 slashPercent, uint256 totalPayout + ); + event Slashed( + uint64 indexed blueprintId, uint64 indexed serviceId, bytes offender, uint8 slashPercent, uint256 totalPayout + ); + + function setUp() public { + // Impersonate the ROOT_CHAIN address for deployment + vm.startPrank(ROOT_CHAIN); + masterManager = new MasterBlueprintServiceManager(protocolFeesReceiver); + vm.stopPrank(); + + // Create mock blueprint service manager + mockManager = new MockBlueprintServiceManager(); + mockManager.setMasterBlueprintServiceManager(address(masterManager)); + mockManager.setBlueprintOwner(address(payable(blueprintOwner))); + + // Deploy a mock ERC20 token + erc20Token = new MockERC20(); + erc20Token.initialize("Mock Token", "MTK", 18); + erc20Token.mint(address(this), 1_000_000 ether); + + // Grant necessary roles to the test contract + vm.startPrank(ROOT_CHAIN); + masterManager.grantRole(masterManager.DEFAULT_ADMIN_ROLE(), address(this)); + masterManager.grantRole(PAUSER_ROLE, address(this)); + masterManager.grantRole(TRENCH_UPDATE_ROLE, address(this)); + vm.stopPrank(); + } + + modifier asRootChain() { + vm.startPrank(ROOT_CHAIN); + _; + vm.stopPrank(); + } + + // Test onBlueprintCreated + function test_onBlueprintCreated() public asRootChain { + uint64 blueprintId = 1; + address owner = address(this); + + // Prepare Blueprint data + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + // Expect the BlueprintCreated event + vm.expectEmit(true, true, false, true); + emit BlueprintCreated(owner, blueprintId, blueprint); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + } + + function test_onBlueprintCreated_NotFromRootChain() public { + uint64 blueprintId = 1; + address owner = address(this); + + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + } + + // Test onRegister + function test_onRegister() public asRootChain { + uint64 blueprintId = 1; + address owner = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "", + author: "", + category: "", + codeRepository: "", + logo: "", + website: "", + license: "" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Prepare operator preferences + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + + bytes memory registrationInputs = hex""; + + // Expect the OperatorRegistered event + vm.expectEmit(true, false, false, true); + emit OperatorRegistered(blueprintId, operatorPrefs); + + masterManager.onRegister(blueprintId, operatorPrefs, registrationInputs); + } + + function test_onRegister_NotFromRootChain() public { + uint64 blueprintId = 1; + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + bytes memory registrationInputs = hex""; + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onRegister(blueprintId, operatorPrefs, registrationInputs); + } + + // Test onUnregister + function test_onUnregister() public asRootChain { + uint64 blueprintId = 1; + address owner = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "", + author: "", + category: "", + codeRepository: "", + logo: "", + website: "", + license: "" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Prepare operator preferences + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + + // Expect the OperatorUnregistered event + vm.expectEmit(true, false, false, true); + emit OperatorUnregistered(blueprintId, operatorPrefs); + + masterManager.onUnregister(blueprintId, operatorPrefs); + } + + function test_onUnregister_NotFromRootChain() public { + uint64 blueprintId = 1; + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onUnregister(blueprintId, operatorPrefs); + } + + // Test onUpdatePriceTargets + function test_onUpdatePriceTargets() public asRootChain { + uint64 blueprintId = 1; + address owner = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "", + author: "", + category: "", + codeRepository: "", + logo: "", + website: "", + license: "" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Prepare operator preferences + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + // Expect the PriceTargetsUpdated event + vm.expectEmit(true, false, false, true); + emit PriceTargetsUpdated(blueprintId, operatorPrefs); + + masterManager.onUpdatePriceTargets(blueprintId, operatorPrefs); + } + + function test_onUpdatePriceTargets_NotFromRootChain() public { + uint64 blueprintId = 1; + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onUpdatePriceTargets(blueprintId, operatorPrefs); + } + + // Test onRequest + function test_onRequest() public asRootChain { + uint64 blueprintId = 1; + address requester = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "", + author: "", + category: "", + codeRepository: "", + logo: "", + website: "", + license: "" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, requester, blueprint); + + // Prepare request parameters + uint64 requestId = 1; + uint64 ttl = 1000; + + ServiceOperators.OperatorPreferences[] memory operators = new ServiceOperators.OperatorPreferences[](1); + operators[0] = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + address[] memory permittedCallers = new address[](1); + permittedCallers[0] = address(this); + + Assets.Asset memory paymentAsset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(erc20Token)))) }); + + uint256 amount = 100 ether; + + ServiceOperators.RequestParams memory params = ServiceOperators.RequestParams({ + requestId: requestId, + requester: requester, + operators: operators, + requestInputs: hex"", + permittedCallers: permittedCallers, + ttl: ttl, + paymentAsset: paymentAsset, + amount: amount + }); + + // Expect the ServiceRequested event + vm.expectEmit(true, true, true, true); + emit ServiceRequested(blueprintId, requestId, requester, ttl, paymentAsset, amount); + + masterManager.onRequest(blueprintId, params); + } + + function test_onRequest_NotFromRootChain() public { + uint64 blueprintId = 1; + address requester = address(this); + + // Prepare request parameters + uint64 requestId = 1; + uint64 ttl = 1000; + + ServiceOperators.OperatorPreferences[] memory operators = new ServiceOperators.OperatorPreferences[](1); + operators[0] = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + address[] memory permittedCallers = new address[](1); + permittedCallers[0] = address(this); + + Assets.Asset memory paymentAsset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(erc20Token)))) }); + + uint256 amount = 100 ether; + + ServiceOperators.RequestParams memory params = ServiceOperators.RequestParams({ + requestId: requestId, + requester: requester, + operators: operators, + requestInputs: hex"", + permittedCallers: permittedCallers, + ttl: ttl, + paymentAsset: paymentAsset, + amount: amount + }); + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onRequest(blueprintId, params); + } + + // Test onApprove + function test_onApprove() public asRootChain { + uint64 blueprintId = 1; + address owner = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "", + author: "", + category: "", + codeRepository: "", + logo: "", + website: "", + license: "" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Prepare operator preferences + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + uint64 requestId = 1; + uint8 restakingPercent = 10; + + // Expect the RequestApproved event + vm.expectEmit(true, true, false, true); + emit RequestApproved(blueprintId, requestId, operatorPrefs, restakingPercent); + + masterManager.onApprove(blueprintId, operatorPrefs, requestId, restakingPercent); + } + + function test_onApprove_NotFromRootChain() public { + uint64 blueprintId = 1; + + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + uint64 requestId = 1; + uint8 restakingPercent = 10; + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onApprove(blueprintId, operatorPrefs, requestId, restakingPercent); + } + + // Test onReject + function test_onReject() public asRootChain { + uint64 blueprintId = 1; + address owner = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "", + author: "", + category: "", + codeRepository: "", + logo: "", + website: "", + license: "" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Prepare operator preferences + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + uint64 requestId = 1; + + // Expect the RequestRejected event + vm.expectEmit(true, true, false, true); + emit RequestRejected(blueprintId, requestId, operatorPrefs); + + masterManager.onReject(blueprintId, operatorPrefs, requestId); + } + + function test_onReject_NotFromRootChain() public { + uint64 blueprintId = 1; + + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + uint64 requestId = 1; + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onReject(blueprintId, operatorPrefs, requestId); + } + + // Test onServiceInitialized + function test_onServiceInitialized() public asRootChain { + uint64 blueprintId = 1; + uint64 requestId = 1; + uint64 serviceId = 1; + uint64 ttl = 1000; + address owner = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Prepare permitted callers + address[] memory permittedCallers = new address[](1); + permittedCallers[0] = address(this); + + // Expect the ServiceInitialized event + vm.expectEmit(true, true, true, true); + emit ServiceInitialized(blueprintId, requestId, serviceId, owner, ttl); + + masterManager.onServiceInitialized(blueprintId, requestId, serviceId, owner, permittedCallers, ttl); + } + + function test_onServiceInitialized_NotFromRootChain() public { + uint64 blueprintId = 1; + uint64 requestId = 1; + uint64 serviceId = 1; + uint64 ttl = 1000; + address owner = address(this); + + address[] memory permittedCallers = new address[](1); + permittedCallers[0] = address(this); + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onServiceInitialized(blueprintId, requestId, serviceId, owner, permittedCallers, ttl); + } + + // Test onJobCall + function test_onJobCall() public asRootChain { + uint64 blueprintId = 1; + uint64 serviceId = 1; + uint8 job = 1; + uint64 jobCallId = 1; + bytes memory inputs = hex""; + + // Create a blueprint first + address owner = address(this); + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Expect the JobCalled event + vm.expectEmit(true, true, false, true); + emit JobCalled(blueprintId, serviceId, job, jobCallId); + + masterManager.onJobCall(blueprintId, serviceId, job, jobCallId, inputs); + } + + function test_onJobCall_NotFromRootChain() public { + uint64 blueprintId = 1; + uint64 serviceId = 1; + uint8 job = 1; + uint64 jobCallId = 1; + bytes memory inputs = hex""; + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + masterManager.onJobCall(blueprintId, serviceId, job, jobCallId, inputs); + } + + // Test onJobResult + function test_onJobResult() public asRootChain { + uint64 blueprintId = 1; + uint64 serviceId = 1; + uint8 job = 1; + uint64 jobCallId = 1; + bytes memory inputs = hex""; + bytes memory outputs = hex""; + + // Prepare operator preferences + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"abcd", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 200, + mem: 300, + storage_hdd: 400, + storage_ssd: 500, + storage_nvme: 600 + }) + }); + + // Create a blueprint first + address owner = address(this); + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Expect the JobResultReceived event + vm.expectEmit(true, true, false, true); + emit JobResultReceived(blueprintId, serviceId, job, jobCallId, operatorPrefs); + + masterManager.onJobResult(blueprintId, serviceId, job, jobCallId, operatorPrefs, inputs, outputs); + } + + function test_onJobResult_NotFromRootChain() public { + uint64 blueprintId = 1; + uint64 serviceId = 1; + uint8 job = 1; + uint64 jobCallId = 1; + bytes memory inputs = hex""; + bytes memory outputs = hex""; + + // Prepare operator preferences + ServiceOperators.OperatorPreferences memory operatorPrefs = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"abcd", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 200, + mem: 300, + storage_hdd: 400, + storage_ssd: 500, + storage_nvme: 600 + }) + }); + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + + masterManager.onJobResult(blueprintId, serviceId, job, jobCallId, operatorPrefs, inputs, outputs); + } + + // Test onServiceTermination + function test_onServiceTermination() public asRootChain { + uint64 blueprintId = 1; + uint64 serviceId = 1; + address owner = address(this); + + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Expect the ServiceTerminated event + vm.expectEmit(true, true, false, true); + emit ServiceTerminated(blueprintId, serviceId, owner); + + masterManager.onServiceTermination(blueprintId, serviceId, owner); + } + + function test_onServiceTermination_NotFromRootChain() public { + uint64 blueprintId = 1; + uint64 serviceId = 1; + address owner = address(this); + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + + masterManager.onServiceTermination(blueprintId, serviceId, owner); + } + + // Test onUnappliedSlash + function test_onUnappliedSlash() public asRootChain { + uint64 blueprintId = 1; + uint64 serviceId = 1; + bytes memory offender = hex"abcd"; + uint8 slashPercent = 20; + uint256 totalPayout = 1000 ether; + + // Create a blueprint first + address owner = address(this); + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Expect the UnappliedSlash event + vm.expectEmit(true, true, false, true); + emit UnappliedSlash(blueprintId, serviceId, offender, slashPercent, totalPayout); + + masterManager.onUnappliedSlash(blueprintId, serviceId, offender, slashPercent, totalPayout); + } + + function test_onUnappliedSlash_NotFromRootChain() public { + uint64 blueprintId = 1; + uint64 serviceId = 1; + bytes memory offender = hex"abcd"; + uint8 slashPercent = 20; + uint256 totalPayout = 1000 ether; + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + + masterManager.onUnappliedSlash(blueprintId, serviceId, offender, slashPercent, totalPayout); + } + + // Test onSlash + function test_onSlash() public asRootChain { + uint64 blueprintId = 1; + uint64 serviceId = 1; + bytes memory offender = hex"abcd"; + uint8 slashPercent = 15; + uint256 totalPayout = 500 ether; + + // Create a blueprint first + address owner = address(this); + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + // Expect the Slashed event + vm.expectEmit(true, true, false, true); + emit Slashed(blueprintId, serviceId, offender, slashPercent, totalPayout); + + masterManager.onSlash(blueprintId, serviceId, offender, slashPercent, totalPayout); + } + + function test_onSlash_NotFromRootChain() public { + uint64 blueprintId = 1; + uint64 serviceId = 1; + bytes memory offender = hex"abcd"; + uint8 slashPercent = 15; + uint256 totalPayout = 500 ether; + + vm.expectRevert( + abi.encodeWithSelector(RootChainEnabled.OnlyRootChainAllowed.selector, address(this), ROOT_CHAIN) + ); + + masterManager.onSlash(blueprintId, serviceId, offender, slashPercent, totalPayout); + } + + // Test querySlashingOrigin + function test_querySlashingOrigin() public { + uint64 blueprintId = 1; + uint64 serviceId = 1; + address expectedOrigin = address(mockManager); + + // Create a blueprint first + address owner = address(this); + + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + vm.startPrank(ROOT_CHAIN); + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + vm.stopPrank(); + + address slashingOrigin = masterManager.querySlashingOrigin(blueprintId, serviceId); + assertEq(slashingOrigin, expectedOrigin); + } + + // Test queryDisputeOrigin + function test_queryDisputeOrigin() public { + uint64 blueprintId = 1; + uint64 serviceId = 1; + address expectedOrigin = address(mockManager); + + // Create a blueprint first + address owner = address(this); + + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + vm.startPrank(ROOT_CHAIN); + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + vm.stopPrank(); + + address disputeOrigin = masterManager.queryDisputeOrigin(blueprintId, serviceId); + assertEq(disputeOrigin, expectedOrigin); + } + + // Test pause and unpause + function test_pauseAndUnpause() public { + masterManager.pause(); + assertTrue(masterManager.paused()); + + // Attempt to call a function that is affected by whenNotPaused + vm.startPrank(ROOT_CHAIN); + + uint64 blueprintId = 1; + address owner = address(this); + + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "", + author: "", + category: "", + codeRepository: "", + logo: "", + website: "", + license: "" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + vm.expectRevert(Pausable.EnforcedPause.selector); + masterManager.onBlueprintCreated(blueprintId, owner, blueprint); + + vm.stopPrank(); + + masterManager.unpause(); + assertFalse(masterManager.paused()); + } + + function test_pause_Unauthorized() public { + assertFalse(masterManager.hasRole(PAUSER_ROLE, address(0x1234))); + vm.prank(address(0x1234)); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, address(0x1234), PAUSER_ROLE + ) + ); + masterManager.pause(); + } + + // Test setTranches + function test_setTranches() public { + MasterBlueprintServiceManager.Tranche[] memory newTranches = new MasterBlueprintServiceManager.Tranche[](2); + newTranches[0] = MasterBlueprintServiceManager.Tranche({ + kind: MasterBlueprintServiceManager.TrancheKind.Developer, + percent: 7000 // 70% + }); + newTranches[1] = MasterBlueprintServiceManager.Tranche({ + kind: MasterBlueprintServiceManager.TrancheKind.Protocol, + percent: 3000 // 30% + }); + + masterManager.setTranches(newTranches); + + // Since tranches is public, we can check the new values + (MasterBlueprintServiceManager.TrancheKind kind0, uint16 percent0) = masterManager.tranches(0); + assertEq(uint8(kind0), uint8(MasterBlueprintServiceManager.TrancheKind.Developer)); + assertEq(percent0, 7000); + + (MasterBlueprintServiceManager.TrancheKind kind1, uint16 percent1) = masterManager.tranches(1); + assertEq(uint8(kind1), uint8(MasterBlueprintServiceManager.TrancheKind.Protocol)); + assertEq(percent1, 3000); + } + + function test_setTranches_InvalidSum() public { + MasterBlueprintServiceManager.Tranche[] memory newTranches = new MasterBlueprintServiceManager.Tranche[](2); + newTranches[0] = MasterBlueprintServiceManager.Tranche({ + kind: MasterBlueprintServiceManager.TrancheKind.Developer, + percent: 5000 // 50% + }); + newTranches[1] = MasterBlueprintServiceManager.Tranche({ + kind: MasterBlueprintServiceManager.TrancheKind.Protocol, + percent: 4000 // 40% + }); + + vm.expectRevert(MasterBlueprintServiceManager.InvalidTranches.selector); + masterManager.setTranches(newTranches); + } + + function test_setTranches_Unauthorized() public { + MasterBlueprintServiceManager.Tranche[] memory newTranches = new MasterBlueprintServiceManager.Tranche[](2); + newTranches[0] = MasterBlueprintServiceManager.Tranche({ + kind: MasterBlueprintServiceManager.TrancheKind.Developer, + percent: 7000 // 70% + }); + newTranches[1] = MasterBlueprintServiceManager.Tranche({ + kind: MasterBlueprintServiceManager.TrancheKind.Protocol, + percent: 3000 // 30% + }); + + vm.prank(address(0x1234)); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, address(0x1234), TRENCH_UPDATE_ROLE + ) + ); + + masterManager.setTranches(newTranches); + } + + // Test spliting the payment with ERC20 + function test_paymentSplit_erc20() public asRootChain { + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(1, address(this), blueprint); + + ServiceOperators.OperatorPreferences[] memory operators = new ServiceOperators.OperatorPreferences[](1); + operators[0] = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + address[] memory permittedCallers = new address[](1); + permittedCallers[0] = address(this); + + Assets.Asset memory paymentAsset = + Assets.Asset({ kind: Assets.Kind.Erc20, data: bytes32(uint256(uint160(address(erc20Token)))) }); + + uint256 amount = 100 ether; + + ServiceOperators.RequestParams memory params = ServiceOperators.RequestParams({ + requestId: 1, + requester: address(this), + operators: operators, + requestInputs: hex"", + permittedCallers: permittedCallers, + ttl: 1000, + paymentAsset: paymentAsset, + amount: amount + }); + + masterManager.onRequest(1, params); + + // Approve the request + uint8 restakingPercent = 10; + masterManager.onApprove(1, operators[0], 1, restakingPercent); + + vm.startPrank(address(this)); + erc20Token.transfer(address(masterManager), amount); + vm.stopPrank(); + + vm.startPrank(ROOT_CHAIN); + + masterManager.onServiceInitialized(1, 1, 1, address(this), permittedCallers, 1000); + + // query the tranches to check the payment split + (MasterBlueprintServiceManager.TrancheKind kind, uint16 percent) = masterManager.tranches(0); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Developer); + address dev = mockManager.queryDeveloperPaymentAddress(1); + uint256 expectedBalance = (amount * percent) / BASE_PERCENT; + uint256 actualBalance = IERC20(erc20Token).balanceOf(dev); + assertEq(actualBalance, expectedBalance); + + (kind, percent) = masterManager.tranches(1); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Protocol); + + address protocol = masterManager.protocolFeesReceiver(); + expectedBalance = (amount * percent) / BASE_PERCENT; + actualBalance = IERC20(erc20Token).balanceOf(protocol); + assertEq(actualBalance, expectedBalance); + + (kind, percent) = masterManager.tranches(2); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Operators); + uint16 operatorPercent = percent; + (kind, percent) = masterManager.tranches(3); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Restakers); + uint16 restakerPercent = percent; + uint16 rewardsPalletPercent = operatorPercent + restakerPercent; + address rewardsPallet = masterManager.rewardsPallet(); + expectedBalance = (amount * rewardsPalletPercent) / BASE_PERCENT; + actualBalance = IERC20(erc20Token).balanceOf(rewardsPallet); + assertEq(actualBalance, expectedBalance); + + vm.stopPrank(); + } + + // Test spliting the payment with Native Token + function test_paymentSplit_native() public asRootChain { + // Create a blueprint first + MasterBlueprintServiceManager.ServiceMetadata memory metadata = MasterBlueprintServiceManager.ServiceMetadata({ + name: "Test Blueprint", + description: "Test Description", + author: "Test Author", + category: "Test Category", + codeRepository: "https://github.com/example", + logo: "https://example.com/logo.png", + website: "https://example.com", + license: "MIT" + }); + + MasterBlueprintServiceManager.Blueprint memory blueprint = MasterBlueprintServiceManager.Blueprint({ + metadata: metadata, + manager: address(mockManager), + mbsmRevision: 1 + }); + + masterManager.onBlueprintCreated(1, address(this), blueprint); + + ServiceOperators.OperatorPreferences[] memory operators = new ServiceOperators.OperatorPreferences[](1); + operators[0] = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: hex"1234", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + address[] memory permittedCallers = new address[](1); + permittedCallers[0] = address(this); + + Assets.Asset memory paymentAsset = Assets.Asset({ kind: Assets.Kind.Custom, data: bytes32(0x0) }); + + uint256 amount = 100 ether; + + ServiceOperators.RequestParams memory params = ServiceOperators.RequestParams({ + requestId: 1, + requester: address(this), + operators: operators, + requestInputs: hex"", + permittedCallers: permittedCallers, + ttl: 1000, + paymentAsset: paymentAsset, + amount: amount + }); + + masterManager.onRequest(1, params); + + // Approve the request + uint8 restakingPercent = 10; + masterManager.onApprove(1, operators[0], 1, restakingPercent); + + vm.startPrank(address(this)); + payable(address(ROOT_CHAIN)).transfer(amount); + vm.stopPrank(); + + vm.startPrank(ROOT_CHAIN); + + masterManager.onServiceInitialized{ value: amount }(1, 1, 1, address(this), permittedCallers, 1000); + + // query the tranches to check the payment split + (MasterBlueprintServiceManager.TrancheKind kind, uint16 percent) = masterManager.tranches(0); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Developer); + address dev = mockManager.queryDeveloperPaymentAddress(1); + uint256 expectedBalance = (amount * percent) / BASE_PERCENT; + uint256 actualBalance = dev.balance; + assertEq(actualBalance, expectedBalance); + + (kind, percent) = masterManager.tranches(1); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Protocol); + + address protocol = masterManager.protocolFeesReceiver(); + expectedBalance = (amount * percent) / BASE_PERCENT; + actualBalance = protocol.balance; + assertEq(actualBalance, expectedBalance); + + (kind, percent) = masterManager.tranches(2); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Operators); + uint16 operatorPercent = percent; + (kind, percent) = masterManager.tranches(3); + assert(kind == MasterBlueprintServiceManager.TrancheKind.Restakers); + uint16 restakerPercent = percent; + uint16 rewardsPalletPercent = operatorPercent + restakerPercent; + address rewardsPallet = masterManager.rewardsPallet(); + expectedBalance = (amount * rewardsPalletPercent) / BASE_PERCENT; + actualBalance = rewardsPallet.balance; + assertEq(actualBalance, expectedBalance); + + vm.stopPrank(); + } +} diff --git a/test/MockBlueprintServiceManager.sol b/test/MockBlueprintServiceManager.sol index a42a85e..2e608fe 100644 --- a/test/MockBlueprintServiceManager.sol +++ b/test/MockBlueprintServiceManager.sol @@ -6,11 +6,11 @@ import "src/IBlueprintServiceManager.sol"; contract MockBlueprintServiceManager is BlueprintServiceManagerBase { // Expose internal functions for testing - function permitAsset(uint64 serviceId, ServiceOperators.Asset calldata asset) external returns (bool) { + function permitAsset(uint64 serviceId, Assets.Asset calldata asset) external returns (bool) { return _permitAsset(serviceId, asset); } - function revokeAsset(uint64 serviceId, ServiceOperators.Asset calldata asset) external returns (bool) { + function revokeAsset(uint64 serviceId, Assets.Asset calldata asset) external returns (bool) { return _revokeAsset(serviceId, asset); } @@ -22,24 +22,19 @@ contract MockBlueprintServiceManager is BlueprintServiceManagerBase { return _getPermittedAssetsAsAddresses(serviceId); } - function getPermittedAssets(uint64 serviceId) external view returns (ServiceOperators.Asset[] memory) { + function getPermittedAssets(uint64 serviceId) external view returns (Assets.Asset[] memory) { return _getPermittedAssets(serviceId); } - function assetIdToAddress(bytes32 assetId) external pure returns (address) { - return _assetIdToAddress(assetId); - } - - function addressToAssetId(address assetAddress) external pure returns (bytes32) { - return _addressToAssetId(assetAddress); + function setMasterBlueprintServiceManager(address mbsm) external { + masterBlueprintServiceManager = mbsm; } - function isNativeAsset(ServiceOperators.Asset calldata asset) external pure returns (bool) { - return _isNativeAsset(asset); + function setBlueprintOwner(address owner) external { + blueprintOwner = owner; } - // Override required as BlueprintServiceManagerBase inherits RootChainEnabled - function setMasterBlueprintServiceManager(address mbsm) external { - masterBlueprintServiceManager = mbsm; + function setCurrentBlueprintId(uint64 id) external { + currentBlueprintId = id; } }