Skip to content

feat: add safeAccountService for Safe smart-account detection - #94

Merged
Seranged merged 9 commits into
mainfrom
feat/safe-account-service
Aug 11, 2026
Merged

Seranged merged 9 commits into
mainfrom
feat/safe-account-service

Conversation

@kasperpawlowski

@kasperpawlowski kasperpawlowski commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds safeAccountService — on-chain detection of Safe (ex Gnosis Safe) smart accounts with signer configuration reads. First consumer is the euler-lite governance-address badge (euler-xyz/euler-lite#796); it also serves connected-wallet Safe checks in app transaction flows.

API

const info = await sdk.safeAccountService.fetchSafeAccount({ chainId, account })
// SafeAccountInfo | null:
// { address, singleton, version, threshold, owners }

getSafeSingletonVersion(address) and safeAccountAbi are exported for callers that need the pieces directly.

Implementation

  • Probe fires masterCopy() / getThreshold() / getOwners() concurrently; the provider's multicall batching coalesces them into a single RPC request. masterCopy() is special-cased in every Safe proxy's fallback since v1.1.1, so EOAs and non-Safe contracts read as null rather than erroring
  • The singleton is validated against the canonical deployment list vendored from safe-deployments (v1.1.1–v1.5.0, incl. eip155 variants); deterministic deployments mean one list covers every chain, no Safe Transaction Service dependency
  • Safe threshold/owner invariants are enforced so lookalike contracts are rejected
  • Results cached per ${chainId}:${account} (5-min default TTL, configurable) with in-flight dedup; RPC failures are not cached so later calls retry
  • Wired through buildSDK/EulerSDK with servicesOverrides.safeAccountService support; docs page + services map + README index + skills updated

Test plan

  • test/safeAccountService.test.ts: canonical detection, unknown singleton, EOA/non-Safe, invariant lookalikes, per-key caching, concurrent probe dedup, TTL expiry, buildEulerSDK wiring + override
  • Full suite: 522 passing; release:check (clean + build + typecheck) green; biome clean
  • Probe a known production Safe on mainnet via a live provider

Summary by CodeRabbit

  • New Features

    • Added Safe Account detection to the SDK.
    • Retrieve singleton version, signer owners, and signing threshold.
    • Non-Safe, invalid, or unrecognized accounts return no account data.
    • Added configurable caching, concurrent request sharing, and custom service overrides.
    • Exposed the Safe Account Service through the main SDK interface.
  • Documentation

    • Added usage, API behavior, limitations, failure, and caching guidance.
  • Tests

    • Added coverage for detection, validation, caching, error handling, and SDK integration.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The SDK adds a SafeAccountService that detects Safe accounts, reads signer configuration, validates results, and caches probes. The service is wired into SDK construction, exported publicly, tested, documented, and included in the 1.4.0 release metadata.

Changes

Safe Account Service

Layer / File(s) Summary
Safe detection contracts and probing
packages/euler-v2-sdk/src/services/safeAccountService/..., packages/euler-v2-sdk/test/safeAccountService.test.ts
Defines Safe account types, singleton version mappings, ABI reads, validation, TTL caching, concurrent request sharing, failure handling, and detection tests.
SDK service construction and exposure
packages/euler-v2-sdk/src/index.ts, packages/euler-v2-sdk/src/sdk/..., packages/euler-v2-sdk/test/safeAccountService.test.ts
Adds Safe Account Service construction, override support, SDK exposure, public exports, and integration coverage.
Service documentation and release guidance
packages/euler-v2-sdk/README.md, packages/euler-v2-sdk/docs/..., skills/euler-sdk/...
Documents the service API, detection behavior, caching, SDK entry points, and 1.4.0 release metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant SafeAccountService
  participant ProviderService
  participant SafeContract
  Caller->>SafeAccountService: fetchSafeAccount(chainId, account)
  SafeAccountService->>ProviderService: probe Safe view functions
  ProviderService->>SafeContract: read masterCopy, getThreshold, getOwners
  SafeContract-->>ProviderService: return probe results
  ProviderService-->>SafeAccountService: return singleton, threshold, owners
  SafeAccountService-->>Caller: return SafeAccountInfo or null
Loading

Suggested reviewers: seranged, dglowinski

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding safeAccountService for Safe smart-account detection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/safe-account-service

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 `@packages/euler-v2-sdk/docs/safe-account-service.md`:
- Line 38: Update the safe-account probe caching described in the service
documentation so rejected masterCopy, getThreshold, and getOwners RPC reads are
not stored as null; distinguish transport failures from expected contract-call
reverts, cache only valid results or expected null outcomes, and ensure each
transient failure retries on the next call.
- Line 30: Update fetchSafeAccount so Safe detection validates proxy bytecode,
the implementation slot, nonzero unique owners, and threshold invariants before
accepting or authorizing the account; otherwise explicitly mark the result
heuristic and prevent authorization use. Also revise the Promise.allSettled
failure path so transient null probe results are not cached, or document the
resolved-null caching behavior in the relevant caching documentation.

In `@packages/euler-v2-sdk/src/sdk/sdk.ts`:
- Line 61: Update EulerSDKOptions.safeAccountService to be optional and, during
EulerSDK construction, default a missing value to new
SafeAccountService(options.providerService). Preserve explicitly supplied
safeAccountService instances and ensure sdk.safeAccountService is always
initialized for direct callers.

In `@packages/euler-v2-sdk/src/services/safeAccountService/safeAccountService.ts`:
- Around line 145-168: Update probeSafeAccount and fetchSafeAccount so transient
provider/RPC rejections from masterCopy, getThreshold, or getOwners are
propagated or otherwise excluded from the five-minute negative cache, while
preserving null for confirmed non-Safe contract failures. Add a test where the
first probe fails at the RPC layer and the second call retries the reads instead
of returning the cached null.
- Around line 163-183: Update the Safe metadata probe around the existing
singleton, threshold, and owners checks to fetch the account runtime bytecode
with getBytecode and validate it against the known SafeProxy runtime bytecode.
Return null when bytecode is missing or does not match a recognized SafeProxy
runtime, and only construct SafeAccountInfo after this validation succeeds.
🪄 Autofix

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: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 54deab59-1089-4633-9232-d4b727d66259

📥 Commits

Reviewing files that changed from the base of the PR and between 1a4d6f9 and b3fc678.

📒 Files selected for processing (13)
  • packages/euler-v2-sdk/README.md
  • packages/euler-v2-sdk/docs/safe-account-service.md
  • packages/euler-v2-sdk/docs/services.md
  • packages/euler-v2-sdk/src/index.ts
  • packages/euler-v2-sdk/src/sdk/buildSDK.ts
  • packages/euler-v2-sdk/src/sdk/sdk.ts
  • packages/euler-v2-sdk/src/services/safeAccountService/index.ts
  • packages/euler-v2-sdk/src/services/safeAccountService/safeAccountService.ts
  • packages/euler-v2-sdk/src/services/safeAccountService/safeAccountServiceTypes.ts
  • packages/euler-v2-sdk/test/safeAccountService.test.ts
  • skills/euler-sdk/AGENTS.md
  • skills/euler-sdk/SKILL.md
  • skills/euler-sdk/rules/sdk-architecture.md

Comment thread packages/euler-v2-sdk/docs/safe-account-service.md Outdated
Comment thread packages/euler-v2-sdk/docs/safe-account-service.md Outdated
Comment thread packages/euler-v2-sdk/src/sdk/sdk.ts Outdated
Transport-level RPC failures now reject fetchSafeAccount instead of being cached as null. Owner lists violating Safe's OwnerManager invariants (zero, sentinel, duplicate owners) are rejected. EulerSDKOptions.safeAccountService is optional with a providerService-backed default so direct EulerSDK construction stays source-compatible. Docs state the heuristic nature of detection explicitly.
@kasperpawlowski

Copy link
Copy Markdown
Collaborator Author

Review findings addressed in 5ebab4b:

  • Transport failures cached as null: probeSafeAccount now rethrows anything that is not a definitive contract-level failure (empty call data / revert), so fetchSafeAccount rejects on RPC problems and nothing is cached; added a retry test.
  • Owner invariants: zero, sentinel (0x…01), and duplicate owners are rejected, mirroring OwnerManager.
  • Required option breaking direct construction: EulerSDKOptions.safeAccountService is now optional and defaults to new SafeAccountService(options.providerService).
  • Proxy identity: took the document-as-heuristic route rather than bytecode validation — the intended consumers are display badges and flow selection, not authorization. The docs and the interface JSDoc now state explicitly that detection must not gate authorization decisions. Runtime bytecode-hash validation can be layered on later if an authorization-grade consumer appears.

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The earlier transport-cache, owner-invariant, direct-constructor, and heuristic-authorization findings are addressed cleanly on this head. The current SDK suite is green locally (524 tests, lint, build, and typecheck), and the mainnet KPK fixtures also resolve correctly: Safe v1.4.1 at 2/5, fee receiver 5/8, and the router governor matches the displayed Safe.

One API-semantics blocker remains: a non-Safe contract that returns malformed non-empty fallback data causes viem to reject with an ABI-decoding error, and the service currently rethrows it as if it were a transport outage. I reproduced this with viem's public AbiDecodingDataSizeTooSmallError; fetchSafeAccount rejects instead of returning null. That contradicts the public interface/docs for non-Safe contracts and diverges from the Lite implementation, which only rethrows errors classified as transport failures.

The fix can remain narrow: classify ABI decode failures from successful contract responses as definitive non-Safe outcomes (while preserving transport rethrows), and add the malformed-response regression case.

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One additional control-boundary wording point survived the earlier-head review and is still present on this exact head.

Comment thread packages/euler-v2-sdk/src/services/safeAccountService/safeAccountServiceTypes.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
packages/euler-v2-sdk/docs/safe-account-service.md (1)

30-30: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Qualify the one-RPC-request claim for custom providers.

ProviderService enables viem multicall batching, but IProviderService does not require it and buildEulerSDK() accepts provider overrides. Qualify the claims at safe-account-service.md:30 and safe-account-service.md:42 and the method comment, or issue the probe through provider.multicall explicitly.

🤖 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 `@packages/euler-v2-sdk/docs/safe-account-service.md` at line 30, Qualify the
one-RPC-request and multicall batching claims in safe-account-service.md around
the probe description and the referenced method comment to apply only when the
provider supports multicall batching. Also update the corresponding method
documentation to describe the behavior for custom provider overrides, without
asserting batching as a universal guarantee.
🤖 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.

Outside diff comments:
In `@packages/euler-v2-sdk/docs/safe-account-service.md`:
- Line 30: Qualify the one-RPC-request and multicall batching claims in
safe-account-service.md around the probe description and the referenced method
comment to apply only when the provider supports multicall batching. Also update
the corresponding method documentation to describe the behavior for custom
provider overrides, without asserting batching as a universal guarantee.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 712eb5b9-68c9-4f0d-9491-58ca0dfd811e

📥 Commits

Reviewing files that changed from the base of the PR and between b3fc678 and 212e720.

📒 Files selected for processing (5)
  • packages/euler-v2-sdk/docs/safe-account-service.md
  • packages/euler-v2-sdk/src/sdk/sdk.ts
  • packages/euler-v2-sdk/src/services/safeAccountService/safeAccountService.ts
  • packages/euler-v2-sdk/src/services/safeAccountService/safeAccountServiceTypes.ts
  • packages/euler-v2-sdk/test/safeAccountService.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/euler-v2-sdk/test/safeAccountService.test.ts
  • packages/euler-v2-sdk/src/services/safeAccountService/safeAccountService.ts

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed exact head 212e72093e6d9593d546fb904aeb1429a38871c3.

The current commit is formatting-only. It fixes the Biome delta, but the two previously identified semantics/control-boundary findings remain unchanged:

  1. Malformed non-Safe contract responses are still treated as transport outages. A non-Safe fallback returning malformed non-empty data produces viem AbiDecodingDataSizeTooSmallError (wrapped by ContractFunctionExecutionError). The classifier does not recognize it, so fetchSafeAccount() rejects rather than resolving null as the public interface and docs promise. An exact-head runtime probe rejected twice and performed six reads; a definitive negative should resolve null and be cached after one three-read probe. Classify ABI response-decoding failures as definitive contract negatives while preserving genuine transport rethrows, and add the malformed-response regression test.

  2. The exported threshold wording still overstates the control boundary. SafeAccountInfo.threshold and the service docs describe the number of signatures required to execute a transaction. Enabled Safe modules can call execTransactionFromModule without owner confirmations, and this heuristic does not inspect modules or guards. Describe this as the owner-signature threshold for owner-authorized Safe transactions / configured owner threshold.

Validation:

  • focused safeAccountService.test.ts — 11/11 passed
  • full SDK suite — 524/524 passed, no type errors
  • release:check / build / typecheck — passed
  • changed Safe service files — formatted cleanly
  • package lint still reports only pre-existing warnings outside this PR's files
  • git diff --check — clean

The implementation is otherwise well bounded: transient transport failures are not cached, in-flight probes coalesce, owner invariants are enforced, direct constructor compatibility is preserved, and heuristic-only authorization guidance is explicit.

@LeonardEulerXYZ

Copy link
Copy Markdown
Contributor

Supplement to the exact-head review

The independent fresh-challenge pass found one additional blocker on 212e72093e6d9593d546fb904aeb1429a38871c3:

Reject Safe self-ownership when applying OwnerManager invariants.

safeAccountService.ts:210-229 says these checks mirror Safe OwnerManager, but the implementation omits owner != address(this). Safe v1.4.1 enforces that condition in both setupOwners and addOwnerWithThreshold (GS203). A recognized-singleton lookalike returning its own account address as its sole owner currently passes and is returned as a Safe.

Please compare normalized owners against the probed account, return null on self-ownership, and add the adversarial regression test alongside zero/sentinel/duplicate-owner coverage.

Non-blocking documentation point from the same pass: the "one RPC request" claim is guaranteed only when the injected provider supports/configures batching; IProviderService overrides do not require that property.

Malformed non-empty fallback data (viem ABI-decoding errors) now classifies as a definitive non-Safe negative instead of a transport rethrow. Self-ownership is rejected per OwnerManager GS203. Threshold docs describe the owner-signature threshold for owner-authorized transactions only, and the single-RPC-request claim is scoped to batching providers.
@kasperpawlowski

Copy link
Copy Markdown
Collaborator Author

All second-round findings addressed in aba22c0:

  • Malformed fallback data → definitive negative: AbiDecodingDataSizeTooSmallError / AbiDecodingDataSizeInvalidError (and their message shapes) are now classified as contract-level failures, so fetchSafeAccount resolves null and caches it after one three-read probe. Regression test constructs the real viem error wrapped in ContractFunctionExecutionError and asserts null + caching (3 reads total across two calls).
  • Self-ownership (GS203): normalized owners are compared against the probed account; self-owned lookalikes return null. Covered in the adversarial owner-list test alongside zero/sentinel/duplicate.
  • Threshold wording: SafeAccountInfo.threshold JSDoc and the docs now describe the configured owner-signature threshold for owner-authorized transactions, noting modules execute via execTransactionFromModule without owner confirmations and that the probe does not inspect modules/guards.
  • Non-blocking batching claim: scoped to providers built by the SDK's ProviderService; custom IProviderService implementations without batching issue three reads.

525 tests + typecheck + biome check clean. The same self-ownership fix landed in Lite's local probe (euler-xyz/euler-lite@6adf4f99).

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed exact head aba22c0f503e4b7441ac516e7587ea96d4479db9.

The self-ownership check, owner-threshold wording, and custom-provider batching qualification are fixed correctly. The new malformed-scalar regression also proves the original 0x01 / AbiDecodingDataSizeTooSmallError case.

One narrowed blocker remains: the implementation and updated docs now classify malformed non-empty ABI responses generally as definitive non-Safe results, but the classifier covers only two decoder error names/message shapes. Real malformed dynamic getOwners() output commonly raises other viem decoder errors. I decoded a truncated address[] result using viem 2.48.8; it raised PositionOutOfBoundsError, wrapped it in ContractFunctionExecutionError as readContract does, and passed that through SafeAccountService. fetchSafeAccount() rejected instead of resolving/caching null. A malicious huge offset similarly raises IntegerOutOfRangeError.

Please classify the contract-response decoding family rather than only the scalar-size subtypes, and add at least one dynamic-array regression. Genuine HTTP/RPC transport failures must continue to reject and remain uncached.

Validation:

  • committed Safe service tests: 12/12 passed
  • full SDK suite: 525/525 passed, no type errors
  • package lint: passed with pre-existing informational warnings outside this PR
  • build, typecheck, and release check: passed
  • git diff --check: clean
  • adversarial truncated-getOwners() regression: failed as described; temporary test removed and worktree clean

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed exact head aba22c0f503e4b7441ac516e7587ea96d4479db9.

The self-owner invariant, threshold/module wording, batching-provider wording, and scalar malformed-data regression are fixed correctly. One ABI-decoding boundary remains, so I am keeping this blocked.

Blocker — dynamic-result decoder errors still escape as transport failures.

getOwners() returns a dynamic array. Malformed non-empty returndata can therefore make viem throw decoder errors outside the two newly allowlisted names. I reproduced both through the real service and real viem error wrappers:

truncated array -> PositionOutOfBoundsError
2 calls -> 2 rejected ContractFunctionExecutionError results, 6 reads

huge offset -> IntegerOutOfRangeError
2 calls -> 2 rejected ContractFunctionExecutionError results, 6 reads

Both are definitive malformed contract responses, not RPC/transport failures. Under the documented contract they should return null and cache that negative after the first three reads. The new test only exercises scalar malformed data (AbiDecodingDataSizeTooSmallError), so it does not discriminate malformed dynamic-array results.

Please classify the relevant viem ABI-decoder family for these fixed no-argument reads and add at least one malformed dynamic getOwners() regression asserting null plus one-probe caching.

Validation:

  • focused Safe service tests: 12/12 passed
  • full SDK suite: 525/525 passed, no type errors
  • pnpm run release:check: passed
  • git diff --check origin/main...HEAD: clean
  • exact-head runtime challenge above: failed as described

@LeonardEulerXYZ

Copy link
Copy Markdown
Contributor

Supplement to the exact-head review

One non-blocking wording inconsistency remains on aba22c0f503e4b7441ac516e7587ea96d4479db9:

  • safeAccountService.ts's in-flight deduplication comment and docs/safe-account-service.md still say concurrent callers share one RPC round-trip.
  • With a custom non-batching IProviderService, they share one probe, but that probe performs three reads.

The batching-provider qualification is correct elsewhere. Replacing these two remaining instances with "one probe" (or provider-dependent wording) would make the public explanation consistent. This does not add another merge blocker beyond the dynamic-decoder failure in the formal review.

…gatives

Truncated getOwns arrays and out-of-range dynamic offsets raise viem cursor/encoding errors (PositionOutOfBoundsError, IntegerOutOfRangeError) that were falling through to the transport rethrow. The classifier now covers the AbiDecoding family by name pattern plus the cursor/encoding response-decoding classes; regression tests generate the real errors via decodeFunctionResult so they track viem's actual behavior.
@kasperpawlowski

Copy link
Copy Markdown
Collaborator Author

Dynamic ABI boundary finding fixed in c0fc64a:

  • The classifier now treats the whole AbiDecoding* family (matched by name pattern) plus viem's cursor/encoding response-decoding classes (PositionOutOfBoundsError, IntegerOutOfRangeError, NegativeOffsetError, SliceOffsetOutOfBoundsError, SizeOverflowError, …) as definitive contract-level negatives. These only arise post-response inside readContract, so they cannot mask genuine transport failures.
  • Regression test generates the real errors by running decodeFunctionResult against the two hostile payloads from your challenge (truncated array with claimed length 2, and a 2^200 dynamic offset), wraps them in ContractFunctionExecutionError the way readContract does, and asserts null is returned and cached after a single three-read probe.

526 tests + typecheck + biome check clean.

Concurrent callers share one in-flight three-read probe; only batching providers collapse the reads into a single RPC request.
@kasperpawlowski

Copy link
Copy Markdown
Collaborator Author

Non-blocking wording supplement addressed in fdcfc59: the two remaining "one RPC round-trip" claims (fetchSafeAccount JSDoc + docs caching section) now say concurrent callers share one in-flight three-read probe, consistent with the earlier batching-scoped wording.

@LeonardEulerXYZ LeonardEulerXYZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed exact head fdcfc59b35d7a3ab958b02748bf039dea9a00b36.

All published findings are resolved.

The dynamic ABI classifier now handles the real viem PositionOutOfBoundsError and IntegerOutOfRangeError paths, and the regression generates those errors through decodeFunctionResult rather than synthetic names. I independently reran both hostile getOwners() cases twice:

truncated dynamic array -> [null, null], 3 reads
huge dynamic offset    -> [null, null], 3 reads

That confirms definitive-negative classification and one-probe caching. Transport failures remain retryable, self-ownership is rejected, threshold wording is scoped to owner-authorized transactions/modules, and the remaining in-flight wording now accurately says one shared three-read probe.

Validation:

  • focused Safe service tests: 13/13 passed
  • full SDK suite: 526/526 passed, no type errors
  • release check/build/typecheck: passed
  • exact-head real-viem challenge: passed
  • git diff --check origin/main...HEAD: clean

No blockers.

@kasperpawlowski

Copy link
Copy Markdown
Collaborator Author

Docs completeness check against the #93 pattern (a4e7d08): the SDK-side docs were already in place (docs/safe-account-service.md, docs/services.md map + capability matrix context, README index, and one-liners in skills/euler-sdk SKILL/AGENTS/rules) — what was missing was the skill version convention #93 follows: a skills/euler-sdk/CHANGELOG.md entry and the metadata.json version/date bump. Added as skill 1.4.0 (new-capability guidance = minor, matching how 1.3.0 covered positionMigrationService).

Coordination note: #93's branch bumps the skill to 1.3.1. Whichever PR merges second should rebase its changelog/metadata on top of the other (if #93 lands first, this stays 1.4.0; if this lands first, #93's 1.3.1 becomes 1.4.1).

…-shape guidance

Safe v1.1.1/v1.2.0 permitted self-ownership (the GS203 restriction arrived
in v1.3.0); the probe deliberately applies the strict rule to every
allowlisted version, so a canonical legacy Safe listing itself as an owner
reads as null. Documented in the service docs, the invariant comment, and
a regression test with a legacy positive control.

The skill guidance claimed all fetch* methods return { result, errors };
fetchSafeAccount() returns SafeAccountInfo | null — exception added to
SKILL.md, AGENTS.md, and the sdk-architecture rule.
@Seranged
Seranged merged commit 5d80486 into main Aug 11, 2026
1 check passed
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.

3 participants