-
-
Notifications
You must be signed in to change notification settings - Fork 1
Dev -> Main #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Dev -> Main #54
Conversation
* 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 detectedLatest commit: 5765abc The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
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 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. 📒 Files selected for processing (11)
WalkthroughThis 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
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
Poem
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 issueProxy does not propagate return data – breaks every
view/purecall
_fallback()swallows the return data fromdelegatecall; callers will always receive an empty byte string, so any ABI-decoded return value becomes0/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 issueUnchecked ERC-20
transferFrommay silently fail
IERC20.transferFromreturns a boolean. If the token returnsfalse, the distributor will continue as though payment succeeded.
UseSafeERC20for 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 suggestionCharge 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 allgetinvocations 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
source2below.
🧹 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 unusedERC165import & explicitly importIERC165
CloneDistributionneither inherits fromERC165nor 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 fortype(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-caseThe
bytes memoryargument ininstantiateis 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 incontractURICasting a fixed-size
bytes32tostringviaabi.encodePackedproduces a UTF-8 string padded with zero bytes, which most clients strip unexpectedly.
Consider storing the URI asstringinstead, 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 functionThe new
isEIP7702function 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 variablerepoName.The variable
repoNameis 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 beforepragmaThe 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,IDistributorand the customInvalidMiddlewareerror 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
_adminbecomes the middleware address, while_ownerbecomes 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 satisfymarkdownlint(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
MockDistributoractually represents theUpgradableDistributionfactory.
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
disallowedAddressdoesn't follow PascalCase convention used for other errors likeOnlyTargetAllowed. 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
ContractFactoryimport 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
instantiatefunction checks ifkeccak256(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
supportsInterfacefunction 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
instantiatefunction accepts abytes memoryparameter 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 flexibleThe 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 importThe contract imports
ProxyAdminbut doesn't appear to use it anywhere in the implementation.-import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
10-13: Unnamed parametersThe 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 codeThere'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 requirementThe 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 codeThere'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 calculationThe 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:
!!!WARNINGis 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 beinstanceContracts(or simplyinstances). 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
IInstallersection.Also applies to: 820-827
src/mocks/MockMiddlewareTarget.sol (1)
6-12:middlewarefield is never read – remove or use for consistency
The contract storesmiddlewarein the constructor but all calls take an explicithookparameter. Either:
- Delete the state variable and constructor arg; or
- Remove the
hookparameter 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 variableseare 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 –joinis already available viapath
path.joinis available on the defaultpathimport, so importingjoinseparately is unnecessary noise.-import path, { join } from "path"; +import path from "path";
15-17: Duplicatefsimports can be consolidatedYou already import
writeFile/mkdirfromfs/promises. Pulling in the whole synchronousfsmodule increases bundle size and mixes sync + async styles. Prefer one of:
- Stick to async (
fs/promises) everywhere.- Keep the plain
fsimport and usefs.promises.*.Either way, avoid holding two different interfaces for the same API.
66-73: Globalconsole.logmonkey-patch is brittleOverwriting
console.log:
- Breaks parallel tasks/tests that rely on logging.
- Risks leaving the stub active if an exception is thrown before the restore.
Consider passing a silent logger (e.g.
debuglibrary) instead, or wrap the critical section intry/finallyto 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
constantsis 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
cis 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 forget()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 inmigrate.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-optimisesources()by avoiding the extra memory allocationCreating 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 callsThe 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 usemsg.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
appInstancesis 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
renounceAppandsetMigrationare publicly callable by any account.
While acceptable for a mock, consider restricting them toonlyOwnerso tests continue to surface access-control issues instead of masking them.src/distributors/TokenizedDistributor.sol (1)
13-29: Key name ininstantiationCostsmapping is misleading and error-proneThe mapping is declared as
mapping(bytes32 codeHash => uint256) public instantiationCosts;Yet every call site (e.g.
_setInstantiationCost,_instantiate) usesdistributorsIdas 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-compatibleAt the moment the contract declares support only for
IDistributionandIERC165.
IfIDistributionitself 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.solnow contains only commented lines; it adds no byte-code but still gets compiled, which:
- Generates a compilation unit with no pragma (potential compiler warning).
- Creates noise when browsing the codebase.
- 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.
UseBigNumber.eq/lt/gtassertions 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 expectationsThe happy-path coverage is thorough, but the library also has several
requirechecks (e.g. index out-of-bounds, empty layers).
AddingexpectRevertcases will prevent accidental relaxation of those validations in future refactors.src/interfaces/IRepository.sol (3)
4-7: Unused import
IMigration.solis imported but never referenced, triggeringSpdxLicenseIdentifier/no-unused-importwarnings in Solidity ≥0.8.19.
Remove the import or referenceIMigrationwhere 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, andresolveVersionare 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 theinstallTxvariable to silence ESLint warnings
installTxis awaited but never referenced afterwards, triggeringno-unused-varsin 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:appComponentsis fetched but never inspected – expand the assertionThe 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-standardThe 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: Useconst appId = 1consistently instead of a secondappComponentsfetchYou already store
appId; retrievingappComponentsagain 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 markingtestModifierWithParamsaspayablefor parity
testDefaultModifierispayable, while the parametrised variant is not.
If the underlying middleware relies onmsg.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: acceptstring calldatainsetValueWithoutMiddlewareKeeping 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: Removehardhat/console.solimport to avoid bytecode bloat
console.solis not used; its presence adds unnecessary code size and intrinsics.-import "hardhat/console.sol";
35-43: Secondinitializeoverload shouldreturns (address[] memory)for interface symmetryWhile 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 andpop-ping costsO(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_Trackedshadows the original semantics but adds events.
Consider naming itERC7746C_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.revertedWithCustomErrorwill still pass if the call reverts with
another error. Preferto.not.be.revertedto 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
ReturninginterfaceId == IERC165.interfaceIdis 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:_changeMigrationScriptshould validate the new script’s interfaceYou 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: UnusedcallCounterwastes storage/gas
callCounteris 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 intestAccessLayersStorage
layersis declared but unused, which triggers a compiler warning.
Cheap fix: mark the variable as unused.-LibMiddleware.LayerStruct[] storage layers = LibMiddleware.accessLayersStorage(); +LibMiddleware.accessLayersStorage(); // access onlyREADME.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
executeBeforeCallfunction initializeslastResultbut 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 addingoverridefor clearer intent
TokenizedDistributordoes not expose an externaladdDistribution(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 asfunction 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:externalis cheaper thanpublicfor write-only admin functions
setInstantiationCostis never called internally, so changing the visibility toexternalwill 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 twoaddDistributionoverloads 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.28may hinder patch-level upgrades
Using the exact equality operator prevents compilation with security-patched releases (e.g. 0.8.29). Prefer^0.8.20or>=0.8.20 <0.9.0unless you have a strict audit requirement.src/distributors/OwnableDistributor.sol (2)
5-8: DuplicateIMigrationimport
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 withoutoverride– future-proofing
Same comment as in the mock: addoverrideif 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: Duplicateusingdirectives – noise & potential linter warnings.
EnumerableSetis already imported for bothAddressSetandBytes32Setat 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 duplicatesenforceActiveDistribution.
_installPublicfirst re-implements a subset of the allowlist logic then callsenforceActiveDistribution, which performs (almost) the same work. Removing the firstifsimplifies reasoning and reduces gas.src/mocks/MockDistributorForTest.sol (1)
85-90:addDistributiondeclared twice – keep only the required overloads.The first overload (
bytes32,address,string) is no longer part of the updatedIDistributorinterface, 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:delegatecallerror bubbling loses revert reason
delegatecallreturns raw data; casting it tostring(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:createVersionRequirementduplicated – move to helper utilThe same helper is re-declared in multiple
describeblocks.
Export a single utility fromtest/utils/semver.tsand 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
instantiateis markedpayablebut silently ignoresmsg.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: UnusedDistributorLayerConfig?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 inconsistentMost 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 readableNameHashZis redundant
stringcan beindexed, but Solidity stores the keccak256 hash of the value, giving you the same data you already expose throughreadableName.
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 themselvesThis saves one log topic per emission.
239-245: Preferuint256overuintin custom errors
uintis an alias foruint256, 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:calculateDistributorIdshould bepure, but alsoview?Marking it
pureis 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 aviewhelper that wraps thepurehash check.Just a design note; no change strictly required.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.envto 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
LayerStructdefinition, 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
setLayerfunction.
31-42: New function to efficiently update middleware layers.The
changeLayersfunction is a useful addition that efficiently updates the stored array of middleware layers to match a provided memory array. It handles three operations:
- Expands the array if needed (lines 33-35)
- Updates existing elements (lines 36-38)
- Trims excess elements (lines 39-41)
This provides a more gas-efficient way to update multiple layers at once compared to the existing
setLayersfunction.
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.5to^0.8.14aligns with the addition of the.solcover.jsconfiguration 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 correctlyThe addition of an
Initializedevent andinitialize()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 goodThis 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 testingThe added
instantiatefunction 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 correctlyThe import path for LibMiddleware.sol has been updated to reflect the new file organization structure.
7-14: Contract renamed appropriatelyThe rename from
ERC7746MiddlewaretoERC7746Hookedbetter 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-documentedThe 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 purposeThe 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 functionThe 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 behaviorThe 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 failuresThis mock contract is well-designed for testing error handling in the migration process. It correctly:
- Implements the
IMigrationinterface- Deliberately fails during migration with a clear error message
- Properly implements
supportsInterfacefor ERC165 support- 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 transferThis mock contract is well-designed for testing ownership transfer scenarios related to
onDistributorChanged. It:
- Correctly inherits from OpenZeppelin's
Ownableand implementsIAdminGetter- Uses an appropriate constructor to initialize ownership
- Correctly implements
getWrappedProxyAdmin()to return its own address- 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 libraryThis 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 handlingGood 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 implementationConstructor cleanly handles the conversion from string to ShortString format.
26-26: LGTM: Properly updated sources() functionThe 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 documentationThe 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 valueThe 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 implementationGood 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 constructorThe constructor properly sets the ERC165 interface ID as always supported, which is required by the ERC165 standard.
26-29: Good validation in setSupportedInterfaceThe function correctly prevents modification of ERC165 interface support, which must always remain true per the standard.
34-36: Properly implemented supportsInterface overrideThe 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 fromdistributionIdtodistributedCodeHashreflects 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:
- Update the MockCloneDistribution constructor with a name parameter
- 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: UpdatedaddDistributioncall with new parameters.The function call has been properly updated to match the new signature which includes:
- Code hash instead of ID
- Zero address for middleware (when not using middleware)
- A human-readable name for the distribution
This improves the contract's usability and aligns with the modular architecture.
87-91: Consistent updates toaddDistributioncalls.Both instances of
addDistributionhave 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:
- Updated import path for Repository
- Added IMigration and ShortStrings imports
These imports support the new migration functionality and metadata handling.
15-17: Added migration hash parameter tonewRelease.The function signature now includes a
migrationHashparameter, which allows associating migration scripts with releases. This is an important addition for supporting upgradeability.
19-21: NewchangeMigrationScriptfunction 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:
- Implements two function signatures to match different possible calls from the Distributor
- Uses assembly to revert without a reason string
- 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 thatsuper._fallback()still executes ERC-7746 hooksBecause
superresolves to the next contract in the linearisation after this one (i.e.TransparentUpgradeableProxy), the hook provided byERC7746Hookedmight 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 whetherinitializeis required
OwnableInstallerappears to mimic OpenZeppelin clone patterns, normally requiring an explicitinitialize()call.
The commented line suggests this step was skipped. If the contract relies on an initialised state (e.g.distributoraddress), 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_POSITIONtoAUTH_MIDDLEWARE_STORAGE_POSITIONto match the contract rename, which maintains consistency.
30-30: Initializer struct renamed for consistency.The initializer struct has been renamed from
SimpleAccessManagerInitializertoAuthorizationMiddlewareInitializerto match the contract rename, maintaining consistency in naming conventions.
41-41: Updated function parameter type to match renamed struct.The
initializefunction 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 implementationThe 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 implementationGood implementation of
supportsInterfacethat properly declares support for theIRepositoryinterface alongside any inherited interfaces.src/distributors/WrappedProxyInitializer.sol (1)
16-29: Good error handlingThe implementation correctly wraps the call to
instantiatein 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 testingThe 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 coverageThe 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 testsThe 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 testingThe 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 callIf
INDEX_CONTRACTis 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:newReleasesignature changed – update NatSpecThe function now takes
bytes32 migrationHashbut the documentation does not mention it.
Please add the missing@param migrationHashdescription and update the “MUST emit” note if you intend to emitMigrationScriptAddedas well.test/eds/UpgradabilityWorkflows.ts (1)
221-225: Verify thatDistributorChangedis actually emitted bydistributor
changeDistributoris invoked oninstaller, so the event may originate from that contract instead ofdistributor.
If the event is emitted byinstaller, 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:testERC7746Modifiermarkedpayablebut never consumesmsg.value
If the function shouldn’t accept ether, droppayable; otherwise emit / store the
value to prove it is handled.test/eds/Installer.ts (2)
42-47: Byte-hash mismatch risk
provider.getCodereturns runtime byte-code, whereas many indices expect the
creation byte-code. If the on-chain index was built from creation bytecode,
cloneDistributionIdwill 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 index1; if not,
subtract1.const idx = (await installer.getAppsNum()).sub(1); const app = await installer.getApp(idx);src/distributions/UpgradableDistribution.sol (1)
34-38:sources()visibility &@inheritdocmismatch
Declaring the functioninternalwhile tagging@inheritdocmeans the public
interface is missing. If the intention is to expose sources via
get(), mark the functioninternalwithout the inherit-doc tag, or make
itexternal/publicto 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 anERC7744...(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: DeploysWrappedTransparentUpgradeableP...(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
MiddlewareConfigstruct that mirrors whatLibMiddlewareuses 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
executeAfterCallfunction correctly iterates through middleware layers in reverse order, properly accounting for array indexing to avoid underflow withi-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
LibSemverlibrary, 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
CloneDistributionand its concrete implementations (CodeHashDistributionandLatestVersionDistribution) 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
UpgradableDistributionthoroughly 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
UpgradableDistributionis particularly important.src/mocks/MockTokenizedDistributor.sol (6)
4-4: Import path correction looks good
The switch to the relative../distributors/TokenizedDistributor.solpath aligns the mock with the new folder structure.
50-52: No issues with makinginstantiatepayable
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 todisableDistributionmatches 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_addVersionMigrationdoes 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 byonlyOwner
The thin wrappers delegate to the internal helpers and preserve modifiers – no change requested.src/distributors/OwnableDistributor.sol (4)
12-16: Payableinstantiatemirrors base behaviour – OK
Delegation to_instantiatepreserves value forwarding viamsg.value.
39-42: Disable wrapper matches new semantics
Straight delegation with ownership guard – looks correct.
43-52: Event emission for migration helpers
Ensure_addVersionMigrationand_removeVersionMigrationemit 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: Usingaddress(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: Missingoverridespecifier for new‐signatureaddDistribution.Solidity requires explicit
overridefor every base function; depending on the interface version, the current signature might not satisfy the compiler. Verify against the latestIDistributor.src/distributors/Distributor.sol (1)
286-289: Interface detection omits IERC7746
DistributorimplementsIERC7746via the hooks, butsupportsInterface
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: Manyexpect(...).to.be.revertedlack reason checksSeveral 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
supportsInterfaceomitsIERC7746(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:Instantiatedevent: rethink the third indexed topicThree indexed parameters are already used (
distributionId,newAppId,version).
If downstream consumers need to filter byversion, keep it; otherwise movingversionto 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.
| "@openzeppelin/contracts": "^5.3.0", | ||
| "@openzeppelin/contracts-upgradeable": "^5.3.0", |
There was a problem hiding this comment.
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
doneLength 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 safeApproveis deprecated—migrate tosafeIncreaseAllowance/safeDecreaseAllowance.Timers,ERC777,ERC1820Implementer, preset contracts are deprecated—remove or replace as needed.
• Draft-EIP imports updated- Replace any
draft-IERC20Permit.sol/draft-ERC20Permit.solwithIERC20Permit.sol/ERC20Permit.sol. - Update
draft-EIP712.sol,draft-ERC721Votes.solto their non-draft equivalents.
• Governor interface changes - Add the new internal
_getVotes(address,uint256)and update_countVoteoverrides to include thebytes memory paramsargument.
• 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.
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.
There was a problem hiding this 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.getis still called with a single object that bundles bothversionandrequirement.
The ABI expects two separate positional arguments (Versionstruct,requirementsenum).
Passing a struct containing both fields results in incorrect ABI encoding and a silentrevert.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:Installedevent signature is underspecified and may cause downstream type issues.
- The
permissionsslot is declared asbytes32 indexed, yet the only known emission (seeInstallerClonable._install) passes a string literal ("0x") which will not compile – a string/bytes literal cannot be implicitly converted tobytes32.- Nothing in the interface explains how the
permissionsvalue 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:MigrationPlanis missing the migration contract pointerThe 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 anaddress 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 forinstantiateThe NatSpec says:
@return distributionName The name of the distribution.but the signature returns
bytes32 distributionName.
Either change the return type tostring(preferred for human-readable names) or update the comment to stress it is the 32-byte hash.
150-151: Overload ambiguity risk foraddDistributionThere are now two overloads:
(bytes32,address,string)(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 anaddressthat is also abytes32cast).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.requirementsis undefinedBoth events reference
LibSemver.requirements, but the library exportsVersionRequirement.- 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 importedERC7744symbolWithin the
beforeEachyou create a local constant namedERC7744, 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 duplicatedEach 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 followsThe 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 publicaddDistributionoverloads make the API ambiguousThe contract exposes:
addDistribution(bytes32,address,string)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
setInstantiationCostanddisableDistributioncan be markedexternalrather thanpublic; they are never used internally._instantiatealready returnsmemoryarrays; addingmemoryon the return parameters ofinstantiateis 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 structThe 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 referenceThere'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 referenceThere'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 parameterThe
changeDistributorfunction has an undocumentedappDataparameter. 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 preventionThere'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 explanationThe 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
📒 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
changeDistributoris invoked oninstaller, but the test expects the event to be emitted bydistributor.
UnlesschangeDistributoremits 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:migrationHashis not a migration-ID
upgradeAppexpects amigrationId(the keccak of distributionId + migrationHash + strategy).
Passing the rawmigrationHashhappens to revert (thus the test passes) but for the wrong reason.
Construct the realmigrationIdor 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 modelThe 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 mechanismThe 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 testingThe 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 methodsThese methods provide good control over the mock's behavior during tests, especially for simulating success and failure scenarios during upgrades.
183-191: Configurable hook behaviorThe implementation allows tests to control the return value of the
beforeCallhook, which is essential for testing middleware interactions and upgrade workflows.
147-161: Comprehensive upgrade testing supportThe
upgradeUserInstanceimplementation 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 supportThe
supportsInterfaceimplementation properly declares support for bothIDistributorandIERC7746interfaces, which is essential for contract discovery and validation.test/eds/Distributor.ts (5)
35-50: Useful helper function for version requirementsThe
createVersionRequirementhelper function improves test readability and reduces duplication when creating version requirements throughout the test suite.
81-141: Comprehensive test setupThe 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 uniquenessThis 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 testsThese 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 testsThis 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 APIThe 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 interfaceThe
upgradeUserInstancefunction 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 failuresThe 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 trackingThe 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.
| await expect( | ||
| repository.connect(owner).changeMigrationScript(3, dummyMigrationCodeHash) | ||
| ).to.be.revertedWith("Major version does not exist"); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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"); | |
| }); |
- 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.
There was a problem hiding this 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 suggestionMixed revert styles & missing validation make
_addDistributionbrittle
- A string-based
require(line 157) is used next to custom errors → keep the style consistent and cheaper by using custom errors everywhere.- No check that
repositoryContract.resolveVersion(requirement)actually belongs to the same repo; consider reverting with a dedicatedVersionDoesNotExisterror.- 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.requirementsdoes not existBoth events reference
LibSemver.requirements, but the library exportsVersionRequirement.
This is a straight compilation blocker.- LibSemver.requirements indexed semanticRequirement, + LibSemver.VersionRequirement indexed semanticRequirement,Apply the same fix for the
MigrationContractAddedToVersionsevent.
🧹 Nitpick comments (8)
src/distributors/OwnableTokenizedDistributor.sol (1)
6-8: Remove duplicate import.
IMigration.solis 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.solappears twice.-import "../interfaces/IMigration.sol";
37-44: Consistency nit – prefersuper._addDistributionfor clarity.In the repository-based overload you call the inherited
_addDistributiondirectly, while the hash-based overload prefixes it withsuper.. Usingsuper.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:supportsInterfacedoes not advertise ERC7746
beforeCall/afterCallimplementIERC7746, butsupportsInterfaceonly returnsIDistributor& 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 bepureThe function is a deterministic hash and never touches state; marking it
pure(or at leastview) 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 foraddDistributionpersistsWith two overloads
addDistribution(bytes32,address,string)addDistribution(IRepository,address,VersionRequirement,string)a simple
addresstyped variable can satisfy both the first parameters by implicit cast tobytes32, 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
createVersionRequirementis 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
📒 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 issueConstructor triggers a double-initialisation guard – will revert when deployed via proxy.
TokenizedDistributor’s constructor already calls its owninitialize(...)(which setsInitializable._initialized = 1).
Your constructor then callsinitialize(_owner)(taggedinitializer) a second time. Because_initializedis already1, 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
initializemodifier toreinitializer(2)or merge the owner set-up into the baseinitializecall to keep a single initializer.Likely an incorrect or invalid review comment.
src/distributors/TokenizedDistributor.sol (1)
43-50: 🛠️ Refactor suggestion
⚠️ Potential issueConstructor calls
initialize⇒ implementation is pre-initialised.Calling
initializein the constructor sets_initialized = 1, so any proxy/cloned instance attempting to callinitializewill revert. This defeats the purpose of usingInitializable.-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:DELEGATECALLmigration – surface-level sanity checks missingBefore performing a
delegatecallyou only verify that the target supportsIMigration, but not thatresultdecodes to the expected shape or thatnewVersion > 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
resultif you intend to use it.test/eds/Distributor.ts (1)
2038-2044: Expecting generic.revertedWith("")hides the real revert reasonThe 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");
| constructor(address _owner) { | ||
| initialize(_owner); | ||
| } | ||
|
|
||
| contract OwnableDistributor is Distributor, Ownable { | ||
| constructor(address _owner) Ownable(_owner) {} | ||
| function initialize(address _owner) public initializer { | ||
| __Ownable_init(_owner); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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:
- Remove the call from the constructor and rely on an external/initializer call post-deployment, or
- Delete the constructor entirely and expose only
initialize(...)to be invoked once.
This keeps the contract compliant with OpenZeppelin’s upgradeable pattern.
| 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; |
There was a problem hiding this comment.
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. viabeforeCall) 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 thefor-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.
| 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; |
- 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.
There was a problem hiding this 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 suggestionConsider 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 functionalityDistributorAPI.test.ts- API interaction testsVersionManagement.test.ts- Version-related testsMigrationStrategies.test.ts- Migration testsExternalInitializers.test.ts- Initializer testsMiddlewareHooks.test.ts- Hook-related testsThis would improve:
- Test maintainability
- Run speeds (if running specific test files)
- Readability and organization
- 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
setMiddlewarefunction 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
afterCallresult 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
requirementvalues 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
addDistributionfunction's behavior.Consider adding tests that:
- Directly call
addDistributionand verify costs- Test changing the default cost before adding a new distribution
- 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:
- Changing the beneficiary mid-test to ensure payments redirect correctly
- Testing with a zero instantiation cost (if supported) to ensure no payment is required
- Verifying payment behavior when a distribution is removed and re-added
src/mocks/MockERC165WithFunctions.sol (3)
86-99: Consider enhancing the_getSelectorStringimplementation.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
setFunctionResultfunction 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:
- Using a proxy-specific method to retrieve the implementation address if available
- Calling a function on the proxy that should delegate to the implementation
- 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'sexpect().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:
- Removing these commented-out contract definitions if they're already implemented elsewhere
- Moving them to proper files under
contracts/mocks/if they're still needed- 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:
- Setting custom instance addresses to test different scenarios
- Forcing reverts to test error handling
- 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:
- Breaking these tests into separate, focused test files organized by feature
- Using more helper functions to reduce repetition
- Adding detailed documentation explaining the migration workflows being tested
For example, you could split into:
VersionManagement.test.ts- Basic version requirement functionsMigrationStrategies.test.ts- Tests for different migration approachesMigrationErrorHandling.test.ts- Error cases and validationUpgradeSecurity.test.ts- Authorization and permission tests
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 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
InstantiationCostChangedevent 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
ERC20InvalidReceivererror 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 solidityLength 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" || trueLength 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 || trueLength of output: 3402
Ignore middleware interface validation here
ThebeforeCallin 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:
- Uncommenting and completing the test
- Adding more detailed assertions
- 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
createVersionRequirementhelper 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.
| 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"); | ||
| } | ||
| } |
There was a problem hiding this comment.
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:
- No check if middleware is set before usage
- Hardcoded empty values for context and amount parameters
- 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.
| 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"); | |
| } | |
| } |
| 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; | ||
| }); |
There was a problem hiding this comment.
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:
- Call ERC20 functions through the proxy
- Verify that state changes in the implementation are reflected when accessed through the proxy
- 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;
});| 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(); | ||
| }); |
There was a problem hiding this comment.
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:
- A missing
receive()orfallback()function - 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.
| 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); | |
| }); |
| 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); | ||
| }); |
There was a problem hiding this comment.
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:
- Implement proper mock contracts to enable these tests
- Modify the tests to use available functionality
- 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.
| }); | ||
| }); | ||
|
|
||
| // 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); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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:
- Initializers that return invalid data structures
- Initializers that run out of gas
- Re-entrancy scenarios
- Initializers that revert without specific reasons (currently tested)
- 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;
});There was a problem hiding this 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
whitelistedDistributionsfunctionality are included but may not function correctly due to incomplete mock implementations.test/eds/RepositoryExtended.ts (3)
270-277: Incorrect parameter packing when callingrepository.getThe
getmethod 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
changeMigrationScriptin 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 issueIncorrect parameter packing when calling
repository.get(multiple instances)The
getmethod 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 testsThe external initializer tests are good but could be expanded to cover more edge cases, particularly around error handling and validation.
Consider adding tests for:
- Initializers that return invalid data structures
- Initializers that run out of gas
- Re-entrancy scenarios
- 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 ownererror 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 contractstest/eds/RepositoryExtended.ts (2)
382-390: Consider adding assertions for skipped version resolution testsThese 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 testThe 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
📒 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 functionThe
createVersionRequirementhelper 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 testsThe 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 testsThe 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.
| // 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 ... | ||
| } | ||
| */ |
There was a problem hiding this comment.
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
}| 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 | ||
| }); |
There was a problem hiding this comment.
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
});| 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" | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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 () { |
There was a problem hiding this comment.
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.
| describe("InstallerClonable & InstallerOwnable", function () { | |
| describe("OwnableInstaller", function () { |
| ["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) | ||
| ]); | ||
| }); |
There was a problem hiding this comment.
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)
]);
}| // 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 | ||
| }); |
There was a problem hiding this comment.
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.
| // 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"); | |
| }); |
| await expect( | ||
| distributor | ||
| .connect(owner) | ||
| [ | ||
| "addDistribution(bytes32,address,string)" | ||
| ](distributionId, ethers.constants.AddressZero, uniqueAlias) | ||
| ).to.be.reverted; // Should fail because alias already exists | ||
| }); |
There was a problem hiding this comment.
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.
| 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"); | |
| }); |
| // Try to upgrade - should fail because version is not in range | ||
| await expect(distributor.connect(owner).upgradeUserInstance(appId!, migrationId, "0x")).to.be | ||
| .reverted; | ||
| }); |
There was a problem hiding this comment.
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.
| // 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"); | |
| }); |
| // 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 | ||
| ); |
There was a problem hiding this comment.
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 solidityLength 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.tsLength 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.
There was a problem hiding this 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 issueFix the invalid require statement with custom error.
The
requirestatement 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 issueFix 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 issueFix 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
📒 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
IERC20interface and OpenZeppelin'sSafeERC20utilities, 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.safeTransferFrominstead of the unsafetransferFrommethod, 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
getTokenizedDistributorStoreusing 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
initializermodifier from OpenZeppelin's Initializable contract, preventing multiple initializations and setting up the contract state properly.
65-69: Good event emission for state changes.The
_setInstantiationCostfunction 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
_addDistributioncorrectly 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
_addDistributionmethod 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
_instantiatecorrectly 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] = distributorsIdbefore 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.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Refactor
Style
Tests
Chores
Revert