diff --git a/src/BlueprintServiceManagerBase.sol b/src/BlueprintServiceManagerBase.sol index 68b4b7b..717a756 100644 --- a/src/BlueprintServiceManagerBase.sol +++ b/src/BlueprintServiceManagerBase.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import "src/Permissions.sol"; import "src/IBlueprintServiceManager.sol"; @@ -13,12 +15,22 @@ import "src/IBlueprintServiceManager.sol"; /// Each function serves as a hook for different lifecycle events, and reverting any /// of these functions interrupts the process flow. contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabled { + using EnumerableSet for EnumerableSet.AddressSet; + /// @dev The Current Blueprint Id uint256 public currentBlueprintId; /// @dev The address of the owner of the blueprint address public blueprintOwner; + /// @dev a mapping between service id and permitted payment assets. + /// @dev serviceId => EnumerableSet of permitted payment assets. + /// @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; @@ -152,7 +164,243 @@ contract BlueprintServiceManagerBase is IBlueprintServiceManager, RootChainEnabl } /// @inheritdoc IBlueprintServiceManager - function queryDeveloperPaymentAddress(uint64) external view virtual returns (address developerPaymentAddress) { + function queryDeveloperPaymentAddress(uint64) + external + view + virtual + returns (address payable developerPaymentAddress) + { return payable(blueprintOwner); } + + /// @inheritdoc IBlueprintServiceManager + function queryIsPaymentAssetAllowed( + uint64 serviceId, + ServiceOperators.Asset calldata asset + ) + external + view + virtual + returns (bool isAllowed) + { + return _isAssetPermitted(serviceId, asset); + } + + /** + * @notice Permits a specific asset for a given service. + * @dev Adds the asset to the set of permitted payment assets based on its kind. + * @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; + } + } + + /** + * @notice Revokes a previously permitted asset for a given service. + * @dev Removes the asset from the set of permitted payment assets based on its kind. + * @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; + } + } + + /** + * @notice Clears all permitted assets for a given service. + * @dev Iterates through the set of permitted assets and removes each one. + * @param serviceId The ID of the service for which permitted assets are being cleared. + */ + function _clearPermittedAssets(uint64 serviceId) internal virtual returns (bool cleared) { + EnumerableSet.AddressSet storage permittedAssets = _permittedPaymentAssets[serviceId]; + uint256 length = permittedAssets.length(); + while (length > 0) { + address assetAddress = permittedAssets.at(0); + permittedAssets.remove(assetAddress); + length = permittedAssets.length(); + } + + // The set should be empty after clearing all permitted assets. + return permittedAssets.length() == 0; + } + + /** + * @notice Retrieves all permitted assets for a given service as an array of addresses. + * @dev Converts the EnumerableSet of permitted assets to a dynamic array of addresses. + * @param serviceId The ID of the service for which permitted assets are being retrieved. + * @return assets An array of addresses representing the permitted assets. + */ + function _getPermittedAssetsAsAddresses(uint64 serviceId) internal view virtual returns (address[] memory) { + EnumerableSet.AddressSet storage permittedAssets = _permittedPaymentAssets[serviceId]; + address[] memory assets = new address[](permittedAssets.length()); + for (uint256 i = 0; i < permittedAssets.length(); i++) { + assets[i] = permittedAssets.at(i); + } + return assets; + } + + /** + * @notice Retrieves all permitted assets for a given service as an array of Asset structs. + * @dev Converts the EnumerableSet of permitted assets to a dynamic array of ServiceOperators.Asset. + * @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) { + EnumerableSet.AddressSet storage permittedAssets = _permittedPaymentAssets[serviceId]; + ServiceOperators.Asset[] memory assets = new ServiceOperators.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); + } + return assets; + } + + /** + * @notice Checks if a specific asset is permitted for a given service. + * @dev Determines if the asset is contained within the set of permitted payment assets based on its kind. + * @param serviceId The ID of the service to check. + * @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) + { + // Native assets are always permitted. + if (_isNativeAsset(asset)) { + 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; + } + } } diff --git a/src/IBlueprintServiceManager.sol b/src/IBlueprintServiceManager.sol index 48bb855..0a45c00 100644 --- a/src/IBlueprintServiceManager.sol +++ b/src/IBlueprintServiceManager.sol @@ -220,5 +220,20 @@ interface IBlueprintServiceManager { /// @notice This function should be implemented by the Blueprint Service Manager contract. /// @param serviceId The ID of the service. /// @return developerPaymentAddress The address of the developer payment address for that service - function queryDeveloperPaymentAddress(uint64 serviceId) external view returns (address developerPaymentAddress); + function queryDeveloperPaymentAddress(uint64 serviceId) + external + view + returns (address payable developerPaymentAddress); + + /// @dev Determines if a specified payment asset is permitted for a given service. + /// @param serviceId The ID of the service to check against. + /// @param asset The asset to verify for allowance. + /// @return isAllowed Returns true if the asset is allowed, false otherwise. + function queryIsPaymentAssetAllowed( + uint64 serviceId, + ServiceOperators.Asset calldata asset + ) + external + view + returns (bool isAllowed); } diff --git a/test/BlueprintServiceManagerBase.t.sol b/test/BlueprintServiceManagerBase.t.sol new file mode 100644 index 0000000..8b7817b --- /dev/null +++ b/test/BlueprintServiceManagerBase.t.sol @@ -0,0 +1,726 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { Test } from "forge-std/Test.sol"; +import "src/BlueprintServiceManagerBase.sol"; +import "src/IBlueprintServiceManager.sol"; +import "./MockERC20.sol"; +import "./MockBlueprintServiceManager.sol"; + +contract BlueprintServiceManagerBaseTest is Test { + MockBlueprintServiceManager manager; + address rootChain = 0x1111111111111111111111111111111111111111; + address masterBlueprintServiceManager = address(0x2222222222222222222222222222222222222222); + address blueprintOwner = address(0x3333333333333333333333333333333333333333); + MockERC20 mockToken; + + function setUp() public { + // Deploy Mock Blueprint Service Manager + manager = new MockBlueprintServiceManager(); + + // Set the master blueprint service manager + manager.setMasterBlueprintServiceManager(masterBlueprintServiceManager); + + // Deploy a Mock ERC20 token + mockToken = new MockERC20(); + mockToken.initialize("MockToken", "MTK", 18); + } + + // Utility modifier to simulate calls from root chain + modifier onlyRootChain() { + vm.prank(rootChain); + _; + } + + // Utility modifier to simulate calls from master blueprint service manager + modifier onlyMaster() { + vm.prank(masterBlueprintServiceManager); + _; + } + + // Test onBlueprintCreated + function test_OnBlueprintCreated_SetsStateCorrectly() public onlyRootChain { + uint64 blueprintId = 1; + manager.onBlueprintCreated(blueprintId, blueprintOwner, masterBlueprintServiceManager); + + assertEq(manager.currentBlueprintId(), blueprintId, "Incorrect blueprint ID set"); + assertEq(manager.blueprintOwner(), blueprintOwner, "Incorrect blueprint owner set"); + assertEq(manager.masterBlueprintServiceManager(), masterBlueprintServiceManager, "Incorrect MBSM set"); + } + + // Test onRegister + function test_OnRegister_CalledByMaster() public onlyMaster { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + bytes memory registrationInputs = "registration data"; + + // Expect no revert + manager.onRegister(operator, registrationInputs); + } + + function test_OnRegister_RevertsWhenNotMaster() public { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + bytes memory registrationInputs = "registration data"; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onRegister(operator, registrationInputs); + } + + // Test onUnregister + function test_OnUnregister_CalledByMaster() public onlyMaster { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + + // Expect no revert + manager.onUnregister(operator); + } + + function test_OnUnregister_RevertsWhenNotMaster() public { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 100, + mem: 200, + storage_hdd: 300, + storage_ssd: 400, + storage_nvme: 500 + }) + }); + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onUnregister(operator); + } + + // Test onUpdatePriceTargets + function test_OnUpdatePriceTargets_CalledByMaster() public onlyMaster { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + // Expect no revert + manager.onUpdatePriceTargets(operator); + } + + function test_OnUpdatePriceTargets_RevertsWhenNotMaster() public { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 150, + mem: 250, + storage_hdd: 350, + storage_ssd: 450, + storage_nvme: 550 + }) + }); + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onUpdatePriceTargets(operator); + } + + // Test onRequest + function test_OnRequest_CalledByMaster() public onlyMaster { + ServiceOperators.RequestParams memory params = ServiceOperators.RequestParams({ + requestId: 1, + requester: address(0x444), + operators: new ServiceOperators.OperatorPreferences[](0), + requestInputs: "input data", + permittedCallers: new address[](0), + ttl: 1000, + paymentAsset: ServiceOperators.Asset({ + kind: ServiceOperators.AssetKind.Erc20, + data: bytes32(uint256(uint160(address(mockToken)))) + }), + amount: 1000 + }); + + // Expect no revert + manager.onRequest(params); + } + + function test_OnRequest_RevertsWhenNotMaster() public { + ServiceOperators.RequestParams memory params = ServiceOperators.RequestParams({ + requestId: 1, + requester: address(0x444), + operators: new ServiceOperators.OperatorPreferences[](0), + requestInputs: "input data", + permittedCallers: new address[](0), + ttl: 1000, + paymentAsset: ServiceOperators.Asset({ + kind: ServiceOperators.AssetKind.Erc20, + data: bytes32(uint256(uint160(address(mockToken)))) + }), + amount: 1000 + }); + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onRequest(params); + } + + // Test onApprove + function test_OnApprove_CalledByMaster() public onlyMaster { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 200, + mem: 300, + storage_hdd: 400, + storage_ssd: 500, + storage_nvme: 600 + }) + }); + uint64 requestId = 2; + uint8 restakingPercent = 10; + + // Expect no revert + manager.onApprove(operator, requestId, restakingPercent); + } + + function test_OnApprove_RevertsWhenNotMaster() public { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 200, + mem: 300, + storage_hdd: 400, + storage_ssd: 500, + storage_nvme: 600 + }) + }); + uint64 requestId = 2; + uint8 restakingPercent = 10; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onApprove(operator, requestId, restakingPercent); + } + + // Test onReject + function test_OnReject_CalledByMaster() public onlyMaster { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 250, + mem: 350, + storage_hdd: 450, + storage_ssd: 550, + storage_nvme: 650 + }) + }); + uint64 requestId = 3; + + // Expect no revert + manager.onReject(operator, requestId); + } + + function test_OnReject_RevertsWhenNotMaster() public { + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0xabcdef", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 250, + mem: 350, + storage_hdd: 450, + storage_ssd: 550, + storage_nvme: 650 + }) + }); + uint64 requestId = 3; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onReject(operator, requestId); + } + + // Test onServiceInitialized + function test_OnServiceInitialized_CalledByMaster() public onlyMaster { + uint64 requestId = 4; + uint64 serviceId = 100; + address owner = address(0x555); + address[] memory permittedCallers = new address[](2); + permittedCallers[0] = address(0x666); + permittedCallers[1] = address(0x777); + uint64 ttl = 2000; + + // Expect no revert + manager.onServiceInitialized(requestId, serviceId, owner, permittedCallers, ttl); + + // Verify state or behaviors as needed (if any) + // Since the base contract does not implement, no state change to verify + } + + function test_OnServiceInitialized_RevertsWhenNotMaster() public { + uint64 requestId = 4; + uint64 serviceId = 100; + address owner = address(0x555); + address[] memory permittedCallers = new address[](2); + permittedCallers[0] = address(0x666); + permittedCallers[1] = address(0x777); + uint64 ttl = 2000; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onServiceInitialized(requestId, serviceId, owner, permittedCallers, ttl); + } + + // Test onJobCall + function test_OnJobCall_CalledByMaster() public onlyMaster { + uint64 serviceId = 101; + uint8 job = 1; + uint64 jobCallId = 500; + bytes memory inputs = "job inputs"; + + // Expect no revert + manager.onJobCall(serviceId, job, jobCallId, inputs); + } + + function test_OnJobCall_RevertsWhenNotMaster() public { + uint64 serviceId = 101; + uint8 job = 1; + uint64 jobCallId = 500; + bytes memory inputs = "job inputs"; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onJobCall(serviceId, job, jobCallId, inputs); + } + + // Test onJobResult + function test_OnJobResult_CalledByMaster() public onlyMaster { + uint64 serviceId = 102; + uint8 job = 2; + uint64 jobCallId = 600; + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0x123456", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 300, + mem: 400, + storage_hdd: 500, + storage_ssd: 600, + storage_nvme: 700 + }) + }); + bytes memory inputs = "job inputs"; + bytes memory outputs = "job outputs"; + + // Expect no revert + manager.onJobResult(serviceId, job, jobCallId, operator, inputs, outputs); + } + + function test_OnJobResult_RevertsWhenNotMaster() public { + uint64 serviceId = 102; + uint8 job = 2; + uint64 jobCallId = 600; + ServiceOperators.OperatorPreferences memory operator = ServiceOperators.OperatorPreferences({ + ecdsaPublicKey: "0x123456", + priceTargets: ServiceOperators.PriceTargets({ + cpu: 300, + mem: 400, + storage_hdd: 500, + storage_ssd: 600, + storage_nvme: 700 + }) + }); + bytes memory inputs = "job inputs"; + bytes memory outputs = "job outputs"; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onJobResult(serviceId, job, jobCallId, operator, inputs, outputs); + } + + // Test onServiceTermination + function test_OnServiceTermination_CalledByMaster() public onlyMaster { + uint64 serviceId = 103; + address owner = address(0x888); + + // Expect no revert + manager.onServiceTermination(serviceId, owner); + } + + function test_OnServiceTermination_RevertsWhenNotMaster() public { + uint64 serviceId = 103; + address owner = address(0x888); + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onServiceTermination(serviceId, owner); + } + + // Test onUnappliedSlash + function test_OnUnappliedSlash_CalledByMaster() public onlyMaster { + uint64 serviceId = 104; + bytes memory offender = "offender data"; + uint8 slashPercent = 5; + uint256 totalPayout = 1000 ether; + + // Expect no revert + manager.onUnappliedSlash(serviceId, offender, slashPercent, totalPayout); + } + + function test_OnUnappliedSlash_RevertsWhenNotMaster() public { + uint64 serviceId = 104; + bytes memory offender = "offender data"; + uint8 slashPercent = 5; + uint256 totalPayout = 1000 ether; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onUnappliedSlash(serviceId, offender, slashPercent, totalPayout); + } + + // Test onSlash + function test_OnSlash_CalledByMaster() public onlyMaster { + uint64 serviceId = 105; + bytes memory offender = "offender data"; + uint8 slashPercent = 10; + uint256 totalPayout = 2000 ether; + + // Expect no revert + manager.onSlash(serviceId, offender, slashPercent, totalPayout); + } + + function test_OnSlash_RevertsWhenNotMaster() public { + uint64 serviceId = 105; + bytes memory offender = "offender data"; + uint8 slashPercent = 10; + uint256 totalPayout = 2000 ether; + + vm.expectRevert( + abi.encodeWithSelector( + RootChainEnabled.OnlyMasterBlueprintServiceManagerAllowed.selector, + address(0x999), + masterBlueprintServiceManager + ) + ); + vm.prank(address(0x999)); + manager.onSlash(serviceId, offender, slashPercent, totalPayout); + } + + // Test querySlashingOrigin + function test_QuerySlashingOrigin_ReturnsCorrectAddress() public view { + uint64 serviceId = 106; + address expected = address(manager); + + address result = manager.querySlashingOrigin(serviceId); + assertEq(result, expected, "Slashing origin should be the contract itself"); + } + + // Test queryDisputeOrigin + function test_QueryDisputeOrigin_ReturnsCorrectAddress() public view { + uint64 serviceId = 107; + address expected = address(manager); + + address result = manager.queryDisputeOrigin(serviceId); + assertEq(result, expected, "Dispute origin should be the contract itself"); + } + + // Test queryDeveloperPaymentAddress + function test_QueryDeveloperPaymentAddress_ReturnsBlueprintOwner() public view { + uint64 serviceId = 108; + + address payable result = manager.queryDeveloperPaymentAddress(serviceId); + assertEq(result, payable(manager.blueprintOwner()), "Developer payment address should be blueprint owner"); + } + + // 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)))) + }); + + // Permit the asset + manager.permitAsset(serviceId, asset); + + bool isAllowed = manager.queryIsPaymentAssetAllowed(serviceId, asset); + assertTrue(isAllowed, "ERC20 asset should be allowed"); + } + + 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)))) + }); + + bool isAllowed = manager.queryIsPaymentAssetAllowed(serviceId, asset); + assertFalse(isAllowed, "ERC20 asset should not be allowed initially"); + } + + 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 }); + + // Permit the asset + manager.permitAsset(serviceId, asset); + + bool isAllowed = manager.queryIsPaymentAssetAllowed(serviceId, asset); + assertTrue(isAllowed, "Custom asset should be allowed"); + } + + 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 }); + + bool isAllowed = manager.queryIsPaymentAssetAllowed(serviceId, asset); + assertFalse(isAllowed, "Custom asset should not be allowed initially"); + } + + // 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)))) + }); + bytes32 customAssetId = bytes32(uint256(789_012)); + ServiceOperators.Asset memory customAsset = + ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: customAssetId }); + + // Permit both assets + manager.permitAsset(serviceId, erc20Asset); + manager.permitAsset(serviceId, customAsset); + + // Check if allowed + assertTrue(manager.queryIsPaymentAssetAllowed(serviceId, erc20Asset), "ERC20 asset should be allowed"); + assertTrue(manager.queryIsPaymentAssetAllowed(serviceId, customAsset), "Custom asset should be allowed"); + + // Revoke ERC20 asset + manager.revokeAsset(serviceId, erc20Asset); + assertFalse(manager.queryIsPaymentAssetAllowed(serviceId, erc20Asset), "ERC20 asset should be revoked"); + assertTrue(manager.queryIsPaymentAssetAllowed(serviceId, customAsset), "Custom asset should still be allowed"); + + // Revoke Custom asset + manager.revokeAsset(serviceId, customAsset); + assertFalse(manager.queryIsPaymentAssetAllowed(serviceId, customAsset), "Custom asset should be revoked"); + } + + // 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)))) + }); + bytes32 customAssetId = bytes32(uint256(890_123)); + ServiceOperators.Asset memory customAsset = + ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: customAssetId }); + + // Permit both assets + manager.permitAsset(serviceId, erc20Asset); + manager.permitAsset(serviceId, customAsset); + + // Verify assets are permitted + assertTrue(manager.queryIsPaymentAssetAllowed(serviceId, erc20Asset), "ERC20 asset should be allowed"); + assertTrue(manager.queryIsPaymentAssetAllowed(serviceId, customAsset), "Custom asset should be allowed"); + + // Clear all permitted assets + manager.clearPermittedAssets(serviceId); + + // Verify assets are revoked + assertFalse(manager.queryIsPaymentAssetAllowed(serviceId, erc20Asset), "ERC20 asset should be revoked"); + assertFalse(manager.queryIsPaymentAssetAllowed(serviceId, customAsset), "Custom asset should be revoked"); + } + + // 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)))) + }); + + manager.permitAsset(serviceId, erc20Asset); + + address[] memory permitted = manager.getPermittedAssetsAsAddresses(serviceId); + assertEq(permitted.length, 1, "Should have one permitted asset"); + assertEq(permitted[0], address(mockToken), "Permitted asset address mismatch"); + } + + // 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)))) + }); + bytes32 customAssetId = bytes32(uint256(345_678)); + ServiceOperators.Asset memory customAsset = + ServiceOperators.Asset({ kind: ServiceOperators.AssetKind.Custom, data: customAssetId }); + + manager.permitAsset(serviceId, erc20Asset); + manager.permitAsset(serviceId, customAsset); + + ServiceOperators.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"); + 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"); + 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); + 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) + ); + + manager.addressToAssetId(invalidAssetAddress); + } + + // 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)))) + }); + + bool isNative = manager.isNativeAsset(erc20Asset); + 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)) }); + + bool isNative = manager.isNativeAsset(customAsset); + 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)))) + }); + + bool isNative = manager.isNativeAsset(nativeErc20); + 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)) }); + + bool isNative = manager.isNativeAsset(nativeCustom); + assertTrue(isNative, "Custom asset with ID 0 should be native"); + } +} diff --git a/test/MockBlueprintServiceManager.sol b/test/MockBlueprintServiceManager.sol new file mode 100644 index 0000000..a42a85e --- /dev/null +++ b/test/MockBlueprintServiceManager.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "src/BlueprintServiceManagerBase.sol"; +import "src/IBlueprintServiceManager.sol"; + +contract MockBlueprintServiceManager is BlueprintServiceManagerBase { + // Expose internal functions for testing + function permitAsset(uint64 serviceId, ServiceOperators.Asset calldata asset) external returns (bool) { + return _permitAsset(serviceId, asset); + } + + function revokeAsset(uint64 serviceId, ServiceOperators.Asset calldata asset) external returns (bool) { + return _revokeAsset(serviceId, asset); + } + + function clearPermittedAssets(uint64 serviceId) external returns (bool) { + return _clearPermittedAssets(serviceId); + } + + function getPermittedAssetsAsAddresses(uint64 serviceId) external view returns (address[] memory) { + return _getPermittedAssetsAsAddresses(serviceId); + } + + function getPermittedAssets(uint64 serviceId) external view returns (ServiceOperators.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 isNativeAsset(ServiceOperators.Asset calldata asset) external pure returns (bool) { + return _isNativeAsset(asset); + } + + // Override required as BlueprintServiceManagerBase inherits RootChainEnabled + function setMasterBlueprintServiceManager(address mbsm) external { + masterBlueprintServiceManager = mbsm; + } +}