diff --git a/src/core/ManifestValidator.sol b/src/core/ManifestValidator.sol index e69de29..fe3ecdc 100644 --- a/src/core/ManifestValidator.sol +++ b/src/core/ManifestValidator.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ManifestValidator +/// @notice Validates EthPM v3 manifest structure and naming conventions (EIP-2678) +library ManifestValidator { + + /// @notice Validate contract name matches EIP-2678 regex: ^[a-zA-Z_$][a-zA-Z0-9_$]{0,255}$ + /// @param name Contract name to validate + /// @return valid True if name is valid + function isValidContractName(string memory name) internal pure returns (bool) { + bytes memory nameBytes = bytes(name); + + // Must have at least 1 character, max 256 + if (nameBytes.length == 0 || nameBytes.length > 256) { + return false; + } + + // First character must be: a-z, A-Z, _, $ + bytes1 first = nameBytes[0]; + bool validFirst = (first >= 0x61 && first <= 0x7A) || // a-z + (first >= 0x41 && first <= 0x5A) || // A-Z + (first == 0x5F) || // _ + (first == 0x24); // $ + + if (!validFirst) { + return false; + } + + // Rest can be: a-z, A-Z, 0-9, _, $ + for (uint256 i = 1; i < nameBytes.length; i++) { + bytes1 char = nameBytes[i]; + bool validChar = (char >= 0x61 && char <= 0x7A) || // a-z + (char >= 0x41 && char <= 0x5A) || // A-Z + (char >= 0x30 && char <= 0x39) || // 0-9 + (char == 0x5F) || // _ + (char == 0x24); // $ + + if (!validChar) { + return false; + } + } + + return true; + } + + /// @notice Validate package name matches EIP-2678: lowercase, numbers, hyphens only + /// @param name Package name to validate + /// @return valid True if name is valid + function isValidPackageName(string memory name) internal pure returns (bool) { + bytes memory nameBytes = bytes(name); + + if (nameBytes.length == 0) { + return false; + } + + for (uint256 i = 0; i < nameBytes.length; i++) { + bytes1 char = nameBytes[i]; + + bool isLowercase = (char >= 0x61 && char <= 0x7A); // a-z + bool isNumber = (char >= 0x30 && char <= 0x39); // 0-9 + bool isHyphen = (char == 0x2D); // - + + if (!isLowercase && !isNumber && !isHyphen) { + return false; + } + } + + return true; + } + + /// @notice Validate contract alias matches EIP-2678: or + /// @param contractAlias Contract alias to validate + /// @return valid True if alias is valid + function isValidContractAlias(string memory contractAlias) internal pure returns (bool) { + bytes memory aliasBytes = bytes(contractAlias); + if (aliasBytes.length == 0 || aliasBytes.length > 256) { + return false; + } + + // Alias can contain: a-z, A-Z, 0-9, -, _ + for (uint256 i = 0; i < aliasBytes.length; i++) { + bytes1 char = aliasBytes[i]; + + bool isLetter = (char >= 0x61 && char <= 0x7A) || (char >= 0x41 && char <= 0x5A); + bool isNumber = (char >= 0x30 && char <= 0x39); + bool isHyphen = (char == 0x2D); + bool isUnderscore = (char == 0x5F); + + if (!isLetter && !isNumber && !isHyphen && !isUnderscore) { + return false; + } + } + + return true; +} + + /// @notice Validate manifest version is "ethpm/3" + /// @param version Manifest version string + /// @return valid True if version is correct + function isValidManifestVersion(string memory version) internal pure returns (bool) { + return keccak256(bytes(version)) == keccak256(bytes("ethpm/3")); + } + + /// @notice Check if key is forbidden in v3 manifest + /// @param key Manifest key to check + /// @return forbidden True if key is forbidden + function isForbiddenKey(string memory key) internal pure returns (bool) { + // "manifest_version" is forbidden in v3, must use "manifest" + return keccak256(bytes(key)) == keccak256(bytes("manifest_version")); + } +} \ No newline at end of file diff --git a/src/core/PackageIndex.sol b/src/core/PackageIndex.sol deleted file mode 100644 index e69de29..0000000 diff --git a/src/core/PackageRegistry.sol b/src/core/PackageRegistry.sol index e69de29..f5fc2c3 100644 --- a/src/core/PackageRegistry.sol +++ b/src/core/PackageRegistry.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../interfaces/IPackageRegistry.sol"; +import "../structs/PackageStructs.sol"; + +/// @title PackageRegistry +/// @notice On-chain registry for EthPM v3 packages (EIP-2678) +/// @dev Stores package references with content-addressable URIs (IPFS) +contract PackageRegistry is IPackageRegistry { + + // Package name => version => manifest URI (IPFS/Swarm) + mapping(string => mapping(string => string)) private packages; + + // Package name => owner address + mapping(string => address) private packageOwners; + + // Package name => all versions + mapping(string => string[]) private packageVersions; + + // Events + event PackagePublished(string indexed name, string version, string manifestURI, address publisher); + event PackageOwnershipTransferred(string indexed name, address previousOwner, address newOwner); + + /// @notice Publish a package to the registry + /// @param name Package name (must be lowercase, numbers, hyphens only) + /// @param version Package version (semver format) + /// @param manifestURI Content-addressable URI (ipfs:// or bzz://) + function publish(string calldata name, string calldata version, string calldata manifestURI) external override { + require(bytes(name).length > 0, "Package name cannot be empty"); + require(bytes(version).length > 0, "Version cannot be empty"); + require(bytes(manifestURI).length > 0, "Manifest URI cannot be empty"); + require(_isValidPackageName(name), "Invalid package name format"); + + // Check ownership + if (packageOwners[name] == address(0)) { + // First time publishing this package + packageOwners[name] = msg.sender; + } else { + // Only owner can publish new versions + require(packageOwners[name] == msg.sender, "Only package owner can publish"); + } + + // Check if version already exists + require(bytes(packages[name][version]).length == 0, "Version already exists"); + + // Store package + packages[name][version] = manifestURI; + packageVersions[name].push(version); + + emit PackagePublished(name, version, manifestURI, msg.sender); + } + + /// @notice Get package manifest URI + /// @param name Package name + /// @param version Package version + /// @return manifestURI The content-addressable URI of the package manifest + function getPackageURI(string calldata name, string calldata version) external view override returns (string memory) { + string memory uri = packages[name][version]; + require(bytes(uri).length > 0, "Package version does not exist"); + return uri; + } + + /// @notice Check if package version exists + /// @param name Package name + /// @param version Package version + /// @return exists True if package exists + function packageExists(string calldata name, string calldata version) external view override returns (bool) { + return bytes(packages[name][version]).length > 0; + } + + /// @notice Get all versions of a package + /// @param name Package name + /// @return versions Array of all versions + function getVersions(string calldata name) external view returns (string[] memory) { + return packageVersions[name]; + } + + /// @notice Get package owner + /// @param name Package name + /// @return owner Address of package owner + function getOwner(string calldata name) external view returns (address) { + return packageOwners[name]; + } + + /// @notice Transfer package ownership + /// @param name Package name + /// @param newOwner New owner address + function transferOwnership(string calldata name, address newOwner) external { + require(packageOwners[name] == msg.sender, "Only owner can transfer ownership"); + require(newOwner != address(0), "New owner cannot be zero address"); + + address previousOwner = packageOwners[name]; + packageOwners[name] = newOwner; + + emit PackageOwnershipTransferred(name, previousOwner, newOwner); + } + + /// @notice Validate package name format (lowercase, numbers, hyphens) + /// @param name Package name to validate + /// @return valid True if name is valid + function _isValidPackageName(string calldata name) private pure returns (bool) { + bytes memory nameBytes = bytes(name); + + for (uint256 i = 0; i < nameBytes.length; i++) { + bytes1 char = nameBytes[i]; + + // Allow: a-z, 0-9, hyphen + bool isLowercase = (char >= 0x61 && char <= 0x7A); // a-z + bool isNumber = (char >= 0x30 && char <= 0x39); // 0-9 + bool isHyphen = (char == 0x2D); // - + + if (!isLowercase && !isNumber && !isHyphen) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/core/PackageStorage.sol b/src/core/PackageStorage.sol new file mode 100644 index 0000000..3bc9396 --- /dev/null +++ b/src/core/PackageStorage.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../structs/PackageStructs.sol"; + +/// @title PackageStorage +/// @notice Storage and retrieval of package metadata (EIP-2678) +contract PackageStorage { + + // Package name => version => PackageMeta + mapping(string => mapping(string => PackageStructs.PackageMeta)) private packageMeta; + + // Package name => version => CompilerInfo[] + mapping(string => mapping(string => PackageStructs.CompilerInfo[])) private compilers; + + // Package name => version => contract alias => ContractType + mapping(string => mapping(string => mapping(string => PackageStructs.ContractType))) private contractTypes; + + // Events + event PackageMetaStored(string indexed name, string version); + event CompilerInfoStored(string indexed name, string version, uint256 compilerIndex); + event ContractTypeStored(string indexed name, string version, string contractAlias); + + /// @notice Store package metadata + function storePackageMeta( + string calldata name, + string calldata version, + string[] calldata authors, + string calldata license, + string calldata description + ) external { + PackageStructs.PackageMeta storage meta = packageMeta[name][version]; + + // Clear old authors if any + delete meta.authors; + + // Store new data + for (uint256 i = 0; i < authors.length; i++) { + meta.authors.push(authors[i]); + } + meta.license = license; + meta.description = description; + + emit PackageMetaStored(name, version); + } + + /// @notice Get package metadata + function getPackageMeta(string calldata name, string calldata version) + external + view + returns (string[] memory authors, string memory license, string memory description) + { + PackageStructs.PackageMeta storage meta = packageMeta[name][version]; + return (meta.authors, meta.license, meta.description); + } + + /// @notice Store compiler information + function storeCompilerInfo( + string calldata name, + string calldata version, + string calldata compilerName, + string calldata compilerVersion + ) external { + PackageStructs.CompilerInfo memory compiler; + compiler.name = compilerName; + compiler.version = compilerVersion; + + compilers[name][version].push(compiler); + + emit CompilerInfoStored(name, version, compilers[name][version].length - 1); + } + + /// @notice Get compiler information + function getCompilers(string calldata name, string calldata version) + external + view + returns (PackageStructs.CompilerInfo[] memory) + { + return compilers[name][version]; + } + + /// @notice Store contract type + function storeContractType( + string calldata name, + string calldata version, + string calldata contractAlias, + string calldata contractName, + uint256 compilerIndex + ) external { + PackageStructs.ContractType storage cType = contractTypes[name][version][contractAlias]; + cType.contractName = contractName; + cType.compilerIndex = compilerIndex; + + emit ContractTypeStored(name, version, contractAlias); + } + + /// @notice Get contract type + function getContractType(string calldata name, string calldata version, string calldata contractAlias) + external + view + returns (string memory contractName, uint256 compilerIndex) + { + PackageStructs.ContractType storage cType = contractTypes[name][version][contractAlias]; + return (cType.contractName, cType.compilerIndex); + } +} \ No newline at end of file diff --git a/src/structs/ContractInstance.sol b/src/structs/ContractInstance.sol deleted file mode 100644 index e69de29..0000000 diff --git a/src/structs/DeploymentInfo.sol b/src/structs/DeploymentInfo.sol deleted file mode 100644 index e69de29..0000000 diff --git a/src/structs/PackageMetadata.sol b/src/structs/PackageMetadata.sol deleted file mode 100644 index e69de29..0000000 diff --git a/test/core/ManifestValidator.t.sol b/test/core/ManifestValidator.t.sol new file mode 100644 index 0000000..340d2f2 --- /dev/null +++ b/test/core/ManifestValidator.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../src/core/ManifestValidator.sol"; + +contract ManifestValidatorTest is Test { + + function testValidContractNames() public { + assertTrue(ManifestValidator.isValidContractName("Wallet")); + assertTrue(ManifestValidator.isValidContractName("_MyContract")); + assertTrue(ManifestValidator.isValidContractName("$Dollar")); + assertTrue(ManifestValidator.isValidContractName("Contract123")); + assertTrue(ManifestValidator.isValidContractName("a")); + } + + function testInvalidContractNames() public { + assertFalse(ManifestValidator.isValidContractName("")); // Empty + assertFalse(ManifestValidator.isValidContractName("123Contract")); // Starts with number + assertFalse(ManifestValidator.isValidContractName("My-Contract")); // Contains hyphen + assertFalse(ManifestValidator.isValidContractName("My Contract")); // Contains space + } + + function testValidPackageNames() public { + assertTrue(ManifestValidator.isValidPackageName("safe-math-lib")); + assertTrue(ManifestValidator.isValidPackageName("wallet")); + assertTrue(ManifestValidator.isValidPackageName("erc20-token")); + assertTrue(ManifestValidator.isValidPackageName("lib123")); + } + + function testInvalidPackageNames() public { + assertFalse(ManifestValidator.isValidPackageName("")); + assertFalse(ManifestValidator.isValidPackageName("Invalid_Name")); // Underscore + assertFalse(ManifestValidator.isValidPackageName("UpperCase")); // Uppercase + assertFalse(ManifestValidator.isValidPackageName("my package")); // Space + } + + function testValidContractAlias() public { + assertTrue(ManifestValidator.isValidContractAlias("Wallet")); + assertTrue(ManifestValidator.isValidContractAlias("Wallet-v2")); + assertTrue(ManifestValidator.isValidContractAlias("owned_Owned")); + } + + function testValidManifestVersion() public { + assertTrue(ManifestValidator.isValidManifestVersion("ethpm/3")); + assertFalse(ManifestValidator.isValidManifestVersion("ethpm/2")); + assertFalse(ManifestValidator.isValidManifestVersion("v3")); + } + + function testForbiddenKeys() public { + assertTrue(ManifestValidator.isForbiddenKey("manifest_version")); + assertFalse(ManifestValidator.isForbiddenKey("manifest")); + assertFalse(ManifestValidator.isForbiddenKey("version")); + } +} \ No newline at end of file diff --git a/test/core/PackageRegistry.t.sol b/test/core/PackageRegistry.t.sol new file mode 100644 index 0000000..53ee7c4 --- /dev/null +++ b/test/core/PackageRegistry.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../src/core/PackageRegistry.sol"; + +contract PackageRegistryTest is Test { + PackageRegistry registry; + address alice = address(0x1); + address bob = address(0x2); + + function setUp() public { + registry = new PackageRegistry(); + } + + function testPublishPackage() public { + vm.prank(alice); + registry.publish("safe-math-lib", "1.0.0", "ipfs://QmTest123"); + + assertTrue(registry.packageExists("safe-math-lib", "1.0.0")); + assertEq(registry.getPackageURI("safe-math-lib", "1.0.0"), "ipfs://QmTest123"); + } + + function testPackageOwnership() public { + vm.prank(alice); + registry.publish("wallet", "1.0.0", "ipfs://QmWallet1"); + + assertEq(registry.getOwner("wallet"), alice); + } + + function testOnlyOwnerCanPublishNewVersion() public { + vm.prank(alice); + registry.publish("token", "1.0.0", "ipfs://QmToken1"); + + vm.prank(bob); + vm.expectRevert("Only package owner can publish"); + registry.publish("token", "1.1.0", "ipfs://QmToken2"); + } + + function testCannotPublishDuplicateVersion() public { + vm.prank(alice); + registry.publish("escrow", "1.0.0", "ipfs://QmEscrow1"); + + vm.prank(alice); + vm.expectRevert("Version already exists"); + registry.publish("escrow", "1.0.0", "ipfs://QmEscrow2"); + } + + function testInvalidPackageName() public { + vm.prank(alice); + vm.expectRevert("Invalid package name format"); + registry.publish("Invalid_Name", "1.0.0", "ipfs://QmTest"); + } + + function testValidPackageNames() public { + vm.startPrank(alice); + + registry.publish("valid-name", "1.0.0", "ipfs://Qm1"); + registry.publish("name123", "1.0.0", "ipfs://Qm2"); + registry.publish("123name", "1.0.0", "ipfs://Qm3"); + registry.publish("my-package-v2", "1.0.0", "ipfs://Qm4"); + + vm.stopPrank(); + + assertTrue(registry.packageExists("valid-name", "1.0.0")); + assertTrue(registry.packageExists("name123", "1.0.0")); + } + + function testGetVersions() public { + vm.startPrank(alice); + registry.publish("mylib", "1.0.0", "ipfs://Qm1"); + registry.publish("mylib", "1.1.0", "ipfs://Qm2"); + registry.publish("mylib", "2.0.0", "ipfs://Qm3"); + vm.stopPrank(); + + string[] memory versions = registry.getVersions("mylib"); + assertEq(versions.length, 3); + } + + function testTransferOwnership() public { + vm.prank(alice); + registry.publish("transferable", "1.0.0", "ipfs://QmTransfer"); + + vm.prank(alice); + registry.transferOwnership("transferable", bob); + + assertEq(registry.getOwner("transferable"), bob); + + // Bob can now publish new versions + vm.prank(bob); + registry.publish("transferable", "1.1.0", "ipfs://QmTransfer2"); + + assertTrue(registry.packageExists("transferable", "1.1.0")); + } +} \ No newline at end of file