# Audit report

> Audit the proposed stateful Claus managed-contract kernel in proposals/managed-contracts/ at the exact requested commit. It is NOT deployed or admitted; existing readonly contracts and runtime admission are unchanged. Focus only on ClausManagedExecutor.sol, ClausManagedProject.sol and their tests. Assess whether arbitrary delegatecalled project code can bypass the separate implementation registry or the required Claus signature; test initial registration's owner AND Claus signatures, exact source/review/configuration binding, cross-project/chain replay, revocation/generation/nonce handling, recovery, reentrancy, initialization atomicity, stale bytecode, storage poisoning, proxy function collisions and malicious module calls. Identify incomplete integration and economic trust assumptions explicitly rather than claiming a general proxy makes custody safe. Consider EIP-6780 selfdestruct during initial construction/initialization and callback behavior. A module can intentionally move project assets; the goal is a non-bypassable upgrade registry, NOT a universal guarantee that arbitrary approved code is harmless. Do not modify files or open a pull request. Return concrete findings with severity, exact code references and reproduction or attack explanation; distinguish prototype-kernel issues from currently deployed readonly infrastructure.

| | |
|---|---|
| Repository | https://github.com/contractclaus/claus-network |
| Commit | `7dbf6e864d4ddbab34fe104023ca3bc0e0742a6b` |
| Job | `8b83f6ca-74a7-495a-b475-d0e2163c303f` |
| Judged | 2026-10-13 19:39 UTC |
| Findings | 3 medium · 7 low · 4 info |

Four agents audited the code as it is at `7dbf6e8`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: Another managed project's proxy is accepted as an implementation, so a project's effective code follows a different project's registry entry with no approval bound to it

`proposals/managed-contracts/ClausManagedExecutor.sol:145`

```
            || r.implementation.code.length == 0 || r.implementation.codehash != r.codeHash
```

Prototype-kernel issue (not present in the deployed readonly contracts). registerProject (line 145) only requires that r.implementation has code and that its codehash matches r.codeHash; _validate (line 271) additionally excludes a.implementation == a.target and == address(this). Neither path excludes a ClausManagedProject proxy created by this executor for a different project. A proxy's runtime bytecode embeds its own executor and projectId as immutables, so when project P's proxy delegatecalls project Q's proxy, Q's fallback runs in P's storage context, reads moduleCode(Q) from the registry and delegatecalls Q's CURRENT module. Every later execute() approved for Q (bound to Q's projectId, target, nonce, mandate and generations) silently changes P's running code, while P's registry still reports implementation()==Q-proxy and version()==1, no UpgradeExecuted is emitted for P, and P's owner controls (setAuthority, cancelApproval, owner transfer) never participate. This breaks the per-project binding the Approval struct exists to enforce and the README line 5 claim that upgrades retain the project's own nonce, mandate and revocation. Both signers must sign the initial registration or approval, so the precondition is a review miss or a cooperating owner; the kernel can nonetheless cheaply and fully enforce this case, unlike the general one-hop forwarding problem reported separately. Note: a mutual arrangement (P -> Q-proxy and Q -> P-proxy) does NOT recurse; it fails closed with InvalidModule because the module==address(this) check trips (verified, test/scratch/Judge.t.sol::testMutualProxyFailsClosed), so only the one-directional case is a defect. Fix: record every created target in mapping(address => bool) isManagedTarget in registerProject and add || isManagedTarget[r.implementation] || r.implementation == address(this) to the registerProject condition and || isManagedTarget[a.implementation] to _validate. Merged from specialists audit_permissions (802e), audit_math (4456) and audit_economics (3a5d).

**Reproduction**

State: executor deployed; project Q registered for owner 0xB0B with module Counter{set,count} and configuration set(7). Input 1: registerProject for owner 0xC0C with r.implementation = Q's target address, r.codeHash = that address's codehash, configuration = set(7), valid owner and Claus signatures. Expected: InvalidRegistration. Actual: succeeds and returns target P; Counter(P).count()==7 through Q's proxy code. Input 2: with P registered normally on Counter, a Claus-signed Approval for P with a.implementation = Q's target; queue(). Expected: InvalidApproval. Actual: queue succeeds (and execute succeeds). Then a Claus-signed Approval for Q only (projectId=Q, fromVersion=1, implementation=CounterV2 with addTen) queued and executed makes CounterV2(P).addTen() succeed while P.version()==1 and P.implementation()==Q-proxy. Verified: all three specialist proofs fail on this code for exactly this reason (test/scratch/Proof_802e0e8572b0.t.sol: both tests fail with "next call did not revert as expected"; test/scratch/Proof_4456edbb7311.t.sol likewise). Run from the repository root with: FOUNDRY_SRC=proposals/managed-contracts FOUNDRY_TEST=test/scratch FOUNDRY_SOLC_VERSION=0.8.27 FOUNDRY_EVM_VERSION=cancun FOUNDRY_VIA_IR=true FOUNDRY_AUTO_DETECT_REMAPPINGS=false forge test --lib-paths contracts/lib -R '@openzeppelin/contracts/=contracts/lib/openzeppelin-contracts/contracts/' -R 'forge-std/=contracts/lib/forge-std/src/' --match-path <file> (the proposal's own foundry.toml cannot resolve its remappings, see the separate finding).

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;
import {Test} from "forge-std/Test.sol";
import {ClausManagedExecutor} from "proposals/managed-contracts/ClausManagedExecutor.sol";
import {ClausManagedProject} from "proposals/managed-contracts/ClausManagedProject.sol";

contract Counter { uint256 public count; function set(uint256 v) external { count = v; } }
contract CounterV2 { uint256 public count; function set(uint256 v) external { count = v + 1000; } }

/// Fails on the current kernel: project P may register project Q's managed proxy as its
/// implementation, after which Q's upgrades silently change P's code with P's registry untouched.
/// Passes once registerProject/_validate reject implementations that are managed project targets.
contract ProofCrossProxyTest is Test {
    ClausManagedExecutor executor; Counter first;
    uint256 constant CLAUS = 0xA11CE; uint256 constant OWNER = 0xB0B; uint256 constant OWNER2 = 0xC0C;
    function sign(uint256 key, bytes32 h) internal pure returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, h); return abi.encodePacked(r, s, v); }
    function reg(uint256 ownerKey, bytes32 salt, address impl, bytes memory cfg) internal view returns (ClausManagedExecutor.Registration memory r) {
        r.projectId = executor.projectKey(vm.addr(ownerKey), salt); r.owner = vm.addr(ownerKey); r.salt = salt; r.implementation = impl;
        r.codeHash = impl.codehash; r.initCallHash = keccak256(cfg); r.mandateHash = keccak256("m"); r.policyVersion = keccak256("p");
        r.nonce = uint256(salt); r.expiresAt = uint64(block.timestamp + 1 days); r.evidenceHash = keccak256("e"); r.dependencySnapshotHash = keccak256("d"); r.clausGeneration = 1;
    }
    function setUp() public { vm.warp(1800000000); executor = new ClausManagedExecutor(vm.addr(CLAUS), address(this), 1 days); first = new Counter(); }

    function testManagedProxyIsRejectedAsImplementation() public {
        bytes memory cfg = abi.encodeCall(Counter.set, (7));
        ClausManagedExecutor.Registration memory rq = reg(OWNER, keccak256("q"), address(first), cfg);
        bytes32 dq = executor.registrationDigest(rq);
        address q = executor.registerProject(rq, cfg, sign(OWNER, dq), sign(CLAUS, dq));

        // P registers Q's proxy as its implementation: the kernel must refuse this.
        ClausManagedExecutor.Registration memory rp = reg(OWNER2, keccak256("p"), q, cfg);
        bytes32 dp = executor.registrationDigest(rp);
        bytes memory os = sign(OWNER2, dp); bytes memory cs = sign(CLAUS, dp);
        vm.expectRevert(ClausManagedExecutor.InvalidRegistration.selector);
        executor.registerProject(rp, cfg, os, cs);
    }

    function testManagedProxyIsRejectedAsUpgradeImplementation() public {
        bytes memory cfg = abi.encodeCall(Counter.set, (7));
        ClausManagedExecutor.Registration memory rq = reg(OWNER, keccak256("q"), address(first), cfg);
        bytes32 dq = executor.registrationDigest(rq);
        address q = executor.registerProject(rq, cfg, sign(OWNER, dq), sign(CLAUS, dq));
        ClausManagedExecutor.Registration memory rp = reg(OWNER2, keccak256("p"), address(first), cfg);
        bytes32 dp = executor.registrationDigest(rp);
        address p = executor.registerProject(rp, cfg, sign(OWNER2, dp), sign(CLAUS, dp));

        // Upgrade P to use Q's proxy as its module: the kernel must refuse this.
        ClausManagedExecutor.Approval memory a = ClausManagedExecutor.Approval(rp.projectId, block.chainid, address(executor), p, 1, address(first).codehash, q, q.codehash, keccak256(""), keccak256("d"), keccak256("e"), keccak256("p"), 1, 1, 1, keccak256("m"), 1, uint64(block.timestamp), uint64(block.timestamp + 1 days));
        bytes memory s = sign(CLAUS, executor.approvalDigest(a));
        vm.expectRevert(ClausManagedExecutor.InvalidApproval.selector);
        executor.queue(a, s);
    }
}
```

### 2. Medium: EIP-6780: a SELFDESTRUCT reached by the initial initializer runs in the proxy's creation transaction, deleting the proxy while the registry keeps an enabled project at a codeless, unrepairable target

`proposals/managed-contracts/ClausManagedExecutor.sol:156`

```
        ClausManagedProject(payable(target)).initialize(configuration);
```

Prototype-kernel issue (the deployed ClausApprovalExecutor never delegatecalls during registration). registerProject creates the ClausManagedProject with CREATE (line 153) and, in the same transaction, calls initialize(configuration) (line 156), which delegatecalls the approved module in the proxy's context. Under Cancun (the configured evm_version) SELFDESTRUCT deletes code and storage only for an account created in the same transaction, which is exactly this window. If the approved module's initializer path executes SELFDESTRUCT (directly, via a library, or via a nested call), the registration transaction succeeds, ProjectRegistered is emitted, projects[id] stores target with enabled=true and modules[id] stores version 1, but the proxy account is removed at the end of the transaction. Consequences: (1) every later execute() for the project reverts because initialize() is a high-level call to a codeless address (Solidity inserts an extcodesize check), so the project can never be repaired by an upgrade; (2) projects[id].target != 0 blocks re-registration of that (owner, salt) forever; (3) the executor's CREATE nonce has moved on so nothing can ever place code at that address again, and any ETH or tokens sent to the published target are unrecoverable; (4) the README's fixed selectors no longer answer at the recorded target. A target.code.length check after initialize() inside registerProject does not detect this because EXTCODESIZE still returns the code until the end of the transaction. Both owner and Claus sign the registration, so this is a review-miss hazard rather than an outsider attack, but it breaks the implied invariant that a registered target is a live proxy under registry control, and the task asked for this case explicitly. A same-transaction SELFDESTRUCT of the implementation (rather than the proxy) only makes the proxy fail closed with InvalidModule and remains repairable by upgrade; a SELFDESTRUCT in a later execute() initializer only forwards the proxy balance (approved code moving assets, within the stated model). Fix: never run project code in the proxy's creation transaction. Either split registration into registerProject (create proxy, store pending configuration) and a later activate(projectId) that performs the first initialize() in a different transaction, or deploy proxies with CREATE2 salted by projectId so a dead target can be re-created at the same address. Merged from specialists audit_flow (7d8d), audit_permissions (48c7), audit_math (3745) and audit_economics (74b6).

**Reproduction**

State: fresh executor; module Destroyer with function boom(address payable to) { selfdestruct(to); } deployed. Input (transaction 1): Registration r with implementation=Destroyer, codeHash=Destroyer.codehash, initCallHash=keccak256(abi.encodeCall(boom,(0xdead))), both owner and Claus signatures valid; registerProject(r, abi.encodeCall(boom,(0xdead)), ownerSig, clausSig). Expected: either the registration reverts, or the recorded target keeps its code and remains upgradeable. Actual: the call returns target and emits ProjectRegistered; in transaction 2, executor.projects(id) still returns (owner, target, ..., enabled=true) while target.code.length == 0; a correctly Claus-signed Approval from version 1 / Destroyer.codehash to Counter queues fine but execute() reverts on the call to the codeless address. Verified: test/scratch/Proof_7d8d2a746cc0.t.sol fails with "registry records a live project at an address with no code: 0 <= 0" and test/scratch/Proof_3745d77af6c2.t.sol fails with "registered, enabled target has no code" on this code. Run from the repository root with: FOUNDRY_SRC=proposals/managed-contracts FOUNDRY_TEST=test/scratch FOUNDRY_SOLC_VERSION=0.8.27 FOUNDRY_EVM_VERSION=cancun FOUNDRY_VIA_IR=true FOUNDRY_AUTO_DETECT_REMAPPINGS=false forge test --lib-paths contracts/lib -R '@openzeppelin/contracts/=contracts/lib/openzeppelin-contracts/contracts/' -R 'forge-std/=contracts/lib/forge-std/src/' --match-path <file> (the proposal's own foundry.toml cannot resolve its remappings, see the separate finding).

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import {Test} from "forge-std/Test.sol";
import {ClausManagedExecutor} from "proposals/managed-contracts/ClausManagedExecutor.sol";
import {ClausManagedProject} from "proposals/managed-contracts/ClausManagedProject.sol";

/// Approved module whose initializer self-destructs the calling context (the proxy).
contract SelfDestructingModule {
    uint256 public count;
    function boom(address payable to) external { selfdestruct(to); }
    function set(uint256 v) external { count = v; }
}

/// Finding: registerProject creates the proxy and delegatecalls the module in the same
/// transaction. Under EIP-6780 a SELFDESTRUCT executed in that delegatecall deletes the
/// proxy's code at the end of the registration transaction, yet the registry keeps a live
/// Project/Module entry pointing at the now codeless address. Every later execute() for the
/// project reverts and value sent to the recorded target is stuck.
///
/// Invariant checked here: after registration (observed in a later transaction) the registry
/// never points at an address without code. setUp() is a separate transaction from the test,
/// so the EIP-6780 deletion is visible. The try/catch lets the test pass under either fix
/// shape: rejecting the registration, or deferring the first delegatecall to a later
/// transaction (where EIP-6780 makes SELFDESTRUCT unable to remove code).
contract SelfdestructInitTest is Test {
    ClausManagedExecutor executor;
    SelfDestructingModule module;
    uint256 constant CLAUS = 0xA11CE;
    uint256 constant OWNER = 0xB0B;
    bytes32 id;

    function sign(uint256 key, bytes32 hash) internal pure returns (bytes memory) {
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, hash);
        return abi.encodePacked(r, s, v);
    }

    function setUp() public {
        vm.warp(1800000000);
        executor = new ClausManagedExecutor(vm.addr(CLAUS), address(this), 1 days);
        module = new SelfDestructingModule();
        bytes memory config = abi.encodeCall(SelfDestructingModule.boom, (payable(address(0xdead))));
        ClausManagedExecutor.Registration memory r;
        r.owner = vm.addr(OWNER);
        r.salt = keccak256("boom");
        r.projectId = executor.projectKey(r.owner, r.salt);
        r.implementation = address(module);
        r.codeHash = address(module).codehash;
        r.initCallHash = keccak256(config);
        r.mandateHash = keccak256("mandate");
        r.policyVersion = keccak256("policy");
        r.nonce = 1;
        r.expiresAt = uint64(block.timestamp + 1 days);
        r.evidenceHash = keccak256("review");
        r.dependencySnapshotHash = keccak256("deps");
        r.clausGeneration = 1;
        id = r.projectId;
        bytes32 digest = executor.registrationDigest(r);
        bytes memory os = sign(OWNER, digest);
        bytes memory cs = sign(CLAUS, digest);
        // Transaction 1: registration. A fixed kernel may reject it; that is acceptable.
        try executor.registerProject(r, config, os, cs) {} catch {}
    }

    /// Transaction 2: the registry must not record a live project at a codeless address.
    function testRegistryNeverPointsAtCodelessTarget() public view {
        (, address recorded,,,,,, bool enabled) = executor.projects(id);
        if (recorded == address(0)) return; // registration was rejected: fine
        assertTrue(enabled);
        assertGt(recorded.code.length, 0, "registry records a live project at an address with no code");
    }
}
```

### 3. Medium: Trust assumption: with the default zero upgrade delay a single Claus signature queues and executes a code replacement in one transaction, so owner revocation cannot pre-empt it

`proposals/managed-contracts/ClausManagedExecutor.sol:23`

```
    uint64 public constant MIN_UPGRADE_DELAY = 0;
```

Reported as an economic trust assumption of the prototype kernel, not as a permission bypass; the brief asks for these to be stated explicitly. Only initial registration requires the owner's signature (lines 150-151). Every later Approval needs only the Claus signature (lines 163, 178) and queue() is permissionless. MIN_UPGRADE_DELAY is 0 and Registration.minDelaySeconds defaults to 0 unless the owner opts in (the proposal's own tests use 0), so queue() sets readyAt[hash] = block.timestamp (lines 165-168) and execute() in the same transaction passes the block.timestamp < readyAt check (line 176). A contract can call queue then execute in one call, so the owner never observes ApprovalQueued before the new code runs and cancelApproval / setAuthority(enabled=false) are unusable as a veto. Because ClausManagedProject delegates all state and assets to the approved module, compromise or coercion of the single clausSigner key (or an ERC-1271 contract behind it) is sufficient to replace the logic controlling every zero-delay project's assets in one block, with no on-chain second party; scheduleSignerRecovery takes at least recoveryDelay (>= 1 day) and only stops future approvals. The README lists "revocation" and "optional owner-selected delay" as retained protections; for zero-delay projects revocation is vacuous, and a delay can later only be raised via setPolicy (line 230), never lowered. The deployed ClausApprovalExecutor has the same MIN_UPGRADE_DELAY = 0 but readonly projects hold no state or assets, so the consequence there is far smaller. Fix options (design decision for the requester, not to be applied silently): require a non-zero minimum delay for stateful managed projects so queue and execute cannot share a transaction; or require an owner co-signature or owner-submitted execute on upgrades; at minimum state in the README and admission policy that minDelaySeconds = 0 means Claus-only, same-block code replacement. Merged from audit_flow (2459), audit_permissions (5599), audit_math (d3e1) and audit_economics (af40).

**Reproduction**

State: project registered with minDelaySeconds = 0 (the test fixture default) running Counter at count 7. Input: contract Atomic.run(executor, a, "", clausSig) which calls executor.queue(a, s) then executor.execute(a, "", s) in the same transaction, where a is a valid Claus-signed Approval from version 1 / Counter.codehash to CounterV2 with notBefore = block.timestamp. Expected under a timelock/revocation model: NotReady until an observable window elapses in which the owner can call cancelApproval or setAuthority. Actual: the single transaction succeeds; target.version() == 2 and target.implementation() == CounterV2 immediately. Verified: test/scratch/Judge.t.sol::testQueueAndExecuteAtomic passes on this code; the proposal's own testStatePersistsAcrossExactApprovedUpgrade (ClausManagedExecutor.t.sol line 34) performs the same queue+execute in one transaction.

### 4. Low: registerProject does not apply the implementation == address(this) check that _validate and the proxy enforce, so a project can be registered with the executor itself as module and is born unusable

`proposals/managed-contracts/ClausManagedExecutor.sol:145`

```
            || r.implementation.code.length == 0 || r.implementation.codehash != r.codeHash
```

Prototype-kernel inconsistency. _validate (line 271) rejects a.implementation == address(this) and ClausManagedProject._implementation (ClausManagedProject.sol line 33) reverts InvalidModule when module == executor, but registerProject (lines 143-149) only requires code to exist and the codehash to match. With configuration = "" (initCallHash = keccak256("")), initialize() returns early (ClausManagedProject.sol line 25) before _implementation() runs, so the registration succeeds, consumes the owner nonce and the (owner, salt) project id, and emits ProjectRegistered for a proxy on which every call reverts InvalidModule until a Claus-approved upgrade replaces the module. With non-empty configuration the registration reverts with the proxy's InvalidModule instead of the kernel's InvalidRegistration, so the rule is enforced by the proxy rather than the registry and the error is misleading. Both signers must sign such a registration, so impact is a mis-registered, dead project rather than third-party loss. Fix: add || r.implementation == address(this) to the registerProject condition (and the managed-target check from the cross-proxy finding). Merged from audit_flow (8e7c), audit_permissions (5afc) and audit_math (92b1).

**Reproduction**

State: fresh executor. Input: Registration r with implementation = address(executor), codeHash = address(executor).codehash, initCallHash = keccak256(""), valid owner and Claus signatures; registerProject(r, "", ownerSig, clausSig). Expected: revert InvalidRegistration (consistent with _validate's InvalidApproval for the same implementation on upgrade). Actual: registration succeeds and returns a target; Counter(target).count() reverts ClausManagedProject.InvalidModule. Verified: test/scratch/Judge.t.sol::testRegisterWithExecutorAsImplementation passes on this code.

### 5. Low: Codehash binding is one hop deep: an approved module that forwards to a mutable address lets its controller swap the project's effective code with no signature while the registry reports nothing

`proposals/managed-contracts/ClausManagedProject.sol:33`

```
        if(module.code.length==0||module==address(this)||module==executor||module.codehash!=expected)revert InvalidModule();
```

Prototype-kernel trust assumption to state explicitly, not a bypass of a guard. The registry binds the module address and its EXTCODEHASH at registration/queue/execute (ClausManagedExecutor.sol lines 145, 272) and on every proxy call (ClausManagedProject.sol line 33). Nothing binds what that bytecode does: a module whose fallback delegatecalls (or calls) an address read from project storage or from another contract keeps a constant codehash while its behaviour is controlled by whoever administers that address. After one Claus-signed registration the module's controller can replace the entire effective code of the managed project and move its assets; implementation(), implementationCodeHash() and version() on the proxy stay unchanged, no UpgradeExecuted is emitted, and neither cancelApproval, setAuthority(enabled=false) nor signer recovery can intervene because no kernel path is used. The same applies to EIP-1167 clones with mutable beacons and, on Prague-era chains, to EIP-7702 delegated EOAs (that variant fails closed because the codehash changes on re-delegation). The kernel cannot enforce this on-chain in general since any approved module may call mutable contracts, so it shifts the security boundary to off-chain review: the admission policy and README must state that codehash binding is one level deep and that evidenceHash must attest the reviewed source contains no DELEGATECALL (or privileged CALL) to a non-constant, non-immutable target. The one sub-case the kernel can block itself (a module that is another managed proxy) is reported separately. Merged from audit_permissions (e623) and audit_economics (e56b).

**Reproduction**

State: module Forwarder with setLogic(address) storing to slot 100 and a fallback that delegatecalls the stored address; project registered (owner 0xC0C, Claus-signed) with implementation = Forwarder and empty configuration. Input: owner calls Forwarder(target).setLogic(Counter); Counter(target).set(5) -> count()==5; target funded with 1 ether; owner calls setLogic(Drainer) where Drainer.set() sets count=42 and transfers the balance to 0xBEEF; then Counter(target).set(1). Expected per README line 5: the project still runs the reviewed code and the registry reflects any code change. Actual: Drainer runs inside the proxy, target.balance == 0, 0xBEEF.balance == 1 ether, count()==42, while target.version()==1, target.implementation()==Forwarder and target.implementationCodeHash()==Forwarder.codehash and the executor emitted nothing. Verified: test/scratch/Judge.t.sol::testForwarderSwapsLogicWithoutApproval passes on this code.

### 6. Low: Claus cannot revoke a Registration it has signed: cancelRegistration keys the nonce on msg.sender, not on the registration owner

`proposals/managed-contracts/ClausManagedExecutor.sol:136`

```
        registrationNonces[msg.sender][nonce] = true;
```

Prototype-kernel flow gap (the deployed executor has no Claus signature on registration, so there is nothing to revoke there). registerProject consumes registrationNonces[r.owner][r.nonce] (line 149) while cancelRegistration marks registrationNonces[msg.sender][nonce], so only the owner can cancel a pending registration. Claus co-signs the same digest but has no per-registration revocation, unlike approvals where cancelApproval (lines 197-203) accepts the clausSigner. If Claus signs a Registration and later learns the reviewed module is defective, the signed artefact stays valid until r.expiresAt, which has no upper bound (only expiresAt > now is checked). The only Claus-side remedies are global: recoveryOwner calling scheduleSignerRecovery/cancelSignerRecovery, which bumps clausGeneration and invalidates every outstanding Claus-signed Registration and Approval for every project and suspends all upgrades while pending. Fix: let clausSigner cancel owner-namespaced registration nonces (e.g. cancelRegistration(address owner, uint256 nonce) permitted when msg.sender == owner || (msg.sender == clausSigner && pendingSigner == 0)), or add a mapping(bytes32 digest => bool) revokedRegistrations settable by clausSigner and checked in registerProject; optionally bound r.expiresAt - block.timestamp. Merged from audit_flow (aece), audit_permissions (f782) and audit_economics (fc98).

**Reproduction**

State: Registration r (owner 0xB0B, nonce N) signed by both owner and Claus, expiresAt in the future. Input: vm.prank(clausSigner); executor.cancelRegistration(N); then executor.registerProject(r, config, ownerSig, clausSig). Expected: Claus's cancellation prevents the registration. Actual: registrationNonces(clausSigner, N) is true, registrationNonces(owner, N) is false, and registerProject succeeds and returns a new target. Verified: test/scratch/Judge.t.sol::testClausCannotRevokeSignedRegistration passes on this code.

### 7. Low: Fixed proxy selectors silently shadow module functions with the same selector (version(), implementation(), configuration(), initialize(bytes), ...)

`proposals/managed-contracts/ClausManagedProject.sol:20`

```
    function version() external view returns(uint64 value) { (,,value,)=IClausManagedRegistry(executor).moduleState(projectId); }
```

Prototype-kernel integration hazard. ClausManagedProject is not a transparent proxy: its fixed functions (executor, projectId, implementation, implementationCodeHash, version, configuration, initialize) are dispatched for every caller, so a module function with a colliding selector is unreachable through the proxy and the registry value is returned instead, with no error. version() (0x54fd4d50) is the standard domain-version getter that permit-style tokens and many EIP-712 contracts expose as a string; implementation() (0x5c60da1b) is the common EIP-1967 getter; initialize(bytes) (0x439fab91) is a common module initializer selector. An integrator computing a permit domain from version(), or a factory calling initialize(bytes), gets a uint64 or an ExecutorOnly revert. The README acknowledges the reserved selectors but the review flow has no automated collision check. Fix: adopt transparent-proxy dispatch (serve the fixed selectors only when msg.sender is the executor or a dedicated admin caller, delegate otherwise), or rename the fixed functions to a namespaced set and have registration/approval validation or the review checklist reject modules whose ABI contains the reserved selectors. Merged from audit_flow (6852) and audit_math (7335).

**Reproduction**

State: project upgraded (Claus-signed) to module TokenLike with function version() external pure returns (string memory) { return "1"; }. Input: address(target).call(abi.encodeWithSignature("version()")). Expected: the module's ABI-encoded string "1" (96 bytes). Actual: 32 bytes containing uint64 2 (the registry version); the module function is never reached. Verified: test/scratch/Judge.t.sol::testProxySelectorShadowsModuleVersion passes on this code.

### 8. Low: receive() performs an external staticcall to the registry before delegating, so 2300-gas transfer()/send() to a managed project always fails

`proposals/managed-contracts/ClausManagedProject.sol:36`

```
    receive() external payable { _delegate(); }
```

Prototype-kernel integration limitation (the deployed readonly contracts are not payable). Every call through the proxy, including a plain value transfer, first runs _implementation(), which makes an external STATICCALL to executor.moduleCode(projectId) (cold account access 2600 gas plus the call and codehash check) before the delegatecall. The 2300-gas stipend forwarded by Solidity transfer()/send() and by many legacy wallet paths is insufficient, so a managed project can never receive ETH through those paths even when the approved module has a payable receive(). Contracts that pay out to a project address with transfer() revert, which can lock funds in the sender if it has no alternative path. The implementation cannot be cached in the proxy without reintroducing a writable slot, so the practical fix is to document that managed projects must be paid with call{value:}() and forwarded gas, and/or to let receive() accept empty-calldata value transfers without delegating when the registry flags the module as accepting plain ETH. From audit_flow (e02d).

**Reproduction**

State: project upgraded to a module with receive() external payable {}. Input: a direct address(target).call{value: 0.5 ether}("") succeeds and target.balance == 0.5 ether; then contract Payer { function pay(address payable to) external payable { to.transfer(msg.value); } } called with 0.5 ether and to = target. Expected: the project's payable receive accepts the value. Actual: Payer.pay reverts (out of gas inside the 2300 stipend during _implementation()). Verified: test/scratch/Judge.t.sol::testTransferToProjectFails passes on this code.

### 9. Low: Disabling a project's authority stops upgrades but not the live module: there is no owner-side emergency stop for approved code that misbehaves

`proposals/managed-contracts/ClausManagedExecutor.sol:192`

```
        p.enabled = enabled;
```

Prototype-kernel integration gap, reported because the brief asks for incomplete integration and trust assumptions to be stated explicitly. Project.enabled is read only by _validate (line 263) to gate upgrades. ClausManagedProject._implementation reads moduleCode(projectId) and never consults projects[id].enabled, so after the owner calls setAuthority(id, mandate, false) every external call to the proxy still delegatecalls the current module. If an approved module turns out to drain or mis-handle assets, the owner has no on-chain way to halt it; the only remedy is a replacement module, which requires a fresh Claus signature, waiting out any chosen delay, and is entirely blocked while a signer recovery is pending (pendingSigner != 0 fails _validate). This is consistent with the README's statement that the proxy provides upgrade control rather than safety, but it should be an explicit admission requirement. Fix (design decision): expose the owner's enabled flag through moduleCode() and have _implementation revert or route to a frozen no-op when the project is disabled, or document that enabled=false does not pause execution. From audit_flow (a31a).

**Reproduction**

State: project registered with Counter at count 7. Input: vm.prank(owner); executor.setAuthority(id, keccak256("mandate"), false); then anyone calls Counter(target).add(). Expected if revocation were an emergency stop: the call reverts. Actual: the module executes and count becomes 8. Verified: test/scratch/Judge.t.sol::testDisableDoesNotStopModule passes on this code.

### 10. Low: Proposal foundry.toml cannot compile as committed: remappings point outside solc's allowed directories, so the five README test cases do not run

`proposals/managed-contracts/foundry.toml:14`

```
remappings = ["@openzeppelin/contracts/=../../contracts/lib/openzeppelin-contracts/contracts/", "forge-std/=../../contracts/lib/forge-std/src/"]
```

The proposal's foundry.toml maps OpenZeppelin and forge-std to ../../contracts/lib/... but declares no libs or allow_paths entry, so solc's allowed-directory list is only proposals/managed-contracts and proposals/managed-contracts/lib and every library import is rejected with "File outside of allowed directories". README line 11 states that five focused local EVM cases cover the kernel; with the committed configuration none of them can be executed, so the review evidence the proposal cites is not reproducible from the tree. With FOUNDRY_ALLOW_PATHS='["../../contracts/lib"]' (or forge test --lib-paths ../../contracts/lib) all five tests pass, confirming the tests themselves are fine and the defect is the configuration. Reported on the proposal's own config file; this review did not modify it. Fix: add libs = ["../../contracts/lib"] or allow_paths = ["../../contracts/lib"] to the proposal profile, or vendor the two libraries under proposals/managed-contracts/lib. Merged from audit_flow (7a50), audit_permissions (56f8), audit_math (8e6d) and audit_economics (b7a4).

**Reproduction**

Input: cd proposals/managed-contracts && forge test (forge 1.8.3, solc 0.8.27). Expected: 5 passing tests as described in README.md. Actual: compilation fails with Error (6275): Source "../../contracts/lib/forge-std/src/console.sol" not found: File outside of allowed directories. The following are allowed: ".../proposals/managed-contracts", ".../proposals/managed-contracts/lib" (and the same for every other library import); zero tests run. With FOUNDRY_ALLOW_PATHS='["../../contracts/lib"]' forge test reports 5 passed, 0 failed. Verified in this review.

### 11. Info: Trust assumption: the immutable recoveryOwner is the root of trust; it can rotate the Claus signer without veto and can freeze every registration and upgrade indefinitely by re-scheduling recovery

`proposals/managed-contracts/ClausManagedExecutor.sol:241`

```
        recoveryReadyAt = uint64(block.timestamp) + recoveryDelay;
```

Privileged-actor power of the prototype kernel, reported so the custody model states it (the deployed executor has the same code, but readonly projects hold no assets). scheduleSignerRecovery has no guard against being called while a recovery is already pending; each call resets recoveryReadyAt to now + recoveryDelay and bumps clausGeneration. While pendingSigner != 0, registerProject (line 143) reverts InvalidRegistration and _validate (line 263) reverts InvalidApproval for every project, and each generation bump invalidates every outstanding Claus-signed Registration and Approval. The recoveryOwner can therefore hold every managed project in a permanent no-upgrade state (combined with the finding that enabled=false does not stop the live module, a permanently running module nobody can replace) without ever completing the recovery. Conversely, finishSignerRecovery is permissionless after recoveryDelay and neither clausSigner nor project owners can veto, so whoever holds the recoveryOwner key can install any signer after >= 1 day and then approve code for every enabled project subject only to each project's own delay; and because recoveryOwner is immutable, if that key is lost the Claus signer can never be rotated. Also note clausGeneration increments on schedule and again on finish, so approvals signed by the new signer during the pending window are dead after finish. Fix options: reject scheduleSignerRecovery while pendingSigner != 0 or cap re-scheduling; document the recoveryOwner's liveness and takeover power in the admission policy and put the key behind a multisig or timelock. Merged from audit_flow (7977), audit_permissions (457e) and audit_math (895c).

**Reproduction**

State: executor with recoveryDelay = 1 day and a registered project. Input: recoveryOwner calls scheduleSignerRecovery(0x1234); after 1 day - 1 second calls it again; after another 1 day - 1 second anyone calls finishSignerRecovery(). Expected: a bounded recovery window. Actual: finishSignerRecovery reverts NotReady, and a freshly Claus-signed Approval carrying the current clausGeneration still fails queue() with InvalidApproval because pendingSigner != 0; this can be repeated indefinitely. Verified: test/scratch/Judge.t.sol::testRecoveryOwnerCanFreezeForever passes on this code.

### 12. Info: cancelSignerRecovery bumps clausGeneration even when no recovery is pending, giving recoveryOwner an instant global revoke of every outstanding Claus signature

`proposals/managed-contracts/ClausManagedExecutor.sol:247`

```
        ++clausGeneration;
```

Prototype-kernel trust assumption (the deployed executor has the same behaviour). cancelSignerRecovery does not check pendingSigner != 0 before incrementing clausGeneration. Since every Registration and Approval binds clausGeneration, a single call by recoveryOwner with nothing pending invalidates all signed-but-unexecuted registrations and all queued approvals across all projects, with no delay and no event other than SignerRecoveryCancelled; the queued readyAt entries remain set forever. This may be intended as an emergency revoke, in which case it should be named and documented as such; otherwise guard it with if (pendingSigner == address(0)) revert InvalidRecovery(). From audit_flow (3148).

**Reproduction**

State: a Claus-signed Approval for the project queued (readyAt set), pendingSigner == 0. Input: recoveryOwner calls cancelSignerRecovery(). Expected: no-op or revert when nothing is pending. Actual: clausGeneration becomes 2 and execute(a, "", s) for the queued approval reverts InvalidApproval. Verified: test/scratch/Judge.t.sol::testCancelRecoveryWithNothingPendingInvalidatesAll passes on this code.

### 13. Info: Shipped tests use bare vm.expectRevert(), never exercise the owner-signature or evidence-hash binding they claim to, and omit recovery, owner-transfer, replay, selfdestruct and reentrancy paths

`proposals/managed-contracts/ClausManagedExecutor.t.sol:46`

```
        bytes memory c=sign(CLAUS,executor.registrationDigest(r));r.evidenceHash=keccak256("different review");
```

Test-coverage gap in the proposal. Four of the five tests assert reverts with an unselective vm.expectRevert(), so any revert reason satisfies them. In testInitialCodeAlsoNeedsClausReviewSignature the first case passes the owner signature in both slots, so only the Claus check (line 151) is observed; the second case mutates r.evidenceHash after both signatures were produced, so the revert comes from the stale OWNER signature at line 150 and the Claus signature is never evaluated against the changed hash. Consequently (a) the test proves nothing about the Claus signature being bound to evidenceHash, and (b) removing the owner check at line 150 leaves all five shipped tests passing (case 1 still reverts on the Claus check, case 2 still reverts on the stale Claus signature, and the other three tests only register with valid signatures), although README line 11 says the suite covers owner/Claus initial signatures. No test exercises signer recovery and the generation bump, owner transfer and mandate reset, cancelRegistration/cancelApproval, setPolicy, cross-chain or cross-executor replay, the zero-delay same-transaction queue+execute path as a property, module self-destruct during initialization, a module calling back into the executor during initialize, ERC-1271 owner signatures, a managed proxy used as a module, or selector collisions; the untested paths are where the findings above were located. Fix: use vm.expectRevert(Selector) everywhere; re-sign the mutated registration with the owner key only and expect InvalidSignature from the Claus check; add a case passing (sign(CLAUS,d), sign(CLAUS,d)) and a third-party signature in the owner slot; add cases for each listed path. Merged from audit_flow (2e68), audit_permissions (6e23), audit_math (86f1) and audit_economics (5ba5).

**Reproduction**

Input: take the fixture of testInitialCodeAlsoNeedsClausReviewSignature, change r.evidenceHash, keep the stale owner signature but supply a FRESH Claus signature over the new digest, and call registerProject. Expected if the test exercised the Claus binding: the call would succeed or fail on the Claus check. Actual: it reverts InvalidSignature at the owner check (line 150) even though the Claus signature is valid, showing the shipped assertion is satisfied by the owner check alone; conversely with a fresh owner signature and the stale Claus signature the call also reverts InvalidSignature, this time from line 151. Verified: test/scratch/Judge.t.sol::testEvidenceHashCaseRevertsAtOwnerCheck passes on this code. The claim that deleting line 150 keeps all five tests green follows from the above and from the other tests only registering with valid signatures; it was not executed because the review may not modify the proposal files.

### 14. Info: Registration does not bind the target address: the proxy address depends on the executor's CREATE nonce, so a pre-announced or pre-funded target can differ from the one deployed

`proposals/managed-contracts/ClausManagedExecutor.sol:153`

```
        target = address(new ClausManagedProject(r.projectId));
```

Integration assumption of the prototype kernel. The proxy is created with plain CREATE, so its address is derived from the executor's nonce and is not part of the signed Registration struct (lines 33-48 have no target field); neither signer commits to it, and any other registration mined first changes the address the submitter receives. Later Approvals are safe because _validate reads the target from storage, but off-chain systems that publish, whitelist or fund the expected address before the registration transaction is mined will point at an address the executor will never deploy to. Fix if address pre-commitment is wanted: deploy with CREATE2 salted by projectId so the target equals a computable address and include it in the Registration; this would also enable the re-creation remedy for the EIP-6780 finding. From audit_permissions (fc3c).

**Reproduction**

Input: owner computes expectedTarget from (executor address, executor nonce) and funds it, then another owner's registerProject is mined first, then the first owner's. Expected: the first owner's project lives at expectedTarget. Actual: ProjectRegistered emits the next CREATE address, which differs; the funds sit at an address with no code and the executor can never create there. Code-level check: the Registration struct (ClausManagedExecutor.sol lines 33-48) contains no target or predicted-address field and line 153 uses new ClausManagedProject(r.projectId) (CREATE, nonce-derived) rather than CREATE2.

---

Judge's submission `d60e4082b321f24cc39af1af32b176a61713fca57af54cd09594a4dc64b728c7`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
