Skip to content

Prevent construct() from being called directly on implementation contracts#165

Closed
kbrizzle wants to merge 1 commit intomasterfrom
sec-disable-impl-construct
Closed

Prevent construct() from being called directly on implementation contracts#165
kbrizzle wants to merge 1 commit intomasterfrom
sec-disable-impl-construct

Conversation

@kbrizzle
Copy link
Copy Markdown
Contributor

@kbrizzle kbrizzle commented Mar 11, 2026

Sets a constructed flag in the Implementation constructor's ERC-7201 storage slot, causing direct calls to construct() on the implementation to revert with ImplementationAlreadyConstructedError. Proxy (delegatecall) usage is unaffected since the proxy's own storage is read.

Ref: OZ _disableInitializers


Note

Medium Risk
Touches core upgrade/initialization plumbing by introducing a new constructed-lock in Implementation, so mistakes could brick initialization if the flag is set or cleared incorrectly. Changes are small and covered by updated tests, but the area is security- and lifecycle-sensitive.

Overview
Prevents calling construct() directly on implementation contracts by adding a persistent constructed flag in Implementation’s ERC-7201 storage, set during the implementation constructor via a new _disableInitializers() hook.

construct() now reverts with the new ImplementationAlreadyConstructedError when the implementation instance is already locked, while proxy/delegatecall flows continue to rely on the proxy’s own storage. Tests are updated to account for the lock (clearing the storage slot for direct unit tests) and a new regression test asserts direct construct() on an implementation reverts.

Written by Cursor Bugbot for commit 139a210. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Implementation contracts now enforce single initialization with automatic safeguards against duplicate construction.
  • Improvements

    • Introduced new error for detecting and handling duplicate construction attempts on implementation contracts.
  • Tests

    • Updated test suite infrastructure to accommodate and validate new initialization protection mechanism.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Added a constructed boolean to ImplementationStorage, set during initialization and checked to prevent direct construct() calls on implementation contracts; new ImplementationAlreadyConstructedError() introduced. Tests were updated to clear that storage slot for unit testing and a new test asserts the revert.

Changes

Cohort / File(s) Summary
Core Implementation
src/mutability/Implementation.sol, src/mutability/interfaces/IImplementation.sol
Added bool constructed to ImplementationStorage, introduced _disableInitializers() and a constructor call to disable initializers, added a pre-check in construct() to revert with ImplementationAlreadyConstructedError(); added the new error to the interface.
Test Setup Updates
test/attribute/Attribute.t.sol, test/attribute/Delegatable.t.sol, test/attribute/Executable.t.sol, test/attribute/Ownable.t.sol, test/attribute/Pausable.t.sol, test/attribute/Withdrawable.t.sol, test/utils/OwnableStub.t.sol
Inserted vm.store calls to clear the implementation constructed flag in test setups so tests can call construct() on implementations.
New Test Case
test/mutability/Mutable.t.sol
Added test_noDirectConstructorAccessOnImplementation() asserting construct() on implementation reverts with ImplementationAlreadyConstructedError().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main change: preventing direct construct() calls on implementation contracts by introducing a constructed flag check.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sec-disable-impl-construct

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 11, 2026

Slither report

Static Analysis Report**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. Summary - [locked-ether](#locked-ether) (1 results) (Medium) - [reentrancy-no-eth](#reentrancy-no-eth) (3 results) (Medium) - [unused-return](#unused-return) (9 results) (Medium) - [incorrect-modifier](#incorrect-modifier) (1 results) (Low) - [calls-loop](#calls-loop) (2 results) (Low) - [reentrancy-benign](#reentrancy-benign) (3 results) (Low) - [reentrancy-events](#reentrancy-events) (4 results) (Low) - [dead-code](#dead-code) (45 results) (Informational) - [solc-version](#solc-version) (3 results) (Informational) - [missing-inheritance](#missing-inheritance) (2 results) (Informational) - [naming-convention](#naming-convention) (13 results) (Informational) - [unimplemented-functions](#unimplemented-functions) (4 results) (Informational) - [unindexed-event-address](#unindexed-event-address) (1 results) (Informational) ## locked-ether Impact: Medium Confidence: High - [ ] ID-0 Contract locking ether found: Contract [MutablePauseTarget](https://github.com/equilibria-xyz/root/blob/5c6fb9d7068b0e88ea339f96a1baab206f68372e/src/mutability/Mutable.sol#L138-L143) has payable functions: - [MutablePauseTarget.fallback()](https://github.com/equilibria-xyz/root/blob/5c6fb9d7068b0e88ea339f96a1baab206f68372e/src/mutability/Mutable.sol#L139-L141) But does not have a function to withdraw the ether

https://github.com/equilibria-xyz/root/blob/5c6fb9d7068b0e88ea339f96a1baab206f68372e/src/mutability/Mutable.sol#L138-L143

reentrancy-no-eth

Impact: Medium
Confidence: Medium

function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
ShortString name = ShortStrings.toShortString(implementation.name());
if (_nameToMutable[name] != IMutable(address(0))) revert MutatorMutableAlreadyExists();
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[name] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}

function _unpause() private whenPaused {
ERC1967Utils.upgradeToAndCall(Mutable$().paused, "");
Mutable$().paused = address(0);
emit Unpaused();
}

function _upgrade(IImplementation newImplementation, bytes memory data) private {
// validate the upgrade metadata of the new implementation
if (!Strings.equal(
(_implementation() == address(0) ? NULL_VERSION : IImplementation(_implementation()).version()),
newImplementation.predecessor()
)) revert MutablePredecessorMismatch();
if (Strings.equal(
newImplementation.version(),
ShortStrings.toString(Mutable$().version)
)) revert MutableVersionMismatch();
// update the implementation and call its constructor
ERC1967Utils.upgradeToAndCall(address(newImplementation), abi.encodeCall(IImplementation.construct, (data)));
// record the new implementation version
Mutable$().version = ShortStrings.toShortString(newImplementation.version());
}

unused-return

Impact: Medium
Confidence: Medium

function approve(Token6 self, address grantee) internal {
IERC20(Token6.unwrap(self)).approve(grantee, type(uint256).max);
}

function removeDistribution(bytes32 merkleRoot) external onlyOwner {
if (!_merkleRoots.contains(merkleRoot)) revert AirdropRootDoesNotExist();
_merkleRoots.remove(merkleRoot);
distributions[merkleRoot] = Token18.wrap(address(0));
emit DistributionRemoved(merkleRoot);
}

function approve(Token self, address grantee, uint256 amount) internal {
IERC20(Token.unwrap(self)).approve(grantee, amount);
}

function approve(Token18 self, address grantee, UFixed18 amount) internal {
IERC20(Token18.unwrap(self)).approve(grantee, UFixed18.unwrap(amount));
}

function approve(Token6 self, address grantee, UFixed6 amount) internal {
IERC20(Token6.unwrap(self)).approve(grantee, UFixed6.unwrap(amount));
}

function addDistributions(Token18 token, bytes32 merkleRoot) external override onlyOwner {
if (_merkleRoots.contains(merkleRoot)) revert AirdropDistributionAlreadyExists();
distributions[merkleRoot] = token;
_merkleRoots.add(merkleRoot);
emit DistributionAdded(token, merkleRoot);
}

function approve(Token self, address grantee) internal {
IERC20(Token.unwrap(self)).approve(grantee, type(uint256).max);
}

function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
ShortString name = ShortStrings.toShortString(implementation.name());
if (_nameToMutable[name] != IMutable(address(0))) revert MutatorMutableAlreadyExists();
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[name] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}

function approve(Token18 self, address grantee) internal {
IERC20(Token18.unwrap(self)).approve(grantee, type(uint256).max);
}

incorrect-modifier

Impact: Low
Confidence: High

modifier initializer(string memory attribute) {
if (!_constructing()) revert AttributeNotConstructing();
if (!Attribute$().attributes[attribute]) _;
Attribute$().attributes[attribute] = true;
}

calls-loop

Impact: Low
Confidence: Medium

function _unpause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).unpause();
super._unpause();
}

function _pause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).pause();
super._pause();
}

reentrancy-benign

Impact: Low
Confidence: Medium

function _pause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).pause();
super._pause();
}

function _unpause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).unpause();
super._unpause();
}

function create(
IImplementation implementation,
bytes calldata data
) public onlyOwner returns (IMutableTransparent newMutable) {
ShortString name = ShortStrings.toShortString(implementation.name());
if (_nameToMutable[name] != IMutable(address(0))) revert MutatorMutableAlreadyExists();
_mutables.add(address(newMutable = new Mutable(implementation, data)));
_nameToMutable[name] = IMutable(address(newMutable));
// ensure state of new mutable is consistent with mutator
if (paused()) IMutable(address(newMutable)).pause();
}

reentrancy-events

Impact: Low
Confidence: Medium

function _pause() private whenUnpaused {
Mutable$().paused = _implementation();
ERC1967Utils.upgradeToAndCall(_pauseTarget, "");
emit Paused();
}

function _unpause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).unpause();
super._unpause();
}

function _unpause() private whenPaused {
ERC1967Utils.upgradeToAndCall(Mutable$().paused, "");
Mutable$().paused = address(0);
emit Unpaused();
}

function _pause() internal override {
for (uint256 i = 0; i < _mutables.length(); i++) IMutable(_mutables.at(i)).pause();
super._pause();
}

dead-code

Impact: Informational
Confidence: Medium

function eq(UFixed18 a, UFixed18 b) pure returns (bool) {
return UFixed18.unwrap(a) == UFixed18.unwrap(b);
}

function add(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) + Fixed6.unwrap(b));
}

function eq(UFixed6 a, UFixed6 b) pure returns (bool) {
return UFixed6.unwrap(a) == UFixed6.unwrap(b);
}

  • ID-26
    neg(Fixed6) is never used and should be removed

function neg(Fixed6 a) pure returns (Fixed6) {
return Fixed6.wrap(-Fixed6.unwrap(a));
}

function _constructing() internal view override returns (bool) {
return Implementation$().constructing;
}

function add(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) + Fixed18.unwrap(b));
}

function mul(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) * Fixed6.unwrap(b) / Fixed6Lib.BASE);
}

function eq(Fixed6 a, Fixed6 b) pure returns (bool) {
return Fixed6.unwrap(a) == Fixed6.unwrap(b);
}

function gt(UFixed6 a, UFixed6 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed6.unwrap(a), UFixed6.unwrap(b));
return au > bu;
}

function neq(Fixed6 a, Fixed6 b) pure returns (bool) {
return Fixed6.unwrap(a) != Fixed6.unwrap(b);
}

function mul(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) * Fixed18.unwrap(b) / Fixed18Lib.BASE);
}

function gt(Fixed6 a, Fixed6 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed6.unwrap(a), Fixed6.unwrap(b));
return au > bu;
}

function lt(UFixed18 a, UFixed18 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed18.unwrap(a), UFixed18.unwrap(b));
return au < bu;
}

function div(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) * UFixed18Lib.BASE / UFixed18.unwrap(b));
}

function gte(UFixed6 a, UFixed6 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function neg(Fixed18 a) pure returns (Fixed18) {
return Fixed18.wrap(-Fixed18.unwrap(a));
}

function sub(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) - Fixed6.unwrap(b));
}

function gte(Fixed6 a, Fixed6 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function mul(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) * UFixed6.unwrap(b) / UFixed6Lib.BASE);
}

function lte(Fixed6 a, Fixed6 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function lte(UFixed18 a, UFixed18 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function mul(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) * UFixed18.unwrap(b) / UFixed18Lib.BASE);
}

function lt(Fixed6 a, Fixed6 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed6.unwrap(a), Fixed6.unwrap(b));
return au < bu;
}

function div(Fixed6 a, Fixed6 b) pure returns (Fixed6) {
return Fixed6.wrap(Fixed6.unwrap(a) * Fixed6Lib.BASE / Fixed6.unwrap(b));
}

function neq(UFixed18 a, UFixed18 b) pure returns (bool) {
return UFixed18.unwrap(a) != UFixed18.unwrap(b);
}

function _deployer() internal view override returns (address) {
return IMutator(msg.sender).owner();
}

function div(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) * Fixed18Lib.BASE / Fixed18.unwrap(b));
}

function lt(UFixed6 a, UFixed6 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed6.unwrap(a), UFixed6.unwrap(b));
return au < bu;
}

function lt(Fixed18 a, Fixed18 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed18.unwrap(a), Fixed18.unwrap(b));
return au < bu;
}

function gt(Fixed18 a, Fixed18 b) pure returns (bool) {
(int256 au, int256 bu) = (Fixed18.unwrap(a), Fixed18.unwrap(b));
return au > bu;
}

function lte(Fixed18 a, Fixed18 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function eq(Fixed18 a, Fixed18 b) pure returns (bool) {
return Fixed18.unwrap(a) == Fixed18.unwrap(b);
}

function sub(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) - UFixed18.unwrap(b));
}

function neq(UFixed6 a, UFixed6 b) pure returns (bool) {
return UFixed6.unwrap(a) != UFixed6.unwrap(b);
}

function gt(UFixed18 a, UFixed18 b) pure returns (bool) {
(uint256 au, uint256 bu) = (UFixed18.unwrap(a), UFixed18.unwrap(b));
return au > bu;
}

function gte(UFixed18 a, UFixed18 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function sub(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) - UFixed6.unwrap(b));
}

function _disableInitializers() internal virtual {
if (Implementation$().constructing) revert ImplementationAlreadyConstructedError();
Implementation$().constructed = true;
}

function add(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) + UFixed6.unwrap(b));
}

function sub(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) - Fixed18.unwrap(b));
}

function gte(Fixed18 a, Fixed18 b) pure returns (bool) {
return eq(a, b) || gt(a, b);
}

function lte(UFixed6 a, UFixed6 b) pure returns (bool) {
return eq(a, b) || lt(a, b);
}

function div(UFixed6 a, UFixed6 b) pure returns (UFixed6) {
return UFixed6.wrap(UFixed6.unwrap(a) * UFixed6Lib.BASE / UFixed6.unwrap(b));
}

function add(UFixed18 a, UFixed18 b) pure returns (UFixed18) {
return UFixed18.wrap(UFixed18.unwrap(a) + UFixed18.unwrap(b));
}

function neq(Fixed18 a, Fixed18 b) pure returns (bool) {
return Fixed18.unwrap(a) != Fixed18.unwrap(b);
}

solc-version

Impact: Informational
Confidence: High

pragma solidity ^0.8.13;

pragma solidity ^0.8.19;

pragma solidity >=0.8.20;

missing-inheritance

Impact: Informational
Confidence: High

contract OwnableStub {
/// @notice Accepts ownership of the contract
/// @dev Can only be called by the pending owner to ensure correctness.
function acceptOwner(address ownable) external {
Ownable(ownable).acceptOwner();
}
}

https://github.com/equilibria-xyz/root/blob/5c6fb9d7068b0e88ea339f96a1baab206f68372e/src/mutability/Mutable.sol#L138-L143

naming-convention

Impact: Informational
Confidence: High

function __Pausable__constructor() internal initializer("Pausable") {
_updatePauser(_deployer());
}

function Mutable$() private pure returns (MutableStorage storage $) {
assembly {
$.slot := MutableStorageLocation
}
}

bytes32 private constant MutableStorageLocation = 0xb906736fa3fc696e6c19a856e0f8737e348fda5c7f33a32db99da3b92f19a800;

bytes32 private constant OwnableStorageLocation = 0x863176706c9b4c9b393005d0714f55de5425abea2a0b5dfac67fac0c9e2ffe00;

function __Ownable__constructor() internal initializer("Ownable") {
_updateOwner(_deployer());
}

bytes32 private constant ImplementationStorageLocation = 0x3c57b102c533ff058ebe9a7c745178ce4174563553bb3edde7874874c532c200;

function Ownable$() private pure returns (OwnableStorage storage $) {
assembly {
$.slot := OwnableStorageLocation
}
}

function Implementation$() private pure returns (ImplementationStorage storage $) {
assembly {
$.slot := ImplementationStorageLocation
}
}

bytes32 private constant PausableStorageLocation = 0x3f6e81f1674f7eaca7e8904fa6f14f10175d4d641e37fc18a3df849e00101900;

function Pausable$() private pure returns (PausableStorage storage $) {
assembly {
$.slot := PausableStorageLocation
}
}

function __constructor(bytes memory data) internal virtual returns (string memory);

function Attribute$() private pure returns (AttributeStorage storage $) {
assembly {
$.slot := AttributeStorageLocation
}
}

bytes32 private constant AttributeStorageLocation = 0x429797e2de2710eed6bc286312ff2c2286e5c3e13ca14d38e450727a132bfa00;

unimplemented-functions

Impact: Informational
Confidence: High

abstract contract Withdrawable is IWithdrawable, Attribute, Ownable {
/// @notice Withdraws all ERC20 tokens from the contract to the owner
/// @dev Can only be called by the owner
/// @param token Address of the ERC20 token
function withdraw(Token18 token) public virtual onlyOwner {
token.push(owner());
}
}

abstract contract Implementation is IImplementation, Contract {
/// @custom:storage-location erc7201:equilibria.root.Implementation
struct ImplementationStorage {
bool constructing;
bool constructed;
}
/// @dev The erc7201 storage location of the mix-in
// solhint-disable-next-line const-name-snakecase
bytes32 private constant ImplementationStorageLocation = 0x3c57b102c533ff058ebe9a7c745178ce4174563553bb3edde7874874c532c200;
/// @dev The erc7201 storage of the mix-in
function Implementation$() private pure returns (ImplementationStorage storage $) {
assembly {
$.slot := ImplementationStorageLocation
}
}
/// @dev The version of this implementation.
ShortString private immutable _version;
/// @dev The version of the predecessor implementation.
ShortString private immutable _predecessor;
/// @dev Constructor for the implementation.
constructor(string memory version_, string memory predecessor_) {
_version = ShortStrings.toShortString(version_);
_predecessor = ShortStrings.toShortString(predecessor_);
_disableInitializers();
}
/// @dev The name of the implementation.
function name() external view virtual returns (string memory);
/// @dev The version of this implementation.
function version() public view virtual returns (string memory) {
return ShortStrings.toString(_version);
}
/// @dev The version of the predecessor implementation.
function predecessor() external view virtual returns (string memory) {
return ShortStrings.toString(_predecessor);
}
/// @dev Called at upgrade time to initialize the contract with `data`.
function construct(bytes memory data) external {
if (Implementation$().constructed) revert ImplementationAlreadyConstructedError();
Implementation$().constructing = true;
string memory constructorVersion = __constructor(data);
if (!Strings.equal(constructorVersion, version())) revert ImplementationConstructorVersionMismatch();
Implementation$().constructing = false;
}
/// @dev Whether the contract is initializing.
function _constructing() internal view override returns (bool) {
return Implementation$().constructing;
}
/// @dev The deployer of the contract.
function _deployer() internal view override returns (address) {
return IMutator(msg.sender).owner();
}
/// @dev Locks the contract, preventing any future reinitialization. Called in the constructor to
/// prevent the implementation contract from being used directly.
/// @custom:oz-ref https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/Initializable.sol#L192C14-L192C34
function _disableInitializers() internal virtual {
if (Implementation$().constructing) revert ImplementationAlreadyConstructedError();
Implementation$().constructed = true;
}
/// @dev Hook for inheriting contracts to construct the contract.
function __constructor(bytes memory data) internal virtual returns (string memory);
}

abstract contract Delegatable is IDelegatable, Attribute, Ownable {
/// @notice Delegates voting power for a specific token to an address
/// @dev Can only be called by the owner
/// @param token The IVotes-compatible token to delegate
/// @param delegatee The address to delegate voting power to
function delegate(IVotes token, address delegatee) public virtual onlyOwner {
token.delegate(delegatee);
}
}

abstract contract Executable is IExecutable, Attribute, Ownable {
/// @notice Executes a call to a target contract
/// @dev Can only be called by the owner
/// @param target Address of the target contract
/// @param data Calldata to be executed
/// @return result The result of the call
function execute(address target, bytes calldata data) public payable virtual onlyOwner returns (bytes memory) {
return Address.functionCallWithValue(target, data, msg.value);
}
}

unindexed-event-address

Impact: Informational
Confidence: High


Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/attribute/Ownable.t.sol (1)

34-35: Extract this storage-slot write behind a shared helper/constant.

This raw slot literal is now duplicated across several suites. If the ImplementationStorage slot changes, these tests will start mutating the wrong word in a non-obvious way. Please centralize the slot in one test helper and call that instead of copying the magic value inline.

♻️ Example cleanup
-        vm.store(address(ownable), 0x3c57b102c533ff058ebe9a7c745178ce4174563553bb3edde7874874c532c200, bytes32(0));
+        _clearConstructedFlag(address(ownable));
bytes32 internal constant IMPLEMENTATION_STORAGE_SLOT =
    0x3c57b102c533ff058ebe9a7c745178ce4174563553bb3edde7874874c532c200;

function _clearConstructedFlag(address target) internal {
    vm.store(target, IMPLEMENTATION_STORAGE_SLOT, bytes32(0));
}
test/attribute/Delegatable.t.sol (1)

40-41: LGTM! Consider using a shared constant for the storage slot.

The storage manipulation correctly clears the constructed flag to enable unit testing. Since the AI summary indicates multiple test files use the same pattern with this magic number, extracting ImplementationStorageLocation into a shared test helper or constant would reduce maintenance burden if the slot ever changes.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f4f30a7-b5fd-4ef8-a2ce-23107fe12f12

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd20c6 and 66ff672.

📒 Files selected for processing (10)
  • src/mutability/Implementation.sol
  • src/mutability/interfaces/IImplementation.sol
  • test/attribute/Attribute.t.sol
  • test/attribute/Delegatable.t.sol
  • test/attribute/Executable.t.sol
  • test/attribute/Ownable.t.sol
  • test/attribute/Pausable.t.sol
  • test/attribute/Withdrawable.t.sol
  • test/mutability/Mutable.t.sol
  • test/utils/OwnableStub.t.sol

@kbrizzle kbrizzle force-pushed the sec-disable-impl-construct branch from 66ff672 to 139a210 Compare March 11, 2026 07:45
@github-actions
Copy link
Copy Markdown

Unit Test Coverage Report

Coverage after merging sec-disable-impl-construct into master will be
99.94%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/attribute
   Attribute.sol90%50%100%100%30
   Delegatable.sol100%100%100%100%
   Executable.sol100%100%100%100%
   Ownable.sol100%100%100%100%
   Pausable.sol100%100%100%100%
   Withdrawable.sol100%100%100%100%
src/distribution
   Airdrop.sol100%100%100%100%
   Treasury.sol100%100%100%100%
src/mutability
   Derived.sol100%100%100%100%
   Implementation.sol94.12%66.67%100%95.65%82, 82
   Mutable.sol100%100%100%100%
   Mutator.sol100%100%100%100%
src/number
   NumberMath.sol100%100%100%100%
src/number/types
   Fixed18.sol100%100%100%100%
   Fixed6.sol100%100%100%100%
   UFixed18.sol100%100%100%100%
   UFixed6.sol100%100%100%100%
src/token/types
   Token.sol100%100%100%100%
   Token18.sol100%100%100%100%
   Token6.sol100%100%100%100%
src/utils
   OwnableStub.sol100%100%100%100%
   console.sol99.89%60%100%100%44, 65
src/vrgda
   VRGDADecayMath.sol100%100%100%100%
   VRGDAIssuanceMath.sol100%100%100%100%
src/vrgda/types
   LinearExponentialVRGDA.sol100%100%100%100%

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/attribute/Ownable.t.sol (1)

34-35: Extract this slot reset into a shared test helper.

The same private ERC-7201 slot literal is now duplicated across multiple suites. Centralizing it in one helper would reduce drift and make it easier to swap these tests to a delegatecall-based harness later if you want them to follow production semantics more closely.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d86074c0-d07a-4cfc-aa90-4e7aa1128954

📥 Commits

Reviewing files that changed from the base of the PR and between 66ff672 and 139a210.

📒 Files selected for processing (10)
  • src/mutability/Implementation.sol
  • src/mutability/interfaces/IImplementation.sol
  • test/attribute/Attribute.t.sol
  • test/attribute/Delegatable.t.sol
  • test/attribute/Executable.t.sol
  • test/attribute/Ownable.t.sol
  • test/attribute/Pausable.t.sol
  • test/attribute/Withdrawable.t.sol
  • test/mutability/Mutable.t.sol
  • test/utils/OwnableStub.t.sol
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/utils/OwnableStub.t.sol
  • test/attribute/Attribute.t.sol
  • test/mutability/Mutable.t.sol

@kbrizzle kbrizzle marked this pull request as draft March 11, 2026 07:55
@kbrizzle kbrizzle closed this Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant