Skip to content

Add support for other ownable key systems including P256.#1

Open
reednaa wants to merge 39 commits intomainfrom
other-keys
Open

Add support for other ownable key systems including P256.#1
reednaa wants to merge 39 commits intomainfrom
other-keys

Conversation

@reednaa
Copy link
Member

@reednaa reednaa commented Oct 7, 2025

Closes: https://linear.app/lifi-linear/issue/API-173/finish-p256-and-webauth-pr
Closes: API-173

Summary by CodeRabbit

  • New Features

    • On‑chain multi‑key ownership, per‑digest signature approvals, digest‑enabled deploy flows and new deploy/predict variants; Stowaway integration.
  • Bug Fixes & Improvements

    • Stricter nonce handling, hardened signature validation, replay‑protection across chains, updated hashing/salt logic and version bump.
  • Documentation

    • README expanded with deployment examples, execution modes, Digest Proxy, embedded call guidance and safety notes.
  • Tests

    • New/updated tests and refreshed snapshots covering keyed ownership, digest flows, nonces and signature paths.
  • Chores

    • Added Stowaway submodule, remapping and config updates.

✏️ Tip: You can customize this high-level summary in your review settings.

asUnsafeBytes32(address(this)),
hash
);
if (signature.length == 0 && approvedDigest[digest] == type(uint256).max) return bytes4(0xffffffff);

Check notice

Code scanning / Olympix Integrated Security

Performing a narrowing downcast may result in silent overflow due to bit truncation. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/unsafe-downcast

Performing a narrowing downcast may result in silent overflow due to bit truncation. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/unsafe-downcast
@coderabbitai
Copy link

coderabbitai bot commented Nov 12, 2025

Walkthrough

Replace single-address ownership with a KeyedOwnable multi-key model; add per-digest approvals and digest-based deploy/signature flows; update factory APIs and salt derivation to accept KeyType+keys; integrate Stowaway submodule and fallback; adjust nonce/Calls layout; update docs, tests, and snapshots.

Changes

Cohort / File(s) Summary
Submodule & Remapping
/.gitmodules, remappings.txt, lib/stowaway
Add lib/stowaway git submodule entry and remapping stowaway/=lib/stowaway/.
Ownership Library
\src/libs/KeyedOwnable.sol``
New KeyedOwnable contract: multi-key ownership model, KeyType enum, on-chain key slices, signature validation for multiple schemes, transfer APIs, errors/events, and onlyOwnerOrSelf modifier.
Core Contract
\src/Catapultar.sol``
Replace Ownable with KeyedOwnable; add DigestApproval enum, approvedDigest mapping, SignatureSet event; add init(KeyType, bytes32[]) and setSignature; update isValidSignature and _validateOpData for per-digest and multichain digests; call Stowaway.searchAndCall in fallback; bump version.
Factory & Deployment
\src/CatapultarFactory.sol``
APIs now accept KeyedOwnable.KeyType + bytes32[] owner; add deployWithDigest/predictDeployWithDigest; introduce _saltPrefix and use EfficientHashLib to derive saltWithDigest; forward msg.value to init; update ownerInSalt modifier and predictors.
Nonce / Calls / Exec Layout
\src/libs/BitmapNonce.sol`, `src/libs/ERC7821LIFI.sol`, `src/libs/LibCalls.sol``
Disallow nonce 0 in _useUnorderedNonce; adjust extraData bit-field nonce/index ranges and padded-length calc; reorder Calls struct to (nonce, mode, calls) impacting hashing/layout.
Tests & Mocks
\test/Catapultar/`, `test/CatapultarFactory.t.sol`, `test/Integration.t.sol`, `test/libs/`, `test/mocks/*``
Migrate tests to KeyType+keys[] init; add digest/signature flows and tests (setSignature/approvedDigest); adjust nonces and embedded-call expectations; add KeyedOwnable tests and MockKeyedOwnable; formatting-only changes to MockERC20.
Snapshots
\snapshots/Catapultar.json``
Update numeric snapshot values across Catapultar snapshots and add deployWithDigest key; adjust deploy/predict baseline values.
Scripts & Config
\script/deploy.s.sol`, `foundry.toml``
Add KeyedOwnable import; add deployFactory() and account(...) helpers using factory and KeyType; set bytecode_hash = "none" under [profile.default].
Docs
\README.md``
Document Digest Proxy and deployWithDigest, custodianless flow, Stowaway integration and LibZip notes; update examples to pass ktp in factory.deploy, update feature tables, costs, and security warnings.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Factory as CatapultarFactory
    participant Deployer as CREATE2/Cloner
    participant Proxy as Catapultar
    participant KeyStore as KeyedOwnable
    participant Stowaway

    User->>Factory: deployWithDigest(ktp, owner[], salt, digest, isSignature)
    Factory->>Factory: compute _saltPrefix(ktp, owner[]) & ownerInSalt check
    Factory->>Deployer: create2/clone using saltWithDigest
    Deployer->>Proxy: deploy proxy
    Factory->>Proxy: init(ktp, owner[]) (forwards msg.value)
    Proxy->>KeyStore: store owner key slice / set ownerKeyType
    Factory->>Proxy: setSignature(digest, DigestApproval)
    User->>Proxy: submit tx referencing digest
    Proxy->>Proxy: isValidSignature — short-circuit if approvedDigest==Signature
    alt not pre-approved
        Proxy->>KeyStore: validate signature via KeyedOwnable
    end
    Proxy->>Stowaway: fallback -> searchAndCall(ERC7821.execute.selector)
    Stowaway-->>Proxy: result (call performed / none)
    alt no Stowaway result
        Proxy->>Proxy: LibZip.cdFallback()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • src/libs/KeyedOwnable.sol (key slice storage, _isValidKey, multi-scheme signature logic).
    • src/Catapultar.sol (approvedDigest handling, isValidSignature/_validateOpData changes, Stowaway fallback).
    • src/CatapultarFactory.sol (salt prefix derivation, ownerInSalt modifier, deployWithDigest/predict logic).
    • Tests & snapshots for nonce/index shifts and new deployWithDigest behaviors.

Poem

🐰 I dug a keyhole, set keys in rows,

Digests hum like secret-stringed bows,
Stowaway whispers where fallback goes,
Proxies wear many-owner clothes,
Hoppy deploys — the garden grows!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add support for other ownable key systems including P256' accurately reflects the main change: introducing KeyedOwnable contract that supports multiple key types (ECDSA, P256, WebAuthnP256) throughout the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch other-keys

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

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

❤️ Share

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

) external initializer {
_initializeOwner(owner);
if (ALLOW_EMBEDDED_CALLS && !_notUpgradeable()) revert CannotBeUpgradeable();
function setSignature(

Check failure

Code scanning / Olympix Integrated Security

The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion
/// @param salt The first 20 bytes of salt has to be the owner or 0.
function deploy(address owner, bytes32 salt) external ownerInSalt(salt, owner) returns (address proxy) {
proxy = LibClone.cloneDeterministic_PUSH0(address(EXECUTOR_NO_EMBEDDED_CALLS), salt);
function deploy(

Check failure

Code scanning / Olympix Integrated Security

The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion
/// @param salt The first 20 bytes of salt has to be the owner or 0.
function deployUpgradeable(address owner, bytes32 salt) external ownerInSalt(salt, owner) returns (address proxy) {
proxy = LibClone.deployDeterministicERC1967(address(EXECUTOR_NO_EMBEDDED_CALLS), salt);
function deployUpgradeable(

Check failure

Code scanning / Olympix Integrated Security

The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7289f54 and 1e73e70.

📒 Files selected for processing (25)
  • .gitmodules (1 hunks)
  • README.md (2 hunks)
  • lib/stowaway (1 hunks)
  • remappings.txt (1 hunks)
  • snapshots/CatapultarCallsTest.json (1 hunks)
  • snapshots/CatapultarFactoryTest.json (1 hunks)
  • snapshots/CatapultarMinimalTest.json (1 hunks)
  • snapshots/CatapultarUpgradeableTest.json (1 hunks)
  • src/Catapultar.sol (9 hunks)
  • src/CatapultarFactory.sol (5 hunks)
  • src/libs/BitmapNonce.sol (1 hunks)
  • src/libs/ERC7821LIFI.sol (4 hunks)
  • src/libs/KeyedOwnable.sol (1 hunks)
  • test/Catapultar/Catapultar.base.t.sol (15 hunks)
  • test/Catapultar/Catapultar.calls.t.sol (0 hunks)
  • test/Catapultar/Catapultar.minimal.t.sol (1 hunks)
  • test/Catapultar/Catapultar.upgradable.t.sol (1 hunks)
  • test/CatapultarFactory.t.sol (3 hunks)
  • test/Integration.t.sol (9 hunks)
  • test/libs/ERC7821LIFI.t.sol (2 hunks)
  • test/libs/LibCalls.t.sol (1 hunks)
  • test/mocks/MockCatapultar.sol (1 hunks)
  • test/mocks/MockERC20.sol (2 hunks)
  • test/solady/ERC7821.t.sol (1 hunks)
  • test/solady/mocks/MockERC7821.sol (2 hunks)
💤 Files with no reviewable changes (1)
  • test/Catapultar/Catapultar.calls.t.sol
🧰 Additional context used
🪛 GitHub Actions: CI
src/libs/KeyedOwnable.sol

[error] 262-262: forge fmt --check: formatting mismatch detected. This is simply abi.decode(signature, (WebAuthn.WebAuthnAuth)).

src/libs/ERC7821LIFI.sol

[error] 151-151: forge fmt --check: formatting mismatch detected. Compute the padded length for ABI alignment. (rdsize + rdsize % 32)

🪛 GitHub Check: Olympix Integrated Security
src/Catapultar.sol

[failure] 106-106:
The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

src/CatapultarFactory.sol

[failure] 46-46:
The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion


[failure] 95-95:
The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

🪛 LanguageTool
README.md

[grammar] ~185-~185: Ensure spelling is correct
Context: ...` call into a bytes field which will be delievered to the account. You can optionally LibZ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~191-~191: Ensure spelling is correct
Context: ... bypassed if called from itself: - The exeute endpoint does not require a signature I...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[uncategorized] ~192-~192: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...f the underlying SCA if the contract is upgradable. - The SCA can upgrade the contract own...

(EN_WORD_COHERENCY)


[style] ~195-~195: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...es themselves are not structured. It is very important to parse and validate that ALL sign...

(EN_WEAK_ADJECTIVE)


[grammar] ~210-~210: Use a hyphen to join words.
Context: ... remember that token allowances are long lived. If a token allowance is set on a ...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
README.md

204-204: Hard tabs
Column: 80

(MD010, no-hard-tabs)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (26)
test/libs/ERC7821LIFI.t.sol (2)

64-96: Function signature and call formatting improvements look good.

The parameter list split across multiple lines (64–66) and the abi.encodeWithSignature arguments split across lines (73–74) improve readability and maintain consistency with updated test style across the suite. No logic changes; test behavior remains identical.


122-128: Commented test block reformatting is clean.

The commented-out test maintains proper indentation and structure. Readability improves without affecting any active test logic.

remappings.txt (1)

3-3: LGTM!

The remapping correctly aligns with the Stowaway submodule addition in .gitmodules and follows the standard remapping pattern.

test/solady/ERC7821.t.sol (1)

97-100: LGTM!

The multiline function signature formatting improves readability without altering semantics.

snapshots/CatapultarUpgradeableTest.json (1)

4-7: Verify the gas cost increases are expected.

All ERC-20 transfer operations show increased gas costs:

  • Single transfers: +372 gas
  • Double transfers: +501 gas

These increases likely result from the KeyedOwnable integration and updated signature validation mechanisms. Please confirm these overhead costs are acceptable for the added functionality.

test/mocks/MockCatapultar.sol (2)

7-7: LGTM!

The simplified constructor signature aligns with the refactored base Catapultar initialization mechanism described in the PR.


9-13: LGTM!

The multiline function signature formatting improves readability without altering semantics.

snapshots/CatapultarCallsTest.json (1)

4-7: Verify the gas cost increases are expected.

All ERC-20 transfer operations show increased gas costs:

  • Single transfers: +329 gas
  • Double transfers: +458 gas

These increases likely result from the KeyedOwnable integration and updated signature validation mechanisms. Please confirm these overhead costs are acceptable for the added functionality.

test/mocks/MockERC20.sol (1)

15-19: LGTM!

The multiline function signature formatting throughout the file improves readability without altering semantics or behavior.

Also applies to: 42-45, 49-52, 56-60, 64-68, 72-75, 79-83

README.md (2)

183-211: Excellent security documentation!

The Usage Warnings section provides critical security guidance about the self-calling bypass mechanism and potential ERC-1271 attack vectors. This documentation is essential for users to understand the security implications of batch operations.


248-248: LGTM!

The Stowaway library is correctly added to the license notice section.

.gitmodules (1)

7-9: Verify trust before integrating this new submodule dependency.

The Stowaway repository (created 2025-10-04, pushed only once) lacks indicators of established maintenance or security review. With 0 stars and an unknown maintainer "reednaa," this dependency has no community validation. Before merging, manually confirm:

  • The maintainer's identity and trustworthiness
  • Whether this code has undergone security review
  • Your organization's risk tolerance for unproven dependencies in your build/deployment pipeline
test/libs/LibCalls.t.sol (1)

40-44: LGTM! Formatting improvement.

The multiline parameter layout improves readability without altering behavior.

test/solady/mocks/MockERC7821.sol (2)

34-40: LGTM! Formatting improvement.

The multiline parameter layout improves readability without altering behavior.


51-56: LGTM! Formatting improvement.

The multiline parameter layout improves readability.

test/Catapultar/Catapultar.upgradable.t.sol (2)

7-7: LGTM! Import added for KeyedOwnable support.

The import aligns with the PR's multi-key ownership refactoring.


14-14: LGTM! Constructor updated to parameterless form.

This change aligns with the removal of the embedded calls parameter from MockCatapultar, consistent with the PR's refactoring away from embedded-call mechanisms.

snapshots/CatapultarFactoryTest.json (1)

2-4: LGTM! Gas snapshots updated.

The increased gas values reflect the KeyedOwnable-based ownership model introduced in this PR. The changes are expected and reasonable.

snapshots/CatapultarMinimalTest.json (1)

4-7: LGTM! Gas snapshots updated.

The gas increases are consistent across operations and reflect the new signature validation mechanisms introduced with KeyedOwnable. The pattern of increases is logical.

test/Catapultar/Catapultar.minimal.t.sol (1)

11-11: LGTM! Constructor updated to parameterless form.

This change is consistent with the removal of the embedded calls parameter from MockCatapultar across all test files.

test/Integration.t.sol (5)

11-11: LGTM! KeyedOwnable import added.

This import enables the test to use the new multi-key ownership model.


62-65: LGTM! Migration to KeyedOwnable ownership model.

The deployment now uses KeyedOwnable.KeyType.ECDSAOrSmartContract with a keys array instead of a direct owner address. This properly demonstrates the new multi-key ownership API introduced in this PR.


87-130: LGTM! Nonce values updated to start at 1.

The encoded nonce/index values now range from 1-6 instead of (presumably) 0-5. This change coordinates with the new guard in BitmapNonce.sol that disallows nonce 0, ensuring test consistency across the refactoring.


197-247: LGTM! Test expectations updated for new nonce values.

The assembleExtraData calls have been updated to reflect the new nonce values (3-6), maintaining consistency with the encoded call data above and the BitmapNonce zero guard.


255-272: LGTM! Formatting improvements.

The multiline parameter layouts improve readability without altering behavior.

src/libs/BitmapNonce.sol (1)

26-31: Code change is well-tested and documented—no action needed.

Verification confirms:

  • ✓ NatSpec documentation present: /// @dev Disallows nonce 0 (line 26)
  • ✓ Guard implemented: if (nonce == 0) revert InvalidNonce(); (line 31)
  • ✓ Explicit test coverage: test_useUnorderedNonce() in Catapultar.base.t.sol expects revert for nonce 0
  • ✓ Fuzz tests exclude nonce 0 with vm.assume conditions

The nonce 0 restriction is properly documented in code comments and thoroughly tested. This is an appropriate implementation detail.

@@ -0,0 +1 @@
Subproject commit bf862027851229fa6b00cdf625872a6f2f74b3a6
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the submodule commit is pinned to a stable, documented version.

Adding an external submodule introduces a supply-chain dependency. Ensure the commit hash is:

  • Tagged or documented in the Stowaway repository (e.g., as a release or version tag)
  • From a maintained, trustworthy source (reednaa/stowaway)
  • Compatible with the Catapultar architecture and the new KeyedOwnable/ERC7821 systems

To verify the commit and integration, run the following script:

Additionally, verify that the Stowaway library integrates correctly with the KeyedOwnable and ERC7821 systems by confirming:

  • The fallback routing via Stowaway.searchAndCall does not conflict with signature validation
  • The interaction with embedded-call mechanisms and LibZip is well-tested

🏁 Script executed:

#!/bin/bash
# Description: Verify the Stowaway submodule commit is tagged/documented and check repository health.

# 1. Clone the Stowaway repository to inspect the commit
git clone https://github.com/reednaa/stowaway.git /tmp/stowaway_check 2>/dev/null || true

# 2. Check if commit exists and retrieve commit metadata
cd /tmp/stowaway_check 2>/dev/null && \
  git show bf862027851229fa6b00cdf625872a6f2f74b3a6 --no-patch --format="%H %ai %s" 2>/dev/null || \
  echo "Commit not found or repository unavailable"

# 3. Check if commit is tagged
cd /tmp/stowaway_check 2>/dev/null && \
  git describe --exact-match --tags bf862027851229fa6b00cdf625872a6f2f74b3a6 2>/dev/null || \
  echo "Commit is not tagged"

# 4. Check repository metadata (stars, last update)
curl -s https://api.github.com/repos/reednaa/stowaway | jq '{full_name, description, stars: .stargazers_count, forks: .forks_count, last_push: .pushed_at}'

# Cleanup
rm -rf /tmp/stowaway_check

Length of output: 324



---

🏁 Script executed:

```shell
#!/bin/bash
# Check if any tags exist in the Stowaway repository and list them
git clone https://github.com/reednaa/stowaway.git /tmp/stowaway_tags 2>/dev/null || true

cd /tmp/stowaway_tags 2>/dev/null && {
  echo "=== All tags in repository ===" && \
  git tag -l || echo "No tags found"
  
  echo -e "\n=== Repository commit history (last 10 commits) ===" && \
  git log --oneline -10 || echo "Cannot retrieve history"
  
  echo -e "\n=== Repository age (first commit) ===" && \
  git log --oneline --reverse | head -1 || echo "Cannot determine"
}

rm -rf /tmp/stowaway_tags

Length of output: 244


Do not merge without addressing the supply-chain risk of this unversioned dependency.

The Stowaway repository has:

  • No release tags or versioning (zero tags exist)
  • Only 4 commits spanning ~5 weeks
  • Zero stars, zero forks, zero adoption
  • Pinned commit is untagged, arbitrary

Pinning to an untagged commit of a brand-new, unadopted library introduces material supply-chain risk. The maintainer can delete the repository, rewrite history, or abandon the project without warning.

Required before merge:

  1. Request the Stowaway maintainer create a release tag (e.g., v0.1.0) for this commit
  2. Update the submodule to reference the tag instead of a raw commit hash, or
  3. Re-evaluate whether this dependency is production-ready and warranted

If the library is critical and the maintainer is unresponsive, consider inlining the functionality to avoid external dependency risk.

🤖 Prompt for AI Agents
In lib/stowaway around lines 1 to 1, the PR pins an unversioned submodule commit
from a very new/unadopted repository which creates a supply-chain risk; request
the upstream maintainer to create a release tag (e.g., v0.1.0) for the current
commit and update the submodule to reference that tag instead of the raw commit
hash, or alternatively remove/replace the dependency (inline the needed code or
choose a vetted library) if the maintainer is unresponsive or the project is not
production-ready; ensure the submodule update and commit reference use the tag
or document the justification and risk acceptance in the repo if you must
temporarily keep the untagged pin.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/libs/KeyedOwnable.sol (1)

274-283: Fix dirty-address detection before returning owner.

This is the same critical issue flagged in the previous review. The assembly block computes dirty from addr before addr is assigned from elem, so dirty always remains false and the DirtyEthereumAddress check never triggers. When a non-ECDSA key (e.g., P256) is stored, owner() will silently return a truncated address instead of reverting.

Additionally, there's a typo in the comment on line 279: "addres" should be "address".

Apply this diff to fix both issues:

         assembly ("memory-safe") {
-            // Shift away 20 bytes of the addres.
+            // Shift away 20 bytes of the address.
             // Check if not 0.
-            dirty := iszero(eq(shr(mul(8, 20), addr), 0))
+            dirty := iszero(eq(shr(mul(8, 20), elem), 0))
             addr := elem
         }
🧹 Nitpick comments (1)
src/libs/KeyedOwnable.sol (1)

12-12: Remove unused debug import.

The console import from forge-std is a testing/debugging utility and should not be present in production contract code. There are no console.log calls in this file, making this import unused.

Apply this diff to remove the unused import:

-import { console } from "forge-std/console.sol";
-
 /**
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1e73e70 and 52d0d95.

📒 Files selected for processing (1)
  • src/libs/KeyedOwnable.sol (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: CI
src/libs/KeyedOwnable.sol

[error] 262-262: forge fmt --check failed: formatting issues detected in the diff (comment alignment for abi.decode statement).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (5)
src/libs/KeyedOwnable.sol (5)

35-42: LGTM: Storage layout is well-designed.

The storage slot is intentionally chosen to overlap with Solady's Ownable for upgrade compatibility, which is a thoughtful design decision. The comment clearly explains the rationale and edge case handling.


48-79: LGTM: Key storage implementation is correct.

The key slice storage and retrieval functions correctly use assembly for efficient storage slot access. The comments appropriately document the lack of bounds checking, which is acceptable since getOwnerKey properly bounds reads using ownerKeyType.


99-128: LGTM: Access control logic is correct.

The owner() function correctly validates that the stored value is a clean address. The onlyOwnerOrSelf modifier appropriately restricts access to either the contract itself or the stored owner address, with correct handling for non-ECDSA key types (which would fail the equality check due to dirty upper bits).


130-180: LGTM: Key validation logic is sound.

The validation correctly enforces constraints for each key type:

  • ECDSAOrSmartContract: clean 20-byte address (upper bits zero, lower bits non-zero)
  • P256/WebAuthnP256: both coordinate words must be non-zero

187-227: LGTM: Ownership transfer implementation is correct.

The ownership transfer functions properly validate keys, update storage, and emit events. The design intentionally does not clear old key slices when transferring to shorter keys, which is acceptable since all read operations use ownerKeyType to determine the valid slice count.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

42-60: Update the Integration Example to the new factory and execute signatures.

The “Account Deployment” section documents factory.deploy(ktp, owner, salt), but the later Integration Example still shows factory.deploy(owner, salt) and an older proxy.execute(...) shape. Please update that example to use KeyedOwnable.KeyType plus a bytes32[] keys array and the current execute/opData wiring so README users don’t copy a stale interface.

Also applies to: 165-179

♻️ Duplicate comments (2)
README.md (1)

62-68: Clean up remaining typos and wording in the Stowaway / warnings docs.

There are a few small issues that were already noted previously but are still present:

  • “delievered” → “delivered”.
  • “exeute” → “execute”.
  • “upgradable” vs “Upgradeable” – pick one spelling and use it consistently (the table row/header vs later bullet currently differ).
  • “long lived” → “long‑lived”.
  • Optional: remove hard tabs in lists/tables to satisfy markdownlint.

These are non‑functional, but worth tightening given this section is security‑sensitive documentation.

Also applies to: 183-211, 248-249

src/libs/KeyedOwnable.sol (1)

78-84: Correct small NatSpec typo in _keyTypeLength comment.

The comment currently says “occopy”; please change it to “occupy”:

-    /// @notice Returns the number of words that a key should occopy.
+    /// @notice Returns the number of words that a key should occupy.
🧹 Nitpick comments (1)
test/CatapultarFactory.t.sol (1)

24-26: Consider extracting the keys construction to a helper function.

The pattern keys[0] = bytes32(uint256(uint160(owner))) is repeated in every test function. Extracting it to a helper would improve maintainability.

Apply this diff to add a helper function:

+    function _makeKeys(address owner) internal pure returns (bytes32[] memory keys) {
+        keys = new bytes32[](1);
+        keys[0] = bytes32(uint256(uint160(owner)));
+    }
+
     /// forge-config: default.isolate = true
     function test_deploy() external {
         address owner = makeAddr("owner");
         bytes32 salt = bytes32(bytes20(uint160(owner)));
 
-        bytes32[] memory keys = new bytes32[](1);
-        keys[0] = bytes32(uint256(uint160(owner)));
+        bytes32[] memory keys = _makeKeys(owner);

Then update all test functions to use _makeKeys(owner).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 52d0d95 and 0d166b2.

📒 Files selected for processing (15)
  • README.md (4 hunks)
  • script/deploy.s.sol (2 hunks)
  • snapshots/CatapultarFactoryTest.json (1 hunks)
  • snapshots/CatapultarMinimalTest.json (1 hunks)
  • snapshots/CatapultarUpgradeableTest.json (1 hunks)
  • src/Catapultar.sol (9 hunks)
  • src/CatapultarFactory.sol (4 hunks)
  • src/libs/BitmapNonce.sol (1 hunks)
  • src/libs/KeyedOwnable.sol (1 hunks)
  • src/libs/LibCalls.sol (1 hunks)
  • test/Catapultar/Catapultar.base.t.sol (16 hunks)
  • test/Catapultar/Catapultar.upgradable.t.sol (1 hunks)
  • test/CatapultarFactory.t.sol (2 hunks)
  • test/Integration.t.sol (9 hunks)
  • test/mocks/MockERC20.sol (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/Catapultar/Catapultar.upgradable.t.sol
  • snapshots/CatapultarMinimalTest.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • test/Integration.t.sol
  • test/CatapultarFactory.t.sol
  • test/Catapultar/Catapultar.base.t.sol
  • src/Catapultar.sol
  • src/CatapultarFactory.sol
🪛 GitHub Actions: CI
src/libs/KeyedOwnable.sol

[error] 259-259: Formatting differences detected by forge fmt --check in KeyedOwnable.sol. Run 'forge fmt' to auto-format.

🪛 LanguageTool
README.md

[uncategorized] ~64-~64: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...--------------: | :---------------: | | Upgradable | No | No...

(EN_WORD_COHERENCY)


[grammar] ~185-~185: Ensure spelling is correct
Context: ...` call into a bytes field which will be delievered to the account. You can optionally LibZ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~191-~191: Ensure spelling is correct
Context: ... bypassed if called from itself: - The exeute endpoint does not require a signature I...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[uncategorized] ~192-~192: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...f the underlying SCA if the contract is upgradable. - The SCA can upgrade the contract own...

(EN_WORD_COHERENCY)


[style] ~195-~195: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...es themselves are not structured. It is very important to parse and validate that ALL sign...

(EN_WEAK_ADJECTIVE)


[grammar] ~210-~210: Use a hyphen to join words.
Context: ... remember that token allowances are long lived. If a token allowance is set on a ...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
README.md

44-44: Hard tabs
Column: 1

(MD010, no-hard-tabs)


45-45: Hard tabs
Column: 1

(MD010, no-hard-tabs)


46-46: Hard tabs
Column: 1

(MD010, no-hard-tabs)


49-49: Hard tabs
Column: 1

(MD010, no-hard-tabs)


50-50: Hard tabs
Column: 1

(MD010, no-hard-tabs)


204-204: Hard tabs
Column: 80

(MD010, no-hard-tabs)

🔇 Additional comments (22)
snapshots/CatapultarUpgradeableTest.json (1)

4-7: Verify gas increase justification for KeyedOwnable-based changes.

The snapshot shows consistent gas increases across four metrics (+435 gas for single transfers, +564 gas for double transfers). This correlates with the introduction of KeyedOwnable multi-key ownership and digest-based approval flows. However, confirm that:

  1. These increases are intentional and reflect necessary changes (not unintended complexity bloat)
  2. The digest-based flow and nonce tightening account for the observed gas deltas
  3. No further optimization opportunities exist in the KeyType key handling or signature validation paths
test/mocks/MockERC20.sol (1)

4-4: Formatting-only changes and lint directive look good

The added forge-lint directive and the multiline reformatting of the constructor and function signatures are non-functional changes: parameter order/types, visibility, overrides, and call sites remain the same. No issues from a behavior or testing standpoint.

Also applies to: 17-21, 44-47, 51-54, 58-62, 66-70, 74-77, 81-85

src/libs/LibCalls.sol (1)

7-40: Calls struct layout and typehash remain consistent.

The Calls field order and CALLS_TYPE_HASH string are aligned, and typehash still hashes (nonce, mode, calls) in that order. No issues from this refactor.

snapshots/CatapultarFactoryTest.json (1)

2-4: Snapshot updates look consistent with new factory APIs.

The updated gas snapshots and deployWithDigest key align with the new deployment paths; nothing to change here.

src/libs/BitmapNonce.sol (1)

21-40: Nonce‑0 guard in unordered nonce usage is sound.

Rejecting nonce 0 up front in _useUnorderedNonce matches the bitmap layout and updated tests, and avoids reserving a “valid” zero nonce. The extra forge‑lint suppressions around the casts/shifts are reasonable here.

test/Integration.t.sol (1)

65-69: Integration test wiring matches new ownership and extraData semantics.

Using KeyedOwnable.KeyType.ECDSAOrSmartContract with a 1‑element keys array and shifting nonces to start at 1 keeps the integration test consistent with KeyedOwnable and the new BitmapNonce guard. The updated assembleExtraData helper also still matches the documented bytes1 | bytes23 | bytes8 packing.

Also applies to: 197-251, 259-277

test/Catapultar/Catapultar.base.t.sol (1)

34-41: Tests correctly exercise new KeyedOwnable + BitmapNonce behavior.

Switching initialization to KeyedOwnable.KeyType.ECDSAOrSmartContract with a single‑word key and moving all unordered‑nonce tests to start at 1 lines up with the new BitmapNonce zero‑nonce guard. The added digest and token‑transfer‑fallback tests give good coverage of the new approval and callback paths.

Also applies to: 60-66, 115-217, 237-320, 338-348, 348-370, 372-478

src/libs/KeyedOwnable.sol (1)

226-263: Run forge fmt on KeyedOwnable.sol to resolve CI formatting check.

The file has formatting issues with inline comments in the WebAuthn.verify() function call (around line 251-252) that are causing forge fmt --check to fail in CI.

test/CatapultarFactory.t.sol (2)

27-27: LGTM! Tests properly exercise the new KeyedOwnable-based API.

The migration from owner address to KeyType/keys[] parameters is consistent across all test functions, properly exercising the new initialization flow.

Also applies to: 85-87


124-136: LGTM! Salt validation tests properly updated.

All salt validation fuzz tests correctly construct the keys[] array and exercise the ownerInSalt modifier with the new KeyedOwnable-based parameters.

Also applies to: 138-150, 152-164, 166-178, 180-192, 194-206

src/Catapultar.sol (6)

4-4: LGTM! New imports support the multi-key ownership and Stowaway integration.

The imports of Stowaway, ERC7821, and KeyedOwnable are properly used throughout the contract to enable the new digest-based approval flow and fallback routing.

Also applies to: 6-6, 15-15


65-71: LGTM! Digest approval mechanism properly defined.

The DigestApproval enum and approvedDigest mapping correctly implement the three-state approval system (Unset/Call/Signature), with enum values that align with the factory's assembly logic.


95-100: LGTM! Initialization properly migrated to KeyedOwnable.

The init function correctly accepts KeyType and bytes32[] owner parameters and delegates to _transferOwnership, maintaining the initializer pattern.


223-232: LGTM! Digest-based approval properly integrated into validation.

The refactored _validateOpData correctly handles three cases:

  1. Self-calls with 32-byte opData (batch execution)
  2. Pre-approved Call digests with 32-byte opData
  3. Standard signature validation for longer opData

This aligns with the new digest approval mechanism.


73-76: LGTM! Constructor properly simplified for proxy pattern.

The removal of the allowOneTimeCall parameter and unconditional initialization disabling correctly supports the unified executor approach in the factory. The version bump to 0.1.0 appropriately reflects the breaking changes.

Also applies to: 80-80


252-252: Stowaway integration is intentionally designed and documented.

The fallback routing is correctly implemented: Stowaway.searchAndCall(ERC7821.execute.selector) routes encoded execute calls, followed by LibZip.cdFallback() for decompression. This two-layer pattern is documented in the README as the intended design—Stowaway catches stray fallback functions while LibZip provides gas efficiency on expensive calldata chains. The ordering is intentional and the integration is complete.

src/CatapultarFactory.sol (6)

4-4: LGTM! New imports support the KeyedOwnable-based deployment.

The imports of EfficientHashLib and KeyedOwnable are properly used for salt derivation and key type handling throughout the factory.

Also applies to: 8-8


37-42: LGTM! Consolidation to single executor simplifies deployment.

The removal of separate EXECUTOR_NO_EMBEDDED_CALLS and EXECUTOR_EMBEDDED_CALLS in favor of a single EXECUTOR aligns with the unified digest-based approval mechanism, reducing complexity and deployment cost.


46-54: LGTM! Deploy function properly migrated to KeyedOwnable API.

The deploy function correctly accepts KeyType and bytes32[] owner parameters, validates them via the ownerInSalt modifier, and initializes the proxy with the new ownership model.


75-88: Assembly logic correctly maps isSignature to DigestApproval enum.

The assembly blocks correctly compute:

  • nonce = isSignature + 1 → 1 (false) or 2 (true)
  • This maps to DigestApproval.Call (1) and DigestApproval.Signature (2)

The salt derivation includes the nonce to ensure distinct addresses for Call vs. Signature approvals.


59-65: LGTM! Predict functions properly mirror deploy functions.

All predictDeploy* functions correctly use the same ownerInSalt validation and salt derivation logic as their corresponding deploy* functions, ensuring consistent address prediction.

Also applies to: 97-113, 131-137


159-170: The concern raised in this review comment is unfounded. Evidence from the codebase shows that EfficientHashLib explicitly supports bytes32[] arrays through the using statement using EfficientHashLib for bytes32[]; in src/libs/LibCalls.sol. The code at line 167 correctly hashes the entire owner array as a single input, which is the intended behavior. There are no compilation issues or incorrect hashing logic.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
README.md (1)

201-229: Clean up typos and wording in Stowaway “Usage Warnings” section.

The content is good, but there are a few remaining typos and minor style nits (many of which were flagged in a previous review):

  • Line 203: delievereddelivered; optionally clarify casing: “You can optionally LibZip the call” or “you can optionally libzip the call” depending on project convention.
  • Line 209: exeuteexecute; also consider spelling out “if and only if (IFF)” or just “if and only if” instead of bare “IFF”.
  • Lines 97 & 210: avoid mixing Upgradable / upgradeable variants; pick one spelling and use it consistently.
  • Line 213: “very important” can be softened if you want to follow the style suggestion (e.g., “crucial to parse and validate”).
  • Line 219: “If A implement ERC-1271” → “If A implements ERC-1271”.
  • Line 222: remove the stray tab in “signed (by A).” → “signed (by A).”
  • Line 228: “long lived” → “long‑lived”.
  • Line 266: consider adding a short license note for Stowaway (MIT/other), similar to the Solady/Permit2 entries.

These are all editorial and don’t affect behavior, but tightening them up will make the security guidance easier to read.

Also applies to: 266-266

src/Catapultar.sol (1)

63-72: Pre‑approved signature digests are treated as invalid in isValidSignature.

Right now this branch:

if (signature.length == 0 && approvedDigest[digest] == DigestApproval.Signature) return bytes4(0xffffffff);

returns the ERC‑1271 “invalid” value for an empty signature even when the digest has been explicitly approved via setSignature(..., DigestApproval.Signature). That contradicts the docstring on setSignature (“Allow setting a signed object as validated through owner or self call”) and defeats the purpose of pre‑approving a signing digest.

To align behavior with ERC‑1271 and your setSignature semantics, the empty‑signature + approved‑Signature case should return the magic “valid” selector (0x1626ba7e), not 0xffffffff. For example:

-        if (signature.length == 0 && approvedDigest[digest] == DigestApproval.Signature) return bytes4(0xffffffff);
+        if (signature.length == 0 && approvedDigest[digest] == DigestApproval.Signature) {
+            return bytes4(0x1626ba7e);
+        }

The rest of the function can continue to delegate to _validateSignature for non‑empty signatures.

What are the ERC-1271 magic return values for `isValidSignature(bytes32,bytes)` (valid vs invalid), and is returning `0x1626ba7e` for valid and `0xffffffff` for invalid consistent with the specification and common implementations?

Also applies to: 102-120, 131-147

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0d166b2 and bf7504b.

📒 Files selected for processing (5)
  • README.md (4 hunks)
  • foundry.toml (1 hunks)
  • src/Catapultar.sol (7 hunks)
  • src/CatapultarFactory.sol (4 hunks)
  • src/libs/ERC7821LIFI.sol (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/libs/ERC7821LIFI.sol
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • src/CatapultarFactory.sol
  • src/Catapultar.sol
🪛 LanguageTool
README.md

[uncategorized] ~97-~97: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...--------------: | :---------------: | | Upgradable | No | No...

(EN_WORD_COHERENCY)


[grammar] ~203-~203: Ensure spelling is correct
Context: ...` call into a bytes field which will be delievered to the account. You can optionally LibZ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~209-~209: Ensure spelling is correct
Context: ... bypassed if called from itself: - The exeute endpoint does not require a signature I...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[uncategorized] ~210-~210: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...f the underlying SCA if the contract is upgradable. - The SCA can upgrade the contract own...

(EN_WORD_COHERENCY)


[style] ~213-~213: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...es themselves are not structured. It is very important to parse and validate that ALL sign...

(EN_WEAK_ADJECTIVE)


[grammar] ~228-~228: Use a hyphen to join words.
Context: ... remember that token allowances are long lived. If a token allowance is set on a ...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
README.md

77-77: Hard tabs
Column: 1

(MD010, no-hard-tabs)


78-78: Hard tabs
Column: 1

(MD010, no-hard-tabs)


79-79: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


84-84: Hard tabs
Column: 1

(MD010, no-hard-tabs)


85-85: Hard tabs
Column: 1

(MD010, no-hard-tabs)


222-222: Hard tabs
Column: 80

(MD010, no-hard-tabs)

🔇 Additional comments (5)
foundry.toml (1)

9-9: Configuration change aligns with reproducible builds best practice.

The addition of bytecode_hash = 'none' enables reproducible builds by excluding the bytecode hash from compiled artifacts. This is a standard practice for smart contract projects.

Note: Only the foundry configuration file was provided for review. The actual smart contract implementation changes (KeyedOwnable contract, factory updates, Stowaway integration, signature approval mechanism) mentioned in the PR summary are not visible in this review. Please ensure those changes are thoroughly reviewed separately.

README.md (1)

75-86: Docs correctly reflect new factory deployment signatures.

The deployment examples and the new proxy feature table match the updated factory APIs (deploy(ktp, owner, salt) and deployWithDigest(ktp, owner, salt, digest, isSignature) plus Digest Proxy row), so integrators see the right parameter shapes and capabilities.

Also applies to: 93-101

src/Catapultar.sol (2)

55-56: KeyedOwnable integration and initializer wiring look sound.

Inheriting KeyedOwnable and exposing a single init(KeyType ktp, bytes32[] owner) initializer that just delegates to _transferOwnership matches the new factory deployment flow, and the constructor’s _disableInitializers() correctly protects the implementation contract. Domain name/version ("Catapultar", "0.1.0") are consistent with the @custom:version tag.

Also applies to: 73-76, 78-81, 95-101


211-233: _validateOpData and fallback integration accurately implement documented behavior.

  • _validateOpData cleanly supports three cases: (a) self-call with 32-byte opData (line 223), (b) pre-approved call digests via approvedDigest[callTypeHash] == DigestApproval.Call (line 227), and (c) standard EIP‑712 signature validation via _hashTypedData + _validateSignature (lines 229-230). Nonce consumption via _useUnorderedNonce at line 220 is consistent with unordered nonce semantics.
  • The fallback function correctly chains Stowaway.searchAndCall(ERC7821.execute.selector) before LibZip.cdFallback(), matching the README's documented intent to catch stray fallback functions and optionally handle zipped calldata.
src/CatapultarFactory.sol (1)

4-8: Multi-key deploy APIs and digest-salt flow are correct and atomic.

  • The factory APIs consistently take KeyedOwnable.KeyType ktp and bytes32[] owner, forwarding them into Catapultar.init{value: msg.value}(ktp, owner), matching the new initializer signature.
  • deployWithDigest/predictDeployWithDigest derive deterministic saltWithDigest = EfficientHashLib.hash(salt, digest, bytes32(nonce)) where nonce encodes DigestApproval.Call vs .Signature, keeping the user-controlled salt while namespacing by digest and approval kind.
  • Within a single deployWithDigest transaction, the sequence cloneDeterministic_PUSH0setSignatureinit executes synchronously and atomically; EVM transactions are sequential at the call-stack level, so no external transaction can interleave before the factory call completes.
  • deployUpgradeable/predictDeployUpgradeable mirror the minimal deploy semantics for the ERC1967 path, threading through ktp/owner and ownerInSalt.

Factory wiring aligns with the KeyedOwnable/DigestApproval model.

Also applies to: 13-21, 39-43, 45-55, 59-65, 67-93, 115-137

@lifinance lifinance deleted a comment from zeropath-ai bot Dec 10, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
README.md (1)

97-105: Clarify P256 salt derivation to match _saltPrefix implementation.

The text says “for P256 accounts, the first 20 bytes should be the last 20 bytes of the hash of the entire key”, but _saltPrefix derives an address as bytes20(keccak256(ktp, keccak256(owner[]))), i.e. effectively “the 20 bytes taken via bytes20 from that hash.” To avoid confusion for off‑chain tooling, consider rephrasing this to something like “the 20 bytes obtained from bytes20(keccak256(ktp, keccak256(owner)))” (or at least drop “last/first”) so the README matches the actual factory logic.

src/Catapultar.sol (1)

95-119: setSignature access control is safe under current invariants; treat “no external calls” as part of the design contract.

Allowing setSignature to be callable by anyone while _getInitializedVersion() == 0 is safe here because:

  • The proxy is created and setSignature + init are invoked sequentially within a single transaction from the factory.
  • Neither setSignature nor init performs external calls, so there is no reentrancy window for third‑party injection of digests before initialization completes.

However, this safety depends on those functions remaining free of external calls (including via hooks or delegatecalls). If either ever starts calling out, you’ll want to re‑evaluate this pattern (e.g., by tightening the guard or adding an explicit non‑reentrancy invariant) to avoid digest pollution on uninitialized proxies.

src/libs/KeyedOwnable.sol (1)

92-121: owner() semantics for non‑ECDSA key types may surprise integrators.

owner() always reads _OWNER_SLOT and passes it through _asAddressNotDirty, which will revert for P256 / WebAuthn keys (the high 12 bytes are not zero). This is consistent with your access‑control design (P256/WebAuthn ownership is enforced via _validateSignature and ownerOrSelf using address(this)), but it means:

  • For non‑ECDSA keys, owner() cannot be used off‑chain to discover a controlling EOA/contract; callers will either see a revert or have to special‑case based on ownerKeyType.

If that behavior is intentional, consider documenting it explicitly in the NatSpec for owner() and/or ownerKeyType so downstream tooling doesn’t assume owner() is always callable and meaningful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e9e4074 and ede6b66.

📒 Files selected for processing (7)
  • README.md (7 hunks)
  • script/deploy.s.sol (2 hunks)
  • src/Catapultar.sol (7 hunks)
  • src/CatapultarFactory.sol (4 hunks)
  • src/libs/ERC7821LIFI.sol (2 hunks)
  • src/libs/KeyedOwnable.sol (1 hunks)
  • test/CatapultarFactory.t.sol (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/libs/ERC7821LIFI.sol
  • test/CatapultarFactory.t.sol
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • script/deploy.s.sol
  • src/CatapultarFactory.sol
  • src/Catapultar.sol
  • src/libs/KeyedOwnable.sol
🪛 LanguageTool
README.md

[grammar] ~9-~9: Use a hyphen to join words.
Context: ...d user. It should provide durable double spend protection ensuring dispatched tra...

(QB_NEW_EN_HYPHEN)


[grammar] ~13-~13: Use a hyphen to join words.
Context: ...0000078210001` can be used. It is a once callable, revert ignoring batch call. It...

(QB_NEW_EN_HYPHEN)


[grammar] ~13-~13: Use a hyphen to join words.
Context: ...n be used. It is a once callable, revert ignoring batch call. It allows a set of ...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...cking others. To provide durable double spend protections, execution mode `0x010...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...0000078210001` can be used. It is a once executable, revert raising batch call. I...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...be used. It is a once executable, revert raising batch call. It allows a set of t...

(QB_NEW_EN_HYPHEN)


[grammar] ~17-~17: Use a hyphen to join words.
Context: ...10001` calling itself allowing for a one time callable batch with inner unsigned ...

(QB_NEW_EN_HYPHEN)


[uncategorized] ~101-~101: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...--------------: | :---------------: | | Upgradable | No | No...

(EN_WORD_COHERENCY)


[style] ~217-~217: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...es themselves are not structured. It is very important to parse and validate that ALL sign...

(EN_WEAK_ADJECTIVE)

🪛 markdownlint-cli2 (0.18.1)
README.md

38-38: Hard tabs
Column: 83

(MD010, no-hard-tabs)


69-69: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


69-69: Hard tabs
Column: 1

(MD010, no-hard-tabs)


81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)


87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)


88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)


89-89: Hard tabs
Column: 1

(MD010, no-hard-tabs)


92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)


93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)


94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)


95-95: Hard tabs
Column: 1

(MD010, no-hard-tabs)


226-226: Hard tabs
Column: 80

(MD010, no-hard-tabs)

🔇 Additional comments (3)
script/deploy.s.sol (1)

33-51: Salt and owner key packing in account() are consistent with the factory’s expectations.

ownerArray[0] = bytes32(uint256(uint160(owner))) matches KeyedOwnable’s ECDSA key format (address in the low 20 bytes, high 12 bytes zero), and salt = bytes32(bytes20(owner)) encodes the address into the high 20 bytes of the salt, which aligns with the “first 20 bytes” requirement enforced via ownerInSalt / _saltPrefix and LibClone.checkStartsWith. The predict‑then‑deploy flow plus the post‑deploy equality check give a good safety net around deterministic deployment.

src/CatapultarFactory.sol (2)

64-110: Digest‑based deploy path correctly couples salt derivation and DigestApproval enum values.

deployWithDigest / predictDeployWithDigest derive nonce = isSignature ? 2 : 1, fold (salt, digest, bytes32(nonce)) into saltWithDigest, and then cast nonce back to Catapultar.DigestApproval for the setSignature call. Given DigestApproval { Unset, Call, Signature }, this keeps:

  • Salt derivation and approval flag in sync (1 → Call, 2 → Signature).
  • Address prediction deterministic and collision‑free across digest/type combinations.

With setSignature and init both remaining internal‑only (no external calls), this deploy path stays atomic and does not introduce a new reentrancy surface.


138-167: _saltPrefix design matches the multi‑key model and keeps the owner‑in‑salt invariant.

The updated ownerInSalt modifier now defers to _saltPrefix(ktp, owner), which:

  • For ECDSAOrSmartContract with a single word key, uses the literal address encoded into owner[0].
  • For larger keys (P256/WebAuthn or multi‑word), derives a 20‑byte prefix as bytes20(keccak256(ktp, keccak256(owner[])))).

This preserves the “salt starts with an owner‑derived address” invariant across all key types and is compatible with the script’s salt = bytes32(bytes20(owner)) pattern for the ECDSA case. The trade‑off (hash‑based prefix for large keys) is reasonable and well‑documented in the modifier comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
README.md (1)

214-241: Clean up remaining grammar/whitespace issues in the Stowaway warning example.

There are still a couple of small issues in this section:

  • Line 232: “If A implement ERC-1271” → “If A implements ERC-1271”
  • Line 235: remove the stray tab: “set custom calldata as signed (by A).” → “set custom calldata as signed (by A).”
  • Optional: “It is very important to parse and validate…” could drop “very” to keep the tone a bit crisper.

These are minor, but this section is security-sensitive documentation, so it’s worth keeping it as polished as possible.

🧹 Nitpick comments (2)
README.md (2)

9-20: Tighten wording around design/intro for readability.

Minor language nits you may want to clean up:

  • “durable double spend protection” → “durable double-spend protection”
  • “execute a complex actions on behalf of a user” → “execute a complex action on behalf of a user” (or “execute complex actions”)
  • “deterministic deployed with a pre-configured call” → “deterministically deployed with a pre-configured call”

These don’t affect meaning but make the intro read more smoothly.


35-40: Fix markdown hard tabs and list indentation flagged by markdownlint.

On several of the changed lines (e.g., the feature tables, bullet lists, and code examples), there are literal tab characters and slightly off indentation (MD010, MD007). This can cause noisy diffs and tooling warnings.

Consider normalizing these to spaces only, for example:

  • Replace tab-aligned cells like Yes\t\t\t\t | with space-based padding.
  • Replace leading tabs before list items and fenced code (e.g., under “Signature Validation”, “Account Deployment”, “Execution Models”) with 2–4 spaces, consistently.

This should clear the reported MD010 and MD007 issues.

Also applies to: 69-70, 79-90, 97-105, 116-116, 131-135, 198-200

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ede6b66 and 8655cd2.

📒 Files selected for processing (4)
  • README.md (9 hunks)
  • snapshots/CatapultarMinimalTest.json (1 hunks)
  • snapshots/CatapultarUpgradeableTest.json (1 hunks)
  • src/Catapultar.sol (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • snapshots/CatapultarMinimalTest.json
  • snapshots/CatapultarUpgradeableTest.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • src/Catapultar.sol
🪛 LanguageTool
README.md

[grammar] ~9-~9: Use a hyphen to join words.
Context: ...d user. It should provide durable double spend protection ensuring dispatched tra...

(QB_NEW_EN_HYPHEN)


[grammar] ~13-~13: Use a hyphen to join words.
Context: ...0000078210001` can be used. It is a once callable, revert ignoring batch call. It...

(QB_NEW_EN_HYPHEN)


[grammar] ~13-~13: Use a hyphen to join words.
Context: ...n be used. It is a once callable, revert ignoring batch call. It allows a set of ...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...cking others. To provide durable double spend protections, execution mode `0x010...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...0000078210001` can be used. It is a once executable, revert raising batch call. I...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...be used. It is a once executable, revert raising batch call. It allows a set of t...

(QB_NEW_EN_HYPHEN)


[grammar] ~17-~17: Use a hyphen to join words.
Context: ...10001` calling itself allowing for a one time callable batch with inner unsigned ...

(QB_NEW_EN_HYPHEN)


[uncategorized] ~101-~101: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...--------------: | :---------------: | | Upgradable | No | No...

(EN_WORD_COHERENCY)


[style] ~226-~226: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...es themselves are not structured. It is very important to parse and validate that ALL sign...

(EN_WEAK_ADJECTIVE)

🪛 markdownlint-cli2 (0.18.1)
README.md

38-38: Hard tabs
Column: 83

(MD010, no-hard-tabs)


69-69: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


69-69: Hard tabs
Column: 1

(MD010, no-hard-tabs)


81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)


87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)


88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)


89-89: Hard tabs
Column: 1

(MD010, no-hard-tabs)


92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)


93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)


94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)


95-95: Hard tabs
Column: 1

(MD010, no-hard-tabs)


116-116: Hard tabs
Column: 40

(MD010, no-hard-tabs)


131-131: Hard tabs
Column: 1

(MD010, no-hard-tabs)


135-135: Hard tabs
Column: 1

(MD010, no-hard-tabs)


235-235: Hard tabs
Column: 80

(MD010, no-hard-tabs)

🔇 Additional comments (3)
README.md (1)

69-70: New key-type and digest-proxy documentation looks consistent and helpful.

The additions for:

  • Supported signature types (ECDSA, ERC-1271, P256, WebAuth P256),
  • Updated CatapultarFactory deployment signatures (deploy(ktp, owner, salt) and deployWithDigest(ktp, owner, salt, callsTypeHash, isSignature)),
  • The digest-proxy feature table,

all read coherently and match the described KeyedOwnable / per-digest approval model in the contracts.

No changes requested here.

Also applies to: 79-90, 97-105, 198-200

src/Catapultar.sol (2)

58-72: Per-digest approval wiring (setSignature, approvedDigest, isValidSignature, _validateOpData) looks consistent.

A quick sanity pass over the new approval/digest flow:

  • setSignature:

    • Allows calls on uninitialized clones (version 0), but restricts to ownerOrSelf() once initialized.
    • Writes the approvedDigest flag and emits SignatureSet.
    • Contains no external calls, so it remains safe to use before init in the factory’s atomic deployment sequence.
  • isValidSignature:

    • Wraps the incoming hash using REPLAY_PROTECTION and address(this), matching the documented replay-protection envelope.
    • Supports pre-approved signature digests via the signature.length == 0 && approvedDigest[digest] == DigestApproval.Signature fast path, returning the ERC‑1271 “valid” selector.
    • Otherwise delegates to _validateSignature(digest, signature) as expected.
  • _validateOpData:

    • Always consumes the unordered nonce (_useUnorderedNonce(nonce)), including for self-calls, which is consistent with the replay model.
    • Permits no-signature execution only when opData.length == 32 && msg.sender == address(this), or when there is a matching DigestApproval.Call entry for the computed callTypeHash.
    • For all other cases, it falls back to signature validation using the digest selected via the (fixed) isMultichain flag.

This all matches the intended per-digest approval model and the README’s description of embedded calls and replay protection. No changes requested here. Based on learnings, I also agree with keeping this logic lean and not adding extra require checks that just guard obviously malformed input.

Also applies to: 95-101, 111-120, 131-141, 211-230


259-264: Fallback composition with Stowaway and LibZip is straightforward.

The updated fallback that calls Stowaway.searchAndCall(ERC7821.execute.selector) followed by LibZip.cdFallback() is clear and keeps all logic on-chain in a single entry point. There are no additional external calls or state writes here, so this doesn’t introduce new reentrancy or ownership concerns within Catapultar itself.

No action needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/CatapultarFactory.sol (1)

64-85: LGTM: Digest-based deployment is correctly implemented.

The assembly logic correctly maps isSignature to the enum values (1 for Call, 2 for Signature), and the salt derivation through triple-hashing ensures distinct addresses per digest/approval-type. The initialization sequence (deploy → setSignature → init) executes atomically within a single transaction.

Optional: The assembly block (lines 73–78) is duplicated in predictDeployWithDigest (lines 98–103). Consider extracting to a private pure function:

function _digestApprovalNonce(bool isSignature) private pure returns (uint256 nonce) {
    assembly ("memory-safe") {
        // Catapultar.DigestApproval.Call == 1
        // Catapultar.DigestApproval.Signature == 2
        nonce := add(isSignature, 1)
    }
}

Then replace both blocks with:

uint256 nonce = _digestApprovalNonce(isSignature);

This reduces duplication and simplifies future maintenance, though the gas impact is negligible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8655cd2 and ddd7998.

📒 Files selected for processing (2)
  • snapshots/CatapultarFactoryTest.json (1 hunks)
  • src/CatapultarFactory.sol (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • snapshots/CatapultarFactoryTest.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • src/CatapultarFactory.sol
🔇 Additional comments (7)
src/CatapultarFactory.sol (7)

4-4: LGTM: Appropriate imports and version bump.

The new imports support the multi-key ownership model, and the version bump correctly reflects the breaking API changes.

Also applies to: 8-8, 13-13


43-51: LGTM: Clean multi-key deployment.

The function correctly integrates the new KeyedOwnable model, forwards msg.value safely, and maintains deterministic deployment via the updated ownerInSalt modifier.


56-62: LGTM: Consistent prediction API.

The signature changes align with deploy, and the deterministic address prediction remains correct.


90-106: LGTM: Prediction logic mirrors deployment.

The function correctly replicates the salt derivation and address prediction for digest-based deployments.


109-130: LGTM: Upgradeable deployment functions updated consistently.

Both deployUpgradeable and predictDeployUpgradeable correctly integrate the new multi-key parameters and maintain proper value forwarding.


143-150: LGTM: Clean salt validation.

The modifier correctly delegates to _saltPrefix for computing the expected prefix and validates the salt accordingly.


152-161: LGTM: Efficient salt prefix computation.

The helper correctly handles the ECDSA single-address optimization (lines 156–157) and hashes arbitrary key types with their owner arrays (line 159). The implementation prioritizes gas efficiency by omitting explicit validation in the else branch, consistent with the team's established approach. Invalid inputs will fail downstream or result in unusable deployments, which is acceptable given users control the input data.

Based on learnings, explicit length checks are intentionally omitted to avoid wasting gas on malformed user input.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

69-105: Replace hard tabs with spaces for consistent formatting.

The code blocks and list items (lines 69–95) use hard tabs instead of spaces, which violates Markdown best practices and causes linting warnings. Replace all tabs with spaces (typically 2 or 4 spaces per indentation level depending on context).

Also review line 38 which contains a hard tab in the table; this should also be converted to spaces.

🧹 Nitpick comments (2)
src/libs/KeyedOwnable.sol (1)

146-171: Duplicate validation logic for P256 and WebAuthnP256.

Cases 1 and 2 have identical validation logic (checking both key words are non-zero). Consider consolidating:

             case 1 {
                 valid := and(
                     valid,
                     not(
                         or(
                             eq(calldataload(key.offset), 0),
                             eq(calldataload(add(key.offset, 0x20)), 0)
                         )
                     )
                 )
             }
-            case 2 {
-                valid := and(
-                    valid,
-                    not(
-                        or(
-                            // Check if first word of key is 0
-                            eq(calldataload(key.offset), 0),
-                            // Check if second word of key is 0.
-                            eq(calldataload(add(key.offset, 0x20)), 0)
-                        )
-                    )
-                )
-            }
+            // case 2: same validation as case 1 (falls through)

Or use default with an early return for case 0. This is a minor readability/gas nit.

test/libs/KeyedOwnable.t.sol (1)

326-353: Consider adding WebAuthnP256 signature validation test.

The test suite covers ECDSA, ERC-1271, and P256 signature validation but lacks WebAuthnP256 coverage. Given the complexity of WebAuthn authentication (challenge encoding, authenticator data, client data JSON), a dedicated test would catch integration issues with Solady's WebAuthn.verify.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ddd7998 and 41d6af2.

📒 Files selected for processing (7)
  • README.md (9 hunks)
  • snapshots/CatapultarFactoryTest.json (1 hunks)
  • src/Catapultar.sol (8 hunks)
  • src/CatapultarFactory.sol (4 hunks)
  • src/libs/KeyedOwnable.sol (1 hunks)
  • test/libs/KeyedOwnable.t.sol (1 hunks)
  • test/mocks/MockKeyedOwnable.sol (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • snapshots/CatapultarFactoryTest.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • src/libs/KeyedOwnable.sol
  • src/CatapultarFactory.sol
  • src/Catapultar.sol
🪛 LanguageTool
README.md

[grammar] ~9-~9: Use a hyphen to join words.
Context: ...d user. It should provide durable double spend protection ensuring dispatched tra...

(QB_NEW_EN_HYPHEN)


[grammar] ~11-~11: Ensure spelling is correct
Context: ...execute a complex action on behalf of a use. To scale a transaction dispatch envir...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~13-~13: Use a hyphen to join words.
Context: ...0000078210001` can be used. It is a once callable, revert ignoring batch call. It...

(QB_NEW_EN_HYPHEN)


[grammar] ~13-~13: Use a hyphen to join words.
Context: ...n be used. It is a once callable, revert ignoring batch call. It allows a set of ...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...0000078210001` can be used. It is a once executable, revert raising batch call. I...

(QB_NEW_EN_HYPHEN)


[grammar] ~15-~15: Use a hyphen to join words.
Context: ...be used. It is a once executable, revert raising batch call. It allows a set of t...

(QB_NEW_EN_HYPHEN)


[grammar] ~17-~17: Use a hyphen to join words.
Context: ...10001` calling itself allowing for a one time callable batch with inner unsigned ...

(QB_NEW_EN_HYPHEN)


[uncategorized] ~101-~101: Do not mix variants of the same word (‘upgradable’ and ‘upgradeable’) within a single text.
Context: ...--------------: | :---------------: | | Upgradable | No | No...

(EN_WORD_COHERENCY)


[style] ~226-~226: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...es themselves are not structured. It is very important to parse and validate that ALL sign...

(EN_WEAK_ADJECTIVE)

🪛 markdownlint-cli2 (0.18.1)
README.md

38-38: Hard tabs
Column: 83

(MD010, no-hard-tabs)


69-69: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


69-69: Hard tabs
Column: 1

(MD010, no-hard-tabs)


81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)


87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)


88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)


89-89: Hard tabs
Column: 1

(MD010, no-hard-tabs)


92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)


93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)


94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)


95-95: Hard tabs
Column: 1

(MD010, no-hard-tabs)


116-116: Hard tabs
Column: 40

(MD010, no-hard-tabs)


131-131: Hard tabs
Column: 1

(MD010, no-hard-tabs)


135-135: Hard tabs
Column: 1

(MD010, no-hard-tabs)


235-235: Hard tabs
Column: 80

(MD010, no-hard-tabs)

🔇 Additional comments (12)
README.md (1)

1-282: Approve README updates for multi-key and P256 support.

The documentation updates effectively communicate the new capabilities: KeyedOwnable multi-key support, digest-based deployments (deployWithDigest), Stowaway integration for fallback handling, and expanded signature support (ECDSA, ERC-1271, P256, WebAuth P256). The execution mode descriptions, deployment strategies, and salt handling guidance for P256 accounts are clear and accurate.

Key areas verified:

  • New deployment methods and parameters (KeyType, salt format for P256)
  • Execution mode specifications with multi-chain signing support
  • Feature comparison tables updated consistently
  • Stowaway integration documented with appropriate security warnings

Minor formatting issues (hard tabs) and one remaining style issue (line 226) have been flagged separately above.

src/libs/KeyedOwnable.sol (4)

81-90: _keyTypeLength assembly logic is correct but could benefit from a brief inline comment.

The assembly computes len = keyType + (keyType < 2 ? 1 : 0), yielding:

  • ECDSAOrSmartContract (0): 0 + 1 = 1
  • P256 (1): 1 + 1 = 2
  • WebAuthnP256 (2): 2 + 0 = 2

This is correct. Consider adding a brief inline comment explaining the arithmetic for future maintainers.


206-222: Verify intentional design: transferOwnership(address) allows zero-address owner.

The comment mentions resigning ownership by setting to 0, but this bypasses _isValidKey validation (which rejects zero addresses for ECDSAOrSmartContract). This creates an inconsistency:

  • transferOwnership(KeyType.ECDSAOrSmartContract, [0x0]) → reverts with InvalidKey()
  • transferOwnership(address(0)) → succeeds

If renunciation is intentional, consider:

  1. Adding explicit documentation that this is the only way to renounce
  2. Adding a dedicated renounceOwnership() function for clarity
  3. Emitting a distinct event for renunciation

If unintentional, add validation:

     function transferOwnership(
         address newOwner
     ) public payable onlyOwnerOrSelf {
+        if (newOwner == address(0)) revert InvalidKey();
         // ...
     }

271-274: Verify: ECDSAOrSmartContract fallback receives truncated signature.

When ownerKeyType == ECDSAOrSmartContract but signature length is not 64/65 bytes (e.g., 66+ bytes), the code:

  1. Truncates the signature by 1 byte (line 245)
  2. Falls through to line 271-273, passing the truncated signature to SignatureCheckerLib

This seems intentional (allowing ECDSA signatures with an appended prehash byte), but:

  • The prehash is applied (SHA256) before ECDSA verification
  • Standard ECDSA/EIP-1271 verifiers don't expect prehashed digests

Is this path intentional for smart contract wallets that support prehashed digests, or should line 237 include other ECDSA-compatible lengths?


283-294: LGTM!

The dirty-address detection correctly examines elem before assignment. The shift-and-compare logic properly identifies when upper 96 bits are non-zero.

test/mocks/MockKeyedOwnable.sol (1)

10-37: LGTM!

Test mock correctly exposes internal functions for testing. The warning comment appropriately signals this is not for production use. ERC-1271 magic values are correct.

src/Catapultar.sol (4)

111-119: LGTM!

The access control correctly allows setSignature during atomic factory deployment (when uninitialized) while requiring owner/self authorization post-initialization. The extensive prior review discussion confirmed this pattern is secure.


131-147: LGTM!

The replay protection envelope correctly prevents cross-account signature replay. The pre-approved digest path now correctly returns the valid magic value.


231-237: Multichain flag detection is now correct.

The assembly extracts byte index 2 (0-indexed from MSB) of mode:

  • shl(16, mode) shifts away the 2 MSB bytes
  • shr(248, ...) isolates the new MSB (originally byte 2)
  • Comparison with 1 correctly identifies the multichain flag

This addresses the previously flagged issue where the wrong shift amounts were used.


261-264: LGTM!

The fallback correctly integrates Stowaway for dynamic call resolution before falling back to LibZip's calldata decompression. This maintains backward compatibility while adding the new execution routing.

src/CatapultarFactory.sol (2)

65-86: LGTM!

The digest-based deployment correctly:

  1. Derives a unique salt from (salt, digest, approvalType) preventing address collisions
  2. Executes setSignature and init atomically within a single transaction
  3. Properly forwards msg.value to init

The prior review discussion confirmed the reentrancy annotation is appropriate.


153-162: LGTM!

The salt prefix logic correctly handles both cases:

  • ECDSA: Uses the owner address directly (efficient, backward-compatible)
  • P256/WebAuthn: Hashes the full key data to derive a 20-byte prefix

The double-hashing approach for multi-word keys prevents length-extension attacks and ensures consistent prefix derivation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
test/libs/KeyedOwnable.t.sol (2)

238-243: Bug: owner[1] is never assigned.

Line 241 assigns owner[0] twice, leaving owner[1] as 0. This causes the test to fail for the wrong reason (zero key instead of "too long").

Apply this diff:

         // too long
         owner = new bytes32[](3);
         owner[0] = keccak256(bytes("x"));
-        owner[0] = keccak256(bytes("y"));
+        owner[1] = keccak256(bytes("y"));
         vm.expectRevert(KeyedOwnable.InvalidKey.selector);
         ownable.transferOwnership(KeyedOwnable.KeyType.P256, owner);

267-272: Same bug: owner[1] not assigned.

         // too long
         owner = new bytes32[](3);
         owner[0] = keccak256(bytes("x"));
-        owner[0] = keccak256(bytes("y"));
+        owner[1] = keccak256(bytes("y"));
         vm.expectRevert(KeyedOwnable.InvalidKey.selector);
         ownable.transferOwnership(KeyedOwnable.KeyType.WebAuthnP256, owner);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 41d6af2 and bb14439.

📒 Files selected for processing (2)
  • src/libs/KeyedOwnable.sol (1 hunks)
  • test/libs/KeyedOwnable.t.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • test/libs/KeyedOwnable.t.sol
  • src/libs/KeyedOwnable.sol
🔇 Additional comments (7)
test/libs/KeyedOwnable.t.sol (1)

15-81: LGTM!

The P256VerifierEtcher helper provides robust test infrastructure for managing P256 verification bytecode across different execution environments (native RIP precompile vs. etched verifier).

src/libs/KeyedOwnable.sol (6)

28-94: LGTM!

The storage slot design provides upgrade safety by overlapping with Solady's Ownable slot, ensuring that if an ECDSAOrSmartContract key is in use, accidental upgrades to non-KeyedOwnable contracts may still maintain access control.


74-90: LGTM!

Clever branchless arithmetic to compute key lengths. The expression add(keyType, lt(keyType, 2)) correctly produces 1 for ECDSA and 2 for P256/WebAuthnP256.


123-173: LGTM!

The key validation correctly enforces constraints for each KeyType: clean 20-byte addresses for ECDSA, and non-zero coordinate pairs for P256/WebAuthnP256.

Based on learnings, the absence of explicit length checks before array indexing is intentional for gas efficiency—out-of-bounds access will panic.


175-222: LGTM!

Ownership transfer logic correctly validates keys, writes storage slices, updates ownerKeyType, and emits events. The address-based overload provides a convenient path for ECDSA transfers.


224-278: LGTM!

The signature validation flow correctly handles all three key types:

  • Lines 237-240: Fast path for standard Ethereum signatures
  • Lines 242-251: Prehash flag extraction is correct—truncatedCalldata adjusts length but preserves offset, so loadCalldata(signature, n-32) reads a 32-byte word whose last byte (at signature.offset + n - 1) is the prehash flag
  • Lines 253-277: Proper routing to P256, WebAuthnP256, or ECDSA/1271 verification

280-296: LGTM!

The dirty-address detection correctly validates that the upper 12 bytes are zero before returning the address, protecting against returning truncated addresses when non-ECDSA keys are stored.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

60-60: Fix typo: "dangourus" should be "dangerous".

Apply this diff:

-- Catapultar does not support external delegate calls. Delegate calls are dangourus, particularly for upgradeable contracts. They can change the owner of Catapultar but also the implementation of a proxy (ERC-1967).
+- Catapultar does not support external delegate calls. Delegate calls are dangerous, particularly for upgradeable contracts. They can change the owner of Catapultar but also the implementation of a proxy (ERC-1967).
♻️ Duplicate comments (1)
README.md (1)

235-235: Remove hard tab character before "(by A)."

Line 235 contains a hard tab character that should be replaced with a space for consistent formatting.

Apply this diff:

-2. Store custom calldata in transient storage and set custom calldata as signed	(by A).
+2. Store custom calldata in transient storage and set custom calldata as signed (by A).
🧹 Nitpick comments (1)
test/libs/KeyedOwnable.t.sol (1)

330-330: Optional: Comment refers to "WebAuthnP256Owner" but test uses P256.

The comment on line 330 mentions "WebAuthnP256Owner" but the test is actually for P256 (not WebAuthnP256), as evidenced by the KeyType.P256 used on line 332. Consider updating the comment for clarity.

Apply this diff:

-        // We need to switch the account to a P256 owner.
-        (uint256 privatekeyP256, bytes32[] memory publickeyP256) = makeP256("WebAuthnP256Owner");
+        // We need to switch the account to a P256 owner.
+        (uint256 privatekeyP256, bytes32[] memory publickeyP256) = makeP256("P256Owner");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb14439 and 84fea46.

📒 Files selected for processing (2)
  • README.md (6 hunks)
  • test/libs/KeyedOwnable.t.sol (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • test/libs/KeyedOwnable.t.sol
🪛 markdownlint-cli2 (0.18.1)
README.md

69-69: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


69-69: Hard tabs
Column: 1

(MD010, no-hard-tabs)


70-70: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


70-70: Hard tabs
Column: 1

(MD010, no-hard-tabs)


72-72: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


72-72: Hard tabs
Column: 1

(MD010, no-hard-tabs)


73-73: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


73-73: Hard tabs
Column: 1

(MD010, no-hard-tabs)


80-80: Hard tabs
Column: 1

(MD010, no-hard-tabs)


81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)


87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)


88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)


89-89: Hard tabs
Column: 1

(MD010, no-hard-tabs)


92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)


93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)


94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)


95-95: Hard tabs
Column: 1

(MD010, no-hard-tabs)


131-131: Hard tabs
Column: 1

(MD010, no-hard-tabs)


135-135: Hard tabs
Column: 1

(MD010, no-hard-tabs)


235-235: Hard tabs
Column: 80

(MD010, no-hard-tabs)

🔇 Additional comments (5)
test/libs/KeyedOwnable.t.sol (5)

14-81: LGTM: P256 verifier etching infrastructure is well-structured.

The P256VerifierEtcher contract provides clean test infrastructure for conditionally etching verifier bytecode and handling native RIP precompile detection. The logic correctly:

  • Detects native RIP precompile support
  • Conditionally etches or clears bytecode based on the active flag
  • Provides helper methods for common etching patterns

83-110: LGTM: Test setup and P256 key generation are correct.

The setup properly:

  • Generates deterministic P256 key pairs with validation (ensuring privateKey is non-zero and within P256.N)
  • Etches the necessary verifier infrastructure
  • Initializes the mock contract with an ECDSA owner

112-177: LGTM: Comprehensive ownership transfer test.

The test thoroughly validates:

  • Ownership transfers across all key types (ECDSA, P256, WebAuthnP256)
  • Event emissions with correct parameters
  • Dirty slot behavior when switching from P256/WebAuthnP256 back to ECDSA
  • The revert when calling owner() with a dirty Ethereum address slot

179-273: LGTM: Invalid key test cases are comprehensive and correct.

The test validates all invalid key scenarios:

  • Zero/empty keys for all key types
  • Dirty Ethereum addresses for ECDSA
  • Invalid array lengths (too short, too long) for all key types
  • Both x and y coordinates being non-zero for P256/WebAuthnP256

The previously flagged array assignment bugs have been fixed.


275-407: LGTM: Signature validation tests are comprehensive and correct.

The test suite thoroughly validates:

  • ECDSA signatures (both compact and standard formats)
  • ERC-1271 signature delegation to smart contract owners
  • P256 signatures with optional sha256 rehashing
  • WebAuthn signatures with optional rehashing
  • Negative test cases (invalid digests, wrong rehash flags)

The previously flagged typos ("choise" → "choice") have been corrected.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 84fea46 and 47704ff.

📒 Files selected for processing (4)
  • snapshots/CatapultarMinimalTest.json (1 hunks)
  • snapshots/CatapultarUpgradeableTest.json (1 hunks)
  • src/Catapultar.sol (8 hunks)
  • test/Catapultar/Catapultar.base.t.sol (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • snapshots/CatapultarMinimalTest.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • src/Catapultar.sol
🪛 GitHub Actions: CI
test/Catapultar/Catapultar.base.t.sol

[error] 424-435: forge fmt --check failed. Formatting differences detected in test/Catapultar/Catapultar.base.t.sol. Run 'forge fmt' (or 'forge fmt --check' after formatting) to fix code style issues. Process completed with exit code 1.

🔇 Additional comments (11)
src/Catapultar.sol (6)

4-16: LGTM! Clean integration of KeyedOwnable and digest-based approval system.

The migration from Ownable to KeyedOwnable enables support for multiple key systems (including P256), and the DigestApproval enum with approvedDigest mapping provides a type-safe mechanism for per-digest authorizations.

Also applies to: 55-71


95-100: LGTM! Init function properly integrates KeyedOwnable.

The function signature correctly accepts KeyType and keys array, delegates to KeyedOwnable's _transferOwnership, and is protected by the initializer modifier.


111-119: LGTM! Access control correctly implements factory-safe initialization pattern.

The condition on line 115 allows setSignature to be called during factory deployment (when uninitialized) or by owner/self post-initialization. This is safe because factory deployment is atomic (clone → setSignature → init all execute in one transaction).

Based on learnings, the design has been thoroughly vetted for reentrancy and front-running concerns.


131-147: LGTM! Signature validation correctly handles pre-approved digests.

The replay protection envelope prevents cross-account signature replay, and line 140 now correctly returns the valid selector (0x1626ba7e) for pre-approved signatures with empty signature data.


231-242: LGTM! Multichain flag extraction logic is correct.

The assembly correctly extracts byte 2 of the mode:

  • shl(16, mode) shifts byte 2 into byte 0 position
  • shr(248, ...) isolates that byte in the least significant position

For multichain mode (byte 2 = 0x01), isMultichain will be truthy and _hashTypedDataSansChainId is used. For standard mode (byte 2 = 0x00), _hashTypedData is used with chain ID binding.


261-264: LGTM! Fallback properly integrates Stowaway for ERC7821 support.

The fallback first attempts Stowaway-based selector lookup for ERC7821.execute, then falls back to LibZip for calldata decompression. Clean integration.

snapshots/CatapultarUpgradeableTest.json (1)

4-7: Gas cost increases are expected given the new features.

The increases (507-636 gas per operation) align with the added functionality: KeyedOwnable-based ownership, per-digest approvals, and multichain mode detection. The increases are consistent across operations and reasonable for the added capabilities.

test/Catapultar/Catapultar.base.t.sol (4)

60-91: LGTM! Tests properly migrated to KeyedOwnable-based initialization.

The init helper and related tests correctly construct the keys array and use KeyType.ECDSAOrSmartContract for initialization. The migration is complete and consistent.


259-267: LGTM! New test validates deterministic cloning produces distinct addresses.

The test confirms that different embeds (salts) produce different clone addresses, which is essential for the digest-based deployment model.


348-370: LGTM! Comprehensive test of digest-based approval flow.

The test validates that pre-approved digests enable validation without signatures, exercising both the rejection path (unapproved) and success path (approved via setSignature).


484-521: LGTM! Fallback test correctly validates ERC721/1155 callback support.

The test confirms that Stowaway-based fallback properly handles token transfer callbacks (onERC721Received, onERC1155Received, onERC1155BatchReceived), enabling the contract to receive NFTs.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
test/Catapultar/Catapultar.base.t.sol (3)

4-7: Reasonable use of forge-lint disables, but consider narrowing scope.

Disabling unsafe-typecast, erc20-unchecked-transfer, and unchecked-call for the whole file is understandable for a low-level test harness, but you might later want to confine these to the few callsites that actually need them to keep lint signal high elsewhere in the file.


348-370: Digest-based validateOpData flow is covered end-to-end.

test_validateOpData_digest nicely captures the “default (no digest approval) returns false” path and the “after setSignature + approvedDigest, the same opData validates” path, including an explicit check of the DigestApproval value. One optional improvement would be to initialize calls[0] with a non‑zero target / data so the digest reflects a realistic call bundle instead of an all‑zero placeholder, but that’s not required for correctness.


491-523: Improved token-transfer fallback test assertions; consider also checking return selectors.

Capturing the success flag from the low-level call and asserting true for each of the ERC721 / ERC1155 / ERC1155Batch callbacks is a solid improvement over relying solely on lack of revert. For even stronger guarantees, you could additionally decode the return data and assert it equals the respective selector, but that’s optional given the current test already catches regressions where the call fails outright.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 47704ff and cb41c1f.

📒 Files selected for processing (1)
  • test/Catapultar/Catapultar.base.t.sol (10 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • test/Catapultar/Catapultar.base.t.sol
🔇 Additional comments (5)
test/Catapultar/Catapultar.base.t.sol (5)

16-18: KeyedOwnable-based init wiring looks consistent with previous owner model.

The switch to KeyedOwnable.KeyType.ECDSAOrSmartContract plus bytes32(uint256(uint160(owner))) keys, and the corresponding updates in init, test_template_init_disabled, and test_init, all line up with the prior “single EOA owner” semantics while exercising the new keyed API. Using makeAddrAndKey("owner") keeps the on-chain address and signing key aligned across tests.

Also applies to: 60-66, 69-73, 80-84, 90-91


259-267: Good coverage for embedded clone address differentiation.

test_embed_new_address nicely asserts that different embed payloads (embedA vs embedB) lead to distinct deterministic clone addresses off the same executorTemplate. That directly guards regressions in your LibClone.cloneDeterministic embedding behavior.

If there are multiple cloneDeterministic overloads in your LibClone, double‑check that the argument order here matches the “embedded data + salt” variant you intend to exercise.


306-307: Updated unordered nonce invalidation behavior is exercised well.

Adjusting the invalidation mask from 3 to 7 and explicitly checking that nonces 1 and 2 now revert while a higher nonce (12) still validates gives clearer coverage of the expanded bitmap behavior. This matches the intent of broader invalidation without over-constraining the implementation.

Also applies to: 309-309, 317-317, 319-320


387-395: Stronger assertions on signature-based validation semantics.

Wrapping the first successful call in assertTrue and explicitly asserting assertFalse when the nonce in opData doesn’t match the signed nonce makes the intended behavior of validateOpData much clearer: boolean false for bad signatures, revert for reused nonces. This is a good tightening of the test.


397-443: Multichain signature semantics and EIP712 domain handling are well tested; domain type string is a coupling point.

This test gives excellent coverage of:

  • Chain‑bound mode: signature bound to chain 100 failing on chain 101.
  • Multichain mode: recomputing a domain without chainId and verifying that the same signature works on both chain 100 and 101 over shared storage.

The main subtle coupling is the hard‑coded domain type string "EIP712Domain(string name,string version,address verifyingContract)" and the assumption that executor.eip712Domain()’s layout matches that exact shape. If the underlying contract’s EIP‑712 domain fields or type string ever change, this test will silently become out of sync.

Please confirm that:

  • executor.eip712Domain() returns (uint8, string, string, address, ...) in the order assumed here, and
  • The contract’s multichain hashing really uses the same EIP712Domain(string name,string version,address verifyingContract) type string.

If those ever diverge, consider centralizing the domain hashing helper in the contract/library and calling that from the test instead of re‑encoding the domain here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cb41c1f and f76fb07.

📒 Files selected for processing (1)
  • README.md (7 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~137-~137: Ensure spelling is correct
Context: ...10001**: Executing a set of conditional transactons. If 1 transaction in a set fails, t...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
README.md

69-69: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


69-69: Hard tabs
Column: 1

(MD010, no-hard-tabs)


70-70: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


70-70: Hard tabs
Column: 1

(MD010, no-hard-tabs)


72-72: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


72-72: Hard tabs
Column: 1

(MD010, no-hard-tabs)


73-73: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


73-73: Hard tabs
Column: 1

(MD010, no-hard-tabs)


80-80: Hard tabs
Column: 1

(MD010, no-hard-tabs)


81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)


87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)


88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)


89-89: Hard tabs
Column: 1

(MD010, no-hard-tabs)


92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)


93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)


94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)


95-95: Hard tabs
Column: 1

(MD010, no-hard-tabs)


131-131: Hard tabs
Column: 1

(MD010, no-hard-tabs)


135-135: Hard tabs
Column: 1

(MD010, no-hard-tabs)


235-235: Hard tabs
Column: 80

(MD010, no-hard-tabs)

🔇 Additional comments (1)
README.md (1)

1-283: Documentation updates properly capture KeyedOwnable multi-key model and Digest Proxy functionality.

The content changes effectively document:

  • The shift to KeyedOwnable multi-key support (shown in feature table as "1" entry instead of "Multiple Keys" comparison)
  • New deployWithDigest() deployment method for digest-based proxy initialization
  • Introduction of Digest Proxy as a deployment strategy
  • Comprehensive Stowaway integration section with security warnings about self-call bypass scenarios
  • Consistent usage of ktp parameter across deployment examples
  • ERC-1271 abuse scenarios and token allowance risks in Usage Warnings

These additions align well with the PR objectives of supporting P256 and other key systems through KeyedOwnable, along with digest-based deployment patterns.

@reednaa
Copy link
Member Author

reednaa commented Dec 12, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
README.md (1)

97-97: Clarify salt derivation guidance for multi-key systems.

Line 97 provides guidance on salt structure for P256 accounts, but the phrasing could be slightly clearer. The instruction "For P256 accounts, the first 20 bytes should be the last 20 bytes of the hash of the entire key" is precise but dense. Consider whether additional context (e.g., a brief note about why hashing is needed for longer keys, or a reference to predictDeploy* functions) would help users implement this correctly.

test/Catapultar/Catapultar.base.t.sol (5)

4-7: Scope forge-lint disables as narrowly as possible.
These disables apply to the whole file; consider wrapping only the specific blocks that need them to avoid masking future issues.


60-66: Reduce duplication when building bytes32[] keys from an owner address.
The same “address → bytes32” packing is repeated in multiple tests; a small helper (e.g., _singleAddressKey(address)) would keep future KeyType expansions cleaner.

Also applies to: 69-73, 80-84


305-320: Avoid magic-number brittleness for nonce invalidation mask/value (7).
If 7 is derived from a specific bitmap layout change, consider computing it from the intended nonces (or documenting why 7 is correct) so future nonce encoding changes don’t silently break this test’s intent.


397-443: Multichain domain-separator reconstruction is brittle—prefer a contract/helper-derived “sans chainId” hash if available.
Manually rebuilding the domain separator (Line 424-433) can diverge from the implementation if the domain fields/typehash differ (e.g., extra fields, different EIP-5267 handling). If Catapultar exposes a helper (or internal logic can be mirrored in a shared test util), prefer that to reduce false negatives.


477-500: Good: using vm.prank(address(executor)) makes setSignature authorization explicit.
Optional: add one negative assertion that a non-self caller reverts (if that’s the intended access control) to lock it in.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f76fb07 and 592b773.

📒 Files selected for processing (2)
  • README.md (7 hunks)
  • test/Catapultar/Catapultar.base.t.sol (12 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-12T15:05:38.106Z
Learnt from: reednaa
Repo: lifinance/catapultar PR: 1
File: src/CatapultarFactory.sol:132-137
Timestamp: 2025-11-12T15:05:38.106Z
Learning: In src/CatapultarFactory.sol, explicit length validation checks for the owner array parameter should not be added before indexing, as the out-of-bounds panic provides sufficient protection and adding require() checks would waste gas when users provide invalid data.

Applied to files:

  • test/Catapultar/Catapultar.base.t.sol
🔇 Additional comments (5)
README.md (2)

69-73: Clear and well-documented new signature and deployment features.

The Key Features section now properly documents multi-key support, P256 signatures, and the new Digest Proxy deployment strategy. The updates align well with the PR objectives to add support for other ownable key systems.


214-241: Stowaway integration and usage warnings are well-documented.

The new Stowaway section clearly explains the fallback integration and its optional LibZip support. The Usage Warnings section provides important security guidance about self-call bypass scenarios and ERC-1271 abuse risks. The documentation appropriately emphasizes the critical nature of validating signed batches.

test/Catapultar/Catapultar.base.t.sol (3)

259-267: Nice regression test for embed-dependent deterministic clone addresses.
This guards against accidental salt/embed collisions in LibClone.cloneDeterministic.


446-475: isValidSignature replay protection coverage looks solid.
Good check that the same signature can’t be replayed across a newly deployed proxy.


521-553: Fallback callback test remains clear after the refactor.
The three interface callbacks are still exercised end-to-end via low-level calls.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
README.md (1)

69-69: Consider adding P256-specific signature validation documentation.

Line 69 advertises "P256, and WebAuth P256 signatures" support, but the signature validation section (lines 150–163) only documents ERC-1271 validation. If P256 validation differs meaningfully from ECDSA/ERC-1271 (e.g., different hashing, different replayability properties, or different message formats), consider adding a parallel section or subsection explaining P256-specific validation steps. Otherwise, clarify in a brief note that P256 validation follows the same replay-protection pattern as documented.

Also applies to: 150-163

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 592b773 and e38e03f.

📒 Files selected for processing (1)
  • README.md (7 hunks)
🔇 Additional comments (1)
README.md (1)

214-241: ✅ Stowaway and usage warnings sections are well-documented.

The Stowaway integration documentation is comprehensive and appropriately security-focused. The usage warnings clearly explain the implications of self-calls and token allowances. Previous typo fixes have been properly applied. No changes needed in this section.

Comment on lines 79 to +95
- **Minimal Proxy:**
```solidity
factory.deploy(owner, salt);
```
Deploys a minimal proxy for batch execution.
```solidity
factory.deploy(ktp, owner, salt);
```
Deploys a minimal proxy for batch execution.

- **Proxy with Embedded Call:**
```solidity
factory.deployWithDigest(ktp, owner, salt, callsTypeHash, isSignature);
```
Deploys a proxy with a pre-configured allowed call.

- **Upgradeable Proxy:**
```solidity
factory.deployUpgradeable(owner, salt);
```
Deploys an ERC1967 upgradeable proxy. Ownership can be transferred and logic upgraded.
```solidity
factory.deployUpgradeable(owner, salt);
```
Deploys an ERC1967 upgradeable proxy. Ownership can be transferred and logic upgraded.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find src -name "*Factory*.sol" -type f

Repository: lifinance/catapultar

Length of output: 89


🏁 Script executed:

cat -n src/CatapultarFactory.sol | head -150

Repository: lifinance/catapultar

Length of output: 7780


🏁 Script executed:

sed -n '79,95p' README.md

Repository: lifinance/catapultar

Length of output: 547


Correct deployment method signatures in documentation.

The documented function calls contain two parameter errors:

  1. Line 87: deployWithDigest uses incorrect parameter name callsTypeHash — should be digest
  2. Line 93: deployUpgradeable is missing the required ktp parameter — should be factory.deployUpgradeable(ktp, owner, salt)

Both methods require the ktp (KeyType) parameter according to the actual factory implementation.

🤖 Prompt for AI Agents
README.md around lines 79 to 95: update the documented factory method signatures
to match the actual implementation: change the second example to use
deployWithDigest(ktp, owner, salt, digest, isSignature) (replace callsTypeHash
with digest and include ktp) and change the third example to include the ktp
parameter as factory.deployUpgradeable(ktp, owner, salt); ensure both examples
list parameters in the correct order and names to reflect the real factory API.

Deploys an ERC1967 upgradeable proxy. Ownership can be transferred and logic upgraded.

For all deployments, the first 20 bytes of `salt` should be the owner address or zero. Use the `predictDeploy*` functions to precompute addresses before deployment.
For all deployments, the first 20 bytes of `salt` should be the owner address or zero. For P256 accounts, the first 20 bytes should be the last 20 bytes of the hash of the entire key. Use the `predictDeploy*` functions to precompute addresses before deployment.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for P256-related salt derivation logic in CatapultarFactory.sol
rg -n 'P256|publicKey' src/CatapultarFactory.sol -A 5 -B 2

Repository: lifinance/catapultar

Length of output: 46


🏁 Script executed:

# Look for salt handling and key hashing logic
rg -n 'salt|hash' src/CatapultarFactory.sol | head -40

Repository: lifinance/catapultar

Length of output: 2541


🏁 Script executed:

# Search for specific deployment functions related to P256
fd 'CatapultarFactory.sol' | xargs cat | grep -n 'deploy\|predictDeploy' -A 10

Repository: lifinance/catapultar

Length of output: 5350


Update P256 salt derivation guidance to match factory implementation.

Line 97 contains inaccurate guidance. The actual implementation in CatapultarFactory.sol (lines 139-160) derives the salt prefix using the first 20 bytes of keccak256(ktp, keccak256(owner)) for all key types including P256, not "the last 20 bytes of the hash of the entire key." This applies equally to ECDSA and P256 accounts—the same ownerInSalt modifier enforces the salt format regardless of key type. Update the README to reflect the correct derivation method to prevent deployment address mismatches.

🤖 Prompt for AI Agents
In README.md around line 97, the guidance about P256 salt derivation is
incorrect; update the sentence to state that for all deployments (including P256
and ECDSA) the salt prefix is the first 20 bytes of keccak256(ktp,
keccak256(owner)) as implemented by CatapultarFactory.sol (lines 139–160) and
enforced by the ownerInSalt modifier; replace the existing line that mentions
"last 20 bytes of the hash of the entire key" with this corrected derivation and
keep the note to use the predictDeploy* functions to precompute addresses.

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