Skip to content

Conversation

@peersky
Copy link
Member

@peersky peersky commented May 2, 2025

Summary by CodeRabbit

  • New Features

    • Made distributor instantiation methods payable to accept payments.
    • Added comprehensive upgradability with multi-party trust, migration scripts, and version management.
    • Enhanced installer and distributor contracts with app-centric management, middleware hooks, and upgrade capabilities.
    • Introduced upgradable proxy contracts, migration handling, tokenized distributors with ERC20 payments, and admin interfaces.
    • Added extensive CLI commands and detailed documentation guides covering distributions, distributors, repositories, indexing, versioning, and upgradability.
    • Supported semantic versioning, migration plans, distributor aliasing, and distributor cost management.
    • Added new middleware contracts and refined middleware proxy patterns.
    • Provided extensive mock contracts and test utilities for middleware, distributors, installers, and migrations.
  • Bug Fixes

    • Corrected typos and naming inconsistencies in contracts, events, and errors.
    • Fixed interface detection and inheritance issues.
    • Improved revert reason handling and error propagation in initializers and migrations.
    • Fixed field name typos in middleware libraries.
  • Documentation

    • Major expansions and restructuring of README and technical guides for clarity and precision.
    • Added consolidated guides and detailed references for core components and workflows.
    • Documented new versioning, migration, distribution, and upgradability concepts and CLI usage.
  • Refactor

    • Shifted from instance-based to app-centric logic and naming across the codebase.
    • Modularized installer, distributor, and middleware patterns.
    • Improved version and migration management in repositories and distributors.
    • Renamed and reorganized middleware contracts and storage for clarity.
    • Replaced deprecated abstract base contracts with new implementations supporting enhanced features.
    • Updated import paths and library usages for consistency.
  • Style

    • Enhanced code clarity, structure, and comments for maintainability.
    • Added NatSpec comments and improved function annotations.
  • Tests

    • Added extensive new tests and updated existing suites covering versioning, distribution, migration, middleware, and upgradability workflows.
    • Included mock contracts and integration tests for middleware and upgrade scenarios.
    • Refined tests to reflect app-centric naming and updated interfaces.
    • Added tests for error handling, migration strategies, and access control.
  • Chores

    • Updated dependencies and development environment configurations.
    • Added ignore files and spell checker settings.
    • Added Solidity coverage configuration and Hardhat tasks for ABI interface generation.
  • Revert

    • Removed deprecated abstract contracts and libraries replaced by new implementations.

peersky and others added 3 commits April 22, 2025 23:25
* wip

* adjusting for ERC7744 for ERC7702

* reduce optimisation heaviness

* initial tests pass

* docs, small tunings in TDD mode

* Add mock distribution contracts and update VSCode settings

- Added `MockFailingDistribution`, `MockLowLevelDistribution`, `MockPanicDistribution`, `MockMigration`, `MockRepository`, and `MockLatestVersionDistribution` for testing purposes.
- Updated `LatestVersionDistribution` to allow instantiation with a virtual method.
- Enhanced `Distributor` and `OwnableDistributor` to support new mock distributions.
- Updated VSCode settings to include a new word for spell checking.

* Enhance distribution contracts and add new functionalities

- Updated `hardhat.config.ts` to include new tasks for generating super interfaces from ABI files.
- Modified `CloneDistribution`, `LatestVersionDistribution`, and `UpgradableDistribution` to implement `supportsInterface` for ERC165 compliance.
- Introduced `SelfInstaller` contract for owner-based installation and management of distributions.
- Enhanced `Distributor` to manage app states and migrations more effectively, including error handling for migration failures.
- Added mock contracts for testing various distribution scenarios, including migration failures and ownership transfers.
- Updated tests to cover new functionalities and ensure proper behavior of distribution and migration workflows.

* Add documentation for EDS components including Distributions, Distributors, Indexer, Repositories, Upgradability, and Versioning

- Introduced comprehensive guides for Distributions and Distributors, detailing their creation, management, and operational commands.
- Added documentation for the EDS Indexing system, explaining its role and usage.
- Created guides for Repositories, outlining their purpose and CLI commands for version management.
- Documented the Upgradability process, including trust assumptions and migration strategies.
- Implemented a section on Versioning, detailing the semantic versioning system and CLI utilities for version management.

* Add mock functionality to `MockRepository` for testing migration scripts

- Introduced a `mock` function to simulate migration script retrieval.
- Added state variables to store mock addresses and calldata.
- Implemented a new function `getMigrationScriptCalldata` to retrieve the mock calldata for testing purposes.

* Remove mock distribution contracts and update distributor logic

- Deleted `MockFailingDistribution`, `MockLowLevelDistribution`, `MockPanicDistribution`, `MockMigration`, and `MockRepository` as they are no longer needed.
- Updated `Distributor` and related contracts to improve handling of app states and migration processes.
- Introduced new mock contracts for testing, including `MockOwnableDistributor`, `MockInitializer`, and `MockNoReasonInitializer` to facilitate better testing scenarios.

* Refactor UpgradableDistribution and remove VersionDistribution

- Refactored `UpgradableDistribution` to enhance its constructor and source management.
- Removed `VersionDistribution` as it is no longer needed, consolidating functionality into `UpgradableDistribution`.
- Updated the `instantiate` function to improve argument handling and ensure compatibility with the new structure.
- Introduced `WrappedProxyInitializer` for better initialization handling in the distribution process.
- Adjusted related tests to reflect changes in distribution handling and ensure proper functionality.

* Refactor error handling in Distributor contract and update tests

- Simplified error handling in the `Distributor` contract by removing redundant revert messages.
- Updated tests to reflect the new behavior of reverting with a generic message when external initializers fail without a reason.
- Cleaned up unnecessary debug logging in `ERC7746Hooked` tests to streamline output and focus on validation.

* Fix typos in migration strategy and update related tests

- Corrected the spelling of `REPOSITORY_MANAGED` in the `Distributor` and `IDistributor` contracts.
- Updated the `AuthorizationMiddleware` to reflect the new naming convention.
- Adjusted tests to ensure they reference the corrected migration strategy name, enhancing clarity and consistency.

* Update LibMiddleware tests to reflect changes in layer count expectations

- Adjusted the expected final layer count from 2 to 1 after modifying the layers array.
- Removed outdated comments regarding the verification of the second layer, as it is no longer applicable.

* Refactor Installer contracts and update related tests

- Renamed `SelfInstaller` to `OwnableInstaller` for clarity and consistency.
- Introduced `enforceActiveDistribution` function to validate distributor and distribution ID permissions.
- Updated installation logic to streamline the process for both owner and public installations.
- Adjusted tests to reflect changes in the installer contract and ensure proper functionality.
- Removed outdated mock installer references and replaced them with the new `OwnableInstaller`.

* Enhance README and documentation for EDS components

- Updated the README to clarify the purpose and features of the Ethereum Distribution System (EDS), emphasizing its decentralized nature and version management capabilities.
- Improved use case descriptions for better clarity and added links to relevant documentation.
- Expanded the overview section to define user roles and interactions more clearly.
- Enhanced documentation for Distributions, Distributors, Repositories, and Upgradability, detailing their functionalities and providing examples for better understanding.
- Updated CLI commands for managing distributions and distributors to reflect recent changes in functionality.
@changeset-bot
Copy link

changeset-bot bot commented May 2, 2025

🦋 Changeset detected

Latest commit: 5765abc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@peeramid-labs/eds Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented May 2, 2025

Warning

Rate limit exceeded

@peersky has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 16 minutes and 7 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a1a3ee9 and 5765abc.

📒 Files selected for processing (11)
  • src/distributors/OwnableTokenizedDistributor.sol (1 hunks)
  • src/distributors/TokenizedDistributor.sol (1 hunks)
  • src/mocks/LibMiddlewareExtendedTest.sol (1 hunks)
  • src/mocks/MockDistribution.sol (1 hunks)
  • src/mocks/MockERC165WithFunctions.sol (1 hunks)
  • src/mocks/MockERC7746Target.sol (1 hunks)
  • src/mocks/MockRepository.sol (1 hunks)
  • src/mocks/MockTokenizedDistributor.sol (0 hunks)
  • test/middleware/AuthorizationMiddleware.test.ts (1 hunks)
  • test/middleware/InstallerClonable.test.ts (1 hunks)
  • test/middleware/MiddlewareProxy.test.ts (1 hunks)

Walkthrough

This update introduces a major architectural refactor and expansion of the Ethereum Distribution System (EDS) codebase. Legacy abstract contracts for code indexing, distribution, and installation were removed and replaced with new modular implementations supporting advanced features such as upgradability, version migrations, and middleware hooks. Interfaces for distributors, distributions, installers, and repositories were significantly extended to include richer versioning, migration workflows, and enhanced error and event handling. New distributor contracts were added with payable instantiation methods and ownership controls. Distribution contracts were improved with proxy-based upgradability and semantic version metadata. Middleware contracts were renamed and enhanced with better storage and hook management. Comprehensive documentation and test suites were added or updated to cover new workflows, error cases, and integration scenarios, emphasizing modularity, semantic versioning, multi-party upgradeability, and middleware extensibility.

Changes

File(s) / Group Change Summary
.changeset/poor-bees-fix.md, .changeset/witty-icons-drive.md Added changeset metadata including a major update making distributor instantiate methods payable and a spelling fix note.
.cursorignore Added .cursorignore file to exclude .env and .secrets/ directories from certain operations.
.solcover.js Added Solidity coverage configuration to exclude mocks/ and erc7744/ folders from coverage.
.vscode/settings.json Added spell checker settings with custom words to prevent false positives.
README.md Extensively revised to improve clarity, structure, and technical detail; added sections on key features, components, and documentation guides.
deploy/ERC7744.js Updated deterministic deployment salt and conditional logging to avoid logs on local networks.
docs/contracts/index.md Updated IInstaller documentation to rename functions from "instance" to "app" terminology.
docs/guides/Distributions.md, docs/guides/Distributors.md, docs/guides/Indexer.md, docs/guides/Repositories.md, docs/guides/Upgradability.md, docs/guides/Versioning.md Added new comprehensive guides covering distributions, distributors, indexing, repositories, upgradability, and versioning concepts.
hardhat.config.ts Added new Hardhat task to merge ABI files into a super interface; updated Solidity compiler settings including EVM version and optimizer runs.
package.json Updated dependency versions for OpenZeppelin contracts and solidity-coverage.
src/abstracts/CodeIndexer.sol, src/abstracts/Distributor.sol, src/abstracts/Installer.sol, src/abstracts/InstallerCloneable.sol, src/libraries/LibInstaller.sol Removed legacy abstract contracts and installer library.
src/distributions/CloneDistribution.sol, src/distributions/CodeHashDistribution.sol, src/distributions/LatestVersionDistribution.sol, src/distributions/UpgradableDistribution.sol Refactored distribution contracts: improved interface support, metadata handling, source resolution, added new upgradable distribution contract.
src/distributors/Distributor.sol, src/distributors/OwnableDistributor.sol, src/distributors/TokenizedDistributor.sol, src/distributors/WrappedProxyInitializer.sol Added new distributor contracts with enhanced versioning, migration, payable instantiation, and readable name metadata.
src/erc7744/ERC7744.sol, src/erc7744/LibERC7744.sol Updated indexer to detect EIP-7702 delegated EOAs by code prefix; added utility library for contract registration and lookup.
src/interfaces/IAdminGetter.sol, src/interfaces/IDistribution.sol, src/interfaces/IDistributor.sol, src/interfaces/IInitializer.sol, src/interfaces/IInstaller.sol, src/interfaces/IMigration.sol, src/interfaces/IRepository.sol Major interface extensions: added migration and versioning support, new errors/events, renamed and expanded functions, improved type safety and documentation.
src/middleware/AuthorizationMiddleware.sol, src/middleware/ERC7746Hooked.sol, src/middleware/InstallerClonable.sol, src/middleware/InstallerOwnable.sol, src/middleware/LibMiddleware.sol, src/middleware/MiddlewareProxy.sol Renamed and refactored middleware contracts; fixed typos; added new installer and ownable installer implementations; improved middleware management and storage logic.
src/mocks/LibMiddlewareExtendedTest.sol, src/mocks/LibMiddlewareTest.sol, src/mocks/LibSemverTest.sol, src/mocks/MockAuthMiddlewareDistributor.sol, src/mocks/MockAuthMiddlewareTarget.sol, src/mocks/MockCloneDistribution.sol, src/mocks/MockDistributorForTest.sol, src/mocks/MockERC165.sol, src/mocks/MockERC20.sol, src/mocks/MockERC7746Hooked.sol, src/mocks/MockERC7746HookedTest.sol, src/mocks/MockFailingDistribution.sol, src/mocks/MockFailingMigration.sol, src/mocks/MockInitializer.sol, src/mocks/MockInstaller.sol, src/mocks/MockLatestVersionDistribution.sol, src/mocks/MockLowLevelDistribution.sol, src/mocks/MockMiddleware.sol, src/mocks/MockMiddlewareTarget.sol, src/mocks/MockMigration.sol, src/mocks/MockNoReasonInitializer.sol, src/mocks/MockOwnableDistributor.sol, src/mocks/MockPanicDistribution.sol, src/mocks/MockRepository.sol, src/mocks/MockTokenizedDistributor.sol, src/mocks/MockTransferOwnershipProxy.sol, src/mocks/TestFacet.sol Added or updated extensive mock contracts for testing new features, error handling, middleware logic, and upgrade flows.
src/proxies/WrappedTransparentUpgradeableProxy.sol Added new proxy contract combining transparent upgradeability with immutable middleware hooks and admin retrieval logic.
src/repositories/OwnableRepository.sol, src/repositories/Repository.sol Updated repositories to support migration scripts, improved version resolution, and added interface checks for migration contracts.
test/eds/CloneDistribution.ts, test/eds/Distributor.ts, test/eds/Installer.ts, test/eds/LatestVersionDistribution.ts, test/eds/Repository.ts, test/eds/RepositoryExtended.ts, test/eds/TokenizedDistributor.ts, test/eds/UpgradabilityWorkflows.ts, test/eds/VersionDistributor.ts, test/eds/VersionWorkflows.ts, test/middleware/AuthorizationMiddleware.test.ts, test/middleware/ERC7746Hooked.test.ts, test/middleware/InstallerClonable.test.ts, test/middleware/InstallerClonableUpgrade.test.ts, test/middleware/LibMiddleware.test.ts, test/versioning/LibSemver.test.ts Comprehensive test suite updates and additions covering new contract logic, error cases, versioning, migrations, upgradability, and middleware integration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Installer
    participant Distributor
    participant Distribution
    participant Repository
    participant Migration
    participant Proxy

    User->>Installer: install(distributor, distributionId, args)
    Installer->>Distributor: instantiate(distributionId, args)
    Distributor->>Distribution: instantiate(args)
    Distribution->>Proxy: deploy new instance(s)
    Proxy-->>Distribution: instance address
    Distribution-->>Distributor: [instance], name, version
    Distributor-->>Installer: [instance], name, version
    Installer-->>User: appId

    User->>Installer: upgradeApp(appId, migrationId, userCalldata)
    Installer->>Distributor: upgradeUserInstance(appId, migrationId, userCalldata)
    Distributor->>Migration: migrate(instances, oldVersion, newVersion, repository, distributorCalldata, userCalldata)
    Migration-->>Distributor: migration result
    Distributor-->>Installer: newVersion
    Installer-->>User: upgrade result
Loading

Poem

A warren of code, now tidy and bright,
With apps and migrations, all working just right.
Installers and proxies, with middleware flair,
Distributors and versions, all handled with care.
From docs to the tests, the system’s in sync,
This rabbit approves—let’s hop to the next link!
🐇✨

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 45

🔭 Outside diff range comments (3)
src/middleware/MiddlewareProxy.sol (1)

33-56: ⚠️ Potential issue

Proxy does not propagate return data – breaks every view/pure call

_fallback() swallows the return data from delegatecall; callers will always receive an empty byte string, so any ABI-decoded return value becomes 0 / false.
This is a critical proxy bug that will surface immediately in production.

-    function _fallback() internal virtual {
-        (bool success, bytes memory result) = _implementation().delegatecall(msg.data);
-        if (!success) {
-            // If the call failed, bubble up the revert reason if present
-            if (result.length > 0) {
-                assembly {
-                    let returndata_size := mload(result)
-                    revert(add(32, result), returndata_size)
-                }
-            } else {
-                revert("delegatecall failed without revert reason");
-            }
-        }
-    }
+    function _fallback() internal virtual {
+        address impl = _implementation();
+        assembly {
+            // Copy calldata
+            calldatacopy(0, 0, calldatasize())
+            // Delegatecall into the implementation
+            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
+            // Copy returned data
+            returndatacopy(0, 0, returndatasize())
+            // Propagate result or revert
+            switch result
+            case 0 { revert(0, returndatasize()) }
+            default { return(0, returndatasize()) }
+        }
+    }
src/distributors/TokenizedDistributor.sol (2)

65-67: ⚠️ Potential issue

Unchecked ERC-20 transferFrom may silently fail

IERC20.transferFrom returns a boolean. If the token returns false, the distributor will continue as though payment succeeded.
Use SafeERC20 for guaranteed behaviour (and it handles non-standard tokens that revert):

-import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
...
-        paymentToken.transferFrom(msg.sender, _beneficiary, instantiationCosts[distributorsId]);
+        SafeERC20.safeTransferFrom(
+            IERC20(paymentToken),
+            msg.sender,
+            _beneficiary,
+            instantiationCosts[distributorsId]
+        );

(remember using SafeERC20 for IERC20;)


56-67: 🛠️ Refactor suggestion

Charge the user after successful instantiation to avoid unwanted loss of funds

If super._instantiate() reverts after the payment is taken, funds are lost and the caller receives no instance.
Move the token transfer below a successful instantiation:

-        paymentToken.transferFrom(msg.sender, _beneficiary, instantiationCosts[distributorsId]);
-        return super._instantiate(distributorsId, args);
+        (address[] memory instances, bytes32 name, uint256 version) =
+            super._instantiate(distributorsId, args);
+
+        SafeERC20.safeTransferFrom(
+            IERC20(paymentToken),
+            msg.sender,
+            _beneficiary,
+            instantiationCosts[distributorsId]
+        );
+        return (instances, name, version);
♻️ Duplicate comments (1)
test/eds/RepositoryExtended.ts (1)

360-377: Same issue – fix all get invocations to avoid runtime reverts
Apply the two-argument call style:

-const source = await repository.get({
-  version: { major: 2, minor: 0, patch: 0 },
-  requirement: 6
-});
+const source = await repository.get(
+  { major: 2, minor: 0, patch: 0 },
+  6 // LibSemver.requirements.LESSER_EQUAL
+);

Do likewise for source2 below.

🧹 Nitpick comments (99)
.vscode/settings.json (1)

2-2: Add technical terms to cSpell dictionary—but verify spelling of “continous”.
Whitelisting "extcodecopy", "mstore", "retval", and "strg" is a solid move to suppress EVM-related false positives. However, "continous" looks like a typo of “continuous”. Please confirm whether the code deliberately uses “continous”; if not, correct this entry to "continuous" (and update occurrences in the code).

test/eds/CloneDistribution.ts (1)

24-26: Consider clarifying ShortStrings handling.

The string manipulation (slicing and appending "00") to handle ShortStrings representation makes the test less intuitive. Consider adding a comment explaining this approach or creating a helper function that handles ShortStrings conversion for better readability and maintainability.

+ // ShortStrings require special handling - we slice the last two characters and append "00"
  expect(ethers.utils.parseBytes32String(name.slice(0, -2) + "00")).to.be.equal(
    "MockCloneDistribution"
  );
src/distributions/CloneDistribution.sol (1)

6-6: Remove unused ERC165 import & explicitly import IERC165

CloneDistribution neither inherits from ERC165 nor uses any of its helpers, so the import only adds byte-code size during compilation.
If you intend to conform to ERC-165, import the interface instead (the type is needed for type(IERC165).interfaceId).

-import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
+import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
src/distributions/CodeHashDistribution.sol (2)

33-35: Parameter is unused – mute the warning or expose a use-case

The bytes memory argument in instantiate is ignored.
Either prefix it with _ (or rename to /*data*/) to silence the compiler warning, or document how it is expected to be used by child contracts.

-function instantiate(bytes memory) external returns (address[] memory instances, bytes32, uint256) {
+function instantiate(bytes memory /*data*/) external returns (address[] memory instances, bytes32, uint256) {

44-45: Trailing-zero artefacts in contractURI

Casting a fixed-size bytes32 to string via abi.encodePacked produces a UTF-8 string padded with zero bytes, which most clients strip unexpectedly.
Consider storing the URI as string instead, or trim the zero padding before returning.

-        return string(abi.encodePacked(metadata)); //ToDo: Add IPFS link with readme!
+        // Store `metadata` as `string` or, if sticking with `bytes32`, strip padding:
+        uint256 len = 0;
+        while (len < 32 && metadata[len] != 0) { ++len; }
+        bytes memory slice = new bytes(len);
+        for (uint256 i; i < len; ++i) slice[i] = metadata[i];
+        return string(slice);
src/erc7744/ERC7744.sol (1)

14-21: Consider memory cleanup in new EIP7702 detection function

The new isEIP7702 function effectively detects EIP7702 delegated EOAs. However, when using inline assembly to read from memory, it's generally a good practice to clean up after yourself.

    function isEIP7702(address account) public view returns (bool) {
        bytes3 prefix;
        assembly {
            extcodecopy(account, 0, mload(0x40), 3) // Copy first 3 bytes to memory
            prefix := mload(0x40) // Load the 3 bytes from memory
+           mstore(0x40, add(mload(0x40), 32)) // Reset free memory pointer
        }
        return prefix == bytes3(0xef0100);
    }

Also, for readability, you might consider using a named return variable:

-    function isEIP7702(address account) public view returns (bool) {
+    function isEIP7702(address account) public view returns (bool isEIP7702Account) {
        bytes3 prefix;
        assembly {
            extcodecopy(account, 0, mload(0x40), 3) // Copy first 3 bytes to memory
            prefix := mload(0x40) // Load the 3 bytes from memory
        }
-        return prefix == bytes3(0xef0100);
+        isEIP7702Account = prefix == bytes3(0xef0100);
    }
test/eds/LatestVersionDistribution.ts (3)

17-17: Rename test suite to match the actual contract being tested.

The test suite is named "LatestUpgradableDistribution" but it's testing the "LatestVersionDistribution" contract. This naming mismatch could cause confusion for other developers.

-describe("LatestUpgradableDistribution", function () {
+describe("LatestVersionDistribution", function () {

27-27: Remove unused variable repoName.

The variable repoName is declared but never used in the test suite, as flagged by the linter.

-  const repoName = ethers.utils.formatBytes32String("MyRepo");

Alternatively, if this variable is needed for future tests, add a comment explaining its purpose.

🧰 Tools
🪛 GitHub Actions: CI

[warning] 27-27: ESLint: 'repoName' is assigned a value but never used. (no-unused-vars)


79-82: Inconsistent metadata values between constructor and repository.

The test uses "testMetadata" (Line 24) for the distribution constructor but "metadata" for the repository release. This can lead to confusion as they're different values but serve similar purposes.

-  const metadataBytes = ethers.utils.toUtf8Bytes("metadata");
+  const metadataBytes = ethers.utils.toUtf8Bytes(metadata);
src/mocks/MockNoReasonInitializer.sol (1)

5-5: Consider implementing the IInitializer interface explicitly.

The contract imports IInitializer but doesn't explicitly implement it. For better type safety and interface verification:

- contract MockNoReasonInitializer {
+ contract MockNoReasonInitializer is IInitializer {
src/proxies/WrappedTransparentUpgradeableProxy.sol (3)

1-2: Put the SPDX-License identifier before pragma

The SPDX header must be the very first non‐blank line so that tooling (licence scanners, Remix, etherscan, etc.) can detect it.

-pragma solidity ^0.8.28;
// SPDX-License-Identifier: MIT
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.28;

4-10: Unused imports add byte-code bloat

ERC165Checker, IDistributor and the custom InvalidMiddleware error are never referenced. Removing them keeps the byte-code (and the deployment cost) minimal.

-import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
-import {IDistributor} from "../interfaces/IDistributor.sol";
-...
-error InvalidMiddleware();

18-29: Parameter naming is misleading

_admin becomes the middleware address, while _owner becomes the EIP-1967 admin (ProxyAdmin) that can perform upgrades.
Swapping these roles in the constructor call is error-prone for integrators reading the ABI.

Consider renaming to make intent crystal-clear:

-        address _admin,        // actually middleware layer
-        address _owner,        // actually proxyAdmin contract
+        address middleware,    // executes ERC-7746 hooks
+        address proxyAdmin,    // EIP-1967 admin (ProxyAdmin contract)
...
-    ) TransparentUpgradeableProxy(_logic, _owner, _data) {
+    ) TransparentUpgradeableProxy(_logic, proxyAdmin, _data) {
-        layers[0] = LibMiddleware.LayerStruct({layerAddress: _admin, layerConfigData: _middlewareData});
+        layers[0] = LibMiddleware.LayerStruct({layerAddress: middleware, layerConfigData: _middlewareData});
docs/guides/Repositories.md (1)

11-15: Fix list indentation to satisfy markdownlint (MD007)

Sub-bullets should be indented by two spaces:

-*   Major version metadata (`releaseMetadata`).
-*   Minor version metadata (`minorReleaseMetadata`).
-*   Patch version metadata (`patchReleaseMetadata`).
-*   Metadata can be updated using `_updateReleaseMetadata`. The `get` function automatically combines metadata from all relevant levels for a resolved version.
+  * **Major version metadata** (`releaseMetadata`).
+  * **Minor version metadata** (`minorReleaseMetadata`).
+  * **Patch version metadata** (`patchReleaseMetadata`).
+  * Metadata can be updated using `_updateReleaseMetadata`.  
+    The `get` function automatically merges metadata from all relevant levels for a resolved version.
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

12-12: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


15-15: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

test/middleware/InstallerClonable.test.ts (1)

74-80: Clarify factory variable names to avoid cognitive load

MockDistributor actually represents the UpgradableDistribution factory.
Using the real contract name keeps the fixture easier to read:

-const MockDistributor = (await ethers.getContractFactory(
-  "UpgradableDistribution"
-)) as UpgradableDistribution__factory;
+const UpgradableDistribution = (await ethers.getContractFactory(
+  "UpgradableDistribution"
+)) as UpgradableDistribution__factory;
src/middleware/AuthorizationMiddleware.sol (1)

65-65: Error naming inconsistency.

The error disallowedAddress doesn't follow PascalCase convention used for other errors like OnlyTargetAllowed. Error names should follow a consistent style throughout the codebase.

-    error disallowedAddress(address sender, bytes4 selector);
+    error DisallowedAddress(address sender, bytes4 selector);

Also update the reference on line 79:

-            revert disallowedAddress(sender, selector);
+            revert DisallowedAddress(sender, selector);
test/middleware/AuthorizationMiddleware.test.ts (1)

1-4: Remove unused import.

The ContractFactory import on line 3 is defined but never used, as identified by the ESLint warning in the pipeline failure logs.

-import { Contract, ContractFactory, Signer } from "ethers";
+import { Contract, Signer } from "ethers";
🧰 Tools
🪛 GitHub Actions: CI

[warning] 3-3: ESLint: 'ContractFactory' is defined but never used. (no-unused-vars)

src/mocks/MockFailingDistribution.sol (2)

8-19: Potential inconsistency in failure condition logic.

The instantiate function checks if keccak256(args) == keccak256(abi.encode(bytes32("FAIL"))) to determine when to revert. This approach may not work reliably if callers encode "FAIL" differently, as the exact encoding format matters for the hash comparison.

Consider a more flexible approach that doesn't rely on exact encoding:

-        if (args.length > 0 && keccak256(args) == keccak256(abi.encode(bytes32("FAIL")))) {
+        if (args.length == 32 && abi.decode(args, (bytes32)) == bytes32("FAIL")) {
             revert("Intentional instantiation failure");
         }

31-33: Improve ERC165 implementation.

The supportsInterface function doesn't fully comply with ERC165 as it doesn't explicitly check for the ERC165 interface ID itself (0x01ffc9a7).

    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
-        return interfaceId == type(IDistribution).interfaceId || interfaceId == type(IERC165).interfaceId;
+        return 
+            interfaceId == type(IDistribution).interfaceId || 
+            interfaceId == type(IERC165).interfaceId || 
+            interfaceId == 0x01ffc9a7;
    }
src/mocks/MockLatestVersionDistribution.sol (1)

19-23: Unused parameter in instantiate method.

The instantiate function accepts a bytes memory parameter but doesn't use it, which might be misleading for callers. Consider documenting this behavior or removing the unused parameter if it's not needed for interface compatibility.

    function instantiate(
-        bytes memory
+        bytes memory /* args - unused */
    ) external override returns (address[] memory instances, bytes32 name, uint256 version) {
        return super._instantiate();
    }
src/mocks/MockRepository.sol (1)

11-11: Constructor could be more flexible

The constructor uses hardcoded values "name" and "cURI". Consider allowing these to be passed as parameters for better test flexibility, even for a mock contract.

-constructor() Repository("name", "cURI") {}
+constructor(string memory name, string memory contentURI) Repository(name, contentURI) {}
src/distributors/WrappedProxyInitializer.sol (3)

6-6: Unused import

The contract imports ProxyAdmin but doesn't appear to use it anywhere in the implementation.

-import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

10-13: Unnamed parameters

The function has two unnamed parameters (bytes32 and uint256) which suggests they're unused. Consider either adding names for clarity or explicitly marking them as unused with comments.

function initialize(
    address distribution,
-   bytes32,
-   uint256,
+   bytes32 /* distributionId */,
+   uint256 /* versionUint */,
    bytes calldata data
) public returns (address[] memory instances) {

15-15: Remove commented code

There's a commented line that seems to be debug or old code. Consider removing it if it's no longer needed.

-    // (address installer, bytes memory args) = abi.decode(data, (address, bytes));
test/versioning/LibSemver.test.ts (1)

251-252: Add test for invalid requirement

The comment "Skip testing invalid requirement to avoid errors" suggests there's no test for handling invalid inputs. Consider adding a test case for invalid requirements to ensure they're handled gracefully.

it("should handle invalid requirement type", async function () {
  const version = { major: 2, minor: 0, patch: 0 };
  
  // Invalid requirement (value outside valid range)
  const invalidRequirement = {
    version: { major: 1, minor: 0, patch: 0 },
    requirement: 8 // Invalid requirement type
  };
  
  await expect(libSemverTest.compare(version, invalidRequirement))
    .to.be.revertedWithCustomError(libSemverTest, "invalidRequirement");
});
test/middleware/InstallerClonableUpgrade.test.ts (2)

63-63: Remove commented code

There's a commented line that appears to be obsolete. If initialization is no longer needed, remove this line; if it's still important, explain why it's commented out.

- //  await mockInstaller.initialize(mockDistributor.address);

144-149: Add explanation for migrationId calculation

The migrationId calculation is complex. Consider adding a comment explaining how this ID is structured and why these specific parameters are used.

+ // Create the migrationId hash by encoding the distributorId, migrationHash and strategy
+ // This matches how the distributors identify migrations for version updates
migrationId = ethers.utils.keccak256(
  ethers.utils.defaultAbiCoder.encode(
    ["bytes32", "bytes32", "uint8"],
    [distributorId, migrationHash, 0]
  )
);
docs/guides/Indexer.md (1)

3-4: Minor language clean-ups improve professionalism and clarity
Several small grammatical issues make the doc feel rough-draft-like:

  • line 3: “permission-less” → “permissionless” (hyphen not used in this context)
  • line 9: “by following in standard described deployment steps” is awkward – suggest “by following the deployment steps described in the standard”
  • line 15-16: !!!WARNING is rendered as raw text. In Markdown use > [!WARNING] (GitHub style) or a regular heading.
  • line 18: “helps to interact” → “helps you interact”.

These are nit-level but quickly polish the doc.

Also applies to: 9-10, 15-16, 18-18

docs/contracts/index.md (1)

272-279: Typo in return variable “instaneContracts”
The word “instaneContracts” is misspelled twice – should be instanceContracts (or simply instances). Typos in API docs propagate into generated bindings and code comments.

-function getApp(uint256 instanceId) public view returns (address[] instaneContracts)
+function getApp(uint256 instanceId) public view returns (address[] instanceContracts)

Apply the same correction in the IInstaller section.

Also applies to: 820-827

src/mocks/MockMiddlewareTarget.sol (1)

6-12: middleware field is never read – remove or use for consistency
The contract stores middleware in the constructor but all calls take an explicit hook parameter. Either:

  1. Delete the state variable and constructor arg; or
  2. Remove the hook parameter and always use the stored address.

Option 1 (delete unused state):

-    address public middleware;
-
-    constructor(address _middleware) {
-        middleware = _middleware;
-    }
+    constructor(address _middleware) {
+        // Intentionally unused – kept only for interface parity? Consider removing.
+    }

Dead fields are a code-smell and complicate tests.

Also applies to: 13-19, 23-32

test/eds/RepositoryExtended.ts (1)

1-14: ESLint warnings on unused identifiers – tidy imports & vars
utils, distributionId, and caught error variables e are unused, generating CI noise. Either remove them or prefix with _ to signal intentional disregard.

-import utils from "../utils";
+// (remove – nothing uses utils)

-let distributionId: string;
-...
-catch (e) {
+catch {

Cleaning these up keeps the test suite warning-free.

Also applies to: 24-24, 54-70

🧰 Tools
🪛 GitHub Actions: CI

[warning] 13-13: ESLint: 'utils' is defined but never used. (no-unused-vars)

hardhat.config.ts (3)

3-3: Redundant named import – join is already available via path

path.join is available on the default path import, so importing join separately is unnecessary noise.

-import path, { join } from "path";
+import path from "path";

15-17: Duplicate fs imports can be consolidated

You already import writeFile/mkdir from fs/promises. Pulling in the whole synchronous fs module increases bundle size and mixes sync + async styles. Prefer one of:

  1. Stick to async (fs/promises) everywhere.
  2. Keep the plain fs import and use fs.promises.*.

Either way, avoid holding two different interfaces for the same API.


66-73: Global console.log monkey-patch is brittle

Overwriting console.log:

  1. Breaks parallel tasks/tests that rely on logging.
  2. Risks leaving the stub active if an exception is thrown before the restore.

Consider passing a silent logger (e.g. debug library) instead, or wrap the critical section in try/finally to guarantee restoration.

-  console.log = () => {}; // avoid noisy output
-  const result = new ethers.utils.Interface(mergedArray);
-  ...
-  console.log = originalConsoleLog;
+  let result: ethers.utils.Interface;
+  try {
+    console.log = () => {};
+    result = new ethers.utils.Interface(mergedArray);
+    ...
+  } finally {
+    console.log = originalConsoleLog;
+  }
test/eds/Repository.ts (1)

17-17: Remove unused import to stop CI warning

constants is never referenced — ESLint flags it.

-import { constants } from "ethers";
🧰 Tools
🪛 GitHub Actions: CI

[warning] 17-17: ESLint: 'constants' is defined but never used. (no-unused-vars)

test/middleware/ERC7746Hooked.test.ts (2)

60-109: Add test case for middleware that reverts or fails.

The current tests cover the happy path where middleware executes successfully, but there are no tests for error handling when middleware reverts or fails.

Consider adding a test case that simulates middleware failure by setting the mock middleware to revert, and verify that the revert is properly propagated:

it("should properly handle middleware revert", async function () {
  // Configure middleware to revert on beforeCall
  await mockMiddleware1.setRevertOnBeforeCall(true);
  
  // Attempt to call the function with the ERC7746C modifier
  await expect(
    mockHookedTest.testERC7746CModifier(
      TEST_SELECTOR,
      userAddress,
      TEST_DATA,
      TEST_VALUE,
      "Will Revert"
    )
  ).to.be.revertedWith("MockMiddleware: beforeCall reverted");
  
  // Reset the revert flag for subsequent tests
  await mockMiddleware1.setRevertOnBeforeCall(false);
});

14-18: Consider using function-derived selectors instead of hardcoded values.

The test uses hardcoded values for TEST_SELECTOR which may become brittle if actual function signatures change.

-  const TEST_SELECTOR = "0x12345678";
+  // Use actual function selector from the contract interface
+  const TEST_SELECTOR = mockHookedTest.interface.getSighash("testERC7746CModifier");
src/mocks/MockPanicDistribution.sol (2)

8-19: Fix unused variable in deliberate panic code.

The variable c is assigned but never used. While the code is intentionally designed to trigger a panic, the unused variable may cause compiler warnings.

    function instantiate(bytes calldata) external override returns (address[] memory, bytes32, uint256) {
        // Intentionally trigger a panic (like division by zero)
        uint256 a = 1;
        uint256 b = 0;
-        uint256 c = a / b; // This will cause a panic
+        a / b; // This will cause a panic without an unused variable

        // This code is unreachable
        address[] memory instances = new address[](1);
        instances[0] = address(this);
        emit Distributed(msg.sender, instances);
        return (instances, bytes32("MockPanicDistribution"), 1);
    }

25-29: Enhance comment documentation for get() method.

The get() method returns a mock implementation but lacks explanatory comments about its purpose within the mock contract.

    function get() external view override returns (address[] memory, bytes32, uint256) {
+        // Mock implementation: Returns this contract as the only instance
+        // with a fixed identifier and version
        address[] memory sources = new address[](1);
        sources[0] = address(this);
        return (sources, bytes32("MockPanicDistribution"), 1);
    }
src/mocks/MockMigration.sol (2)

19-43: Add documentation for failure simulation modes in migrate.

The function implements various failure modes based on the first byte of userCalldata, but these behaviors are not documented with comments.

    function migrate(
        address[] memory instances,
        LibSemver.Version memory oldVersion,
        LibSemver.Version memory newVersion,
-        IRepository ,
+        IRepository /* repository */, // Unused parameter in mock
        bytes calldata distributorCalldata,
        bytes calldata userCalldata
    ) external override {
        uint256[] memory arrayToPanicOn = new uint256[](1);

-        // Emit an event with migration details
+        // Simulate different failure modes based on the first byte of userCalldata:
+        // 0xFF: Revert with a custom error message
         if(userCalldata.length > 0 && userCalldata[0] == 0xFF) revert("test revert");
+        // 0xFE: Trigger a panic by accessing an out-of-bounds array index
         if(userCalldata.length > 0 && userCalldata[0] == 0xFE) arrayToPanicOn[20] = 111;
+        // 0xFD: Revert without an error message
         if(userCalldata.length > 0 && userCalldata[0] == 0xFD) revert();
+        
+        // When no failure mode is triggered, emit an event with migration details

27-32: Consider using more structured error simulation approach.

The current implementation uses hard-coded byte values to trigger different errors. A more maintainable approach would use constants or an enum.

+    // Error simulation constants
+    bytes1 constant REVERT_WITH_MESSAGE = 0xFF;
+    bytes1 constant PANIC_OUT_OF_BOUNDS = 0xFE;
+    bytes1 constant REVERT_WITHOUT_MESSAGE = 0xFD;
+
    function migrate(
        address[] memory instances,
        LibSemver.Version memory oldVersion,
        LibSemver.Version memory newVersion,
        IRepository /* repository */,
        bytes calldata distributorCalldata,
        bytes calldata userCalldata
    ) external override {
        uint256[] memory arrayToPanicOn = new uint256[](1);

        // Simulate different failure modes based on first byte
-        if(userCalldata.length > 0 && userCalldata[0] == 0xFF) revert("test revert");
-        if(userCalldata.length > 0 && userCalldata[0] == 0xFE) arrayToPanicOn[20] = 111;
-        if(userCalldata.length > 0 && userCalldata[0] == 0xFD) revert();
+        if(userCalldata.length > 0) {
+            bytes1 mode = userCalldata[0];
+            if(mode == REVERT_WITH_MESSAGE) revert("test revert");
+            if(mode == PANIC_OUT_OF_BOUNDS) arrayToPanicOn[20] = 111;
+            if(mode == REVERT_WITHOUT_MESSAGE) revert();
+        }
docs/guides/Distributors.md (6)

11-29: Fix Markdown list indentation.

The unordered lists are using 4-space indentation while the Markdown linter expects 2-space indentation for consistency.

Key functionalities include:

-*   **Distribution Registry:** Maintains a registry of distribution components, identified by a unique `distributorId`. Each distribution component consists of:
-    *   A `distributionLocation` (the address of either an `IDistribution` contract or an `IRepository` contract)
-    *   An optional `initializer` contract address (used for specialized initialization logic during instantiation)
-    *   For versioned distributions, it tracks version requirements using `LibSemver`
+* **Distribution Registry:** Maintains a registry of distribution components, identified by a unique `distributorId`. Each distribution component consists of:
+  * A `distributionLocation` (the address of either an `IDistribution` contract or an `IRepository` contract)
+  * An optional `initializer` contract address (used for specialized initialization logic during instantiation)
+  * For versioned distributions, it tracks version requirements using `LibSemver`

-*   **App Instance Management:** Creates and tracks deployed instances of distributions:
-    *   Each time a distribution is instantiated, it's assigned a unique `appId` and the instantiated contract addresses are recorded
-    *   Maintains mappings between app components, app IDs, and distributions
-    *   Records the installed version of each app instance
+* **App Instance Management:** Creates and tracks deployed instances of distributions:
+  * Each time a distribution is instantiated, it's assigned a unique `appId` and the instantiated contract addresses are recorded
+  * Maintains mappings between app components, app IDs, and distributions
+  * Records the installed version of each app instance

-*   **Versioning and Migrations:** For versioned distributions (backed by a `Repository`):
-    *   Tracks version requirements for distributions
-    *   Manages migration plans between different versions
-    *   Provides functionality to upgrade app instances from one version to another
+* **Versioning and Migrations:** For versioned distributions (backed by a `Repository`):
+  * Tracks version requirements for distributions
+  * Manages migration plans between different versions
+  * Provides functionality to upgrade app instances from one version to another

-*   **ERC7746 Middleware Integration:**
-    *   Implements the ERC7746 middleware pattern as a security layer
-    *   Provides hooks (`beforeCall`, `afterCall`) that are triggered by proxies during upgrade attempts
-    *   Uses these hooks to enforce that only authorized parties (the distributor and the app's installer/owner) can perform upgrades
+* **ERC7746 Middleware Integration:**
+  * Implements the ERC7746 middleware pattern as a security layer
+  * Provides hooks (`beforeCall`, `afterCall`) that are triggered by proxies during upgrade attempts
+  * Uses these hooks to enforce that only authorized parties (the distributor and the app's installer/owner) can perform upgrades
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

12-12: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


17-17: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


18-18: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


27-27: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


28-28: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


29-29: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


39-44: Fix Markdown list indentation in OwnableDistributor section.

The unordered lists are using 4-space indentation while the Markdown linter expects 2-space indentation for consistency.

-*   Add new distributions (both unversioned via code hash or versioned via repository)
-*   Change version requirements for distributions
-*   Disable distributions
-*   Add or remove version migrations
-*   The `instantiate` function, however, is public, allowing anyone to create instances of registered distributions
+* Add new distributions (both unversioned via code hash or versioned via repository)
+* Change version requirements for distributions
+* Disable distributions
+* Add or remove version migrations
+* The `instantiate` function, however, is public, allowing anyone to create instances of registered distributions

49-52: Fix Markdown list indentation in TokenizedDistributor section.

The unordered lists are using 4-space indentation while the Markdown linter expects 2-space indentation for consistency.

-*   Requires payment in a specified ERC20 token to instantiate a distribution
-*   Configurable fees per distribution (or a default fee)
-*   Payments are forwarded to a designated beneficiary
-*   This model allows for monetizing access to managed distributions
+* Requires payment in a specified ERC20 token to instantiate a distribution
+* Configurable fees per distribution (or a default fee)
+* Payments are forwarded to a designated beneficiary
+* This model allows for monetizing access to managed distributions

58-61: Fix Markdown list indentation in WrappedProxyInitializer section.

The unordered lists are using 4-space indentation while the Markdown linter expects 2-space indentation for consistency.

-*   Set up the instantiation flow for `UpgradableDistribution` contracts
-*   Properly encode the installer address (the `msg.sender` that calls the distributor) and initialization data
-*   Handle error propagation during instantiation
-*   This initializer enables distributors to create proxies where the caller becomes the proxy owner/installer, while the distributor remains as the admin/middleware
+* Set up the instantiation flow for `UpgradableDistribution` contracts
+* Properly encode the installer address (the `msg.sender` that calls the distributor) and initialization data
+* Handle error propagation during instantiation
+* This initializer enables distributors to create proxies where the caller becomes the proxy owner/installer, while the distributor remains as the admin/middleware

69-73: Fix Markdown list indentation in Upgrade Flow section.

The nested unordered lists are using 4-space indentation while the Markdown linter expects 2-space indentation for consistency.

2.  When an upgrade is initiated (via `upgradeUserInstance`), the distributor:
-    *   Verifies that the app is valid and a migration plan exists for the target version
-    *   Executes the migration, which may involve calling a migration script or directly upgrading the proxy
-    *   During the upgrade, the proxy's ERC7746 middleware call is intercepted by the distributor's hooks
-    *   The hooks verify that both the distributor and the app's installer consent to the upgrade
+    * Verifies that the app is valid and a migration plan exists for the target version
+    * Executes the migration, which may involve calling a migration script or directly upgrading the proxy
+    * During the upgrade, the proxy's ERC7746 middleware call is intercepted by the distributor's hooks
+    * The hooks verify that both the distributor and the app's installer consent to the upgrade

126-133: Remove unnecessary blank lines at end of file.

There are several unnecessary blank lines at the end of the file. Typically, Markdown files should end with a single newline.

--

--

--

--

--

--

--
src/distributions/LatestVersionDistribution.sol (1)

34-38: Gas-optimise sources() by avoiding the extra memory allocation

Creating a fixed-length array twice costs extra mloads.
Store directly into the return variable:

-address[] memory _sources = new address[](1);
-Repository.Source memory latest = repository.getLatest();
-_sources[0] = latest.sourceId.getContainerOrThrow();
-return (_sources, repository.repositoryName(), LibSemver.toUint256(latest.version));
+Repository.Source memory latest = repository.getLatest();
+address[] memory srcs = new address[](1);
+srcs[0] = latest.sourceId.getContainerOrThrow();
+return (srcs, repository.repositoryName(), LibSemver.toUint256(latest.version));

Minor, but worthwhile in frequently-called view functions.

src/mocks/MockAuthMiddlewareTarget.sol (1)

39-47: Flag potential selector collision in self-referential calls

The test helpers obtain the selector via this.testDistributionOnly.selector.
Inside the call the same selector is forwarded to middleware, meaning middleware sees the target as caller & callee; some real-world middlewares use msg.sender/address(this) checks and would treat these differently.

If the goal is to emulate an external caller invoking the target, consider:

-bytes4 selector = this.testDistributionOnly.selector;
+bytes4 selector = MockAuthMiddlewareTarget.testDistributionOnly.selector;

or provide the selector explicitly from the test script to remove ambiguity.

test/eds/VersionWorkflows.ts (1)

323-328: Redundant null-checks hide real failures

appInstances is an array; if (!appInstances || appInstances.length === 0) will never hit the first branch because an empty array is truthy in JS/TS.
Drop the first condition to make the guard meaningful:

-if (!appId || !appInstances || appInstances.length === 0) {
+if (!appId || appInstances.length === 0) {
     throw new Error("Instantiation failed, failing test");
 }
src/mocks/MockOwnableDistributor.sol (1)

18-24: Unrestricted state-mutation helpers could hide access-control regressions

renounceApp and setMigration are publicly callable by any account.
While acceptable for a mock, consider restricting them to onlyOwner so tests continue to surface access-control issues instead of masking them.

src/distributors/TokenizedDistributor.sol (1)

13-29: Key name in instantiationCosts mapping is misleading and error-prone

The mapping is declared as
mapping(bytes32 codeHash => uint256) public instantiationCosts;

Yet every call site (e.g. _setInstantiationCost, _instantiate) uses distributorsId as the key.
Rename the mapping’s key to reflect its real purpose to avoid future mix-ups:

-    mapping(bytes32 codeHash => uint256) public instantiationCosts;
+    // key = distributorsId returned by _addDistribution
+    mapping(bytes32 distributorsId => uint256) public instantiationCosts;
src/mocks/MockLowLevelDistribution.sol (1)

30-32: ERC-165 implementation could be forward-compatible

At the moment the contract declares support only for IDistribution and IERC165.
If IDistribution itself extends other interfaces (e.g. IContractURI) consumers may expect nested interface IDs to be recognised as well. The usual pattern inherited from OZ is:

return
    interfaceId == type(IDistribution).interfaceId ||
    interfaceId == type(IERC165).interfaceId ||
    super.supportsInterface(interfaceId);

A future change in parent interfaces would then be picked up automatically.

src/mocks/MockInstaller.sol (1)

1-52: Remove or delete fully-commented file to avoid dead-code confusion

MockInstaller.sol now contains only commented lines; it adds no byte-code but still gets compiled, which:

  1. Generates a compilation unit with no pragma (potential compiler warning).
  2. Creates noise when browsing the codebase.
  3. Risks re-introducing outdated patterns if someone un-comments parts of it later.

Unless you are keeping the file for historical reference, please delete it, move it to legacy/, or add a clear header such as /* DEPRECATED: kept for reference only */.

test/middleware/LibMiddleware.test.ts (2)

185-186: toNumber() may truncate big values

BigNumber.toNumber() silently truncates values > 2^53-1, which makes tests pass even if the contract returned an unexpected large number.
Use BigNumber.eq/lt/gt assertions instead:

- expect(args1.value.toNumber()).to.equal(123);
+ expect(args1.value).to.equal(ethers.BigNumber.from(123));

Repeat for similar occurrences at lines 278-279 and any other toNumber() conversions.

Also applies to: 278-279


330-346: Consider adding negative-path expectations

The happy-path coverage is thorough, but the library also has several require checks (e.g. index out-of-bounds, empty layers).
Adding expectRevert cases will prevent accidental relaxation of those validations in future refactors.

src/interfaces/IRepository.sol (3)

4-7: Unused import

IMigration.sol is imported but never referenced, triggering SpdxLicenseIdentifier/no-unused-import warnings in Solidity ≥0.8.19.
Remove the import or reference IMigration where intended.


51-57: Event parameter naming & NatSpec typo

event MigrationScriptAdded(uint64 indexed version, …) emits major version only, yet the NatSpec says “version number of the added item.”
Rename the parameter for clarity and update the docblock:

-event MigrationScriptAdded(uint64 indexed version, bytes32 indexed migrationHash);
+event MigrationScriptAdded(uint64 indexed majorVersion, bytes32 indexed migrationHash);

92-106: Public functions lack NatSpec & access control expectations

changeMigrationScript, getMigrationScript, and resolveVersion are publicly exposed but have no NatSpec comments indicating:

• Who is authorised to call them (owner? repository admin?).
• What happens if the script does not exist.

Adding explicit documentation will greatly aid integrators and auditors.

test/eds/UpgradabilityWorkflows.ts (4)

172-176: Remove or use the installTx variable to silence ESLint warnings

installTx is awaited but never referenced afterwards, triggering no-unused-vars in CI.
Either assert on the receipt (preferred) or drop the assignment.

-      const installTx = await installer
-        .connect(user)
-        .install(distributor.address, distributionId, "0x");
+      const installReceipt = await (
+        await installer.connect(user).install(distributor.address, distributionId, "0x")
+      ).wait();
+      expect(installReceipt.status).to.equal(1);
🧰 Tools
🪛 GitHub Actions: CI

[warning] 172-172: ESLint: 'installTx' is assigned a value but never used. (no-unused-vars)


218-224: appComponents is fetched but never inspected – expand the assertion

The variable is created only to satisfy a later call and is otherwise unused, producing another linter warning.
Confirm the content or remove the variable.

-      const appComponents = await installer.getApp(1);
-
-      // Change the distributor
+      const appComponents = await installer.getApp(1);
+      expect(appComponents.distributor).to.equal(distributor.address);
+
+      // Change the distributor
🧰 Tools
🪛 GitHub Actions: CI

[warning] 219-219: ESLint: 'appComponents' is assigned a value but never used. (no-unused-vars)


235-238: Prefix unused catch variable with _ to comply with eslint-config-standard

The caught error isn’t referenced; prefixing silences no-unused-vars.

-      } catch (e) {
+      } catch (_e) { // version might already exist – ignore
🧰 Tools
🪛 GitHub Actions: CI

[warning] 236-236: ESLint: 'e' is defined but never used. (no-unused-vars)


259-262: Use const appId = 1 consistently instead of a second appComponents fetch

You already store appId; retrieving appComponents again adds gas/time and eslint noise.

-      const appComponents = await installer.getApp(1);
-      const appId = 1;
+      const appId = 1;
🧰 Tools
🪛 GitHub Actions: CI

[warning] 260-260: ESLint: 'appComponents' is assigned a value but never used. (no-unused-vars)

src/mocks/MockERC7746Hooked.sol (2)

10-24: Consider marking testModifierWithParams as payable for parity

testDefaultModifier is payable, while the parametrised variant is not.
If the underlying middleware relies on msg.value, calls with non-zero value will revert unexpectedly.

-    function testModifierWithParams(bytes4 _selector, address _sender, bytes calldata _data, uint256 _value, string calldata newValue)
-        external
+    function testModifierWithParams(
+        bytes4 _selector,
+        address _sender,
+        bytes calldata _data,
+        uint256 _value,
+        string calldata newValue
+    ) external payable

37-45: Gas micro-optimisation: accept string calldata in setValueWithoutMiddleware

Keeping the argument in calldata avoids a memory copy.

-    function setValueWithoutMiddleware(string calldata newValue) external returns (bool) {
+    function setValueWithoutMiddleware(string calldata newValue) external returns (bool) {
src/mocks/MockInitializer.sol (2)

7-8: Remove hardhat/console.sol import to avoid bytecode bloat

console.sol is not used; its presence adds unnecessary code size and intrinsics.

-import "hardhat/console.sol";

35-43: Second initialize overload should returns (address[] memory) for interface symmetry

While this signature is never used in production, keeping identical return types eases tooling & static-analyser assumptions.

-    function initialize(bytes32, address[] memory, bytes32, uint256, bytes calldata args) external pure {
+    function initialize(
+        bytes32,
+        address[] memory,
+        bytes32,
+        uint256,
+        bytes calldata args
+    ) external pure returns (address[] memory) {
+        address[] memory empty;
docs/guides/Upgradability.md (3)

35-37: Remove trailing punctuation in heading
The colon at the end of the heading violates MD026 and is unnecessary.

-### How the Middleware Works:
+### How the Middleware Works
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

35-35: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


49-52: Specify a language for the fenced code block
markdownlint (MD040) flags this block because no language is supplied.
If this is a shell‐style sequence, annotate accordingly:

-```
+```text
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

49-49: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


51-51: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)


123-126: Same MD026 issue for “Strategies:” heading

-### Strategies:
+### Strategies
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

123-123: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

src/mocks/MockERC7746HookedTest.sol (2)

20-34: Gas-inefficient layer reset
Iterating and pop-ping costs O(n²) (array length shrinks every pop).
A simple while loop avoids the extra counter & re-reads length each time:

-uint256 length = layers.length;
-for (uint256 i = 0; i < length; i++) {
-    LibMiddleware.popLayer();
-}
+while (layers.length != 0) {
+    LibMiddleware.popLayer();
+}

63-70: Modifier naming deviation
ERC7746C_Tracked shadows the original semantics but adds events.
Consider naming it ERC7746C_Test (or similar) to avoid confusion with the
production modifier list.

test/eds/Installer.ts (1)

118-126: Assertion is too weak
to.be.not.revertedWithCustomError will still pass if the call reverts with
another error. Prefer to.not.be.reverted to guarantee the call succeeds.

-).to.be.not.revertedWithCustomError(installer, "NotAnApp");
+).to.not.be.reverted;
src/distributions/UpgradableDistribution.sol (2)

40-43: contractURI() TODO leaks to production
A TODO comment signals unfinished IPFS integration. Either implement or track it
in an issue before merging.


64-71: ERC-165 check omits parent interface lookup
Returning interfaceId == IERC165.interfaceId is redundant because the default
ERC-165 implementation already handles it. Slight gas win:

-return interfaceId == type(IDistribution).interfaceId || interfaceId == type(IERC165).interfaceId;
+return interfaceId == type(IDistribution).interfaceId || super.supportsInterface(interfaceId);
src/repositories/Repository.sol (1)

229-234: _changeMigrationScript should validate the new script’s interface

You correctly validate the address is non-zero, but you skip the ERC-165 check used in _newRelease.
A rogue address could be stored, breaking migrations later.

 function _changeMigrationScript(uint64 major, bytes32 migrationHash) internal {
-    require(migrationHash.getContainerOrThrow() != address(0), "Invalid migration");
+    address migration = migrationHash.getContainerOrThrow();
+    require(
+        ERC165Checker.supportsInterface(migration, type(IMigration).interfaceId),
+        "Invalid migration"
+    );
     require(major <= majorReleases, "Major version does not exist");
     releaseMigrationHash[major] = migrationHash;
     emit MigrationScriptAdded(major, migrationHash);
 }
src/mocks/MockMiddleware.sol (1)

24-30: Unused callCounter wastes storage/gas

callCounter is reset but never read. Remove it or use it to track per-instance calls to avoid unnecessary state.

src/mocks/LibMiddlewareExtendedTest.sol (1)

11-17: Silence unused‐variable warning in testAccessLayersStorage

layers is declared but unused, which triggers a compiler warning.
Cheap fix: mark the variable as unused.

-LibMiddleware.LayerStruct[] storage layers = LibMiddleware.accessLayersStorage();
+LibMiddleware.accessLayersStorage(); // access only
README.md (1)

7-8: Fix minor grammatical issues in the warning message.

The warning message has two minor grammatical issues:

-> [!WARNING]
-> This repository is still in development. Please treat it as unstable with possible API changes. If you want to use it in 
+> This repository is still in development. Please treat it as unstable with possible API changes. If you want to use it in a
production environment please reach back to Peeramid Labs so we can prioritize accordingly.
+production environment, please reach back to Peeramid Labs so we can prioritize accordingly.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~8-~8: You might be missing the article “a” here.
Context: ...e API changes. If you want to use it in production environment please reach back to Peeram...

(AI_EN_LECTOR_MISSING_DETERMINER_A)


[typographical] ~8-~8: It seems that a comma is missing.
Context: ...want to use it in production environment please reach back to Peeramid Labs so we can p...

(IF_PLEASE_COMMA)

src/mocks/LibMiddlewareTest.sol (1)

34-53: Handle the edge case of empty middleware layers.

The executeBeforeCall function initializes lastResult but doesn't set a meaningful value if there are no middleware layers. Consider handling this edge case explicitly.

function executeBeforeCall(
    bytes[] memory middlewareLayers,
    bytes4 selector,
    address sender,
    uint256 value,
    bytes memory data
) public returns (bytes memory) {
    bytes memory lastResult;
+   // If no middleware layers, return empty result
+   if (middlewareLayers.length == 0) {
+       return lastResult;
+   }
    for (uint256 i = 0; i < middlewareLayers.length; i++) {
        MiddlewareConfig memory config = getConfig(middlewareLayers[i]);
        lastResult = IERC7746(config.middleware).beforeCall(
            config.configData,
            selector,
            sender,
            value,
            data
        );
    }
    return lastResult;
}
src/mocks/MockTokenizedDistributor.sol (3)

24-26: Consider adding override for clearer intent
TokenizedDistributor does not expose an external addDistribution(bytes32, address, string) so compilation is currently fine, but if the base contract later adds a matching external signature this function would silently stop compiling. Pro-actively marking it as

function addDistribution(bytes32 id, address initializer, string memory readableName)
    external
    override
    onlyRole(DEFAULT_ADMIN_ROLE)

makes the intention explicit and future-proofs the contract.


35-37: external is cheaper than public for write-only admin functions
setInstantiationCost is never called internally, so changing the visibility to external will shave a few gas units:

-function setInstantiationCost(bytes32 id, uint256 cost) public onlyRole(DEFAULT_ADMIN_ROLE) {
+function setInstantiationCost(bytes32 id, uint256 cost) external onlyRole(DEFAULT_ADMIN_ROLE) {

85-93: Good overload, but watch for shadowing confusion
Having two addDistribution overloads can cause readability issues. Consider renaming one (e.g. addRepositoryDistribution) to signal the path via repository.

src/middleware/InstallerOwnable.sol (1)

1-2: Pinning the compiler to =0.8.28 may hinder patch-level upgrades
Using the exact equality operator prevents compilation with security-patched releases (e.g. 0.8.29). Prefer ^0.8.20 or >=0.8.20 <0.9.0 unless you have a strict audit requirement.

src/distributors/OwnableDistributor.sol (2)

5-8: Duplicate IMigration import
import "../interfaces/IMigration.sol"; appears twice. Removing the second saves a few bytes of byte-code metadata and avoids linter warnings:

-import "../interfaces/IMigration.sol";

19-21: Overload without override – future-proofing
Same comment as in the mock: add override if the base later exposes this signature.

src/interfaces/IInstaller.sol (1)

37-38: Inconsistent error-name casing (alreadyAllowed).

All other custom errors follow UpperCamelCase (NotAnApp, InvalidDistributor, …).
Consider renaming to keep the public ABI consistent and self-documenting.

-error alreadyAllowed(IDistributor distributor);
+error AlreadyAllowed(IDistributor distributor);
src/middleware/InstallerClonable.sol (2)

42-45: Duplicate using directives – noise & potential linter warnings.

EnumerableSet is already imported for both AddressSet and Bytes32Set at lines 17-19.
The repeated statements add no functionality and may trigger style checks.

-    using EnumerableSet for EnumerableSet.AddressSet;
-    using LibSemver for LibSemver.Version;
-    using EnumerableSet for EnumerableSet.Bytes32Set;
+    using LibSemver for LibSemver.Version;

126-132: Redundant permission check duplicates enforceActiveDistribution.

_installPublic first re-implements a subset of the allowlist logic then calls enforceActiveDistribution, which performs (almost) the same work. Removing the first if simplifies reasoning and reduces gas.

src/mocks/MockDistributorForTest.sol (1)

85-90: addDistribution declared twice – keep only the required overloads.

The first overload (bytes32,address,string) is no longer part of the updated IDistributor interface, so it increases byte-code size and may confuse readers.

-function addDistribution(bytes32, address, string memory) external pure override {
-    // No-op for testing
-}
src/distributors/Distributor.sol (1)

193-206: delegatecall error bubbling loses revert reason

delegatecall returns raw data; casting it to string (revert(string(result)))
will truncate non-string reverts and panic codes.

Prefer decoding like:

if (!success) {
    if (result.length < 68) revert DistributionInstantiationFailed(result);
    assembly {
        result := add(result, 0x04)
    }
    revert(abi.decode(result,(string)));
}

so both string and low-level errors are preserved.

test/eds/Distributor.ts (2)

35-50: createVersionRequirement duplicated – move to helper util

The same helper is re-declared in multiple describe blocks.
Export a single utility from test/utils/semver.ts and import it instead.
DRYing this removes repetition and keeps tests in sync.


420-460: ESLint warns about unused variables – tighten assertions

repositoryCodeHash, fullDistributionId, mockRepository, etc. are created
but never used, polluting the test output and failing CI.

Either remove them or assert against them.
CI is currently yellow-flagging these (no-unused-vars).

src/mocks/MockAuthMiddlewareDistributor.sol (1)

55-61: Payable instantiate returns empty arrays – clarify in docstring

instantiate is marked payable but silently ignores msg.value.
Add a comment or event to indicate that ETH is intentionally discarded,
so future readers don’t mistake this for a bug.

src/interfaces/IDistributor.sol (5)

30-34: Unused DistributorLayerConfig?

The interface defines DistributorLayerConfig, but no function exposes or consumes it. Dead declarations bloat byte-code (even in interfaces) and confuse integrators.

If it is reserved for future work, add a short comment (// @custom:experimental) explaining the intent; otherwise remove it.


56-61: Error & event naming style is inconsistent

Most custom errors follow UpperCamelCase, but new ones (upgradeFailedWithPanic, upgradeFailedWithRevert, etc.) are lower-camel. Mix-and-match styles hinder grep-ability and tooling patterns (e.g., linters).

Unify to UpgradeFailedWithPanic, UpgradeFailedWithRevert, … or vice-versa.


109-115: string indexed readableNameHashZ is redundant

string can be indexed, but Solidity stores the keccak256 hash of the value, giving you the same data you already expose through readableName.
Instead of keeping two separate arguments, consider:

-        string indexed readableNameHashZ,
-        address distribution,
+        address distribution,
         address indexed initializer,
-        string readableName
+        string readableName      // off-chain clients can keccak themselves

This saves one log topic per emission.


239-245: Prefer uint256 over uint in custom errors

uint is an alias for uint256, but using the explicit size improves clarity and prevents accidental width reductions if Solidity ever changes the default.

-error upgradeFailedWithPanic(uint errorCode);
+error UpgradeFailedWithPanic(uint256 errorCode);

Apply the same style to other errors.


246-253: calculateDistributorId should be pure, but also view?

Marking it pure is correct; however, many callers will also need a variant that validates against existing state (e.g., ensuring no collision).
If you anticipate that need, consider adding a view helper that wraps the pure hash check.

Just a design note; no change strictly required.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b13aa90 and e56ddf1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (90)
  • .changeset/poor-bees-fix.md (1 hunks)
  • .changeset/witty-icons-drive.md (1 hunks)
  • .cursorignore (1 hunks)
  • .solcover.js (1 hunks)
  • .vscode/settings.json (1 hunks)
  • README.md (1 hunks)
  • deploy/ERC7744.js (1 hunks)
  • docs/contracts/index.md (2 hunks)
  • docs/guides/Distributions.md (1 hunks)
  • docs/guides/Distributors.md (1 hunks)
  • docs/guides/Indexer.md (1 hunks)
  • docs/guides/Repositories.md (1 hunks)
  • docs/guides/Upgradability.md (1 hunks)
  • docs/guides/Versioning.md (1 hunks)
  • hardhat.config.ts (4 hunks)
  • package.json (1 hunks)
  • src/abstracts/CodeIndexer.sol (0 hunks)
  • src/abstracts/Distributor.sol (0 hunks)
  • src/abstracts/Installer.sol (0 hunks)
  • src/abstracts/InstallerCloneable.sol (0 hunks)
  • src/distributions/CloneDistribution.sol (2 hunks)
  • src/distributions/CodeHashDistribution.sol (3 hunks)
  • src/distributions/LatestVersionDistribution.sol (1 hunks)
  • src/distributions/UpgradableDistribution.sol (1 hunks)
  • src/distributors/Distributor.sol (1 hunks)
  • src/distributors/OwnableDistributor.sol (2 hunks)
  • src/distributors/TokenizedDistributor.sol (2 hunks)
  • src/distributors/WrappedProxyInitializer.sol (1 hunks)
  • src/erc7744/ERC7744.sol (1 hunks)
  • src/erc7744/LibERC7744.sol (1 hunks)
  • src/interfaces/IAdminGetter.sol (1 hunks)
  • src/interfaces/IDistribution.sol (1 hunks)
  • src/interfaces/IDistributor.sol (7 hunks)
  • src/interfaces/IInitializer.sol (1 hunks)
  • src/interfaces/IInstaller.sol (3 hunks)
  • src/interfaces/IMigration.sol (1 hunks)
  • src/interfaces/IRepository.sol (4 hunks)
  • src/libraries/LibInstaller.sol (0 hunks)
  • src/middleware/AuthorizationMiddleware.sol (3 hunks)
  • src/middleware/ERC7746Hooked.sol (1 hunks)
  • src/middleware/InstallerClonable.sol (1 hunks)
  • src/middleware/InstallerOwnable.sol (1 hunks)
  • src/middleware/LibMiddleware.sol (6 hunks)
  • src/middleware/MiddlewareProxy.sol (1 hunks)
  • src/mocks/LibMiddlewareExtendedTest.sol (1 hunks)
  • src/mocks/LibMiddlewareTest.sol (1 hunks)
  • src/mocks/LibSemverTest.sol (1 hunks)
  • src/mocks/MockAuthMiddlewareDistributor.sol (1 hunks)
  • src/mocks/MockAuthMiddlewareTarget.sol (1 hunks)
  • src/mocks/MockCloneDistribution.sol (2 hunks)
  • src/mocks/MockDistributorForTest.sol (1 hunks)
  • src/mocks/MockERC165.sol (1 hunks)
  • src/mocks/MockERC20.sol (1 hunks)
  • src/mocks/MockERC7746Hooked.sol (1 hunks)
  • src/mocks/MockERC7746HookedTest.sol (1 hunks)
  • src/mocks/MockFailingDistribution.sol (1 hunks)
  • src/mocks/MockFailingMigration.sol (1 hunks)
  • src/mocks/MockInitializer.sol (1 hunks)
  • src/mocks/MockInstaller.sol (1 hunks)
  • src/mocks/MockLatestVersionDistribution.sol (1 hunks)
  • src/mocks/MockLowLevelDistribution.sol (1 hunks)
  • src/mocks/MockMiddleware.sol (1 hunks)
  • src/mocks/MockMiddlewareTarget.sol (1 hunks)
  • src/mocks/MockMigration.sol (1 hunks)
  • src/mocks/MockNoReasonInitializer.sol (1 hunks)
  • src/mocks/MockOwnableDistributor.sol (1 hunks)
  • src/mocks/MockPanicDistribution.sol (1 hunks)
  • src/mocks/MockRepository.sol (1 hunks)
  • src/mocks/MockTokenizedDistributor.sol (5 hunks)
  • src/mocks/MockTransferOwnershipProxy.sol (1 hunks)
  • src/mocks/TestFacet.sol (1 hunks)
  • src/proxies/WrappedTransparentUpgradeableProxy.sol (1 hunks)
  • src/repositories/OwnableRepository.sol (1 hunks)
  • src/repositories/Repository.sol (6 hunks)
  • test/eds/CloneDistribution.ts (2 hunks)
  • test/eds/Distributor.ts (6 hunks)
  • test/eds/Installer.ts (6 hunks)
  • test/eds/LatestVersionDistribution.ts (1 hunks)
  • test/eds/Repository.ts (5 hunks)
  • test/eds/RepositoryExtended.ts (1 hunks)
  • test/eds/TokenizedDistributor.ts (6 hunks)
  • test/eds/UpgradabilityWorkflows.ts (1 hunks)
  • test/eds/VersionDistributor.ts (8 hunks)
  • test/eds/VersionWorkflows.ts (1 hunks)
  • test/middleware/AuthorizationMiddleware.test.ts (1 hunks)
  • test/middleware/ERC7746Hooked.test.ts (1 hunks)
  • test/middleware/InstallerClonable.test.ts (1 hunks)
  • test/middleware/InstallerClonableUpgrade.test.ts (1 hunks)
  • test/middleware/LibMiddleware.test.ts (1 hunks)
  • test/versioning/LibSemver.test.ts (1 hunks)
💤 Files with no reviewable changes (5)
  • src/abstracts/CodeIndexer.sol
  • src/libraries/LibInstaller.sol
  • src/abstracts/Distributor.sol
  • src/abstracts/Installer.sol
  • src/abstracts/InstallerCloneable.sol
🧰 Additional context used
🧬 Code Graph Analysis (1)
hardhat.config.ts (1)
deploy/ERC7744.js (2)
  • result (11-15)
  • hre (4-4)
🪛 markdownlint-cli2 (0.17.2)
docs/guides/Repositories.md

12-12: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


15-15: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/guides/Upgradability.md

35-35: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


49-49: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


51-51: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)


123-123: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

docs/guides/Distributors.md

12-12: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


17-17: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


18-18: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


27-27: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


28-28: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


29-29: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🪛 GitHub Actions: CI
test/eds/LatestVersionDistribution.ts

[warning] 27-27: ESLint: 'repoName' is assigned a value but never used. (no-unused-vars)

test/eds/RepositoryExtended.ts

[warning] 13-13: ESLint: 'utils' is defined but never used. (no-unused-vars)


[warning] 24-24: ESLint: 'distributionId' is defined but never used. (no-unused-vars)


[warning] 54-54: ESLint: 'e' is defined but never used. (no-unused-vars)


[warning] 70-70: ESLint: 'e' is defined but never used. (no-unused-vars)

test/middleware/AuthorizationMiddleware.test.ts

[warning] 3-3: ESLint: 'ContractFactory' is defined but never used. (no-unused-vars)

test/eds/Repository.ts

[warning] 17-17: ESLint: 'constants' is defined but never used. (no-unused-vars)

hardhat.config.ts

[warning] 77-77: ESLint: 'hre' is defined but never used. (no-unused-vars)

test/eds/UpgradabilityWorkflows.ts

[warning] 172-172: ESLint: 'installTx' is assigned a value but never used. (no-unused-vars)


[warning] 219-219: ESLint: 'appComponents' is assigned a value but never used. (no-unused-vars)


[warning] 236-236: ESLint: 'e' is defined but never used. (no-unused-vars)


[warning] 260-260: ESLint: 'appComponents' is assigned a value but never used. (no-unused-vars)

src/mocks/MockTokenizedDistributor.sol

[error] 1545-1545: Parsing error: ',' expected

test/eds/Distributor.ts

[warning] 106-106: ESLint: 'repositoryCodeHash' is assigned a value but never used. (no-unused-vars)


[warning] 493-493: ESLint: 'fullDistributionId' is assigned a value but never used. (no-unused-vars)


[warning] 696-696: ESLint: 'mockRepository' is assigned a value but never used. (no-unused-vars)


[warning] 888-888: ESLint: 'receipt' is assigned a value but never used. (no-unused-vars)


[warning] 1434-1434: ESLint: 'changeReceipt' is assigned a value but never used. (no-unused-vars)


[warning] 1581-1581: ESLint: 'mockRepository' is assigned a value but never used. (no-unused-vars)


[warning] 1646-1646: ESLint: 'mockRepository' is assigned a value but never used. (no-unused-vars)


[warning] 1731-1731: ESLint: 'impersonated' is assigned a value but never used. (no-unused-vars)


[warning] 1834-1834: ESLint: 'mockRepository' is assigned a value but never used. (no-unused-vars)


[warning] 1969-1969: ESLint: 'cloneDistribution2Id' is assigned a value but never used. (no-unused-vars)

🪛 LanguageTool
docs/guides/Versioning.md

[uncategorized] ~3-~3: You might be missing the article “a” here.
Context: # Versioning EDS implements Semantic versioning system within one bytes32 wo...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

docs/guides/Indexer.md

[uncategorized] ~18-~18: You might be missing the article “the” here.
Context: ...s a library that helps to interact with ERC7744 indexer. Typical usage is as follows: ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)


[style] ~58-~58: Using many exclamation marks might seem excessive (in this case: 4 exclamation marks for a text that’s 1097 characters long)
Context: ...er.get(codeHash); ``` ## Using CLI > [!NOTE] > The CLI provides utilities to in...

(EN_EXCESSIVE_EXCLAMATION)

README.md

[uncategorized] ~8-~8: You might be missing the article “a” here.
Context: ...e API changes. If you want to use it in production environment please reach back to Peeram...

(AI_EN_LECTOR_MISSING_DETERMINER_A)


[typographical] ~8-~8: It seems that a comma is missing.
Context: ...want to use it in production environment please reach back to Peeramid Labs so we can p...

(IF_PLEASE_COMMA)


[uncategorized] ~58-~58: Loose punctuation mark.
Context: ...rce contract. * CodeHashDistribution: Instantiates code based on an ERC7744...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~59-~59: Loose punctuation mark.
Context: ...de hash. * LatestVersionDistribution: Instantiates the latest version from a ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~60-~60: Loose punctuation mark.
Context: ...epository. * UpgradableDistribution: Deploys WrappedTransparentUpgradeableP...

(UNLIKELY_OPENING_PUNCTUATION)


[style] ~98-~98: Consider an alternative adjective to strengthen your wording.
Context: ... them). ## Documentation Guides For a deeper understanding of each component and con...

(DEEP_PROFOUND)

🪛 GitHub Check: lint
hardhat.config.ts

[warning] 77-77:
'hre' is defined but never used

test/eds/Distributor.ts

[warning] 106-106:
'repositoryCodeHash' is assigned a value but never used


[warning] 1834-1834:
'mockRepository' is assigned a value but never used


[warning] 1731-1731:
'impersonated' is assigned a value but never used


[warning] 1646-1646:
'mockRepository' is assigned a value but never used


[warning] 1581-1581:
'mockRepository' is assigned a value but never used


[warning] 1434-1434:
'changeReceipt' is assigned a value but never used


[warning] 888-888:
'receipt' is assigned a value but never used


[warning] 696-696:
'mockRepository' is assigned a value but never used


[warning] 493-493:
'fullDistributionId' is assigned a value but never used

🔇 Additional comments (104)
.cursorignore (2)

1-1: Securely ignore local environment files
Adding .env to the ignore list prevents accidental indexing or exposure of sensitive environment variables.


2-2: Protect secrets directory from indexing
Ignoring .secrets/ ensures that any stored credentials or secret material remains out of tooling operations.

.changeset/witty-icons-drive.md (1)

1-6: Version bumping is appropriate for this release.

This changeset correctly sets up a major version update for the "@peeramid-labs/eds" package, which is aligned with the significant architectural changes being made across the codebase as mentioned in the PR summary.

src/middleware/LibMiddleware.sol (6)

9-9: Fixed spelling error: "layerAddress" instead of "layerAddess".

This spelling correction properly fixes the field name in the LayerStruct definition, matching other references to this field throughout the codebase.


22-22: Consistent spelling fix for layerAddress.

This change consistently applies the spelling correction in the field reference within the setLayer function.


31-42: New function to efficiently update middleware layers.

The changeLayers function is a useful addition that efficiently updates the stored array of middleware layers to match a provided memory array. It handles three operations:

  1. Expands the array if needed (lines 33-35)
  2. Updates existing elements (lines 36-38)
  3. Trims excess elements (lines 39-41)

This provides a more gas-efficient way to update multiple layers at once compared to the existing setLayers function.


53-53: Spelling fix applied to struct initialization.

Correctly updates the field name in the struct initialization to use the fixed spelling.


89-89: Spelling fix in IERC7746 function call.

Consistently applies the spelling correction in the function that calls the middleware layer.


137-137: Final spelling fix in afterCall function.

Completes the consistent spelling correction throughout the file.

package.json (1)

67-67: Updated solidity-coverage to latest version.

Updating from ^0.8.5 to ^0.8.14 aligns with the addition of the .solcover.js configuration file and should provide improved coverage reporting capabilities.

.solcover.js (1)

1-3: Appropriate coverage configuration.

This configuration sensibly excludes mock contracts and ERC7744 implementation from coverage reporting, which helps focus the coverage reports on the core functionality. This is a good practice as mock files are typically used only for testing and don't require the same level of coverage scrutiny.

src/mocks/TestFacet.sol (1)

6-10: Initialization functionality added correctly

The addition of an Initialized event and initialize() function aligns well with the upgradeable contracts pattern being implemented across the codebase. This simple implementation properly emits the event when the function is called, providing a way to track initialization in tests.

src/interfaces/IAdminGetter.sol (1)

1-6: Interface design looks good

This new interface provides a clean way to standardize proxy admin retrieval. The function name getWrappedProxyAdmin() clearly describes its purpose, and the interface follows good naming conventions with the "I" prefix.

src/mocks/MockERC20.sol (1)

10-18: Mock instantiate implementation is sufficient for testing

The added instantiate function properly implements a minimal mock for testing distribution instantiation. The comments clearly explain its purpose, and the implementation correctly returns this contract's address as the instance.

src/middleware/ERC7746Hooked.sol (2)

4-4: Import path updated correctly

The import path for LibMiddleware.sol has been updated to reflect the new file organization structure.


7-14: Contract renamed appropriately

The rename from ERC7746Middleware to ERC7746Hooked better reflects the contract's role in providing hook functionality for middleware. This naming is more precise and aligns well with its purpose in the architecture.

.changeset/poor-bees-fix.md (1)

1-6: Appropriate versioning for breaking changes.

The major version bump correctly reflects that distributor instantiate methods becoming payable is a breaking change to the contract interface. This aligns with semantic versioning principles.

test/eds/CloneDistribution.ts (1)

9-9: Constructor now requires name parameter.

The MockCloneDistribution deployment now requires a name parameter, which aligns with the updated contract implementation using named distributions.

src/interfaces/IDistribution.sol (3)

4-4: Added ERC165 support for interface detection.

Adding IERC165 import enables standard interface detection capabilities.


8-8: Fixed documentation typo.

Corrected "respositories" to "repositories" in the documentation comment.


11-11: Enhanced interface with ERC165 support.

Inheriting from IERC165 improves contract interoperability by allowing other contracts to detect and verify the interface implementation using standard ERC165 mechanisms.

deploy/ERC7744.js (2)

8-9: Improved deterministic deployment with fixed salt.

Using a predefined salt value that starts with a recognizable pattern ("c0de1d") improves deployment consistency across environments while maintaining readability.


17-17: Reduced console output during local development.

Conditionally displaying deployment information only on non-local networks reduces noise during development and testing while preserving useful logs in production environments.

Also applies to: 20-20

docs/guides/Versioning.md (1)

34-55: The CLI helpers section is well-documented

The CLI helpers section is comprehensive and clearly documents all available commands with examples. It also includes common options applicable to all commands.

src/erc7744/ERC7744.sol (3)

5-8: Documentation update accurately reflects the contract's purpose

The updated description better reflects the contract's role as a "global immutable library for functional code" rather than just an indexer contract.


23-27: Improved validation check in isValidContainer function

The updated condition appropriately adds a check to exclude EIP7702 delegated EOAs from being considered valid containers. This aligns with the contract's purpose and prevents potential issues with inappropriate container registration.


29-34: Updated documentation accurately reflects function behavior

The updated comment in line 33 correctly clarifies that the register function will revert if the contract is already indexed or if it returns an EIP7702 delegated EOA.

src/mocks/MockFailingMigration.sol (1)

1-34: Well-implemented mock for testing migration failures

This mock contract is well-designed for testing error handling in the migration process. It correctly:

  1. Implements the IMigration interface
  2. Deliberately fails during migration with a clear error message
  3. Properly implements supportsInterface for ERC165 support
  4. Includes comprehensive documentation

The implementation is clean and serves its purpose as a testing utility perfectly.

src/mocks/MockTransferOwnershipProxy.sol (1)

1-22: Well-structured mock for testing ownership transfer

This mock contract is well-designed for testing ownership transfer scenarios related to onDistributorChanged. It:

  1. Correctly inherits from OpenZeppelin's Ownable and implements IAdminGetter
  2. Uses an appropriate constructor to initialize ownership
  3. Correctly implements getWrappedProxyAdmin() to return its own address
  4. Includes clear documentation explaining its dual role as both a proxy and a ProxyAdmin

The implementation is clean and appropriately serves its testing purpose.

src/mocks/MockCloneDistribution.sol (4)

3-3: Good choice using OpenZeppelin's ShortStrings library

This is a gas-efficient way to store short strings (up to 31 bytes) in a single storage slot, reducing storage costs compared to regular strings.


8-9: Improved distribution metadata handling

Good design using immutable for the name (set only once) and a constant for the version. This approach is gas-efficient and properly aligns with the broader versioning enhancements in the system.


11-13: LGTM: Clean constructor implementation

Constructor cleanly handles the conversion from string to ShortString format.


26-26: LGTM: Properly updated sources() function

The function correctly returns the unwrapped distributionName and the distributionVersion, maintaining consistency with the rest of the system's versioning approach.

src/interfaces/IInitializer.sol (3)

16-17: Improved parameter naming and documentation

The change from distributionId to distribution address and the clearer parameter naming improves the interface design.


19-20: Clearer argument naming with 'userArgs'

Renaming 'args' to 'userArgs' makes the purpose of this parameter more explicit, improving code readability.


22-26: Enhanced function signature with useful return value

The function now returns deployed instances (address[] memory), which is more useful than the previous void return. This change properly aligns with the broader architectural enhancements for app lifecycle management.

src/mocks/MockERC165.sol (4)

10-15: Well-structured ERC165 mock implementation

Good implementation of a flexible ERC165 mock contract. The constant definition of the ERC165 interface ID and proper mapping setup follow best practices.


16-19: Correct ERC165 default support in constructor

The constructor properly sets the ERC165 interface ID as always supported, which is required by the ERC165 standard.


26-29: Good validation in setSupportedInterface

The function correctly prevents modification of ERC165 interface support, which must always remain true per the standard.


34-36: Properly implemented supportsInterface override

The override correctly combines the custom mapping with the parent implementation, ensuring both custom interfaces and those supported by the parent contract are properly reported.

test/eds/TokenizedDistributor.ts (5)

21-21: Variable name change from distributionId to distributedCodeHash reflects architectural changes.

The change from using distribution IDs to code hashes for identification aligns with the updated contract architecture, which now uses code hashes for better uniqueness and security.


37-41: Contract deployment and identification updates.

Good changes to:

  1. Update the MockCloneDistribution constructor with a name parameter
  2. Use code hash instead of ID for distribution identification

These changes align with the updated contract interfaces and provide better traceability.


55-58: Updated distributor ID calculation to use code hash and zero address.

This change properly reflects the updated method signature in the distributor contract which now uses code hash and middleware address (or zero address when no middleware is used).


71-76: Updated addDistribution call with new parameters.

The function call has been properly updated to match the new signature which includes:

  1. Code hash instead of ID
  2. Zero address for middleware (when not using middleware)
  3. A human-readable name for the distribution

This improves the contract's usability and aligns with the modular architecture.


87-91: Consistent updates to addDistribution calls.

Both instances of addDistribution have been updated with the same parameter pattern. The consistency across multiple test cases is good practice.

Also applies to: 98-102

src/repositories/OwnableRepository.sol (3)

4-8: Updated imports to support migration functionality.

Good changes:

  1. Updated import path for Repository
  2. Added IMigration and ShortStrings imports

These imports support the new migration functionality and metadata handling.


15-17: Added migration hash parameter to newRelease.

The function signature now includes a migrationHash parameter, which allows associating migration scripts with releases. This is an important addition for supporting upgradeability.


19-21: New changeMigrationScript function enables updating migration scripts.

This function allows repository owners to update migration scripts for specific major versions, which is essential for managing the upgrade lifecycle.

Consider adding documentation comments to explain when and why this function would be used, such as:

+/**
+ * @notice Change the migration script for a specific major version
+ * @param major The major version to update the migration script for
+ * @param migrationHash The new migration script hash
+ */
function changeMigrationScript(uint64 major, bytes32 migrationHash) public onlyOwner {
    super._changeMigrationScript(major, migrationHash);
}
src/mocks/MockNoReasonInitializer.sol (1)

1-31: Well-designed mock for testing silent revert scenarios.

This mock contract effectively simulates initializers that revert without providing a reason, which is important for testing error handling in the distributor contract.

Key strengths:

  1. Implements two function signatures to match different possible calls from the Distributor
  2. Uses assembly to revert without a reason string
  3. Includes helpful comments explaining the purpose

This will help ensure the system gracefully handles initialization failures without reason strings.

src/proxies/WrappedTransparentUpgradeableProxy.sol (1)

31-42: Verify that super._fallback() still executes ERC-7746 hooks

Because super resolves to the next contract in the linearisation after this one (i.e. TransparentUpgradeableProxy), the hook provided by ERC7746Hooked might never be invoked.

Please confirm the intended call chain or explicitly invoke the hook, e.g.:

ERC7746Hooked._fallback();

Let me know if you’d like a concrete patch after checking the inheritance graph.

test/middleware/InstallerClonable.test.ts (1)

47-50: Uninitialised installer – confirm whether initialize is required

OwnableInstaller appears to mimic OpenZeppelin clone patterns, normally requiring an explicit initialize() call.
The commented line suggests this step was skipped. If the contract relies on an initialised state (e.g. distributor address), tests may yield false positives.

Please verify the constructor vs. initializer logic and uncomment if necessary.

src/middleware/AuthorizationMiddleware.sol (4)

9-9: Renamed contract from SimpleAccessManager to AuthorizationMiddleware.

The contract name has been updated to better reflect its purpose as a middleware component in the authorization flow, which is a good practice for code clarity.


21-21: Storage position constant should be updated to match new contract name.

The storage position constant name has been updated from SIMPLE_ACCESS_MANAGER_STORAGE_POSITION to AUTH_MIDDLEWARE_STORAGE_POSITION to match the contract rename, which maintains consistency.


30-30: Initializer struct renamed for consistency.

The initializer struct has been renamed from SimpleAccessManagerInitializer to AuthorizationMiddlewareInitializer to match the contract rename, maintaining consistency in naming conventions.


41-41: Updated function parameter type to match renamed struct.

The initialize function parameter type has been updated to match the renamed struct, which is necessary for the contract to function correctly after the renaming changes.

test/middleware/AuthorizationMiddleware.test.ts (1)

43-58: Proper ERC165 interface testing.

The tests correctly verify that the contract supports ERC165 interface detection and the IERC7746 interface methods. The negative test case for random interfaces is also a good practice.

src/mocks/MockLatestVersionDistribution.sol (1)

8-14: Clean implementation of mock contract.

The constructor properly initializes the parent contract by converting the string metadata to ShortString format. This demonstrates good practice in extending base contracts.

src/mocks/MockRepository.sol (2)

13-22: LGTM! Good mock implementation

The contract appropriately exposes internal methods as public functions for testing purposes, allowing test code to directly invoke these otherwise inaccessible functions.


25-27: Correct ERC165 implementation

Good implementation of supportsInterface that properly declares support for the IRepository interface alongside any inherited interfaces.

src/distributors/WrappedProxyInitializer.sol (1)

16-29: Good error handling

The implementation correctly wraps the call to instantiate in a try-catch block and properly propagates different types of errors (string, panic, and low-level). This ensures detailed error information reaches the caller.

test/versioning/LibSemver.test.ts (3)

39-136: Comprehensive version requirements testing

The test cases thoroughly cover all comparison requirements (exact match, major, major-minor, greater/lesser than or equal) with both matching and non-matching scenarios, ensuring the library handles all comparison cases correctly.


138-253: Complete version comparison test coverage

The tests comprehensively validate all version comparison operations, including equality checks and the various requirement types (ANY, EXACT, MAJOR, MAJOR_MINOR, GREATER/LESSER than or equal).


255-282: Well-structured version increment tests

The version increment tests clearly validate that incrementing major, minor, and patch versions produces the expected results, with proper resetting of lower version components.

test/middleware/InstallerClonableUpgrade.test.ts (1)

173-183: Thorough error testing

The test properly checks the error case when trying to upgrade a non-installed app, ensuring the contract provides a clear and specific error message.

src/erc7744/LibERC7744.sol (1)

14-16: No existence check before external call

If INDEX_CONTRACT is unset (zero address) or not a contract on the current network,
index() reverts with a low-level error. Consider:

function index(address source) internal {
    ICodeIndex idx = getContractsIndex();
    require(address(idx).code.length != 0, "LibERC7744: index contract missing");
    idx.register(source);
}
docs/guides/Distributors.md (1)

1-126: Great, comprehensive documentation on distributors!

The documentation provides a clear overview of the Distributor architecture, implementations, and usage patterns. It successfully explains complex concepts like the multi-party upgrade security model and middleware integration.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

12-12: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


17-17: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


18-18: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


27-27: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


28-28: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


29-29: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

src/interfaces/IRepository.sol (1)

84-90: newRelease signature changed – update NatSpec

The function now takes bytes32 migrationHash but the documentation does not mention it.
Please add the missing @param migrationHash description and update the “MUST emit” note if you intend to emit MigrationScriptAdded as well.

test/eds/UpgradabilityWorkflows.ts (1)

221-225: Verify that DistributorChanged is actually emitted by distributor

changeDistributor is invoked on installer, so the event may originate from that contract instead of distributor.
If the event is emitted by installer, the current expectation will always fail silently.

Request double-check and, if needed, alter the expectation:

-      ).to.emit(distributor, "DistributorChanged");
+      ).to.emit(installer, "DistributorChanged");
src/mocks/MockERC7746HookedTest.sol (1)

47-55: testERC7746Modifier marked payable but never consumes msg.value
If the function shouldn’t accept ether, drop payable; otherwise emit / store the
value to prove it is handled.

test/eds/Installer.ts (2)

42-47: Byte-hash mismatch risk
provider.getCode returns runtime byte-code, whereas many indices expect the
creation byte-code. If the on-chain index was built from creation bytecode,
cloneDistributionId will never resolve.

-const code = await cloneDistribution.provider.getCode(cloneDistribution.address);
+const code = CloneDistribution.bytecode;

100-108: Off-by-one possibility when walking app indices
getAppsNum() typically returns the count; using it directly as an index may
overflow by one if the mapping is 0-based internally.
Confirm whether the contract stores apps starting at index 1; if not,
subtract 1.

const idx = (await installer.getAppsNum()).sub(1);
const app = await installer.getApp(idx);
src/distributions/UpgradableDistribution.sol (1)

34-38: sources() visibility & @inheritdoc mismatch
Declaring the function internal while tagging @inheritdoc means the public
interface is missing. If the intention is to expose sources via
get(), mark the function internal without the inherit-doc tag, or make
it external/public to satisfy tooling.

README.md (6)

3-4: Great improvements to the introduction!

The expanded introduction provides a much clearer technical overview of EDS, emphasizing secure upgrade management and multi-party processes. This sets the right expectations for the system's capabilities.


12-19: Excellent improvement to use cases section!

The use case descriptions are now much more technically precise with specific links to relevant documentation guides. This makes it easier for users to understand how different components can be used together.


23-29: Well-defined role descriptions and system architecture.

The overview section clearly defines the roles of Developers, Distributors, and Installers, and explains the proxy and middleware architecture. This is a significant improvement in clarity.


33-39: Key features are well-organized with documentation links.

Each key feature now has a clear link to the relevant documentation guide, making it easier for users to find detailed information on specific capabilities.


45-95: Excellent domain organization and component descriptions.

Breaking down the system into four domains (Permission-less, Developer, Distributor, and User) provides a much clearer mental model for understanding the system architecture. The detailed descriptions of each component within its domain are very helpful.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~58-~58: Loose punctuation mark.
Context: ...rce contract. * CodeHashDistribution: Instantiates code based on an ERC7744...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~59-~59: Loose punctuation mark.
Context: ...de hash. * LatestVersionDistribution: Instantiates the latest version from a ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~60-~60: Loose punctuation mark.
Context: ...epository. * UpgradableDistribution: Deploys WrappedTransparentUpgradeableP...

(UNLIKELY_OPENING_PUNCTUATION)


96-105: Valuable addition of documentation guides section.

The centralized list of documentation guides improves discoverability and helps users quickly find the specific information they need. This is a great addition to the README.

🧰 Tools
🪛 LanguageTool

[style] ~98-~98: Consider an alternative adjective to strengthen your wording.
Context: ... them). ## Documentation Guides For a deeper understanding of each component and con...

(DEEP_PROFOUND)

src/mocks/LibMiddlewareTest.sol (3)

1-13: Good contract structure with clear imports and data definitions.

The contract properly imports related files and defines a MiddlewareConfig struct that mirrors what LibMiddleware uses internally. The structure is clear and follows best practices.


14-32: Well-implemented utility functions for testing middleware configuration.

The functions for creating keys, encoding/decoding configurations, and retrieving layer configurations are well-designed for testing purposes.


55-74: Well-implemented reverse iteration for afterCall execution.

The executeAfterCall function correctly iterates through middleware layers in reverse order, properly accounting for array indexing to avoid underflow with i-1. Good implementation.

src/mocks/LibSemverTest.sol (1)

1-69: Well-structured wrapper for LibSemver testing.

This contract provides a comprehensive set of pure functions that wrap around the LibSemver library, making all its functionality available for external testing. The contract covers version conversion, requirement checks, comparisons, and version increments. No issues found.

docs/guides/Distributions.md (4)

1-9: Clear introduction with important warning about stateful logic.

The introduction effectively explains what Distributions are in the EDS system and includes a valuable warning about deploying stateful logic directly within Distribution contracts. This helps guide developers toward best practices.


10-40: Well-documented Clone-Based Distributions with helpful examples.

The explanation of CloneDistribution and its concrete implementations (CodeHashDistribution and LatestVersionDistribution) is clear and includes relevant code examples. This makes it easy for developers to understand how these contracts work and how to use them.


41-81: Comprehensive explanation of Upgradable Distributions and their security model.

The section on UpgradableDistribution thoroughly explains the multi-party trust system involving distributors, installers, and proxies. The step-by-step explanation of the mechanism and the code example are very helpful for understanding this complex pattern.


82-110: Clear CLI documentation with useful examples.

The CLI section provides practical examples for common operations like deploying, instantiating, and verifying distributions. The note about special data encoding requirements for UpgradableDistribution is particularly important.

src/mocks/MockTokenizedDistributor.sol (6)

4-4: Import path correction looks good
The switch to the relative ../distributors/TokenizedDistributor.sol path aligns the mock with the new folder structure.


50-52: No issues with making instantiate payable
The function correctly bubbles the call to _instantiate, allowing the base logic to handle payment verification.


59-61: Disable instead of remove – LGTM
Renaming the admin entry point to disableDistribution matches the new semantics in the core library.


78-83: Version-change wrapper looks correct
The thin wrapper enforces access control and delegates to _changeVersion; no further feedback.


97-106: Migration management – make sure to emit events
If _addVersionMigration does not emit an event, consider adding one here so off-chain indexers can track migration plans.


108-110: Removal wrapper OK
Straight delegation with access control – no concerns.

src/middleware/InstallerOwnable.sol (2)

14-17: Initializer signature duplicates constructor params
Given the previous comment, this initializer becomes the single source of truth – good separation.


34-60: Wrapper methods all correctly gated by onlyOwner
The thin wrappers delegate to the internal helpers and preserve modifiers – no change requested.

src/distributors/OwnableDistributor.sol (4)

12-16: Payable instantiate mirrors base behaviour – OK
Delegation to _instantiate preserves value forwarding via msg.value.


39-42: Disable wrapper matches new semantics
Straight delegation with ownership guard – looks correct.


43-52: Event emission for migration helpers
Ensure _addVersionMigration and _removeVersionMigration emit events so that external tooling can observe migration plan lifecycle.


58-68: Public upgrade function correctly surfaces to owner
No issues spotted; the override keeps the public API in sync with base distributor.

src/mocks/MockDistributorForTest.sol (2)

74-76: Using address(uint160(appId)) as a fake instance address is fragile.

If any test accidentally installs more than 2^160 apps, or other contracts rely on code.length > 0, this pseudo-address pattern will break. Consider deploying a tiny dummy contract (new Empty()) or pre-computing deterministic create2 addresses.


101-108: Missing override specifier for new‐signature addDistribution.

Solidity requires explicit override for every base function; depending on the interface version, the current signature might not satisfy the compiler. Verify against the latest IDistributor.

src/distributors/Distributor.sol (1)

286-289: Interface detection omits IERC7746

Distributor implements IERC7746 via the hooks, but supportsInterface
does not advertise it. Add the check so tools can recognise compliance.

return interfaceId == type(IDistributor).interfaceId ||
+       interfaceId == type(IERC7746).interfaceId ||
        super.supportsInterface(interfaceId);
test/eds/Distributor.ts (1)

52-60: Many expect(...).to.be.reverted lack reason checks

Several tests only assert that a call reverts, not why.
If the code changes and reverts for the wrong reason these tests will still
pass, masking regressions.

Add reason / custom-error assertions, e.g.:

await expect(
  distributor.connect(owner).instantiate(failingDistId, badArgs)
).to.be.revertedWithCustomError(distributor, "DistributionInstantiationFailed");
src/mocks/MockAuthMiddlewareDistributor.sol (1)

98-103: Advertise all implemented interfaces

supportsInterface omits IERC7746 (the hook interface).
Tools relying on ERC-165 will mis-identify the contract.

return
    interfaceId == type(IDistributor).interfaceId ||
+   interfaceId == type(IERC7746).interfaceId ||
    super.supportsInterface(interfaceId);
src/interfaces/IDistributor.sol (1)

90-96: Instantiated event: rethink the third indexed topic

Three indexed parameters are already used (distributionId, newAppId, version).
If downstream consumers need to filter by version, keep it; otherwise moving version to the data section reduces gas when emitting and prevents exceeding the 3-topic limit if you later add another index.

No action required if the current design is deliberate—just flagging the forward-compatibility trade-off.

Comment on lines +73 to +74
"@openzeppelin/contracts": "^5.3.0",
"@openzeppelin/contracts-upgradeable": "^5.3.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Updated OpenZeppelin dependencies to latest versions.

Updating both OpenZeppelin contracts packages from ^5.0.2 to ^5.3.0 ensures access to the latest security features and improvements. This is appropriate given the substantial architectural changes in this PR.


🏁 Script executed:

#!/bin/bash
# Check for any breaking changes between OpenZeppelin 5.0.2 and 5.3.0 that might affect the project

echo "Checking OpenZeppelin release notes for potential breaking changes..."
curl -s https://github.com/OpenZeppelin/openzeppelin-contracts/releases | grep -A 20 "v5.3.0\|v5.2.0\|v5.1.0" | grep -B 20 "Full Changelog" | grep -i "breaking\|deprecated"

Length of output: 323


🏁 Script executed:

#!/bin/bash
# Fetch and inspect the CHANGELOG for both packages at v5.3.0
for pkg in openzeppelin-contracts openzeppelin-contracts-upgradeable; do
  echo "=== Checking $pkg v5.3.0 CHANGELOG for breaking or deprecated notes ==="
  curl -s "https://raw.githubusercontent.com/OpenZeppelin/$pkg/v5.3.0/CHANGELOG.md" \
    | grep -inE "breaking|deprecated" || echo "No entries found in $pkg"
  echo
done

Length of output: 12493


Ensure compatibility with OpenZeppelin v5.3.0 breaking changes

We’ve upgraded both @openzeppelin/contracts and @openzeppelin/contracts-upgradeable to ^5.3.0. This pull in numerous breaking changes and deprecations that require manual updates:

• Transfer hooks signature changed

  • _beforeTokenTransfer(...) and _afterTokenTransfer(...) now take an extra parameter.
  • Review all overrides in your ERC-20/721/1155 contracts and update the method signatures accordingly.
    • Deprecated/removed modules
  • safeApprove is deprecated—migrate to safeIncreaseAllowance/safeDecreaseAllowance.
  • Timers, ERC777, ERC1820Implementer, preset contracts are deprecated—remove or replace as needed.
    • Draft-EIP imports updated
  • Replace any draft-IERC20Permit.sol/draft-ERC20Permit.sol with IERC20Permit.sol/ERC20Permit.sol.
  • Update draft-EIP712.sol, draft-ERC721Votes.sol to their non-draft equivalents.
    • Governor interface changes
  • Add the new internal _getVotes(address,uint256) and update _countVote overrides to include the bytes memory params argument.
    • AccessControl API
  • _setupRole(bytes32,address) is deprecated—use _grantRole(bytes32,address) instead.

Please audit all custom contracts and off-chain integrations against the full v5.3.0 changelog to ensure nothing breaks.

peersky and others added 5 commits May 2, 2025 17:06
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Updated `hardhat.config.ts` to improve JSON file loading by using `fs.readFileSync` and `JSON.parse`.
- Refactored `UpgradableDistribution`, `OwnableDistributor`, and other contracts to enhance readability and maintainability by adjusting function signatures and removing unnecessary whitespace.
- Improved interface definitions in `IDistributor` and `IInstaller` for clarity.
- Enhanced mock contracts for better testing scenarios, including adjustments to `MockRepository`, `MockMigration`, and others.
- Cleaned up test files by removing redundant code and improving assertions for better test coverage and clarity.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
test/eds/RepositoryExtended.ts (1)

270-273: Repeat of previously-flagged parameter-packing bug

repository.get is still called with a single object that bundles both version and requirement.
The ABI expects two separate positional arguments (Version struct, requirements enum).
Passing a struct containing both fields results in incorrect ABI encoding and a silent revert.

The exact same issue was called out in an earlier review (lines 270-277 of the previous diff).
Please update every invocation accordingly.

-const source = await repository.get({
-  version: { major: 1, minor: 1, patch: 1 },
-  requirement: 1
-});
+const source = await repository.get(
+  { major: 1, minor: 1, patch: 1 }, // Version struct
+  1                                   // LibSemver.requirements.EXACT
+);

Apply the same change to the call at lines 361-365 and any others you might add later.

Also applies to: 361-365

src/interfaces/IInstaller.sol (1)

130-137: Installed event signature is underspecified and may cause downstream type issues.

  1. The permissions slot is declared as bytes32 indexed, yet the only known emission (see InstallerClonable._install) passes a string literal ("0x") which will not compile – a string/bytes literal cannot be implicitly converted to bytes32.
  2. Nothing in the interface explains how the permissions value is derived, so index-based filtering will be unreliable.

Please either document a deterministic encoding scheme or drop the parameter altogether.

src/interfaces/IDistributor.sol (4)

13-20: MigrationPlan is missing the migration contract pointer

The struct stores migrationHash, but there is no address or code-hash‐plus-repository field to actually locate the contract that will be DELEGATECALL/CALLed.
Consider adding an address migrationContract (or equivalent) so that integrators do not have to resolve the hash externally.

 struct MigrationPlan {
     LibSemver.VersionRequirement from;
     LibSemver.VersionRequirement to;
     bytes32 migrationHash;
+    address migrationContract;      // where to find that byte-code
     MigrationStrategy strategy;
     bytes distributorCalldata;
     bytes32 distributionId;
 }

138-141: Documentation/ABI mismatch for instantiate

The NatSpec says:

@return distributionName The name of the distribution.

but the signature returns bytes32 distributionName.
Either change the return type to string (preferred for human-readable names) or update the comment to stress it is the 32-byte hash.


150-151: Overload ambiguity risk for addDistribution

There are now two overloads:

  1. (bytes32,address,string)
  2. (IRepository,address,LibSemver.VersionRequirement,string)

The first two parameters in variant 2 can easily be mistaken for (bytes32,address) in calls with implicit type conversion (e.g., passing an address that is also a bytes32 cast).

Consider renaming one of the functions (addRepositoryDistribution, etc.) to keep the API self-documenting and avoid silent mis-compiles.

Also applies to: 179-184


193-208: Typo: LibSemver.requirements is undefined

Both events reference LibSemver.requirements, but the library exports VersionRequirement.

-        LibSemver.requirements indexed semanticRequirement,
+        LibSemver.VersionRequirement indexed semanticRequirement,

Repeat for the second event.
Failure to fix will break compilation.

🧹 Nitpick comments (11)
test/eds/RepositoryExtended.ts (2)

24-33: Shadowing the imported ERC7744 symbol

Within the beforeEach you create a local constant named ERC7744, which shadows the class imported from ../../types.
While this is legal, it is easy to misread and becomes error-prone if you later need the original symbol in this scope.

-const ERC7744 = await ethers.getContractFactory("ERC7744");
+const ERC7744Factory = await ethers.getContractFactory("ERC7744");

96-126: Test preparation is heavily duplicated

Each describe-block re-mints the same three releases. Consider extracting helper utilities or a shared context() to DRY up the setup. It will shorten the file and speed up execution.

Also applies to: 168-206, 238-267

test/eds/UpgradabilityWorkflows.ts (1)

158-176: Comment says “Allow this distribution in the installer” yet no code follows

The installer currently trusts the distributor because you whitelisted the distributor in beforeEach, not the distribution.
Either delete the misleading comment or explicitly call whatever allow-list function is required for the individual distribution to avoid confusion for future maintainers.

src/mocks/MockTokenizedDistributor.sol (2)

24-30: Two public addDistribution overloads make the API ambiguous

The contract exposes:

  1. addDistribution(bytes32,address,string)
  2. addDistribution(IRepository,address,VersionRequirement,string)

Both are externally callable and easily confused. Consider:

  • Renaming the first to addRawDistribution (or similar), or
  • Making one internal, exposing only a single, clear public entry-point.

This will reduce misuse and simplify ABI generation for front-end consumers.

Also applies to: 88-95


37-39: Visibility / gas micro-optimisations

  • setInstantiationCost and disableDistribution can be marked external rather than public; they are never used internally.
  • _instantiate already returns memory arrays; adding memory on the return parameters of instantiate is redundant in >=0.8.0 (solidity infers it).

Not critical, but tidies bytecode and interface size.

Also applies to: 52-54, 61-63

src/interfaces/IInstaller.sol (4)

5-5: Missing import for the App struct

The App struct is used throughout the interface but it's defined inline instead of being imported from a shared location. This could cause issues with type consistency across the codebase.

Consider moving the App struct to a separate file and importing it to ensure consistency across contracts that need to use this structure.


146-148: Fixed typo in the ERC7746 reference

There's a typo in the documentation - "ERC77446" should be "ERC7746".

-     * @dev After successful installation ERC77446 hooks SHALL NOT revert if called by target, specifying active app in `sender` field.
+     * @dev After successful installation ERC7746 hooks SHALL NOT revert if called by target, specifying active app in `sender` field.

159-160: Fixed typo in the ERC7746 reference

There's another typo in the documentation - "ERC77446" should be "ERC7746".

-     * @dev After successful uninstallation ERC77446 hooks SHALL revert if called by target, specifying uninstalled app in `sender` field.
+     * @dev After successful uninstallation ERC7746 hooks SHALL revert if called by target, specifying uninstalled app in `sender` field.

203-203: Missing documentation for appData parameter

The changeDistributor function has an undocumented appData parameter. This should be explained in the function's NatSpec.

 /**
  * @notice Changes the distributor of a given app.
  * @param appId The unique identifier of the app.
  * @param newDistributor The new distributor to set for the app.
+ * @param appData Array of bytes containing app-specific configuration data for each app component.
  * @dev MUST emit `DistributorChanged` event.
  */
test/eds/Distributor.ts (2)

1104-1188: Commented out test for cross-app call prevention

There's a commented out test for cross-app call prevention. While it's normal to have work-in-progress tests, this should be either fixed and enabled or removed to keep the test suite clean.

Consider either fixing and enabling this test or removing it if it's no longer needed. If it's waiting on other functionality to be implemented, add a TODO comment explaining why it's disabled.


1902-1965: Skipped test with no explanation

The test for cross-app validation when called from a renounced app is skipped without any explanation. This creates uncertainty about whether the functionality is incomplete, untestable, or just temporarily disabled.

Add a TODO comment explaining why this test is skipped and when it should be re-enabled, or fix the issue that's causing it to be skipped.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between e56ddf1 and 9c004f1.

📒 Files selected for processing (35)
  • docs/guides/Versioning.md (1 hunks)
  • hardhat.config.ts (4 hunks)
  • src/distributions/UpgradableDistribution.sol (1 hunks)
  • src/distributors/OwnableDistributor.sol (2 hunks)
  • src/erc7744/LibERC7744.sol (1 hunks)
  • src/interfaces/IDistributor.sol (7 hunks)
  • src/interfaces/IInstaller.sol (3 hunks)
  • src/interfaces/IMigration.sol (1 hunks)
  • src/interfaces/IRepository.sol (4 hunks)
  • src/mocks/LibMiddlewareExtendedTest.sol (1 hunks)
  • src/mocks/LibMiddlewareTest.sol (1 hunks)
  • src/mocks/LibSemverTest.sol (1 hunks)
  • src/mocks/MockAuthMiddlewareDistributor.sol (1 hunks)
  • src/mocks/MockAuthMiddlewareTarget.sol (1 hunks)
  • src/mocks/MockDistributorForTest.sol (1 hunks)
  • src/mocks/MockERC165.sol (1 hunks)
  • src/mocks/MockERC7746Hooked.sol (1 hunks)
  • src/mocks/MockERC7746HookedTest.sol (1 hunks)
  • src/mocks/MockLatestVersionDistribution.sol (1 hunks)
  • src/mocks/MockMiddleware.sol (1 hunks)
  • src/mocks/MockMiddlewareTarget.sol (1 hunks)
  • src/mocks/MockMigration.sol (1 hunks)
  • src/mocks/MockNoReasonInitializer.sol (1 hunks)
  • src/mocks/MockOwnableDistributor.sol (1 hunks)
  • src/mocks/MockPanicDistribution.sol (1 hunks)
  • src/mocks/MockRepository.sol (1 hunks)
  • src/mocks/MockTokenizedDistributor.sol (6 hunks)
  • src/mocks/MockTransferOwnershipProxy.sol (1 hunks)
  • src/repositories/OwnableRepository.sol (1 hunks)
  • test/eds/Distributor.ts (6 hunks)
  • test/eds/LatestVersionDistribution.ts (1 hunks)
  • test/eds/Repository.ts (5 hunks)
  • test/eds/RepositoryExtended.ts (1 hunks)
  • test/eds/UpgradabilityWorkflows.ts (1 hunks)
  • test/middleware/AuthorizationMiddleware.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (28)
  • src/mocks/MockMiddlewareTarget.sol
  • test/eds/LatestVersionDistribution.ts
  • test/eds/Repository.ts
  • src/interfaces/IMigration.sol
  • src/mocks/MockERC165.sol
  • docs/guides/Versioning.md
  • src/mocks/MockTransferOwnershipProxy.sol
  • src/mocks/MockLatestVersionDistribution.sol
  • src/mocks/MockRepository.sol
  • src/mocks/MockPanicDistribution.sol
  • src/repositories/OwnableRepository.sol
  • src/mocks/MockNoReasonInitializer.sol
  • src/erc7744/LibERC7744.sol
  • src/mocks/MockAuthMiddlewareTarget.sol
  • src/mocks/MockOwnableDistributor.sol
  • src/interfaces/IRepository.sol
  • hardhat.config.ts
  • src/mocks/LibMiddlewareTest.sol
  • test/middleware/AuthorizationMiddleware.test.ts
  • src/distributions/UpgradableDistribution.sol
  • src/mocks/MockMigration.sol
  • src/mocks/MockMiddleware.sol
  • src/distributors/OwnableDistributor.sol
  • src/mocks/LibMiddlewareExtendedTest.sol
  • src/mocks/LibSemverTest.sol
  • src/mocks/MockERC7746Hooked.sol
  • src/mocks/MockAuthMiddlewareDistributor.sol
  • src/mocks/MockERC7746HookedTest.sol
🔇 Additional comments (18)
test/eds/UpgradabilityWorkflows.ts (2)

219-222: Event asserted on the wrong contract

changeDistributor is invoked on installer, but the test expects the event to be emitted by distributor.
Unless changeDistributor emits and forwards the event through the distributor, the assertion will fail.

-).to.emit(distributor, "DistributorChanged");
+).to.emit(installer, "DistributorChanged");

Verify where the event is actually emitted and update the expectation accordingly.


317-323: migrationHash is not a migration-ID

upgradeApp expects a migrationId (the keccak of distributionId + migrationHash + strategy).
Passing the raw migrationHash happens to revert (thus the test passes) but for the wrong reason.
Construct the real migrationId or assert on a more specific revert to keep the test meaningful.

src/interfaces/IInstaller.sol (2)

7-11: Good structural change from instance to app model

The introduction of the App struct represents a significant architectural improvement that enables composition of multiple contracts with middleware support. This aligns well with modern smart contract design patterns.


211-231: Well-structured app upgrade mechanism

The addition of upgrade functionality with migration IDs and user calldata provides a robust mechanism for managing app evolution over time. The event is properly defined to track the upgrade process.

src/mocks/MockDistributorForTest.sol (5)

9-27: Well-structured mock implementation for testing

The storage structures are clearly defined and provide a good foundation for testing the distributor functionality. The use of a struct to capture upgrade calls makes it easy to verify the correct parameters are being passed.


35-52: Effective test control methods

These methods provide good control over the mock's behavior during tests, especially for simulating success and failure scenarios during upgrades.


183-191: Configurable hook behavior

The implementation allows tests to control the return value of the beforeCall hook, which is essential for testing middleware interactions and upgrade workflows.


147-161: Comprehensive upgrade testing support

The upgradeUserInstance implementation captures call parameters and supports different error scenarios, making it possible to test all aspects of the upgrade workflow, including error handling.


198-203: Complete interface support

The supportsInterface implementation properly declares support for both IDistributor and IERC7746 interfaces, which is essential for contract discovery and validation.

test/eds/Distributor.ts (5)

35-50: Useful helper function for version requirements

The createVersionRequirement helper function improves test readability and reduces duplication when creating version requirements throughout the test suite.


81-141: Comprehensive test setup

The enhanced setup with mock migration contracts, repositories, and test distributions enables thorough testing of the versioning and migration features. This is a significant improvement that will ensure robustness.


161-186: Important validation test for alias uniqueness

This test confirms that the system correctly prevents alias collisions, which is critical for maintaining a consistent and predictable distribution registry.


304-382: Thorough error handling tests

These tests for instantiation failures (including reason strings, panics, and low-level errors) are essential for ensuring the system gracefully handles edge cases and provides meaningful error messages.


591-1833: Extensive versioning and migration tests

This comprehensive test suite for versioning and migrations covers all aspects of the new functionality, including version constraints, migration strategies, upgrade workflows, access control, and error conditions. This level of coverage is excellent for such critical functionality.

src/interfaces/IDistributor.sol (4)

212-224: Well-designed migration management API

The migration management functions provide a complete set of operations for adding, removing, and retrieving migrations. The API design is clean and follows a consistent pattern.


225-230: Upgrade functionality matches installer interface

The upgradeUserInstance function aligns with the corresponding upgrade functionality in the IInstaller interface, providing a consistent approach to managing app upgrades throughout the system.


237-244: Comprehensive error types for upgrade failures

The different error types for upgrade failures (panic, revert, error) provide detailed information about what went wrong during an upgrade, which will be valuable for debugging and error handling.


251-266: Well-designed events for upgrade tracking

The events for distributor changes, app upgrades, and migration execution provide a comprehensive audit trail of all changes to app state, which is essential for governance and debugging.

Comment on lines +304 to +307
await expect(
repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash)
).to.be.revertedWith("Major version does not exist");
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use custom-error matcher instead of plain revert string

changeMigrationScript in the repository reverts with a custom error (e.g. MajorVersionDoesNotExist()).
The test currently asserts on a plain revert string which will not match and will cause a false-negative pass/fail.

-await expect(
-  repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash)
-).to.be.revertedWith("Major version does not exist");
+await expect(
+  repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash)
+).to.be.revertedWithCustomError(repository, "MajorVersionDoesNotExist");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(
repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash)
).to.be.revertedWith("Major version does not exist");
});
await expect(
repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash)
).to.be.revertedWithCustomError(repository, "MajorVersionDoesNotExist");
});

peersky added 3 commits May 2, 2025 17:29
- Enhanced `CloneDistribution`, `CodeHashDistribution`, `LatestVersionDistribution`, `UpgradableDistribution`, `Distributor`, `OwnableDistributor`, `WrappedProxyInitializer`, `OwnableRepository`, and `Repository` contracts with interface documentation using `@inheritdoc` for better clarity and adherence to standards.
- Updated function signatures in `IAdminGetter` and `IContractURI` interfaces to include detailed descriptions and return types.
- Improved overall code readability and maintainability by standardizing comments across contracts.
…k contracts

- Introduced a new function `calculateDistributorId` in the `IDistributor` interface to compute a unique distributor ID based on a code hash and initializer address.
- Implemented mock versions of `calculateDistributorId` in `MockAuthMiddlewareDistributor` and `MockDistributorForTest` for testing purposes.
- Removed redundant comments and improved code clarity in `Repository` contract by cleaning up the `repositoryName` function documentation.
- Introduced `DistributorStore` struct to centralize state variables in the `Distributor` contract, improving organization and access.
- Updated functions to utilize the new storage structure, enhancing readability and maintainability.
- Refactored `OwnableDistributor` and `TokenizedDistributor` to align with the new storage pattern.
- Added new `OwnableTokenizedDistributor` contract for improved ownership management in tokenized distributions.
- Enhanced test cases to validate changes in the new storage implementation.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
src/distributors/Distributor.sol (1)

146-161: 🛠️ Refactor suggestion

Mixed revert styles & missing validation make _addDistribution brittle

  1. A string-based require (line 157) is used next to custom errors → keep the style consistent and cheaper by using custom errors everywhere.
  2. No check that repositoryContract.resolveVersion(requirement) actually belongs to the same repo; consider reverting with a dedicated VersionDoesNotExist error.
-        require(repositoryContract.resolveVersion(requirement) != 0, "Version does not exist");
+        if (repositoryContract.resolveVersion(requirement) == 0) {
+            revert VersionDoesNotExist(repository, LibSemver.toString(requirement.version));
+        }

(Remember to declare error VersionDoesNotExist(address repo,string version);)

src/interfaces/IDistributor.sol (1)

193-208: Typo – LibSemver.requirements does not exist

Both events reference LibSemver.requirements, but the library exports VersionRequirement.
This is a straight compilation blocker.

-        LibSemver.requirements indexed semanticRequirement,
+        LibSemver.VersionRequirement indexed semanticRequirement,

Apply the same fix for the MigrationContractAddedToVersions event.

🧹 Nitpick comments (8)
src/distributors/OwnableTokenizedDistributor.sol (1)

6-8: Remove duplicate import.

IMigration.sol is imported twice, which is redundant and may introduce “Source file already imported” compiler errors in some Solidity versions.

-import "../interfaces/IMigration.sol";
src/distributors/OwnableDistributor.sol (2)

6-8: Duplicate import can be safely dropped.

IMigration.sol appears twice.

-import "../interfaces/IMigration.sol";

37-44: Consistency nit – prefer super._addDistribution for clarity.

In the repository-based overload you call the inherited _addDistribution directly, while the hash-based overload prefixes it with super.. Using super. in both places avoids confusion and makes it explicit that you delegate to the parent implementation.

-        _addDistribution(address(repository), initializer, requirement, readableName);
+        super._addDistribution(address(repository), initializer, requirement, readableName);
src/distributors/TokenizedDistributor.sol (1)

43-46: Gas micro-optimisation – make the storage slot a file-level constant.

keccak256("distributor.tokenized.distributor.store") is recomputed on every call. Storing it once saves ~200 gas per call.

+bytes32 internal constant TOKENIZED_DISTRIBUTOR_STORAGE_POSITION =
+    keccak256("distributor.tokenized.distributor.store");
 // ...
-        bytes32 TOKENIZED_DISTRIBUTOR_STORAGE_POSITION = keccak256("distributor.tokenized.distributor.store");
src/distributors/Distributor.sol (1)

370-373: supportsInterface does not advertise ERC7746

beforeCall/afterCall implement IERC7746, but supportsInterface only returns IDistributor & super.
Add the selector to keep interface detection reliable.

-        return interfaceId == type(IDistributor).interfaceId || super.supportsInterface(interfaceId);
+        return
+            interfaceId == type(IDistributor).interfaceId ||
+            interfaceId == type(IERC7746).interfaceId ||
+            super.supportsInterface(interfaceId);
src/interfaces/IDistributor.sol (2)

268-275: calculateDistributorId(bytes32,address) should be pure

The function is a deterministic hash and never touches state; marking it pure (or at least view) saves gas and clarifies intent.

-    function calculateDistributorId(bytes32 codeHash, address initializer) external returns (bytes32);
+    function calculateDistributorId(bytes32 codeHash, address initializer) external pure returns (bytes32);

150-156: Overload ambiguity for addDistribution persists

With two overloads

  1. addDistribution(bytes32,address,string)
  2. addDistribution(IRepository,address,VersionRequirement,string)

a simple address typed variable can satisfy both the first parameters by implicit cast to bytes32, leading to silent mis-compiles.

Consider renaming, e.g.:

addCodeHashDistribution(...)
addRepositoryDistribution(...)

or introduce a struct wrapper to make the calls unambiguous.

test/eds/Distributor.ts (1)

35-50: Utility helper is duplicated across describe-blocks

createVersionRequirement is declared twice in this file.
Factor it out to a shared helper (e.g. test/utils/semver.ts) to reduce noise and the risk of divergence.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 9c004f1 and a039596.

📒 Files selected for processing (25)
  • src/distributions/CloneDistribution.sol (2 hunks)
  • src/distributions/CodeHashDistribution.sol (3 hunks)
  • src/distributions/LatestVersionDistribution.sol (1 hunks)
  • src/distributions/UpgradableDistribution.sol (1 hunks)
  • src/distributors/Distributor.sol (1 hunks)
  • src/distributors/OwnableDistributor.sol (1 hunks)
  • src/distributors/OwnableTokenizedDistributor.sol (1 hunks)
  • src/distributors/TokenizedDistributor.sol (1 hunks)
  • src/distributors/WrappedProxyInitializer.sol (1 hunks)
  • src/interfaces/IAdminGetter.sol (1 hunks)
  • src/interfaces/IContractURI.sol (1 hunks)
  • src/interfaces/IDistributor.sol (7 hunks)
  • src/interfaces/IMigration.sol (1 hunks)
  • src/mocks/LibMiddlewareExtendedTest.sol (1 hunks)
  • src/mocks/MockAuthMiddlewareDistributor.sol (1 hunks)
  • src/mocks/MockDistributorForTest.sol (1 hunks)
  • src/mocks/MockInitializer.sol (1 hunks)
  • src/mocks/MockInstaller.sol (0 hunks)
  • src/mocks/MockOwnableDistributor.sol (1 hunks)
  • src/mocks/MockTokenizedDistributor.sol (1 hunks)
  • src/repositories/OwnableRepository.sol (1 hunks)
  • src/repositories/Repository.sol (6 hunks)
  • test/eds/Distributor.ts (6 hunks)
  • test/eds/TokenizedDistributor.ts (5 hunks)
  • test/signatures.json (1 hunks)
💤 Files with no reviewable changes (1)
  • src/mocks/MockInstaller.sol
✅ Files skipped from review due to trivial changes (3)
  • src/interfaces/IContractURI.sol
  • src/mocks/MockTokenizedDistributor.sol
  • test/signatures.json
🚧 Files skipped from review as they are similar to previous changes (14)
  • src/distributions/CloneDistribution.sol
  • test/eds/TokenizedDistributor.ts
  • src/distributions/CodeHashDistribution.sol
  • src/interfaces/IMigration.sol
  • src/mocks/MockOwnableDistributor.sol
  • src/distributions/UpgradableDistribution.sol
  • src/mocks/LibMiddlewareExtendedTest.sol
  • src/distributors/WrappedProxyInitializer.sol
  • src/mocks/MockInitializer.sol
  • src/repositories/OwnableRepository.sol
  • src/repositories/Repository.sol
  • src/distributions/LatestVersionDistribution.sol
  • src/mocks/MockAuthMiddlewareDistributor.sol
  • src/mocks/MockDistributorForTest.sol
🔇 Additional comments (5)
src/interfaces/IAdminGetter.sol (1)

1-16: Interface looks clean – no blocking issues found.

The SPDX header, pragma range, NatSpec comments and the single‐method interface are all spot-on. Nice job keeping the interface minimal and well-documented.

src/distributors/OwnableTokenizedDistributor.sol (1)

9-21: 🛠️ Refactor suggestion

⚠️ Potential issue

Constructor triggers a double-initialisation guard – will revert when deployed via proxy.

TokenizedDistributor’s constructor already calls its own initialize(...) (which sets Initializable._initialized = 1).
Your constructor then calls initialize(_owner) (tagged initializer) a second time. Because _initialized is already 1, the OpenZeppelin guard reverts with “contract is already initialized”. This makes the contract undeployable through a proxy/cloning setup and defeats the whole upgradeable pattern.

Recommended fix (one of):

-    constructor(
-        address _owner,
-        IERC20 _paymentToken,
-        uint256 _defaultInstantiationCost,
-        address _beneficiary
-    ) TokenizedDistributor(_paymentToken, _defaultInstantiationCost, _beneficiary) {
-        initialize(_owner);
-    }
+    // Leave constructor empty (or remove it entirely) –
+    // deploy via proxy and call `initialize(...)` once.
+    constructor(
+        address _owner,
+        IERC20 _paymentToken,
+        uint256 _defaultInstantiationCost,
+        address _beneficiary
+    ) TokenizedDistributor(_paymentToken, _defaultInstantiationCost, _beneficiary) {}

Then change the initialize modifier to reinitializer(2) or merge the owner set-up into the base initialize call to keep a single initializer.

Likely an incorrect or invalid review comment.

src/distributors/TokenizedDistributor.sol (1)

43-50: 🛠️ Refactor suggestion

⚠️ Potential issue

Constructor calls initialize ⇒ implementation is pre-initialised.

Calling initialize in the constructor sets _initialized = 1, so any proxy/cloned instance attempting to call initialize will revert. This defeats the purpose of using Initializable.

-constructor(IERC20 token, uint256 defaultCost, address _beneficiary) Distributor() {
-    initialize(token, defaultCost, _beneficiary);
-}
+// Remove the initializer call – deployment through a proxy
+// should invoke `initialize(...)` once via delegatecall.
+constructor(IERC20 token, uint256 defaultCost, address _beneficiary) Distributor() {}

Also consider deleting the constructor altogether and making the contract purely initializable.

Likely an incorrect or invalid review comment.

src/distributors/Distributor.sol (1)

468-509: DELEGATECALL migration – surface-level sanity checks missing

Before performing a delegatecall you only verify that the target supports IMigration, but not that result decodes to the expected shape or that newVersion > oldVersion. An adversarial migration contract could silently downgrade or corrupt state.

Add:

require(newVersion.toUint256() > oldVersionUint, "Migration did not upgrade version");
(bool success, bytes memory result) =require(success, "Delegatecall failed");

and decode / validate result if you intend to use it.

test/eds/Distributor.ts (1)

2038-2044: Expecting generic .revertedWith("") hides the real revert reason

The contract now emits custom errors (DistributionInstantiationRevert, etc.).
Catching only an empty reason string will mask regressions. Prefer explicit custom-error assertions:

await expect(
  distributor.connect(owner).instantiate(initializerDistributorsId, failArgs)
).to.be.revertedWithCustomError(distributor, "DistributionInstantiationRevert");

Comment on lines +10 to +16
constructor(address _owner) {
initialize(_owner);
}

contract OwnableDistributor is Distributor, Ownable {
constructor(address _owner) Ownable(_owner) {}
function initialize(address _owner) public initializer {
__Ownable_init(_owner);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Calling initialize inside the constructor blocks proxy usage.

Because initialize uses the initializer modifier, invoking it in the constructor sets _initialized = 1.
When this implementation is later used behind a proxy, subsequent delegate-calls to initialize will revert, preventing proper state initialisation for the proxy instance.

Either:

  1. Remove the call from the constructor and rely on an external/initializer call post-deployment, or
  2. Delete the constructor entirely and expose only initialize(...) to be invoked once.

This keeps the contract compliant with OpenZeppelin’s upgradeable pattern.

Comment on lines 266 to 304
if (!externallyInitialized) {
try IDistribution(distributionLocation).instantiate(args) returns (
address[] memory _newAppComponents,
bytes32 _distributionName,
uint256 _distributionVersion
) {
newAppComponents = _newAppComponents;
distributionName = _distributionName;
distributionVersion = _distributionVersion;
} catch Error(string memory reason) {
revert(reason);
} catch Panic(uint errorCode) {
revert DistributionInstantiationPanic(errorCode);
} catch (bytes memory lowLevelData) {
revert DistributionInstantiationFailed(lowLevelData);
}
} else {
(bool success, bytes memory result) = address(distributionComponent.initializer).delegatecall(
abi.encodeWithSelector(selector, distributionLocation, distributionName, distributionVersion, args)
);
if (!success) {
revert(string(result));
}

newAppComponents = abi.decode(result, (address[]));
}

{
uint256 instancesLength = newAppComponents.length;
for (uint256 i; i < instancesLength; ++i) {
distributorStore.appIds[newAppComponents[i]] = appId;
distributorStore.distributionOf[appId] = distributorsId;
}
}
emit Instantiated(distributorsId, appId, distributionVersion, newAppComponents, args);
for (uint256 i; i < newAppComponents.length; i++) {
distributorStore.appComponents[appId].push(newAppComponents[i]);
}
distributorStore.installers[appId] = msg.sender;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Re-entrancy and redundant writes when registering a new app

  • IDistribution.instantiate() or an external initializer is executed before any state-tracking mappings are written.
    If the callee re-enters the Distributor (e.g. via beforeCall) the call will fail because the app is not yet registered.
    Consider creating a temporary “in construction” entry before the external call or documenting the invariants very clearly.

  • distributionOf[appId] = distributorsId; is executed inside the for-loop – it is identical for every iteration and wastes storage writes.

-            for (uint256 i; i < instancesLength; ++i) {
+            distributorStore.distributionOf[appId] = distributorsId; // single write
+            for (uint256 i; i < instancesLength; ++i) {
                 distributorStore.appIds[newAppComponents[i]] = appId;
-                distributorStore.distributionOf[appId] = distributorsId;
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!externallyInitialized) {
try IDistribution(distributionLocation).instantiate(args) returns (
address[] memory _newAppComponents,
bytes32 _distributionName,
uint256 _distributionVersion
) {
newAppComponents = _newAppComponents;
distributionName = _distributionName;
distributionVersion = _distributionVersion;
} catch Error(string memory reason) {
revert(reason);
} catch Panic(uint errorCode) {
revert DistributionInstantiationPanic(errorCode);
} catch (bytes memory lowLevelData) {
revert DistributionInstantiationFailed(lowLevelData);
}
} else {
(bool success, bytes memory result) = address(distributionComponent.initializer).delegatecall(
abi.encodeWithSelector(selector, distributionLocation, distributionName, distributionVersion, args)
);
if (!success) {
revert(string(result));
}
newAppComponents = abi.decode(result, (address[]));
}
{
uint256 instancesLength = newAppComponents.length;
for (uint256 i; i < instancesLength; ++i) {
distributorStore.appIds[newAppComponents[i]] = appId;
distributorStore.distributionOf[appId] = distributorsId;
}
}
emit Instantiated(distributorsId, appId, distributionVersion, newAppComponents, args);
for (uint256 i; i < newAppComponents.length; i++) {
distributorStore.appComponents[appId].push(newAppComponents[i]);
}
distributorStore.installers[appId] = msg.sender;
if (!externallyInitialized) {
try IDistribution(distributionLocation).instantiate(args) returns (
address[] memory _newAppComponents,
bytes32 _distributionName,
uint256 _distributionVersion
) {
newAppComponents = _newAppComponents;
distributionName = _distributionName;
distributionVersion = _distributionVersion;
} catch Error(string memory reason) {
revert(reason);
} catch Panic(uint errorCode) {
revert DistributionInstantiationPanic(errorCode);
} catch (bytes memory lowLevelData) {
revert DistributionInstantiationFailed(lowLevelData);
}
} else {
(bool success, bytes memory result) = address(distributionComponent.initializer).delegatecall(
abi.encodeWithSelector(selector, distributionLocation, distributionName, distributionVersion, args)
);
if (!success) {
revert(string(result));
}
newAppComponents = abi.decode(result, (address[]));
}
{
uint256 instancesLength = newAppComponents.length;
// single write for distributionOf
distributorStore.distributionOf[appId] = distributorsId;
for (uint256 i; i < instancesLength; ++i) {
distributorStore.appIds[newAppComponents[i]] = appId;
}
}
emit Instantiated(distributorsId, appId, distributionVersion, newAppComponents, args);
for (uint256 i; i < newAppComponents.length; i++) {
distributorStore.appComponents[appId].push(newAppComponents[i]);
}
distributorStore.installers[appId] = msg.sender;

peersky added 2 commits May 2, 2025 19:38
- Introduced `MockDistribution`, `MockERC165WithFunctions`, `MockERC7746Target`, and `MockRepository` to facilitate testing of distribution and middleware interactions.
- Enhanced `TokenizedDistributor` tests to cover instantiation and distribution management scenarios.
- Updated `AuthorizationMiddleware` tests to improve coverage and error handling.
- Added `MiddlewareProxy` tests to validate proxy functionality and middleware interactions.
- Refactored existing tests for clarity and maintainability, ensuring comprehensive coverage of new mock implementations.
…lerClonable, and MiddlewareProxy

- Removed skipped tests in `RepositoryExtended` and `AuthorizationMiddleware` to improve test coverage.
- Updated `InstallerClonable` tests to enable previously skipped tests for whitelisted distributions.
- Cleaned up `MiddlewareProxy` tests by removing skipped test cases, enhancing overall test clarity and functionality.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🔭 Outside diff range comments (1)
test/eds/Distributor.ts (1)

1-1: 🛠️ Refactor suggestion

Consider splitting this large test file into multiple focused files.

At over 2,100 lines, this test file has become difficult to maintain. Consider splitting it into multiple smaller, focused test files organized by feature areas.

A possible organization:

  • DistributorBasics.test.ts - Core functionality
  • DistributorAPI.test.ts - API interaction tests
  • VersionManagement.test.ts - Version-related tests
  • MigrationStrategies.test.ts - Migration tests
  • ExternalInitializers.test.ts - Initializer tests
  • MiddlewareHooks.test.ts - Hook-related tests

This would improve:

  1. Test maintainability
  2. Run speeds (if running specific test files)
  3. Readability and organization
  4. Parallel test execution

Also applies to: 2164-2164

🧹 Nitpick comments (14)
src/mocks/MockERC7746Target.sol (2)

11-13: Consider adding a null address check.

The setMiddleware function accepts any address without validation. While this might be acceptable for a mock, adding a simple check could prevent potential test issues.

function setMiddleware(address _middleware) external {
+    require(_middleware != address(0), "Zero address not allowed");
    middleware = _middleware;
}

21-23: Consider checking the afterCall result.

The afterCall result is currently ignored. While the IERC7746 spec might not require returning a value, it's good practice to capture any potential return value to avoid compiler warnings.

// Call middleware.afterCall
- IERC7746(middleware).afterCall("", selector, msg.sender, 0, data, beforeResult);
+ (bool success,) = middleware.call(
+     abi.encodeWithSelector(
+         IERC7746.afterCall.selector,
+         "",
+         selector,
+         msg.sender,
+         0,
+         data,
+         beforeResult
+     )
+ );
+ require(success, "afterCall failed");
test/distributors/TokenizedDistributor.test.ts (4)

6-16: Consider replacing interface definitions with direct imports.

The comment "Fix for LibSemver import" suggests this is a workaround. Consider importing these types directly from the contract or a shared types file to reduce maintenance overhead.

-// Fix for LibSemver import
-interface VersionStruct {
-  major: number;
-  minor: number;
-  patch: number;
-}
-
-interface VersionRequirementStruct {
-  version: VersionStruct;
-  requirement: number;
-}
+import { VersionStruct, VersionRequirementStruct } from "../../typechain-types/common";

Additionally, consider adding constants to clarify what the requirement values represent (e.g., const REQUIREMENT_EXACT = 0).


30-135: Consider refactoring the test fixture for better maintainability.

The fixture is quite lengthy and complex, with multiple setup steps and some duplication (e.g., code hash calculation). Consider breaking it into smaller, focused fixtures or helper functions.

For example, you could extract functions for:

  • Token distribution setup
  • Distribution registration
  • Repository setup

This would make the main fixture more readable and easier to maintain.


326-340: Consider expanding distribution tests.

The current tests verify that default costs are set for distributions added in the fixture, but don't directly test the addDistribution function's behavior.

Consider adding tests that:

  1. Directly call addDistribution and verify costs
  2. Test changing the default cost before adding a new distribution
  3. Verify other distribution properties beyond cost

205-324: Comprehensive payment testing, consider additional scenarios.

The payment testing is thorough, covering default costs, specific costs, and error cases.

Consider adding tests for:

  1. Changing the beneficiary mid-test to ensure payments redirect correctly
  2. Testing with a zero instantiation cost (if supported) to ensure no payment is required
  3. Verifying payment behavior when a distribution is removed and re-added
src/mocks/MockERC165WithFunctions.sol (3)

86-99: Consider enhancing the _getSelectorString implementation.

The current implementation creates a fixed 10-byte array and stores the selector in it, which is a simplistic approach. For more reliable testing, consider implementing a mapping of known function selectors to their string representations, which would provide more accurate function signature matching.

-    function _getSelectorString(bytes4 selector) internal pure returns (string memory) {
-        // For test purposes, this function would return a hardcoded value
-        // for known selectors or a default representation for unknown ones
-
-        // This is a simplification - in practice, you'd match known selectors
-        // or use the string literal signature from the start
-        bytes memory b = new bytes(10);
-        assembly {
-            mstore(add(b, 32), selector)
-        }
-
-        // Return empty string as fallback
-        return string(b);
+    // Known selector mapping
+    mapping(bytes4 => string) private _knownSelectors;
+    
+    // Initialize known selectors in constructor or separate function
+    function registerKnownSelector(bytes4 selector, string memory signature) external {
+        _knownSelectors[selector] = signature;
+    }
+    
+    function _getSelectorString(bytes4 selector) internal view returns (string memory) {
+        // Check if we have a registered signature for this selector
+        if (bytes(_knownSelectors[selector]).length > 0) {
+            return _knownSelectors[selector];
+        }
+        
+        // Fallback to the simplistic approach
+        bytes memory b = new bytes(10);
+        assembly {
+            mstore(add(b, 32), selector)
+        }
+        
+        return string(b);
    }

62-65: Use caution with assembly code.

While the assembly code here is straightforward, it's worth noting that assembly usage always warrants extra scrutiny in code reviews and can make the code less portable across different Solidity versions.


38-40: Consider adding example usage in documentation.

The setFunctionResult function would benefit from an example in the documentation to show how to properly format the function signature string and how the results are used in tests.

 /**
  * @dev Sets the result of a function call
  * @param functionSignature The function signature (e.g., "beforeCall(bytes,bytes4,address,uint256,bytes)")
  * @param result The bytes result to return when the function is called
+ * @example
+ * // Set return value for beforeCall function
+ * mockContract.setFunctionResult(
+ *   "beforeCall(bytes,bytes4,address,uint256,bytes)",
+ *   abi.encode(bytes("success"))
+ * );
  */
test/middleware/MiddlewareProxy.test.ts (1)

98-101: Improve implementation address verification in tests.

Rather than just checking that the proxy deploys successfully, consider adding specific tests to verify the implementation address is correctly set, possibly by:

  1. Using a proxy-specific method to retrieve the implementation address if available
  2. Calling a function on the proxy that should delegate to the implementation
  3. Checking storage slots directly if necessary
-    it("should verify implementation address is set", async function () {
-      // The implementation address should be the mockERC20 address
-      // But since we can't read it directly, we can only test for deployment success
-      expect(proxyContract.address).to.be.properAddress;
-    });
+    it("should verify implementation address is set", async function () {
+      // Verify the implementation by calling a function that should delegate to the implementation
+      const proxyAsERC20 = await ethers.getContractAt("MockERC20", proxyContract.address);
+      
+      // This should be delegated to the implementation
+      const name = await proxyAsERC20.name();
+      const symbol = await proxyAsERC20.symbol();
+      
+      // Verify we're getting the expected values from the implementation
+      expect(name).to.equal("TestToken");
+      expect(symbol).to.equal("TTK");
+    });
test/middleware/InstallerClonable.test.ts (2)

101-109: Use Chai's expect().to.be.revertedWith() pattern for testing reverts.

The test uses try-catch blocks to test reverts instead of Chai's built-in revert checking functionality. This makes the tests less readable and loses the ability to verify specific error messages.

-    it("Should revert if non-owner tries to whitelist a distributor", async function () {
-      try {
-        await installer.connect(user).whitelistDistributor(distributor.address);
-        expect.fail("Should have reverted");
-      } catch (error: any) {
-        // Test passes as long as there's an error
-        expect(error).to.exist;
-      }
-    });
+    it("Should revert if non-owner tries to whitelist a distributor", async function () {
+      await expect(
+        installer.connect(user).whitelistDistributor(distributor.address)
+      ).to.be.revertedWith("Ownable: caller is not the owner");
+      // Or if using custom errors:
+      // ).to.be.revertedWithCustomError(installer, "OwnableUnauthorizedAccount");
+    });

If viaIR optimization is causing issues with exact error matching, you can still use the more concise form:

-    it("Should revert if disallowing distribution for whitelisted distributor", async function () {
-      // First make sure distributor IS whitelisted
-      if (!(await installer.isDistributor(distributor.address))) {
-        await installer.connect(owner).whitelistDistributor(distributor.address);
-      }
-
-      // Try to disallow a distribution for a whitelisted distributor - should revert
-      try {
-        await installer.connect(owner).disallowDistribution(distributor.address, DISTRIBUTION_ID_1);
-        expect.fail("Should have reverted with DisallowDistOnWhitelistedDistributor");
-      } catch (error: any) {
-        // We can't reliably check the exact error due to viaIR, so just check that it failed
-        expect(error).to.exist;
-      }
-    });
+    it("Should revert if disallowing distribution for whitelisted distributor", async function () {
+      // First make sure distributor IS whitelisted
+      if (!(await installer.isDistributor(distributor.address))) {
+        await installer.connect(owner).whitelistDistributor(distributor.address);
+      }
+
+      // Try to disallow a distribution for a whitelisted distributor - should revert
+      await expect(
+        installer.connect(owner).disallowDistribution(distributor.address, DISTRIBUTION_ID_1)
+      ).to.be.reverted;
+    });

Also applies to: 126-133, 163-169, 213-219


263-286: Move mock contract definitions to separate files.

The file includes commented-out code for mock contracts that are intended to be moved to separate files. This clutters the test file and makes it harder to maintain.

Consider:

  1. Removing these commented-out contract definitions if they're already implemented elsewhere
  2. Moving them to proper files under contracts/mocks/ if they're still needed
  3. Adding a TODO comment with a specific action plan if implementation is pending
src/mocks/MockDistribution.sol (1)

8-39: Make the mock distribution more configurable for testing.

While the mock is functional, it always returns its own address as the distribution instance, which limits testing flexibility. Consider making the behavior configurable to cover more test scenarios.

 contract MockDistribution is IDistribution, ERC165 {
     ShortString private immutable distributionName;
     uint256 private constant distributionVersion = 1;
+    address[] private mockInstances;
+    bool private shouldRevert;
 
     constructor() {
         distributionName = ShortStrings.toShortString("MockDistribution");
+        // Initialize with contract's own address by default
+        mockInstances = new address[](1);
+        mockInstances[0] = address(this);
     }
 
+    function setMockInstances(address[] memory _instances) external {
+        mockInstances = _instances;
+    }
+    
+    function setShouldRevert(bool _shouldRevert) external {
+        shouldRevert = _shouldRevert;
+    }
+
     function contractURI() external pure override returns (string memory) {
         return "MockDistribution";
     }
 
     function instantiate(bytes memory) external override returns (address[] memory, bytes32, uint256) {
-        address[] memory instances = new address[](1);
-        instances[0] = address(this);
+        if (shouldRevert) {
+            revert("Mock revert");
+        }
 
         // Emit the required event
-        emit Distributed(msg.sender, instances);
+        emit Distributed(msg.sender, mockInstances);
 
-        return (instances, ShortString.unwrap(distributionName), distributionVersion);
+        return (mockInstances, ShortString.unwrap(distributionName), distributionVersion);
     }
 
     function get() external view override returns (address[] memory, bytes32, uint256) {
-        address[] memory sources = new address[](1);
-        sources[0] = address(this);
-        return (sources, ShortString.unwrap(distributionName), distributionVersion);
+        return (mockInstances, ShortString.unwrap(distributionName), distributionVersion);
     }

This enhancement allows:

  1. Setting custom instance addresses to test different scenarios
  2. Forcing reverts to test error handling
  3. Reusing the same configurable mock for different test cases
test/eds/Distributor.ts (1)

653-1555: Consider refactoring complex migration tests into smaller, focused test files.

The version management and migrations tests are very comprehensive but also complex and lengthy. This can make maintenance challenging and make it harder to understand failures when they occur.

Consider:

  1. Breaking these tests into separate, focused test files organized by feature
  2. Using more helper functions to reduce repetition
  3. Adding detailed documentation explaining the migration workflows being tested

For example, you could split into:

  • VersionManagement.test.ts - Basic version requirement functions
  • MigrationStrategies.test.ts - Tests for different migration approaches
  • MigrationErrorHandling.test.ts - Error cases and validation
  • UpgradeSecurity.test.ts - Authorization and permission tests
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between a039596 and 6e3b38f.

📒 Files selected for processing (10)
  • src/mocks/MockDistribution.sol (1 hunks)
  • src/mocks/MockERC165WithFunctions.sol (1 hunks)
  • src/mocks/MockERC7746Target.sol (1 hunks)
  • src/mocks/MockRepository.sol (1 hunks)
  • test/distributors/TokenizedDistributor.test.ts (1 hunks)
  • test/eds/Distributor.ts (6 hunks)
  • test/eds/TokenizedDistributor.ts (5 hunks)
  • test/middleware/AuthorizationMiddleware.test.ts (1 hunks)
  • test/middleware/InstallerClonable.test.ts (1 hunks)
  • test/middleware/MiddlewareProxy.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/middleware/AuthorizationMiddleware.test.ts
  • test/eds/TokenizedDistributor.ts
  • src/mocks/MockRepository.sol
🧰 Additional context used
🧬 Code Graph Analysis (1)
test/eds/Distributor.ts (1)
deploy/ERC7744.js (3)
  • deployments (5-5)
  • result (11-15)
  • codeHash (19-19)
🔇 Additional comments (12)
src/mocks/MockERC7746Target.sol (2)

1-5: Import and version constraints look good.

The Solidity version constraint (>=0.8.0 <0.9.0) is appropriate for modern contracts, and importing the IERC7746 interface correctly sets up this mock for middleware interactions.


6-10: Contract structure is appropriate for a mock target.

The contract correctly defines a middleware address that can be set externally and a custom error for propagating middleware failures - this is a good practice for error handling.

test/distributors/TokenizedDistributor.test.ts (5)

103-107: Good use of explicit function signatures for overloaded methods.

Using explicit function signatures for overloaded methods ensures the correct implementation is called and makes the code more maintainable.


279-300: Thorough edge case handling for insufficient balance.

The test effectively sets up the preconditions for testing an insufficient balance scenario, and correctly expects the appropriate error. Good job on testing this edge case!


141-156: Good initialization verification.

The tests correctly verify that all constructor parameters are properly initialized in the contract. This ensures the contract starts in the expected state.


159-179: Good event verification for state changes.

The test correctly verifies that the InstantiationCostChanged event is emitted with the expected parameters when the instantiation cost is changed. This is a best practice for testing state changes.


286-291: Smart workaround for ERC20 testing.

Using a non-zero dummy recipient address to avoid the ERC20InvalidReceiver error shows good understanding of ERC20 token behavior. This demonstrates attention to detail in setting up the test scenario.

src/mocks/MockERC165WithFunctions.sol (2)

1-127: Overall well-structured mock implementation with good documentation.

This mock contract is well-designed for testing ERC165 interface support and simulating function calls with predefined return values, which is essential for testing middleware hooks and interface conformance.


103-115:

Details

❌ Incorrect review comment

Validate middleware function signature against standard interfaces.

Ensure these middleware hook signatures match any standard interface you're implementing (such as ERC7746 or similar). If you're creating a custom middleware pattern, document this in the contract comments to clarify the expected hook interface.


🏁 Script executed:

#!/bin/bash
# Check if the contract implements a standard interface for middleware hooks
rg -l "interface.*Middleware|ERC7746|Hook" --type solidity

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Search for ERC7746 references and middleware hooks in solidity files
rg -l "ERC7746" -g "*.sol" || true
rg -l -iE "hook|middleware" -g "*.sol" || true

Length of output: 1182


🏁 Script executed:

#!/bin/bash
# Locate implementations of beforeCall in middleware contracts
rg -n "beforeCall" -g "src/middleware/*.sol" || true

# Show the beginning of the ERC7746 interface for potential hook definitions
sed -n '1,200p' src/interfaces/IERC7746.sol || true

Length of output: 3402


Ignore middleware interface validation here
The beforeCall in MockERC165WithFunctions.sol is part of an ERC-165 test mock, not an ERC-7746 middleware hook. It isn’t intended to implement IERC7746 or any standardized hook interface, so no changes or documentation updates for middleware signatures are required.

Likely an incorrect or invalid review comment.

test/eds/Distributor.ts (3)

1102-1186: Uncomment and finish implementing the cross-app call prevention test.

There's a commented-out test for cross-app call prevention that appears incomplete. Given the middleware security implications, this test should be implemented to ensure proper validation and isolation between different apps.

The test is attempting to verify that calls between different app components are properly validated and prevented when unauthorized. Since this relates to security boundaries, having comprehensive tests for this functionality is important.

Consider:

  1. Uncommenting and completing the test
  2. Adding more detailed assertions
  3. Testing both positive and negative cases

304-382: Well-structured tests for error handling scenarios.

The error handling tests for instantiation are very thorough, covering different failure modes including regular reverts, panics, and low-level errors. This is an excellent approach for ensuring robust error handling and providing clear feedback to users.


35-50: Good helper function for creating version requirements.

The createVersionRequirement helper function is well-designed and promotes code reuse throughout the test suite. This approach makes the tests more readable and less prone to errors when working with version structures.

Comment on lines +15 to +30
function executeWithSelector(bytes4 selector, bytes calldata data) external returns (bytes memory) {
// Call middleware.beforeCall
try IERC7746(middleware).beforeCall("", selector, msg.sender, 0, data) returns (bytes memory beforeResult) {
// The actual execution would happen here
bytes memory result = "0x1234"; // Mock result

// Call middleware.afterCall
IERC7746(middleware).afterCall("", selector, msg.sender, 0, data, beforeResult);

return result;
} catch Error(string memory reason) {
revert MiddlewareReverted(reason);
} catch {
revert MiddlewareReverted("unknown error");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add middleware null check and consider more flexible mock values.

The function has the following potential issues:

  1. No check if middleware is set before usage
  2. Hardcoded empty values for context and amount parameters
  3. Mock result is represented as a string "0x1234" which is not a proper hexadecimal value
function executeWithSelector(bytes4 selector, bytes calldata data) external returns (bytes memory) {
+    require(middleware != address(0), "Middleware not set");
    // Call middleware.beforeCall
    try IERC7746(middleware).beforeCall("", selector, msg.sender, 0, data) returns (bytes memory beforeResult) {
        // The actual execution would happen here
-        bytes memory result = "0x1234"; // Mock result
+        bytes memory result = hex"1234"; // Properly formatted mock result

        // Call middleware.afterCall
        IERC7746(middleware).afterCall("", selector, msg.sender, 0, data, beforeResult);

        return result;
    } catch Error(string memory reason) {
        revert MiddlewareReverted(reason);
    } catch {
        revert MiddlewareReverted("unknown error");
    }
}

For greater testing flexibility, you might consider adding parameters for context and value, rather than hardcoding empty values.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function executeWithSelector(bytes4 selector, bytes calldata data) external returns (bytes memory) {
// Call middleware.beforeCall
try IERC7746(middleware).beforeCall("", selector, msg.sender, 0, data) returns (bytes memory beforeResult) {
// The actual execution would happen here
bytes memory result = "0x1234"; // Mock result
// Call middleware.afterCall
IERC7746(middleware).afterCall("", selector, msg.sender, 0, data, beforeResult);
return result;
} catch Error(string memory reason) {
revert MiddlewareReverted(reason);
} catch {
revert MiddlewareReverted("unknown error");
}
}
function executeWithSelector(bytes4 selector, bytes calldata data) external returns (bytes memory) {
require(middleware != address(0), "Middleware not set");
// Call middleware.beforeCall
try IERC7746(middleware).beforeCall("", selector, msg.sender, 0, data) returns (bytes memory beforeResult) {
// The actual execution would happen here
bytes memory result = hex"1234"; // Properly formatted mock result
// Call middleware.afterCall
IERC7746(middleware).afterCall("", selector, msg.sender, 0, data, beforeResult);
return result;
} catch Error(string memory reason) {
revert MiddlewareReverted(reason);
} catch {
revert MiddlewareReverted("unknown error");
}
}

Comment on lines 46 to 101
describe("Proxy Functionality", function () {
it("should create middleware proxy with the correct layer info", async function () {
// We can't directly inspect layers in the proxy, so we'll test indirectly
// by checking the deployment works
expect(proxyContract.address).to.not.be.null;
});

it("should forward funds to proxy", async function () {
// The proxy should be able to receive funds without reverting
// We'll just skip this test for now as it's problematic
this.skip();
});

it("should increase middleware coverage", async function () {
// Let's access the middleware directly to increase coverage
await mockMiddleware.resetCounters();

// Call middleware methods directly
const configData = "0x1234";
const selector = "0x12345678";
const value = ethers.utils.parseEther("0");
const callData = "0x";

await mockMiddleware.beforeCall(configData, selector, ownerAddress, value, callData);
expect(await mockMiddleware.beforeCallCalled()).to.be.true;

await mockMiddleware.afterCall(configData, selector, ownerAddress, value, callData, "0x");
expect(await mockMiddleware.afterCallCalled()).to.be.true;

// Verify call arguments are stored correctly
const beforeCallArgs = await mockMiddleware.getLastBeforeCallArgs();
expect(beforeCallArgs.configData).to.equal(configData);
expect(beforeCallArgs.selector).to.equal(selector);
expect(beforeCallArgs.sender).to.equal(ownerAddress);
});

it("should allow setting beforeCall return value", async function () {
const returnValue = "0xabcd1234";
await mockMiddleware.setBeforeCallReturn(returnValue);

// Check that the value was set correctly
const storedValue = await mockMiddleware.beforeCallReturnValue();
expect(storedValue).to.equal(returnValue);

// Trigger a call that will use this return value
await mockMiddleware.beforeCall("0x", "0x12345678", ownerAddress, 0, "0x");

// The mock should have returned the value we set
// We don't need to verify the return value directly since we confirmed it was stored
});

it("should verify implementation address is set", async function () {
// The implementation address should be the mockERC20 address
// But since we can't read it directly, we can only test for deployment success
expect(proxyContract.address).to.be.properAddress;
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add tests for actual proxy delegation behavior.

The current tests focus on middleware capabilities but don't test the core functionality of a proxy: delegating calls to the implementation contract. Consider adding tests that:

  1. Call ERC20 functions through the proxy
  2. Verify that state changes in the implementation are reflected when accessed through the proxy
  3. Test the interaction between middleware and implementation
it("should delegate calls to the implementation", async function () {
  // Wrap the proxy with the ERC20 interface to call implementation methods
  const proxyAsERC20 = await ethers.getContractAt("MockERC20", proxyContract.address);
  
  // Call functions on the implementation through the proxy
  const totalSupply = await proxyAsERC20.totalSupply();
  expect(totalSupply).to.equal(INITIAL_SUPPLY);
  
  // Test state-changing function
  const transferAmount = ethers.utils.parseEther("100");
  const userAddress = await user.getAddress();
  
  await proxyAsERC20.connect(owner).transfer(userAddress, transferAmount);
  
  // Check that the state change was persisted in the implementation
  const userBalance = await proxyAsERC20.balanceOf(userAddress);
  expect(userBalance).to.equal(transferAmount);
});

it("should execute middleware hooks during delegation", async function() {
  // Reset middleware counters
  await mockMiddleware.resetCounters();
  
  // Perform a transaction through the proxy
  const proxyAsERC20 = await ethers.getContractAt("MockERC20", proxyContract.address);
  const userAddress = await user.getAddress();
  await proxyAsERC20.connect(owner).transfer(userAddress, ethers.utils.parseEther("1"));
  
  // Verify middleware hooks were called
  expect(await mockMiddleware.beforeCallCalled()).to.be.true;
  expect(await mockMiddleware.afterCallCalled()).to.be.true;
});

Comment on lines 53 to 57
it("should forward funds to proxy", async function () {
// The proxy should be able to receive funds without reverting
// We'll just skip this test for now as it's problematic
this.skip();
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider implementing the skipped test case for proxy fund forwarding.

The test for forwarding funds to the proxy is currently skipped with a comment noting it's "problematic". This suggests a potential issue with a core functionality since proxies often need to receive ETH.

Could you provide details about why this test is problematic? If there's an issue with the proxy receiving funds, it could indicate:

  1. A missing receive() or fallback() function
  2. Issues with the delegation logic

🏁 Script executed:

#!/bin/bash
# Check if the contract has a receive or fallback function

# Find the middleware proxy contract
echo "Checking for receive/fallback functions in MiddlewareProxy contract:"
grep -A 10 "contract MiddlewareProxy" --include="*.sol" -r ./src | grep -E "receive\(\)|fallback\(\)"

Length of output: 250


I’ll locate the exact contract file and explicitly check for receive/fallback definitions:


🏁 Script executed:

#!/bin/bash
# 1. Find the file containing the MiddlewareProxy contract
echo "Locating MiddlewareProxy contract:"
rg -n "contract MiddlewareProxy" --include "*.sol"

# 2. Inspect the contract definition and search for receive()/fallback() within it
FILE=$(rg -l "contract MiddlewareProxy" --include "*.sol")
echo "Examining $FILE for receive/fallback functions:"
rg -A5 "contract MiddlewareProxy" "$FILE"
rg -n "receive\s*\(" "$FILE"
rg -n "fallback\s*\(" "$FILE"

Length of output: 1387


I’ll adjust the search to use --glob and fall back to fd if needed:


🏁 Script executed:

#!/bin/bash
# 1. Try to locate the MiddlewareProxy contract file via ripgrep
echo "Locating contract via ripgrep:"
FILE=$(rg -l "contract MiddlewareProxy" --glob "*.sol")

# 2. If not found, fallback to fd
if [ -z "$FILE" ]; then
  echo "Not found with ripgrep, trying fd:"
  FILE=$(fd --extension sol MiddlewareProxy.sol)
fi

if [ -z "$FILE" ]; then
  echo "ERROR: Could not locate MiddlewareProxy.sol"
  exit 1
fi

echo "Found file: $FILE"
echo "----- Contract Snippet -----"
rg -A20 "contract MiddlewareProxy" "$FILE"

echo "----- receive() occurrences -----"
rg -n "receive\s*\(" "$FILE" || echo "No receive() found"

echo "----- fallback() occurrences -----"
rg -n "fallback\s*\(" "$FILE" || echo "No fallback() found"

Length of output: 1734


Implement Ether‐forwarding test using the payable fallback()

The MiddlewareProxy contract (src/middleware/MiddlewareProxy.sol line 54) already defines a payable fallback() handler, so it can accept ETH. You can un-skip the test in test/middleware/MiddlewareProxy.test.ts:53–57 and assert that sending value to the proxy:

• Does not revert
• Updates the proxy’s balance as expected

For example:

- it("should forward funds to proxy", async function () {
-   // The proxy should be able to receive funds without reverting
-   // We'll just skip this test for now as it's problematic
-   this.skip();
- });
+ it("should forward funds to proxy", async function () {
+   const amount = ethers.utils.parseEther("1");
+   // send ETH directly to the proxy’s fallback()
+   await expect(
+     deployer.sendTransaction({ to: proxy.address, value: amount })
+   ).to.not.be.reverted;
+   // verify the proxy’s balance increased
+   const balance = await ethers.provider.getBalance(proxy.address);
+   expect(balance).to.equal(amount);
+ });

– File: test/middleware/MiddlewareProxy.test.ts (lines 53–57)
– Contract: src/middleware/MiddlewareProxy.sol (fallback() at line 54)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should forward funds to proxy", async function () {
// The proxy should be able to receive funds without reverting
// We'll just skip this test for now as it's problematic
this.skip();
});
it("should forward funds to proxy", async function () {
const amount = ethers.utils.parseEther("1");
// send ETH directly to the proxy’s fallback()
await expect(
deployer.sendTransaction({ to: proxy.address, value: amount })
).to.not.be.reverted;
// verify the proxy’s balance increased
const balance = await ethers.provider.getBalance(proxy.address);
expect(balance).to.equal(amount);
});

Comment on lines 233 to 256
it.skip("whitelistedDistributions should return all distributions for whitelisted distributor", async function () {
// Ensure distributor is whitelisted
await installer.connect(owner).whitelistDistributor(distributor.address);

// Get all the distributions (this will only work if getDistributions is properly implemented in the mock)
const allDistros = await distributor.getDistributions();
const listedDistros = await installer.whitelistedDistributions(distributor.address);
expect(listedDistros).to.deep.equal(allDistros);
});

it.skip("whitelistedDistributions should return only allowed distributions for non-whitelisted distributor", async function () {
// Ensure distributor is NOT whitelisted
if (await installer.isDistributor(distributor.address)) {
await installer.connect(owner).revokeWhitelistedDistributor(distributor.address);
}

// Allow a specific distribution
await installer.connect(owner).allowDistribution(distributor.address, DISTRIBUTION_ID_2);

// Get all the distributions
const listedDistros = await installer.whitelistedDistributions(distributor.address);
expect(listedDistros).to.deep.equal([DISTRIBUTION_ID_2]);
expect(listedDistros).to.not.include(DISTRIBUTION_ID_1);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider implementing the skipped tests or providing a clear roadmap.

Two tests for whitelistedDistributions functionality are skipped with a comment indicating they rely on mock functionality that may not be properly implemented. These tests verify key functionality of the contract, and leaving them skipped could result in incomplete test coverage.

Options to consider:

  1. Implement proper mock contracts to enable these tests
  2. Modify the tests to use available functionality
  3. Document why these scenarios can't or don't need to be tested

If the functionality being tested is critical, implement proper mocks. If not, consider adding more detailed comments explaining why these tests can be skipped safely.

Comment on lines +1960 to 2164
});
});

// Tests for external initializers
describe("External Initializer Tests", function () {
let mockInitializerAddress: string;
let mockNoReasonInitializerAddress: string;
let initializerDistributorsId: string;
let noReasonInitializerDistributorsId: string;

beforeEach(async function () {
// Deploy MockInitializer
const MockInitializer = (await ethers.getContractFactory(
"MockInitializer"
)) as MockInitializer__factory;
const mockInitializer = await MockInitializer.deploy();
await mockInitializer.deployed();
mockInitializerAddress = mockInitializer.address;

// Deploy MockNoReasonInitializer
const MockNoReasonInitializer = (await ethers.getContractFactory(
"MockNoReasonInitializer"
)) as MockNoReasonInitializer__factory;
const mockNoReasonInitializer = await MockNoReasonInitializer.deploy();
await mockNoReasonInitializer.deployed();
mockNoReasonInitializerAddress = mockNoReasonInitializer.address;

// Add distribution using the mock initializer address
await distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](cloneDistributionId, mockInitializerAddress, "initializerTest");

// Calculate the expected distributorId locally based on Distributor.sol logic
initializerDistributorsId = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["bytes32", "address"],
[cloneDistributionId, mockInitializerAddress]
)
);

// Verify it was added correctly by trying to fetch it
const fetchedId = await distributor.getIdFromAlias("initializerTest");
expect(fetchedId).to.equal(initializerDistributorsId);

// Add distribution using the no-reason initializer
await distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](cloneDistributionId, mockNoReasonInitializerAddress, "initializerNoReasonTest");

// Calculate the expected distributorId locally
noReasonInitializerDistributorsId = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["bytes32", "address"],
[cloneDistributionId, mockNoReasonInitializerAddress]
)
);

const fetchedNoReasonId = await distributor.getIdFromAlias("initializerNoReasonTest");
expect(fetchedNoReasonId).to.equal(noReasonInitializerDistributorsId);
});

it("should instantiate successfully when external initializer succeeds", async function () {
// Instantiate with empty args (should succeed in MockInitializer)
await expect(distributor.connect(owner).instantiate(initializerDistributorsId, "0x")).to.emit(
distributor,
"Instantiated"
);
});

it("should revert when external initializer fails with reason", async function () {
// Instantiate with args that cause MockInitializer to revert
const failArgs = ethers.utils.toUtf8Bytes("REVERT");
await expect(
distributor.connect(owner).instantiate(initializerDistributorsId, failArgs)
).to.be.revertedWith(""); // This is the actual behavior
});
});

// Tests for specific Distributor.sol lines
describe("Distributor Implementation Tests", function () {
// Test for line 78: Check that _addDistribution validates IRepository interface support
it("should revert when adding a non-IRepository contract to _addDistribution", async function () {
// Create a mock contract that doesn't implement IRepository
const MockERC20 = await ethers.getContractFactory("MockERC20");
const mockERC20 = await MockERC20.deploy("TestToken", "TST", ethers.utils.parseEther("1000"));
await mockERC20.deployed();

// Try to add it as a repository - should fail
await expect(
distributor
.connect(owner)
[
"addDistribution(address,address,((uint64,uint64,uint128),uint8),string)"
](mockERC20.address, ethers.constants.AddressZero, createVersionRequirement(1, 0, 0, 0), "InvalidRepoTest")
).to.be.revertedWithCustomError(distributor, "InvalidRepository");
});

// Test for line 83: Check that _addDistribution rejects version 0
it("should revert when adding a distribution with version 0", async function () {
// Try to add a distribution with version 0
await expect(
distributor
.connect(owner)
["addDistribution(address,address,((uint64,uint64,uint128),uint8),string)"](
repositoryAddress,
ethers.constants.AddressZero,
createVersionRequirement(0, 0, 0, 0), // Version 0.0.0
"ZeroVersionTest"
)
).to.be.revertedWithCustomError(distributor, "InvalidVersionRequested");
});

// Test for line 114-118: Check that bytes32-based _addDistribution validates IDistribution interface
it("should revert when adding a distribution that doesn't support IDistribution", async function () {
// Create a mock contract that doesn't implement IDistribution
const mockERC20 = await (
await ethers.getContractFactory("MockERC20")
).deploy("TestToken", "TST", ethers.utils.parseEther("1000"));
await mockERC20.deployed();

// Register the code hash in the code index to make the getContainerOrThrow call work
const code = await mockERC20.provider.getCode(mockERC20.address);
const codeHash = ethers.utils.keccak256(code);
await codeIndex.register(mockERC20.address);

// Try to add it as a distribution - should fail
await expect(
distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](codeHash, ethers.constants.AddressZero, "InvalidDistributionTest")
).to.be.revertedWith("Distribution does not support IDistribution interface");
});

// Test for line 203-205: Check that app components are properly registered in mappings
it("should properly register app components in mappings", async function () {
// We'll use the already added clone distribution from beforeEach
// No need to add it again, which could cause conflicts

// Get the alias for the already registered distribution
const cloneAlias = "Clone";

// First check if this alias exists
const existingDistributorId = await distributor.getIdFromAlias(cloneAlias);
let distributorIdToUse;

if (existingDistributorId !== ethers.constants.HashZero) {
// Use the existing one
distributorIdToUse = existingDistributorId;
} else {
// Register a new one with this alias
await distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](cloneDistributionId, ethers.constants.AddressZero, cloneAlias);
distributorIdToUse = await distributor.getIdFromAlias(cloneAlias);
}

// Instantiate the distribution
const tx = await distributor.connect(owner).instantiate(distributorIdToUse, "0x");
const receipt = await tx.wait();

// Get the appId and appComponents from the emitted event
let appId: any;
let componentAddresses: string[] = [];

for (const event of receipt.events || []) {
if (event.event === "Instantiated" && event.args) {
appId = event.args.newAppId;
componentAddresses = event.args.appComponents;
break;
}
}

expect(componentAddresses.length).to.be.greaterThan(
0,
"No components found in the instantiated distribution"
);

// Verify each component is properly registered
for (const componentAddress of componentAddresses) {
// Check appIds mapping: component address -> appId
const registeredAppId = await distributor.getAppId(componentAddress);
expect(registeredAppId).to.equal(appId);

// Check distributionOf mapping: appId -> distributorsId
const distributorsIdFromApp = await distributor.distributionOf(appId);
expect(distributorsIdFromApp).to.equal(distributorIdToUse);

// Verify component is in the appComponents array
const components = await distributor.appComponents(appId);
expect(components[0]).to.equal(componentAddresses[0]);
}

// Verify installer is set correctly
const installer = await distributor.installers(appId);
expect(installer).to.equal(owner.address);
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

External initializer tests need more failure scenarios.

The external initializer tests are good but could be expanded to cover more edge cases, particularly around error handling and validation.

Consider adding tests for:

  1. Initializers that return invalid data structures
  2. Initializers that run out of gas
  3. Re-entrancy scenarios
  4. Initializers that revert without specific reasons (currently tested)
  5. Cross-contract interactions during initialization
it("should handle initializers that consume excessive gas", async function() {
  // Deploy a gas-intensive initializer
  const GasIntensiveInitializer = await ethers.getContractFactory("MockGasIntensiveInitializer");
  const gasIntensiveInitializer = await GasIntensiveInitializer.deploy();
  await gasIntensiveInitializer.deployed();
  
  // Add distribution with this initializer
  await distributor
    .connect(owner)
    ["addDistribution(bytes32,address,string)"](
      cloneDistributionId, 
      gasIntensiveInitializer.address, 
      "gasTest"
    );
  
  const gasTestId = await distributor.getIdFromAlias("gasTest");
  
  // Try to instantiate with gas-intensive behavior
  await expect(
    distributor.connect(owner).instantiate(gasTestId, ethers.utils.toUtf8Bytes("HEAVY_COMPUTATION"))
  ).to.be.reverted;
});

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (5)
test/middleware/InstallerClonable.test.ts (1)

232-242: Consider implementing the skipped tests or providing a clear roadmap.

Two tests for whitelistedDistributions functionality are included but may not function correctly due to incomplete mock implementations.

test/eds/RepositoryExtended.ts (3)

270-277: Incorrect parameter packing when calling repository.get

The get method expects two separate parameters (VersionStruct, requirementsEnum), but a single object with both fields is passed, which will compile but encode incorrectly and revert.

-const source = await repository.get({
-  version: { major: 1, minor: 1, patch: 1 },
-  requirement: 1
-});
+const source = await repository.get(
+  { major: 1, minor: 1, patch: 1 },
+  1 // LibSemver.requirements.EXACT?
+);

303-306: Use custom-error matcher instead of plain revert string

changeMigrationScript in the repository reverts with a custom error (e.g. MajorVersionDoesNotExist()). The test currently asserts on a plain revert string which will not match and will cause a false-negative pass/fail.

-await expect(
-  repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash)
-).to.be.revertedWith("Major version does not exist");
+await expect(
+  repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash)
+).to.be.revertedWithCustomError(repository, "MajorVersionDoesNotExist");

360-376: ⚠️ Potential issue

Incorrect parameter packing when calling repository.get (multiple instances)

The get method expects two separate parameters (VersionStruct, requirementsEnum), but a single object with both fields is passed in multiple places. This will compile but encode incorrectly and revert at runtime.

-const source = await repository.get({
-  version: { major: 2, minor: 0, patch: 0 },
-  requirement: 6 // LESSER_EQUAL
-});
+const source = await repository.get(
+  { major: 2, minor: 0, patch: 0 },
+  6 // LESSER_EQUAL
+);

...

-const source2 = await repository.get({
-  version: { major: 2, minor: 2, patch: 0 },
-  requirement: 6 // LESSER_EQUAL
-});
+const source2 = await repository.get(
+  { major: 2, minor: 2, patch: 0 },
+  6 // LESSER_EQUAL
+);
test/eds/Distributor.ts (1)

1873-2077: Consider expanding external initializer tests

The external initializer tests are good but could be expanded to cover more edge cases, particularly around error handling and validation.

Consider adding tests for:

  1. Initializers that return invalid data structures
  2. Initializers that run out of gas
  3. Re-entrancy scenarios
  4. Cross-contract interactions during initialization
it("should handle initializers that consume excessive gas", async function() {
  // Deploy a gas-intensive initializer
  const GasIntensiveInitializer = await ethers.getContractFactory("MockGasIntensiveInitializer");
  const gasIntensiveInitializer = await GasIntensiveInitializer.deploy();
  await gasIntensiveInitializer.deployed();
  
  // Add distribution with this initializer
  await distributor
    .connect(owner)
    ["addDistribution(bytes32,address,string)"](
      cloneDistributionId, 
      gasIntensiveInitializer.address, 
      "gasTest"
    );
  
  const gasTestId = await distributor.getIdFromAlias("gasTest");
  
  // Try to instantiate with gas-intensive behavior
  await expect(
    distributor.connect(owner).instantiate(gasTestId, ethers.utils.toUtf8Bytes("HEAVY_COMPUTATION"))
  ).to.be.reverted;
});
🧹 Nitpick comments (5)
test/middleware/InstallerClonable.test.ts (3)

101-109: Improve error testing for non-owner whitelist attempt.

The test only checks for the existence of an error without verifying the specific error type or message, which could mask incorrect error handling in the contract.

Use more specific error checking by testing for the Ownable: caller is not the owner error message or error code:

  it("Should revert if non-owner tries to whitelist a distributor", async function () {
-    try {
-      await installer.connect(user).whitelistDistributor(distributor.address);
-      expect.fail("Should have reverted");
-    } catch (error: any) {
-      // Test passes as long as there's an error
-      expect(error).to.exist;
-    }
+    await expect(
+      installer.connect(user).whitelistDistributor(distributor.address)
+    ).to.be.revertedWith("Ownable: caller is not the owner");
  });

163-169: Enhance specificity of error checking in tests.

The test expects a revert with a specific error ("alreadyAllowed") but doesn't actually check for this specific error message, making the test less precise.

While the comment indicates viaIR optimization affects error checking, consider using a more reliable approach:

-  try {
-    await installer.connect(owner).allowDistribution(distributor.address, DISTRIBUTION_ID_1);
-    expect.fail("Should have reverted with alreadyAllowed");
-  } catch (error: any) {
-    // We can't reliably check the exact error due to viaIR, so just check that it failed
-    expect(error).to.exist;
-  }
+  // Use a more resilient approach with regexp that works even with viaIR
+  await expect(
+    installer.connect(owner).allowDistribution(distributor.address, DISTRIBUTION_ID_1)
+  ).to.be.reverted;
+  // If you can match partial strings in errors, use:
+  // .to.be.revertedWithCustomError(installer, /AlreadyAllowed/);

259-261: Clarify the "remaining tests" comment.

The comment implies there are remaining tests to be implemented, but none appear after this comment. This could confuse future contributors.

Either remove this comment if no additional tests are planned, or provide more specificity about what tests are missing:

-  // We can skip the remaining tests as they depend on complex fixture setup
-  // that would require more complicated mock configurations
+  // TODO: Implement additional tests for:
+  // 1. Testing middleware hooks integration
+  // 2. Testing proxy-based upgradability
+  // These will require more sophisticated mock contracts
test/eds/RepositoryExtended.ts (2)

382-390: Consider adding assertions for skipped version resolution tests

These tests are currently skipped but provide valuable validation for version resolution logic. Consider implementing proper assertions and enabling these tests to ensure repository version resolution works correctly for all strategies.

it("should resolve LESSER requirement correctly", async function () {
  // Request version < 2.0.0, expect highest below that
  const source = await repository.get(
    { major: 2, minor: 0, patch: 0 },
    5 // LESSER
  );
  
  // Should get 1.1.1
  expect(source.version.major).to.equal(1);
  expect(source.version.minor).to.equal(1);
  expect(source.version.patch).to.equal(1);
});

it("should resolve ANY requirement correctly", async function () {
  // Request ANY version, expect latest
  const source = await repository.get(
    { major: 0, minor: 0, patch: 0 },
    3 // ANY
  );
  
  // Should get the highest version 2.1.0
  expect(source.version.major).to.equal(2);
  expect(source.version.minor).to.equal(1);
  expect(source.version.patch).to.equal(0);
});

298-300: Consider implementing the skipped migration script test

The skipped test for changing migration scripts for existing major versions leaves an important use case untested. Consider implementing this test to ensure this feature works correctly.

it("should allow changing migration script for existing major version", async function () {
  // Deploy a new migration contract
  const NewMockMigration = await ethers.getContractFactory("MockMigration");
  const newMigration = await NewMockMigration.deploy();
  await newMigration.deployed();
  const newMigrationCode = await newMigration.provider.getCode(newMigration.address);
  const newMigrationCodeHash = ethers.utils.keccak256(newMigrationCode);
  
  // Change the migration script for major version 1
  await expect(
    repository.connect(owner).changeMigrationScript(1, newMigrationCodeHash)
  ).to.emit(repository, "MigrationScriptChanged")
    .withArgs(1, newMigrationCodeHash);
    
  // Verify the migration script was updated
  const migrationHash = await repository.getMigrationScript(1);
  expect(migrationHash).to.equal(newMigrationCodeHash);
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 6e3b38f and d9c231c.

📒 Files selected for processing (5)
  • test/eds/Distributor.ts (6 hunks)
  • test/eds/RepositoryExtended.ts (1 hunks)
  • test/middleware/AuthorizationMiddleware.test.ts (1 hunks)
  • test/middleware/InstallerClonable.test.ts (1 hunks)
  • test/middleware/MiddlewareProxy.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/middleware/MiddlewareProxy.test.ts
  • test/middleware/AuthorizationMiddleware.test.ts
🔇 Additional comments (3)
test/eds/Distributor.ts (3)

35-50: Great addition: Version requirement helper function

The createVersionRequirement helper function is a good addition that improves code readability and maintainability by centralizing the creation of version requirement objects. This follows the DRY principle and makes the tests more consistent.


385-587: Comprehensive Distributor API tests

The new Distributor API tests provide excellent coverage of the core functionality including ID retrieval, alias management, app component mapping, distribution URI access, and duplicate prevention. This significantly improves the test coverage and helps validate the contract's public interface.


592-1700: Extensive version management and migration tests

The added version management and migration tests thoroughly cover important scenarios:

  • Different migration strategies (CALL, DELEGATECALL, REPOSITORY_MANAGED)
  • Error handling in migrations (revert, panic, error)
  • Version change restrictions
  • Migration script validation
  • Proper ownership transfer during upgrades
  • Hook behavior during migrations

This greatly enhances the reliability of these critical features.

Comment on lines +263 to +286
// Mock Contracts (Placeholders - Implement actual mocks as needed)
// You would typically place these in separate files under contracts/mocks/
/*
contract MockDistributor is IDistributor {
// ... Implement mock functions, potentially storing last called params ...
address[] public lastInstantiateResult;
bytes32 public lastInstantiateDistId;
uint256 public lastInstantiateVersion;
function setInstantiateResult(address[] memory _r, bytes32 _id, uint256 _v) external { ... }
function instantiate(...) external payable returns (...) { return (lastInstantiateResult, lastInstantiateDistId, lastInstantiateVersion); }
// ... other mocks ...
}
contract MockERC7746 is IERC7746 {
// ... Implement mock functions ...
}
contract MockApp {
function setValue(uint256) external {}
}
contract MockAdminUpgradeableProxy {
// ... Mock proxy ...
}
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement the mock contracts instead of keeping placeholders.

The file contains commented-out mock contract definitions rather than actual implementations, which likely contributes to skipped tests and conditional logic in the test setup.

Move these from comments to actual contract files in a contracts/mocks/ directory and implement them properly to enable complete test coverage.

For example:

// File: contracts/mocks/MockOwnableDistributor.sol
pragma solidity ^0.8.0;

import "../interfaces/IDistributor.sol";

contract MockOwnableDistributor is IDistributor {
    address public owner;
    bytes32[] private _distributions = [
        bytes32("distro1"),
        bytes32("distro2")
    ];
    
    constructor(address _owner) {
        owner = _owner;
    }
    
    function getDistributions() external view returns (bytes32[] memory) {
        return _distributions;
    }
    
    function addDistribution(
        address implementation,
        address metadata,
        IDistributor.VersionData memory versionData,
        string memory name
    ) external {
        // Implementation logic
    }
    
    // Implement other required interface methods
}

Comment on lines 1 to 261
import { expect } from "chai";
import { ethers } from "hardhat";
import { MockOwnableDistributor, OwnableInstaller } from "../../types";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";

describe("InstallerClonable & InstallerOwnable", function () {
// Constants
const DISTRIBUTION_ID_1 = ethers.utils.formatBytes32String("distro1");
const DISTRIBUTION_ID_2 = ethers.utils.formatBytes32String("distro2");

let installer: OwnableInstaller;
let distributor: MockOwnableDistributor;
let owner: SignerWithAddress;
let user: SignerWithAddress;
let targetAccount: SignerWithAddress;

async function simpleMockSetup() {
const [owner, targetAccount, user] = await ethers.getSigners();

// Deploy mock distributor
const MockDistributorFactory = await ethers.getContractFactory("MockOwnableDistributor");
const distributor = await MockDistributorFactory.deploy(owner.address);
await distributor.deployed();

// Deploy another mock distributor
const distributor2 = await MockDistributorFactory.deploy(owner.address);
await distributor2.deployed();

// Deploy InstallerOwnable
const InstallerFactory = await ethers.getContractFactory("OwnableInstaller");
const installer = await InstallerFactory.deploy(targetAccount.address, owner.address);
await installer.deployed();

// Add a test distribution to distributor
if (typeof distributor.addDistribution === "function") {
await distributor.addDistribution(
ethers.constants.AddressZero, // just a dummy address
ethers.constants.AddressZero,
{
version: { major: 1, minor: 0, patch: 0 },
requirement: 0
},
"TestDistribution"
);
}

// Manually register distribution IDs that we'll use in tests
let distIds: string[] = [];
try {
distIds = await distributor.getDistributions();
} catch (e) {
// If getDistributions fails, use our test IDs
distIds = [DISTRIBUTION_ID_1];
}

return {
owner,
targetAccount,
user,
distributor,
distributor2,
installer,
distIds
};
}

beforeEach(async function () {
const setup = await simpleMockSetup();
owner = setup.owner;
targetAccount = setup.targetAccount;
user = setup.user;
distributor = setup.distributor as MockOwnableDistributor;
installer = setup.installer as OwnableInstaller;
});

describe("Deployment & Initialization", function () {
it("Should set the correct owner", async function () {
expect(await installer.owner()).to.equal(owner.address);
});

it("Should set the correct target account", async function () {
expect(await installer.target()).to.equal(targetAccount.address);
});
});

describe("Distributor & Distribution Permissions (Ownable)", function () {
it("Should allow owner to whitelist a distributor", async function () {
// First make sure distributor is NOT whitelisted
if (await installer.isDistributor(distributor.address)) {
await installer.connect(owner).revokeWhitelistedDistributor(distributor.address);
}

// Now whitelist it and check that it's whitelisted
const tx = await installer.connect(owner).whitelistDistributor(distributor.address);
await tx.wait();

// Check isDistributor directly
expect(await installer.isDistributor(distributor.address)).to.be.true;
});

it("Should revert if non-owner tries to whitelist a distributor", async function () {
try {
await installer.connect(user).whitelistDistributor(distributor.address);
expect.fail("Should have reverted");
} catch (error: any) {
// Test passes as long as there's an error
expect(error).to.exist;
}
});

it("Should allow owner to revoke a whitelisted distributor", async function () {
// First make sure distributor IS whitelisted
if (!(await installer.isDistributor(distributor.address))) {
await installer.connect(owner).whitelistDistributor(distributor.address);
}

// Now revoke it
const tx = await installer.connect(owner).revokeWhitelistedDistributor(distributor.address);
await tx.wait();

// Check isDistributor directly
expect(await installer.isDistributor(distributor.address)).to.be.false;
});

it("Should revert if non-owner tries to revoke a distributor", async function () {
try {
await installer.connect(user).revokeWhitelistedDistributor(distributor.address);
expect.fail("Should have reverted");
} catch (error: any) {
// Test passes as long as there's an error
expect(error).to.exist;
}
});

it("Should allow owner to allow a specific distribution", async function () {
// First make sure distributor is NOT whitelisted (we need to test specific distribution allowance)
if (await installer.isDistributor(distributor.address)) {
await installer.connect(owner).revokeWhitelistedDistributor(distributor.address);
}

// Now allow a specific distribution
const tx = await installer
.connect(owner)
.allowDistribution(distributor.address, DISTRIBUTION_ID_1);
await tx.wait();

// Check that the distribution is in the list of allowed distributions
const allowedDists = await installer.whitelistedDistributions(distributor.address);
expect(
allowedDists.some(
(dist) => ethers.utils.hexlify(dist) === ethers.utils.hexlify(DISTRIBUTION_ID_1)
)
).to.be.true;
});

it("Should revert if allowing distribution for whitelisted distributor", async function () {
// First make sure distributor IS whitelisted
if (!(await installer.isDistributor(distributor.address))) {
await installer.connect(owner).whitelistDistributor(distributor.address);
}

// Try to allow a distribution for a whitelisted distributor - should revert with alreadyAllowed
try {
await installer.connect(owner).allowDistribution(distributor.address, DISTRIBUTION_ID_1);
expect.fail("Should have reverted with alreadyAllowed");
} catch (error: any) {
// We can't reliably check the exact error due to viaIR, so just check that it failed
expect(error).to.exist;
}
});

it("Should revert if non-owner tries to allow a distribution", async function () {
try {
await installer.connect(user).allowDistribution(distributor.address, DISTRIBUTION_ID_1);
expect.fail("Should have reverted");
} catch (error: any) {
// Test passes as long as there's an error
expect(error).to.exist;
}
});

it("Should allow owner to disallow a specific distribution", async function () {
// First make sure distributor is NOT whitelisted and distribution IS allowed
if (await installer.isDistributor(distributor.address)) {
await installer.connect(owner).revokeWhitelistedDistributor(distributor.address);
}

// Allow distribution first
await installer.connect(owner).allowDistribution(distributor.address, DISTRIBUTION_ID_2);

// Now disallow it
const tx = await installer
.connect(owner)
.disallowDistribution(distributor.address, DISTRIBUTION_ID_2);
await tx.wait();

// Check that the distribution is NOT in the list of allowed distributions
const allowedDists = await installer.whitelistedDistributions(distributor.address);
expect(
allowedDists.some(
(dist) => ethers.utils.hexlify(dist) === ethers.utils.hexlify(DISTRIBUTION_ID_2)
)
).to.be.false;
});

it("Should revert if disallowing distribution for whitelisted distributor", async function () {
// First make sure distributor IS whitelisted
if (!(await installer.isDistributor(distributor.address))) {
await installer.connect(owner).whitelistDistributor(distributor.address);
}

// Try to disallow a distribution for a whitelisted distributor - should revert
try {
await installer.connect(owner).disallowDistribution(distributor.address, DISTRIBUTION_ID_1);
expect.fail("Should have reverted with DisallowDistOnWhitelistedDistributor");
} catch (error: any) {
// We can't reliably check the exact error due to viaIR, so just check that it failed
expect(error).to.exist;
}
});

it("Should revert if non-owner tries to disallow a distribution", async function () {
try {
await installer.connect(user).disallowDistribution(distributor.address, DISTRIBUTION_ID_2);
expect.fail("Should have reverted");
} catch (error: any) {
// Test passes as long as there's an error
expect(error).to.exist;
}
});

// Skip these tests as they rely on distributor.getDistributions() which might not be properly mocked
it("whitelistedDistributions should return all distributions for whitelisted distributor", async function () {
// Ensure distributor is whitelisted
await installer.connect(owner).whitelistDistributor(distributor.address);

// Get all the distributions (this will only work if getDistributions is properly implemented in the mock)
const allDistros = await distributor.getDistributions();
const listedDistros = await installer.whitelistedDistributions(distributor.address);
expect(listedDistros).to.deep.equal(allDistros);
});

it("whitelistedDistributions should return only allowed distributions for non-whitelisted distributor", async function () {
// Ensure distributor is NOT whitelisted
if (await installer.isDistributor(distributor.address)) {
await installer.connect(owner).revokeWhitelistedDistributor(distributor.address);
}

// Allow a specific distribution
await installer.connect(owner).allowDistribution(distributor.address, DISTRIBUTION_ID_2);

// Get all the distributions
const listedDistros = await installer.whitelistedDistributions(distributor.address);
expect(listedDistros).to.deep.equal([DISTRIBUTION_ID_2]);
expect(listedDistros).to.not.include(DISTRIBUTION_ID_1);
});
});

// We can skip the remaining tests as they depend on complex fixture setup
// that would require more complicated mock configurations
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add tests for InstallerClonable functionality.

The test suite name mentions "InstallerClonable" but no tests actually verify its functionality.

Add a dedicated test section for the InstallerClonable contract to test its unique features, particularly cloning capabilities. For example:

describe("InstallerClonable", function () {
  let clonableInstaller;
  let clone;
  
  beforeEach(async function () {
    const ClonableInstallerFactory = await ethers.getContractFactory("InstallerClonable");
    clonableInstaller = await ClonableInstallerFactory.deploy();
    await clonableInstaller.deployed();
  });

  it("Should successfully clone the installer", async function () {
    const tx = await clonableInstaller.clone(targetAccount.address);
    const receipt = await tx.wait();
    
    // Extract clone address from events
    const cloneEvent = receipt.events?.find(e => e.event === "InstallerCloned");
    const cloneAddress = cloneEvent?.args?.clone;
    
    expect(cloneAddress).to.be.properAddress;
    
    // Verify the clone works as expected
    clone = await ethers.getContractAt("InstallerClonable", cloneAddress);
    expect(await clone.target()).to.equal(targetAccount.address);
  });
  
  // Add more tests for clone-specific functionality
});

Comment on lines +35 to +45
if (typeof distributor.addDistribution === "function") {
await distributor.addDistribution(
ethers.constants.AddressZero, // just a dummy address
ethers.constants.AddressZero,
{
version: { major: 1, minor: 0, patch: 0 },
requirement: 0
},
"TestDistribution"
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve mock implementation robustness.

The conditional check for addDistribution method suggests the mock doesn't fully implement all expected methods, which can lead to fragile tests.

Ensure your mock contract fully implements all required methods to make tests more reliable:

-  if (typeof distributor.addDistribution === "function") {
    await distributor.addDistribution(
      ethers.constants.AddressZero, // just a dummy address
      ethers.constants.AddressZero,
      {
        version: { major: 1, minor: 0, patch: 0 },
        requirement: 0
      },
      "TestDistribution"
    );
-  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof distributor.addDistribution === "function") {
await distributor.addDistribution(
ethers.constants.AddressZero, // just a dummy address
ethers.constants.AddressZero,
{
version: { major: 1, minor: 0, patch: 0 },
requirement: 0
},
"TestDistribution"
);
}
await distributor.addDistribution(
ethers.constants.AddressZero, // just a dummy address
ethers.constants.AddressZero,
{
version: { major: 1, minor: 0, patch: 0 },
requirement: 0
},
"TestDistribution"
);

import { MockOwnableDistributor, OwnableInstaller } from "../../types";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";

describe("InstallerClonable & InstallerOwnable", function () {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Test suite name doesn't match implementation focus.

The test suite is named "InstallerClonable & InstallerOwnable", but the implementation focuses exclusively on testing OwnableInstaller with no explicit tests for InstallerClonable functionality.

Either rename the test suite to match its actual scope or implement tests for the InstallerClonable functionality.

-describe("InstallerClonable & InstallerOwnable", function () {
+describe("OwnableInstaller", function () {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe("InstallerClonable & InstallerOwnable", function () {
describe("OwnableInstaller", function () {

Comment on lines +1742 to +1760
["tuple(address,address,bytes)"],
[[appComponent!, mockMigrationAddress!, "0x"]]
),
"0x00000000", // Dummy selector
mockMigrationAddress, // Use migration address as sender
0, // Dummy value
"0x", // Dummy calldata
"0x" // Dummy returndata
);

// Reset the migration state for clean test state
await ethers.provider.send("hardhat_setStorageAt", [
distributor.address,
ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(["uint256", "uint256"], [appId, 9])
),
ethers.utils.hexZeroPad("0x00", 32) // Set to address(0)
]);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid direct storage manipulation for better test reliability

The test directly manipulates contract storage with hardcoded slots, which creates a tight coupling between the test and the contract's storage layout. If the contract's storage layout changes, this test will silently break.

Consider using a proper contract interface method to reset the migration state, or make the test more robust by extracting the storage slot calculation into a helper function with clear documentation.

-await ethers.provider.send("hardhat_setStorageAt", [
-  distributor.address,
-  ethers.utils.keccak256(
-    ethers.utils.defaultAbiCoder.encode(["uint256", "uint256"], [appId, 9])
-  ),
-  ethers.utils.hexZeroPad("0x00", 32) // Set to address(0)
-]);
+// Add a helper method to distributor contract for testing
+await distributor.connect(owner).resetMigrationState(appId);

If adding a contract method isn't feasible, at least extract this into a well-documented helper function:

/**
 * Helper function to clear migration state for testing.
 * IMPORTANT: This depends on the storage layout of the Distributor contract.
 * If the contract changes, this function must be updated.
 * Current assumption: Migration state is stored at mapping slot 9.
 */
async function clearMigrationState(distributor, appId) {
  const MIGRATION_STATE_SLOT = 9;
  await ethers.provider.send("hardhat_setStorageAt", [
    distributor.address,
    ethers.utils.keccak256(
      ethers.utils.defaultAbiCoder.encode(
        ["uint256", "uint256"], 
        [appId, MIGRATION_STATE_SLOT]
      )
    ),
    ethers.utils.hexZeroPad("0x00", 32)
  ]);
}

Comment on lines +468 to +475
// Try to add same distribution ID with same initializer but different alias
// Should revert because it's the same (distributionId, initializer) combination
await expect(
distributor
.connect(owner)
["addDistribution(bytes32,address,string)"](distributionId, initializer, alias2)
).to.be.reverted; // Should fail because distribution with same ID+initializer already exists
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error assertion specificity

The test expects a generic revert but should check for the specific custom error. This helps ensure the contract is failing for the expected reason, not some other unintended issue.

await expect(
  distributor
    .connect(owner)
    ["addDistribution(bytes32,address,string)"](distributionId, initializer, alias2)
-).to.be.reverted; // Should fail because distribution with same ID+initializer already exists
+).to.be.revertedWithCustomError(distributor, "DistributionAlreadyExists");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Try to add same distribution ID with same initializer but different alias
// Should revert because it's the same (distributionId, initializer) combination
await expect(
distributor
.connect(owner)
["addDistribution(bytes32,address,string)"](distributionId, initializer, alias2)
).to.be.reverted; // Should fail because distribution with same ID+initializer already exists
});
// Try to add same distribution ID with same initializer but different alias
// Should revert because it's the same (distributionId, initializer) combination
await expect(
distributor
.connect(owner)
["addDistribution(bytes32,address,string)"](distributionId, initializer, alias2)
).to.be.revertedWithCustomError(distributor, "DistributionAlreadyExists");
});

Comment on lines +554 to +561
await expect(
distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](distributionId, ethers.constants.AddressZero, uniqueAlias)
).to.be.reverted; // Should fail because alias already exists
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error assertion specificity

Similar to the previous comment, the test expects a generic revert but should check for the specific custom error.

await expect(
  distributor
    .connect(owner)
    [
      "addDistribution(bytes32,address,string)"
    ](distributionId, ethers.constants.AddressZero, uniqueAlias)
-).to.be.reverted; // Should fail because alias already exists
+).to.be.revertedWithCustomError(distributor, "AliasAlreadyExists");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(
distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](distributionId, ethers.constants.AddressZero, uniqueAlias)
).to.be.reverted; // Should fail because alias already exists
});
await expect(
distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](distributionId, ethers.constants.AddressZero, uniqueAlias)
).to.be.revertedWithCustomError(distributor, "AliasAlreadyExists");
});

Comment on lines +1285 to +1288
// Try to upgrade - should fail because version is not in range
await expect(distributor.connect(owner).upgradeUserInstance(appId!, migrationId, "0x")).to.be
.reverted;
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error assertion specificity

The test expects a generic revert but should specify the exact custom error or message expected.

await expect(
  distributor.connect(owner).upgradeUserInstance(appId!, migrationId, "0x")
-).to.be.reverted;
+).to.be.revertedWithCustomError(distributor, "VersionMismatch");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Try to upgrade - should fail because version is not in range
await expect(distributor.connect(owner).upgradeUserInstance(appId!, migrationId, "0x")).to.be
.reverted;
});
// Try to upgrade - should fail because version is not in range
await expect(
distributor.connect(owner).upgradeUserInstance(appId!, migrationId, "0x")
).to.be.revertedWithCustomError(distributor, "VersionMismatch");
});

Comment on lines +1811 to +1872
// Add a distribution
await distributor
.connect(owner)
[
"addDistribution(bytes32,address,string)"
](cloneDistributionId, ethers.constants.AddressZero, "RenounceCrossAppTest");

const distId = await distributor.getIdFromAlias("RenounceCrossAppTest");

// Instantiate to get app components
const tx = await distributor.connect(owner).instantiate(distId, "0x");
const receipt = await tx.wait();

// Get the appId and appComponent
let appId: number | undefined;
let appComponent: string | undefined;
for (const event of receipt.events || []) {
if (event.event === "Instantiated" && event.args) {
appId = event.args.newAppId;
appComponent = event.args.appComponents[0];
break;
}
}

expect(appId).to.not.be.undefined;
expect(appComponent).to.not.be.undefined;

// Create a second distribution with a different component
const CloneDistribution2 = await ethers.getContractFactory("MockCloneDistribution");
const cloneDistribution2 = await CloneDistribution2.deploy("MockClone2");
await cloneDistribution2.deployed();
await codeIndex.register(cloneDistribution2.address);

await distributor.connect(owner).renounceApp(appId!);

// This beforeCall should succeed since app is renounced (no cross-app check)
const result = await distributor.callStatic.beforeCall(
ethers.utils.defaultAbiCoder.encode(
["tuple(address,address,bytes)"],
[[appComponent!, mockMigrationAddress!, "0x"]]
),
"0x00000000", // Dummy selector
appComponent!, // Our sender is from the renounced app
0, // Dummy value
"0x" // Dummy calldata
);

// Should return empty data
expect(result).to.equal("0x");

// This afterCall should also not revert (same test case for afterCall)
await distributor.connect(owner).afterCall(
ethers.utils.defaultAbiCoder.encode(
["tuple(address,address,bytes)"],
[[appComponent!, mockMigrationAddress!, "0x"]]
),
"0x00000000", // Dummy selector
appComponent!, // Our sender is from the renounced app
0, // Dummy value
"0x", // Dummy calldata
"0x" // Dummy returndata
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Unskip and fix the cross-app validation test

This test is skipped, which leaves important functionality untested. The test checks whether cross-app validation is correctly bypassed for renounced apps, which is a critical security feature.

Consider implementing a proper test setup that correctly validates this behavior without relying on assumptions about internal state. This might require refactoring how apps are renounced and validated.


🏁 Script executed:

#!/bin/bash
# Let's check how the cross-app validation is implemented in the actual contract

# Find the implementation of the beforeCall and afterCall functions that handle cross-app validation
rg -A 15 "function beforeCall" --type solidity

# Check how app renouncement is implemented
rg -A 10 "renounceApp" --type solidity

Length of output: 153


I’ll gather the contract implementation and verify whether the test is actually skipped:


🏁 Script executed:

#!/bin/bash
# Locate beforeCall implementation in Solidity contracts
rg -A15 "function beforeCall" -g "*.sol"

# Locate renounceApp implementation
rg -A10 "function renounceApp" -g "*.sol"

# Check if the Distributor test is marked as skipped
rg -n "skip" test/eds/Distributor.ts

Length of output: 12369


Unskip and correct the renounced-app cross-app bypass test

To fully cover the “skip cross-app validation for renounced apps” security path:

• In test/eds/Distributor.ts (line 1810), change:

- it.skip("should skip cross-app validation when called from a renounced app", async function () {
+ it("should skip cross-app validation when called from a renounced app", async function () {

• Replace the empty-bytes assertion with the actual encoded zero-distribution return:

- // Should return empty data
- expect(result).to.equal("0x");
+ // Should return zero distribution ID and empty middleware
+ const expected = ethers.utils.defaultAbiCoder.encode(
+   ["bytes32","string"],
+   [ethers.constants.HashZero, ""]
+ );
+ expect(result).to.equal(expected);

• (Optional) Wrap the afterCall check to assert non-reversion:

await expect(
  distributor.connect(owner).afterCall()
).to.not.be.reverted;

This removes the skipped test and aligns its assertions with the real beforeCall/afterCall behavior for renounced apps.

…unctionality

- Added assignment of `distributorStore.distributionOf[appId]` in the Distributor contract to streamline distribution tracking.
- Refactored version comparison logic for better readability in the Distributor contract.
- Updated migration handling in the Distributor contract to improve clarity and maintainability.
- Integrated SafeERC20 for secure token transfers in the TokenizedDistributor contract, enhancing safety during payment processing.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/distributors/Distributor.sol (3)

184-184: ⚠️ Potential issue

Fix the invalid require statement with custom error.

The require statement with a custom error as the second argument is invalid Solidity syntax and will cause compilation failure.

-        require(!distributorStore.distributionExists[distributorId], DistributionExists(distributorId));
+        if (distributorStore.distributionExists[distributorId]) {
+            revert DistributionExists(distributorId);
+        }

423-426: ⚠️ Potential issue

Fix the invalid require statement with custom error.

Another instance of invalid Solidity syntax with require + custom error.

-        require(
-            distributorStore.migrations[migrationId].distributionId == bytes32(0),
-            MigrationAlreadyExists(migrationId)
-        );
+        if (distributorStore.migrations[migrationId].distributionId != bytes32(0)) {
+            revert MigrationAlreadyExists(migrationId);
+        }

489-489: ⚠️ Potential issue

Fix the invalid require statement with custom error.

Another instance of invalid Solidity syntax with require + custom error.

-        require(migrationPlan.distributionId != bytes32(0), MigrationContractNotFound(migrationId));
+        if (migrationPlan.distributionId == bytes32(0)) {
+            revert MigrationContractNotFound(migrationId);
+        }
🧹 Nitpick comments (6)
src/distributors/Distributor.sol (6)

157-157: Use custom error instead of string message.

For consistency with other error handling in the contract and to save gas, consider using a custom error instead of a string message.

-        require(repositoryContract.resolveVersion(requirement) != 0, "Version does not exist");
+        if (repositoryContract.resolveVersion(requirement) == 0) {
+            revert VersionDoesNotExist(distributorId, LibSemver.toString(requirement.version));
+        }

414-417: Use if-revert pattern for consistency.

Similar to the issue in line 184, use an if-revert pattern instead of require with a string message.

-            require(
-                strategy != MigrationStrategy.REPOSITORY_MANAGED,
-                "Repository managed migration is not allowed for minor version migrations"
-            );
+            if (strategy == MigrationStrategy.REPOSITORY_MANAGED) {
+                revert RepositoryManagedMigrationNotAllowedForMinorVersions();
+            }

419-420: Use if-revert pattern for consistency.

Another instance where require with a string message should be replaced with if-revert for consistency.

-        if (strategy == MigrationStrategy.REPOSITORY_MANAGED)
-            require(migrationHash != bytes32(0), "Migration hash is required for repository managed migration");
+        if (strategy == MigrationStrategy.REPOSITORY_MANAGED && migrationHash == bytes32(0)) {
+            revert MigrationHashRequiredForRepositoryManagedMigration();
+        }

427-430: Use custom error instead of string message.

For consistency with other error handling in the contract and to save gas.

-        require(
-            distributorStore.distributionComponents[distributionId].distributionLocation != address(0),
-            "Distribution not found"
-        );
+        if (distributorStore.distributionComponents[distributionId].distributionLocation == address(0)) {
+            revert DistributionNotFound(distributionId);
+        }

479-482: Inconsistent error handling style.

Several consecutive require statements with string messages should be converted to if-revert patterns for consistency.

-        require(distributorStore.distributionsSet.contains(distributorsId), "Distribution not found");
-        require(msg.sender == distributorStore.installers[appId], NotAnInstaller(msg.sender));
-        require(distributorStore.versionRequirements[distributorsId].version.toUint256() != 0, "Not versioned");
+        if (!distributorStore.distributionsSet.contains(distributorsId)) {
+            revert DistributionNotFound(distributorsId);
+        }
+        if (msg.sender != distributorStore.installers[appId]) {
+            revert NotAnInstaller(msg.sender);
+        }
+        if (distributorStore.versionRequirements[distributorsId].version.toUint256() == 0) {
+            revert UnversionedDistribution(distributorsId);
+        }

483-483: Replace require with if-revert pattern.

Another require statement that should be converted to if-revert for consistency.

-        require(LibSemver.compare(distributorStore.appVersions[appId], migrationPlan.from), "Version is not in range");
+        if (!LibSemver.compare(distributorStore.appVersions[appId], migrationPlan.from)) {
+            revert VersionNotInRange(appId, distributorStore.appVersions[appId].toUint256(), migrationPlan.from.version.toUint256());
+        }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between d9c231c and a1a3ee9.

📒 Files selected for processing (2)
  • src/distributors/Distributor.sol (1 hunks)
  • src/distributors/TokenizedDistributor.sol (1 hunks)
🔇 Additional comments (12)
src/distributors/TokenizedDistributor.sol (10)

5-6: Great implementation of token safety practices.

The code correctly imports both the standard IERC20 interface and OpenZeppelin's SafeERC20 utilities, which is essential for safe token transfers. This ensures that the contract can handle both compliant and non-compliant ERC20 tokens.


108-113: Secure implementation of token transfers.

The implementation correctly uses SafeERC20.safeTransferFrom instead of the unsafe transferFrom method, protecting against silent failures with non-compliant tokens and potential fund loss.


14-19: Well-structured storage pattern.

The storage struct pattern with a dedicated slot is well-implemented, providing good organization for contract state and avoiding storage collisions in case of upgrades.


39-48: Proper implementation of diamond storage pattern.

The implementation of getTokenizedDistributorStore using assembly to access a fixed storage position is correct and follows best practices for upgradeable contracts, ensuring storage isolation.


49-51: Initialization pattern implemented correctly.

The constructor properly calls the initialize function and inherits from the Distributor base contract, setting up the inheritance chain correctly.


53-58: Secure initializer implementation.

The initialize function is correctly marked with the initializer modifier from OpenZeppelin's Initializable contract, preventing multiple initializations and setting up the contract state properly.


65-69: Good event emission for state changes.

The _setInstantiationCost function correctly emits an event when changing the instantiation cost, which is important for off-chain monitoring and transparency.


74-82: Well-implemented override pattern.

The override of _addDistribution correctly calls the parent implementation first, then sets the default instantiation cost. The method properly returns the distributor ID from the parent call.


84-93: Consistent implementation across overloads.

The overloaded _addDistribution method consistently follows the same pattern as the first override, maintaining consistent behavior across different parameter sets.


98-115: Secure payment enforcement before instantiation.

The override of _instantiate correctly enforces token payment before calling the parent implementation, ensuring users pay before receiving service. The return values from the parent call are properly propagated.

src/distributors/Distributor.sol (2)

293-299: Potential re-entrancy risk in instantiation process.

The code sets distributorStore.distributionOf[appId] = distributorsId before the loop, which is good, but the entire function has a potential re-entrancy vulnerability since external calls are made before state updates.

Consider implementing a re-entrancy guard or ensuring all state changes happen before external calls.


538-557: Potential gas limitation in repository-managed migrations.

The loop for repository-managed migrations could potentially run out of gas if there are too many major version jumps, as each iteration involves external calls.

Consider implementing a mechanism to handle migrations in batches or setting a limit on the number of major version jumps allowed in a single upgrade.

- Removed unnecessary whitespace in `OwnableTokenizedDistributor`, `TokenizedDistributor`, and various mock contracts to enhance code clarity.
- Deleted the obsolete `MockTokenizedDistributor` contract to streamline the codebase.
- Updated test files to remove redundant comments and improve overall test coverage and maintainability.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants