Skip to content

fix: AllowanceModule start-delay semantics and Safe module diagnostics - #217

Merged
Sednaoui merged 4 commits into
devfrom
fix/safe-modules
Jul 23, 2026
Merged

fix: AllowanceModule start-delay semantics and Safe module diagnostics#217
Sednaoui merged 4 commits into
devfrom
fix/safe-modules

Conversation

@sherifahmed990

@sherifahmed990 sherifahmed990 commented Jul 22, 2026

Copy link
Copy Markdown
Member

Fixes for the Safe AllowanceModule and SocialRecoveryModule helpers.

  • Delayed one-time allowances never worked: createOneTimeAllowanceMetaTransaction passed startAfterInMinutes as the contract's resetBaseMin with resetTimeMin = 0, which reverts on-chain for any nonzero value (setAllowance explicitly requires resetTimeMin > 0 whenever resetBaseMin is nonzero). The parameter is removed.
  • Recurring allowance "delay" was misleading and unsafe: the last parameter of createRecurringAllowanceMetaTransaction was documented as a relative activation delay, but on-chain resetBaseMin is an absolute epoch-minutes baseline that must be in the past and only aligns period boundaries — a relative value made funds spendable immediately. Renamed to inThePastPeriodStartBaseTimeStamp, now taking a unix timestamp in seconds (converted to the contract's epoch-minutes resetBaseMin internally, flooring sub-minute precision). The docs state explicitly that the allowance is active immediately — the value only anchors the period accounting, with a worked example. A deterministic uint32 range check on the converted minutes catches millisecond-timestamp mistakes; the contract's own require(resetBaseMin <= currentMin) enforces the past bound at execution time, so a meta-transaction can be prepared before its anchor passes.
  • Diagnostics: SocialRecoveryModule.getGuardians/.nonce reported empty-result errors under the name "threshold"; getRecoveryRequestEip712Data only accepted a URL string while every sibling accepts string | Transport | JsonRpcNode; AllowanceModule.getDelegates with maxNumberOfResults > 255 threw an opaque ABI out-of-bounds error (the module's page size is a uint8) and now fails with a clear RangeError pointing at pagination.

Note: removing/renaming the start-delay parameters is a breaking change to those two helper signatures.

Tests added in test/safe/allowanceModule.test.js; full offline suite passes.

Summary by CodeRabbit

  • New Features

    • Updated allowance meta-transaction creation: one-time allowances removed start-delay input and always use immediate reset values; recurring allowances now take an absolute baseline timestamp in seconds (defaulting to immediate) instead of a relative delay.
  • Bug Fixes

    • Improved Social Recovery JSON-RPC helpers to use correct operation names for empty-result/nonce handling.
    • Added stricter validation for recurring baseline inputs and delegate pagination limits, throwing RangeError for out-of-range values.
  • Tests

    • Added offline calldata decoding tests for allowance setAllowance, including baseline conversion edge cases and invalid-input rejection.

sherifahmed990 and others added 2 commits July 22, 2026 12:40
…isled on-chain

createOneTimeAllowanceMetaTransaction passed startAfterInMinutes as the
contract's resetBaseMin with resetTimeMin=0, which reverts on-chain for
any nonzero value (division by zero in the period alignment) — so a
delayed one-time allowance never worked. The parameter is removed.

createRecurringAllowanceMetaTransaction documented its last parameter as
a relative activation delay, but on-chain resetBaseMin is an absolute
epoch-minutes baseline that must be in the past and only aligns period
boundaries; a relative value made funds spendable immediately. The
parameter is renamed to periodStartBaseInMinutes with truthful docs and
a client-side guard against future baselines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…int8 page cap

- SocialRecoveryModule.getGuardians and .nonce reported empty-result
  errors under the name 'threshold', misdirecting debugging.
- getRecoveryRequestEip712Data only accepted a URL string while every
  sibling method accepts string | Transport | JsonRpcNode.
- AllowanceModule.getDelegates with maxNumberOfResults > 255 threw an
  opaque ABI out-of-bounds error (the module's page size is a uint8);
  it now fails with a clear RangeError pointing at pagination.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 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

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: 073fc733-c4e1-4080-89b8-e8bcc1068d27

📥 Commits

Reviewing files that changed from the base of the PR and between 7b658e5 and 43e45ed.

📒 Files selected for processing (2)
  • src/account/Safe/modules/AllowanceModule.ts
  • test/safe/allowanceModule.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/safe/allowanceModule.test.js
  • src/account/Safe/modules/AllowanceModule.ts

📝 Walkthrough

Walkthrough

Allowance meta-transactions now use absolute recurring-period baselines, fixed one-time reset fields, and bounded delegate pagination. Social recovery helpers correct operation labels and accept transport or JSON-RPC node instances.

Changes

Allowance updates

Layer / File(s) Summary
Allowance timing semantics
src/account/Safe/modules/AllowanceModule.ts, test/safe/allowanceModule.test.js
One-time allowances encode zero reset fields; recurring allowances convert validated second-based baselines to epoch minutes, with calldata and invalid-input tests.
Delegate pagination validation
src/account/Safe/modules/AllowanceModule.ts, test/safe/allowanceModule.test.js
getDelegates rejects maxNumberOfResults values outside the uint8 range, with asynchronous rejection tests.

Social recovery helpers

Layer / File(s) Summary
Recovery query contracts and errors
src/account/Safe/modules/SocialRecoveryModule.ts
Guardian and nonce queries use correct operation names, and recovery request data accepts string, Transport, or JsonRpcNode RPC nodes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

I’m a rabbit with allowances neat,
Zero resets make calldata sweet.
Recurring time is checked with care,
Big pages vanish in the air.
Recovery names now shine bright—
Hop, hop, helpers do things right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the allowance semantics and diagnostics changes.
Description check ✅ Passed The description covers summary, test status, and compatibility impact, though it does not use the template headings verbatim.
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.

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.

🧹 Nitpick comments (1)
test/safe/allowanceModule.test.js (1)

21-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the delegate page-size guard.

The new getDelegates uint8 limit has no regression test. It rejects before RPC access, so it can remain fully offline.

Suggested test
 describe('AllowanceModule setAllowance encoding', () => {
   const module = new ak.AllowanceModule();
 
+  test('delegate page size above uint8 range rejects before RPC', async () => {
+    await expect(
+      module.getDelegates('http://127.0.0.1:8545', DELEGATE, {
+        maxNumberOfResults: 256n,
+      }),
+    ).rejects.toThrow(RangeError);
+  });
+
🤖 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 `@test/safe/allowanceModule.test.js` around lines 21 - 52, Add an offline
regression test in the AllowanceModule test suite for getDelegates, verifying
that a page size above the uint8 maximum is rejected with RangeError before any
RPC access occurs. Use the existing module instance and delegate-related test
setup, and preserve coverage for valid page-size behavior if already available.
🤖 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.

Nitpick comments:
In `@test/safe/allowanceModule.test.js`:
- Around line 21-52: Add an offline regression test in the AllowanceModule test
suite for getDelegates, verifying that a page size above the uint8 maximum is
rejected with RangeError before any RPC access occurs. Use the existing module
instance and delegate-related test setup, and preserve coverage for valid
page-size behavior if already available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3540c5e-6987-4b9b-945a-cb2614478183

📥 Commits

Reviewing files that changed from the base of the PR and between a4f98c7 and 8a05ec0.

📒 Files selected for processing (3)
  • src/account/Safe/modules/AllowanceModule.ts
  • src/account/Safe/modules/SocialRecoveryModule.ts
  • test/safe/allowanceModule.test.js

startAfterInMinutes: bigint,
periodStartBaseInMinutes: bigint = 0n,
): MetaTransaction {
const nowInMinutes = BigInt(Math.floor(Date.now() / 60_000));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The contract validates resetBaseMin against block.timestamp when the Safe transaction executes, while this guard uses the caller’s local clock when calldata is created. This can reject a valid queued transaction whose baseline will be in the past by execution time, or accept one that reverts if the local clock is ahead of the chain. Since this is a synchronous calldata builder, could we remove the Date.now() check and document that the baseline must be no later than the chain timestamp at execution? If immediate executability needs validation, that could be a separate async preflight using the latest block timestamp.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done

* @param periodStartBaseInMinutes - Optional absolute baseline that period
* boundaries are aligned to, in minutes since the unix epoch (not a relative
* delay — the module has no delayed-activation concept and requires this
* baseline to be in the past). Defaults to 0n, aligning periods to the epoch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

0n does not align periods to the epoch. The contract initializes a new allowance to the execution block’s current minute, while an existing allowance keeps its previous lastResetMin. Could we update this documentation accordingly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done

@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)
src/account/Safe/modules/AllowanceModule.ts (1)

393-400: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the lower bound for maxNumberOfResults.

uint8 accepts only 0 through 255, but negative values bypass this guard and reach baseGetDelegates. Validate both bounds so invalid page sizes fail consistently with a clear RangeError.

Proposed fix
-			if (overrides.maxNumberOfResults > 255n) {
+			if (overrides.maxNumberOfResults < 0n || overrides.maxNumberOfResults > 255n) {
🤖 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/account/Safe/modules/AllowanceModule.ts` around lines 393 - 400, Update
the maxNumberOfResults validation in the AllowanceModule delegate-fetch flow to
reject values below 0 as well as values above 255, matching the uint8 range.
Ensure invalid values throw a clear RangeError before reaching baseGetDelegates,
while preserving the existing behavior for valid values and omitted options.
🤖 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 `@src/account/Safe/modules/AllowanceModule.ts`:
- Around line 393-400: Update the maxNumberOfResults validation in the
AllowanceModule delegate-fetch flow to reject values below 0 as well as values
above 255, matching the uint8 range. Ensure invalid values throw a clear
RangeError before reaching baseGetDelegates, while preserving the existing
behavior for valid values and omitted options.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 369a6f52-953a-438e-a9e1-51ab620b1cbc

📥 Commits

Reviewing files that changed from the base of the PR and between 8a05ec0 and f6962c2.

📒 Files selected for processing (2)
  • src/account/Safe/modules/AllowanceModule.ts
  • test/safe/allowanceModule.test.js

…ll-clock guard

- periodStartBaseInMinutes -> inThePastPeriodStartBaseTimeStamp: the
  parameter now takes a unix timestamp in seconds — the natural input
  for callers — and is converted to the contract's epoch-minutes
  resetBaseMin internally (flooring sub-minute precision). The name
  carries the contract's must-be-in-the-past constraint, and the docs
  state explicitly that the allowance is active immediately: the value
  only anchors the period accounting (with a worked example).
- the Date.now() guard is removed: it duplicated the contract's own
  require(resetBaseMin <= currentMin) with a client clock, and blocked
  legitimately preparing a meta-transaction whose anchor is future at
  creation but past by execution. A deterministic uint32 range check on
  the converted minutes replaces it and still catches
  millisecond-timestamp mistakes.
- JSDoc corrections against the deployed contract source: the 0n
  default anchors the period accounting to execution time for a fresh
  allowance (not the epoch), and the one-time + baseline revert comes
  from the explicit resetTimeMin > 0 require, not a division by zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The uint8 guard only checked the upper bound; a negative bigint passed
through to the ABI encoder and failed with an opaque error instead of
the descriptive RangeError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Sednaoui
Sednaoui merged commit 2c57d49 into dev Jul 23, 2026
3 checks passed
@Sednaoui
Sednaoui deleted the fix/safe-modules branch July 23, 2026 12:36
@Sednaoui Sednaoui mentioned this pull request Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants