Skip to content

refactor: replace ethers runtime dependency with internal ethereUtils#185

Open
sherifahmed990 wants to merge 6 commits into
devfrom
reduce_dep
Open

refactor: replace ethers runtime dependency with internal ethereUtils#185
sherifahmed990 wants to merge 6 commits into
devfrom
reduce_dep

Conversation

@sherifahmed990
Copy link
Copy Markdown
Member

@sherifahmed990 sherifahmed990 commented May 23, 2026

Summary

Move ethers from a runtime dependency to a dev dependency only. The new src/ethereUtils.ts copies the relevant ethers v6 surface used across the codebase (keccak256, id, hashMessage, ABI codec, solidityPacked, EIP-712 typed-data hashing, RLP, address checksum, secp256k1 sign/recover) with some modification, backed by @noble/curves and @noble/hashes. Every internal call site is migrated to import from ./ethereUtils.

Also drops the sharedAbiCoder wrapper in Calibur7702Account in favor of direct encodeAbiParameters / decodeAbiParameters calls, and adds comprehensive parity tests (test/ethereUtils.test.js) — deterministic spec fixtures plus seeded property tests asserting byte-identical output against ethers v6 and viem v2 across 100 random inputs per function.

Summary by CodeRabbit

  • New Features

    • Signer APIs now accept either synchronous or asynchronous signing results.
  • Tests

    • Added an extensive test suite validating hashing, ABI encoding/decoding, packed/EIP‑712 hashing, RLP, and signing for parity with reference implementations.
  • Chores

    • Consolidated and replaced internal crypto/ABI utilities with a unified implementation to standardize encoding, hashing, and signing across the codebase.

Review Change Stack

Move ethers from a runtime dependency to a dev dependency only. The new
src/ethereUtils.ts copies the relevant ethers v6 surface used across the
codebase (keccak256, id, hashMessage, ABI codec, solidityPacked, EIP-712
typed-data hashing, RLP, address checksum, secp256k1 sign/recover) with
some modification, backed by @noble/curves and @noble/hashes. Every
internal call site is migrated to import from ./ethereUtils.

Also drops the sharedAbiCoder wrapper in Calibur7702Account in favor of
direct encodeAbiParameters / decodeAbiParameters calls, and adds
comprehensive parity tests (test/ethereUtils.test.js) — deterministic
spec fixtures plus seeded property tests asserting byte-identical output
against ethers v6 and viem v2 across 100 random inputs per function.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sherifahmed990 sherifahmed990 requested review from Sednaoui and andrewwahid and removed request for Sednaoui May 23, 2026 17:11
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b847326f-4ada-44b5-93b4-2f13fb7509a8

📥 Commits

Reviewing files that changed from the base of the PR and between 3d838d0 and 5771924.

📒 Files selected for processing (1)
  • src/ethereUtils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ethereUtils.ts

📝 Walkthrough

Walkthrough

Adds a new local crypto/ABI utility module (src/ethereUtils.ts), updates package.json, replaces selected ethers.js imports across accounts, signers, paymasters, transport, utils, and Tenderly tooling with ethereUtils helpers, and adds comprehensive parity/property tests for the new module.

Changes

Ethers to EtherUtils Migration

Layer / File(s) Summary
ethereUtils module and package dependencies
package.json, src/ethereUtils.ts
New ethereUtils.ts implements hex/bytes, BigNumberish helpers, keccak/id/hashMessage, address checksum/validation, solidityPacked, RLP, ABI encode/decode, EIP-712 hashing, and secp256k1 signing. package.json updated to add @noble dependencies.
Calibur7702Account refactor to ethereUtils
src/account/Calibur/Calibur7702Account.ts
Switches AbiCoder usage to encodeAbiParameters/decodeAbiParameters across calldata construction and RPC decoding; replaces Wallet-based signing/address derivation with signHash/privateKeyToAddress.
SafeAccount refactor to ethereUtils
src/account/Safe/SafeAccount.ts
Replace TypedDataEncoder, AbiCoder, Wallet, and packing/concat helpers with hashTypedData, encode/decode, signHash/privateKeyToAddress, solidityPacked/concat/dataLength from ethereUtils; update multiple eth_call decodes.
SafeMultiChainSigAccount and helpers
src/account/Safe/SafeMultiChainSigAccount.ts, src/account/Safe/MerkleTree.ts, src/account/Safe/multisend.ts, src/account/Safe/adapters.ts
Multi-chain signing and Merkle hashing migrate to hashTypedData/signHash and keccak256 from ethereUtils; multisend and adapters use local decode/byte helpers.
Simple7702Account and SafeModule refactor
src/account/simple/Simple7702Account.ts, src/account/Safe/modules/SafeModule.ts, src/account/Safe/safeMessage.ts
Simple7702Account uses signHash and decodeAbiParameters; SafeModule exposes abiCoder backed by encode/decode helpers; safeMessage imports hashMessage from ethereutils.
Signer adapters and types refactor
src/signer/adapters.ts, src/signer/types.ts
fromPrivateKey now uses privateKeyToAddress and delegates signing to ethereUtils; SignHashFn/SignTypedDataFn types allow sync or async returns.
Paymaster implementations refactor
src/paymaster/CandidePaymaster.ts, src/paymaster/WorldIdPermissionlessPaymaster.ts
CandidePaymaster uses isAddress from ethereutils; WorldIdPermissionlessPaymaster builds paymasterData via encodeAbiParameters.
Transport, utils, and Tenderly tooling refactor
src/transport/JsonRpcNode.ts, src/utils.ts, src/utils7702.ts, src/utilsTenderly.ts
JsonRpcNode, utils, utils7702, and utilsTenderly switch ABI encoding/decoding and EIP-712 hashing to ethereutils; utils7702 uses ecdsa signing helpers from ethereutils.
Comprehensive test suite for ethereUtils
test/_loadEthereUtils.cjs, test/ethereUtils.test.js
Add runtime loader for src/ethereUtils.ts and an extensive Jest test suite validating parity with ethers v6 and viem v2 across hashing, hex/byte utilities, address handling, ABI encode/decode, solidityPacked, EIP-712, RLP, and signing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • andrewwahid

Poem

🐰 The noble curves now guide our way,
Hexes tidy, bytes all in play,
ABI packed, EIP‑712 hashed with cheer,
Signatures neat, tests ringing clear,
A rabbit hops — the code is here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main objective of the PR: replacing the ethers runtime dependency with an internal ethereUtils module.
Description check ✅ Passed The description covers the three main template sections (Summary, Test, Risk/Compatibility) with relevant details about the refactoring scope, test coverage approach, and migration strategy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

package.json declares @noble/curves ^1.2.0 and @noble/hashes ^1.3.2 but
the lockfile still carried the prior ^1.9.1 / ^1.8.0 ranges, causing
`yarn install --frozen-lockfile` to fail in CI. Resolved versions stay
at 1.9.7 / 1.8.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ethereUtils.ts`:
- Around line 679-723: The decode routines (decodeValue and decodeTupleAt)
currently slice the input buffer without validating lengths or offsets, which
causes silent truncation; update decodeValue and decodeTupleAt to validate
bounds before every slice/offset computation: ensure base and head offsets are
within data.length, verify WordSize reads (e.g., for offsets/lengths read with
toNumber/toBigInt) are available, check that offsets are non-negative and that
base+off and base+WordSize+len (for dynamic bytes/string/array) do not exceed
data.length, and for fixed-size reads (uint/address/bool/bytesN) ensure the full
required bytes exist (address needs base+WordSize and base+12..base+WordSize
range). On any violation throw a descriptive Error (include the type and
offending offset/required length). Use existing helpers isDynamic and staticSize
to compute expected static lengths when validating tuples and arrays.
- Around line 439-443: The forEach callback in solidityPacked currently uses a
concise arrow that returns the numeric result of tight.push(...), which triggers
the lint error; change the callback to a void-returning block or replace the
forEach with an explicit loop so it does not return a value—for example, convert
types.forEach((type, index) => tight.push(_pack(type, values[index]))) to a
block-style callback that calls tight.push(_pack(type, values[index])); or use a
for loop iterating over types and calling tight.push(_pack(...)); reference
solidityPacked, the tight array, and the _pack call when making the change.
- Around line 733-736: The function export decodeAbiParameters currently uses an
explicit any[] which violates the noExplicitAny rule; change its return type to
a non-any type (e.g., unknown[] or Array<unknown>) in the signature and remove
the "// eslint-disable-next-line `@typescript-eslint/no-explicit-any`" comment,
keeping the implementation calling parseType, getBytes, and decodeTupleAt as-is
(referencing decodeAbiParameters, types, data, parseType, getBytes,
decodeTupleAt to locate the code).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8eca8ffc-01d2-4894-a09a-d71fbeda3a36

📥 Commits

Reviewing files that changed from the base of the PR and between 9aba193 and 93301ed.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (21)
  • package.json
  • src/account/Calibur/Calibur7702Account.ts
  • src/account/Safe/MerkleTree.ts
  • src/account/Safe/SafeAccount.ts
  • src/account/Safe/SafeMultiChainSigAccount.ts
  • src/account/Safe/adapters.ts
  • src/account/Safe/modules/SafeModule.ts
  • src/account/Safe/multisend.ts
  • src/account/Safe/safeMessage.ts
  • src/account/simple/Simple7702Account.ts
  • src/ethereUtils.ts
  • src/paymaster/CandidePaymaster.ts
  • src/paymaster/WorldIdPermissionlessPaymaster.ts
  • src/signer/adapters.ts
  • src/signer/types.ts
  • src/transport/JsonRpcNode.ts
  • src/utils.ts
  • src/utils7702.ts
  • src/utilsTenderly.ts
  • test/_loadEthereUtils.cjs
  • test/ethereUtils.test.js

Comment thread src/ethereUtils.ts
Comment thread src/ethereUtils.ts
Comment thread src/ethereUtils.ts Outdated
@Sednaoui
Copy link
Copy Markdown
Member

@codex

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93301ed9e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ethereUtils.ts
Comment on lines +682 to +684
const raw = toBigInt(data.slice(base, base + WordSize));
const masked = mask(raw, t.bits);
return t.signed ? fromTwos(masked, t.bits) : masked;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject truncated ABI words when decoding integers

The integer decode path accepts short buffers and silently zero-fills them (data.slice(base, base + 32) can be empty), so decodeAbiParameters(["uint256"], "0x") returns 0n instead of throwing like ethers does. This changes failure behavior for eth_call decoding (e.g., empty return data from undeployed/reverting targets) into seemingly valid numeric values, which can mask RPC/contract errors and produce incorrect state decisions downstream.

Useful? React with 👍 / 👎.

Comment thread src/ethereUtils.ts Outdated
return t.signed ? fromTwos(masked, t.bits) : masked;
}
case "address": return getAddress(hexlify(data.slice(base + 12, base + WordSize)));
case "bool": return data[base + 31] !== 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Throw on out-of-bounds bool decode

This bool decoder reads data[base + 31] without bounds checks; when the buffer is shorter than 32 bytes, that expression is undefined, and undefined !== 0 evaluates to true. As a result, malformed/empty ABI data (e.g., "0x") is decoded as true, which can cause false positives in call-result checks such as module/flag queries instead of surfacing a decode error.

Useful? React with 👍 / 👎.

sherifahmed990 and others added 3 commits May 26, 2026 16:50
Add `ensureRange` guard around all ABI decode reads to surface
out-of-bounds reads with descriptive errors, and replace the `any[]`
return on `decodeAbiParameters` with `unknown[]`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ethereUtils.ts`:
- Around line 715-719: After reading len in the "array" case, validate that len
is safe before allocating Array<AbiType>(len): ensure len is non-negative and
that the buffer has at least len * WordSize bytes remaining for the array head
(i.e. check (data.length - (base + WordSize)) >= len * WordSize), throw a
deterministic ABI decode error if not, then allocate the element-type array and
call decodeTupleAt; reference the "array" case, the variables len, base,
WordSize, ensureRange, t.child and the call to decodeTupleAt to locate where to
insert this check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 24b873ff-0e2e-49e0-b981-89377a7133c1

📥 Commits

Reviewing files that changed from the base of the PR and between 93301ed and 3d838d0.

📒 Files selected for processing (1)
  • src/ethereUtils.ts

Comment thread src/ethereUtils.ts
Comment on lines +715 to +719
case "array": {
if (t.size === -1) {
ensureRange(data, base, WordSize, "array length");
const len = toNumber(data.slice(base, base + WordSize));
return decodeTupleAt(Array<AbiType>(len).fill(t.child), data, base + WordSize);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate dynamic array length before allocating the element type list.

A malformed payload can advertise a huge len and reach Array<AbiType>(len) before we prove the buffer can contain even the minimum array head. That bypasses the new bounds-hardening path and turns an invalid decode into an oversized allocation/failure path instead of a deterministic ABI decode error.

Suggested fix
         case "array": {
             if (t.size === -1) {
                 ensureRange(data, base, WordSize, "array length");
                 const len = toNumber(data.slice(base, base + WordSize));
+                const minHeadSize = isDynamic(t.child) ? WordSize : staticSize(t.child);
+                if (len > Math.floor((data.length - (base + WordSize)) / minHeadSize)) {
+                    throw new Error(
+                        `ABI decode: array length out-of-bounds ` +
+                        `(length=${len}, base=${base}, data.length=${data.length})`,
+                    );
+                }
                 return decodeTupleAt(Array<AbiType>(len).fill(t.child), data, base + WordSize);
             }
             return decodeTupleAt(Array<AbiType>(t.size).fill(t.child), data, base);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ethereUtils.ts` around lines 715 - 719, After reading len in the "array"
case, validate that len is safe before allocating Array<AbiType>(len): ensure
len is non-negative and that the buffer has at least len * WordSize bytes
remaining for the array head (i.e. check (data.length - (base + WordSize)) >=
len * WordSize), throw a deterministic ABI decode error if not, then allocate
the element-type array and call decodeTupleAt; reference the "array" case, the
variables len, base, WordSize, ensureRange, t.child and the call to
decodeTupleAt to locate where to insert this check.

The unknown[] return introduced in aad98f2 forced narrowing at ~30
call sites that relied on the prior any[] passthrough (matching ethers'
Result type). Restore any[] to keep call sites working without
per-site casts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

I think any[] works as a short-term unblocker, but I’d avoid keeping it as the final API because it removes type checking from every decoded value and triggers the existing noExplicitAny lint rule.

A simpler middle ground than the full ABI-string inference is to make the decoder generic and let each call site state the tuple it expects:

export function decodeAbiParameters<T extends readonly unknown[] = unknown[]>(
    types: ReadonlyArray<string>,
    data: BytesLike,
): T {
    return decodeTupleAt(types.map(parseType), getBytes(data), 0) as unknown as T;
}

Then call sites can be explicit without a complex type-level ABI parser:

const decoded = decodeAbiParameters<[bigint]>(["uint256"], result);
const decoded = decodeAbiParameters<[string[], string]>(["address[]", "address"], result);
const decoded = decodeAbiParameters<[[bigint, string]]>(["(uint8,bytes)"], result);

I tested this locally and it passes:

  • yarn test:types
  • yarn test (391 passing)
  • yarn build

This keeps the unsafe assumption localized to each decode site, avoids any[], and is much easier to understand than parsing ABI strings in TypeScript types.

On the IIFE issue: I’m okay with fixing that in a follow-up PR if you prefer, since the tsdown config is easy to get subtly wrong. The important part is that this PR should not be released/published before the follow-up lands, because the current IIFE build still expects _noble_hashes_sha3 and _noble_curves_secp256k1 globals for browser/CDN consumers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants