Skip to content

feat: support args.receiverAddress override + allow zero ERC-20 approvals#19

Merged
ajag408 merged 5 commits intomainfrom
feat/eng-1855-approved-receiver-zero-approval
Apr 1, 2026
Merged

feat: support args.receiverAddress override + allow zero ERC-20 approvals#19
ajag408 merged 5 commits intomainfrom
feat/eng-1855-approved-receiver-zero-approval

Conversation

@ajag408
Copy link
Copy Markdown
Contributor

@ajag408 ajag408 commented Mar 30, 2026

Summary by CodeRabbit

  • New Features

    • Optional receiver address support for supply and withdraw operations.
  • Bug Fixes

    • Receiver address validation enforces that a provided address matches transaction calldata.
    • Approval validation relaxed to accept zero-reset approval patterns.
  • Tests

    • Added coverage for receiver address override, default behaviors, and approval flows.
  • Chores

    • Package version bumped to 1.2.5; test tooling dependency updated.

Fixes two false positive patterns surfaced during the production MONITOR soak:

  1. Receiver address mismatch — Clients like DeBlock pass receiverAddress in the API args to deposit/withdraw on behalf of users (e.g., a funded USDC wallet deposits into a vault, shares go to the user's wallet). Shield only knew the userAddress and flagged the different receiver as suspicious. Shield is fully decoupled from the API runtime and couldn't distinguish a legitimate receiverAddress from a malicious fund redirect.

  2. USDT zero approval blocked — Ethereum USDT requires approve(0) before approve(amount) due to its non-standard ERC-20 implementation. Shield rejected zero-amount approvals as invalid.

Changes

1. args.receiverAddress — intent-based receiver validation

Added receiverAddress?: string to ActionArguments, allowing callers to declare the intended receiver address. The ERC-4626 validator now uses args.receiverAddress ?? userAddress as the expected receiver in both validateSupply() and validateWithdraw().

Behavior:

Scenario Expected receiver Outcome
receiverAddress not provided, calldata receiver == userAddress userAddress Safe (unchanged)
receiverAddress not provided, calldata receiver != userAddress userAddress Blocked (unchanged)
receiverAddress provided, calldata receiver matches it receiverAddress Safe (new)
receiverAddress provided, calldata receiver does NOT match it receiverAddress Blocked — calldata tampered against declared intent (new)

This mirrors how the monorepo's StakeArguments.receiverAddress works — the same field name, same semantics. The monorepo passes the user's declared receiver, and Shield verifies the transaction calldata matches that intent. Standalone Shield consumers who don't pass args.receiverAddress get identical behavior to before.

Note: The owner check in validateWithdraw() is unchanged — the share owner must always match userAddress. Only the asset receiver gets the override.

2. Allow zero-amount ERC-20 approvals

Removed the zero-amount approval block in validateApproval(). A zero approval (approve(spender, 0)) is a revocation — it reduces allowance to zero, making it a strictly defensive operation. The remaining checks (correct approve selector, spender is a whitelisted vault, tx.to matches vault input token) are sufficient.

3. Dev dependency fix (CI)

Updated ts-jest to resolve a critical handlebars vulnerability (GHSA-2w6w-674q-4c4q) in the transitive dependency chain (ts-jest > handlebars). Dev-only — does not affect the published package. Was causing pnpm audit to fail in CI.

Affected soak yields (now resolved)

Yield Type Issue
base-usdc-steakusdc-0xbeef0e...-4626-vault (Morpho/Steakhouse) SUPPLY Receiver mismatch — DeBlock deposits on behalf of user
base-usdc-smusdc-0x616a4e...-4626-vault (Seamless) WITHDRAW Receiver mismatch — withdrawal to different address
ethereum-usdt-gtusdtp-0xf3557ad...-4626-vault (Morpho/Gauntlet) APPROVAL USDT approve(0) before approve(amount)

Files changed

File Change
src/types/index.ts Add receiverAddress?: string to ActionArguments
src/validators/evm/erc4626/erc4626.validator.ts Extract args.receiverAddress; thread to validateSupply() + validateWithdraw(); update receiver checks to use receiverAddress ?? userAddress; remove zero-approval block in validateApproval()
src/validators/evm/erc4626/erc4626.validator.test.ts 6 new receiverAddress tests (SUPPLY: match, mismatch, default with/without override; WITHDRAW: match, mismatch); update zero-approval test to expect isValid: true
src/json/schema.ts Add receiverAddress to args schema (required due to additionalProperties: false)
package.json Version bump
pnpm-lock.yaml Updated ts-jest to resolve critical handlebars vulnerability (CI fix)

Test plan

  • All existing tests pass (with updated zero-approval expectation)
  • New receiverAddress override tests pass:
    • SUPPLY with args.receiverAddress matching calldata receiver → Safe
    • SUPPLY with args.receiverAddress NOT matching calldata receiver → Blocked
    • SUPPLY without args.receiverAddress, receiver != user → Blocked (default behavior preserved)
    • SUPPLY without args.receiverAddress, receiver == user → Safe (default behavior preserved)
    • WITHDRAW with args.receiverAddress matching calldata receiver → Safe
    • WITHDRAW with args.receiverAddress NOT matching calldata receiver → Blocked
  • Zero approval with whitelisted vault spender → Safe (was: Blocked)
  • Full test suite green: pnpm test
  • Lint clean: pnpm lint

Backward compatibility

Fully backward-compatible. args.receiverAddress is optional — consumers who don't provide it get identical validation behavior to the previous version. The zero-approval change is a relaxation (previously blocked, now allowed), which cannot break existing valid flows.

Downstream dependency

The monorepo (PR 4) will bump @yieldxyz/shield to this version and wire args.receiverAddress through to Shield at the call sites in shield-validation.service.ts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

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: c30e9faa-478f-43f3-896a-6b850c1f1787

📥 Commits

Reviewing files that changed from the base of the PR and between 33fcf87 and 58ef47e.

📒 Files selected for processing (1)
  • src/validators/evm/erc4626/erc4626.validator.ts

📝 Walkthrough

Walkthrough

Adds an optional receiverAddress to request schema and action types; ERC4626 validator reads and forwards receiverAddress for SUPPLY/WITHDRAW receiver checks (resolves to receiverAddress ?? userAddress), simplifies approval parsing (ignores amount), and test coverage updated for override and default behaviors.

Changes

Cohort / File(s) Summary
Package & Schema
package.json, src/json/schema.ts
Bumped package version to 1.2.5; added receiverAddress to request schema (type: 'string', minLength: 1, maxLength: 128).
Types
src/types/index.ts
Added optional receiverAddress?: string to ActionArguments.
Validator Implementation
src/validators/evm/erc4626/erc4626.validator.ts
validate(args?: ActionArguments) now reads receiverAddress and passes it into validateSupply/validateWithdraw; supply/withdraw receiver checks compare calldata receiver to receiverAddress ?? userAddress; approval parsing now extracts only spender and no longer blocks zero-amount approvals.
Tests
src/validators/evm/erc4626/erc4626.validator.test.ts
Updated approval test to accept zero-reset behavior; added RECEIVER_ADDRESS constant and tests covering receiver override (match/mismatch) and default behavior for SUPPLY and WITHDRAW.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant Validator
participant Calldata
participant UserStore
Client->>Validator: request(action, args{receiverAddress?})
Validator->>UserStore: fetch userAddress
Validator->>Calldata: parse calldata (type, receiver, spender, ...)
alt approval flow
Validator->>Validator: parse spender only (ignore amount)
Validator-->>Client: validation result
else supply/withdraw flow
Validator->>Validator: expected = args.receiverAddress ?? userAddress
Validator->>Calldata: compare calldata.receiver to expected
alt match
Validator-->>Client: allow
else mismatch
Validator-->>Client: block (reason includes expected address)
end
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through schema, sniffed a name,
A receiver sprung to join the game.
Supply and withdraw now check who's there,
Approvals loosened with gentle care.
Tests applaud the hop, precise and tame.

🚥 Pre-merge checks | ✅ 3
✅ 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 accurately summarizes both main changes: support for receiverAddress override and allowance of zero ERC-20 approvals, matching the core modifications across schema, types, and validator logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ 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/eng-1855-approved-receiver-zero-approval

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

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

🧹 Nitpick comments (1)
src/validators/evm/erc4626/erc4626.validator.test.ts (1)

664-760: Add one JSON-entrypoint test for receiverAddress.

These cases validate ERC4626Validator directly, but the public API path still depends on src/json/schema.ts and src/json/handler.ts accepting and forwarding args.receiverAddress. One handler-level test here would catch schema/forwarding regressions that these unit tests cannot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/validators/evm/erc4626/erc4626.validator.test.ts` around lines 664 - 760,
Add a JSON-entrypoint integration test that constructs a JSON request which
includes args.receiverAddress and sends it through the public JSON path (the
schema/handler code) so schema.ts/handler.ts will parse and forward args to the
ERC4626Validator; specifically, add a test that builds the same calldata/data
and tx object used in these unit tests, invokes the JSON handler (via the public
export in src/json/handler.ts, e.g., handler or handleRequest) after
validating/parsing with src/json/schema.ts, and assert the response mirrors
validator.validate(tx, TransactionType.SUPPLY|WITHDRAW, USER_ADDRESS, {
receiverAddress }) (isValid true/false and reason when blocked) to catch any
schema/forwarding regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/validators/evm/erc4626/erc4626.validator.ts`:
- Around line 128-139: Summary: Blank-string receiverAddress values are
currently treated as valid overrides because of the "??" fallback, causing
receiver mismatches; explicitly reject empty-string overrides. Update the
routing/validation: in the switch block keep passing receiverAddress, but inside
validateSupply and validateWithdraw (and any other places that accept
receiverAddress) add an explicit check that if receiverAddress === '' then fail
validation (return an error/throw/ValidationResult) with a clear message about
blank receiver overrides being forbidden; otherwise treat undefined/null as "use
userAddress" (i.e., only null/undefined fall back, not empty string). Ensure the
error path uses the same validation/error type returned by
validateSupply/validateWithdraw so callers (the switch) handle it consistently.

---

Nitpick comments:
In `@src/validators/evm/erc4626/erc4626.validator.test.ts`:
- Around line 664-760: Add a JSON-entrypoint integration test that constructs a
JSON request which includes args.receiverAddress and sends it through the public
JSON path (the schema/handler code) so schema.ts/handler.ts will parse and
forward args to the ERC4626Validator; specifically, add a test that builds the
same calldata/data and tx object used in these unit tests, invokes the JSON
handler (via the public export in src/json/handler.ts, e.g., handler or
handleRequest) after validating/parsing with src/json/schema.ts, and assert the
response mirrors validator.validate(tx, TransactionType.SUPPLY|WITHDRAW,
USER_ADDRESS, { receiverAddress }) (isValid true/false and reason when blocked)
to catch any schema/forwarding regressions.
🪄 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: f3b1039a-6855-4fa2-8507-42db4aa4b437

📥 Commits

Reviewing files that changed from the base of the PR and between e69634a and c1f9035.

📒 Files selected for processing (5)
  • package.json
  • src/json/schema.ts
  • src/types/index.ts
  • src/validators/evm/erc4626/erc4626.validator.test.ts
  • src/validators/evm/erc4626/erc4626.validator.ts

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.

♻️ Duplicate comments (1)
src/validators/evm/erc4626/erc4626.validator.ts (1)

128-128: ⚠️ Potential issue | 🟡 Minor

Handle receiverAddress explicitly on Line [128] instead of boolean coercion.

args?.receiverAddress || undefined silently treats '' as absent and keeps a strict-null warning alive. Explicitly reject blank overrides (or normalize with explicit branches) so malformed intent isn’t silently defaulted.

🔧 Proposed fix
-    const receiverAddress = args?.receiverAddress || undefined;
+    const receiverAddress = args?.receiverAddress;
+    if (receiverAddress !== undefined && receiverAddress.trim() === '') {
+      return this.blocked('receiverAddress must be a non-empty string');
+    }
#!/bin/bash
set -euo pipefail

echo "== Verify receiverAddress constraints in schema/types/tests =="
fd -i "schema.ts|index.ts|erc4626.validator.test.ts" src | while read -r f; do
  echo "----- $f -----"
  rg -n -C2 "receiverAddress|minLength|pattern|format|empty|blank|ActionArguments" "$f" || true
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/validators/evm/erc4626/erc4626.validator.ts` at line 128, The assignment
const receiverAddress = args?.receiverAddress || undefined silently treats empty
string as absent; change it to explicitly handle blank values by checking
args?.receiverAddress for null/undefined and empty string: if
args?.receiverAddress === '' either normalize to undefined only after explicit
decision or throw a validation error, otherwise set receiverAddress =
args.receiverAddress; update the validation path that uses receiverAddress (the
variable and any consuming function) so blank strings are rejected or normalized
consistently rather than being boolean-coerced away.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/validators/evm/erc4626/erc4626.validator.ts`:
- Line 128: The assignment const receiverAddress = args?.receiverAddress ||
undefined silently treats empty string as absent; change it to explicitly handle
blank values by checking args?.receiverAddress for null/undefined and empty
string: if args?.receiverAddress === '' either normalize to undefined only after
explicit decision or throw a validation error, otherwise set receiverAddress =
args.receiverAddress; update the validation path that uses receiverAddress (the
variable and any consuming function) so blank strings are rejected or normalized
consistently rather than being boolean-coerced away.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f0e0ca8-d953-43f9-9156-77ffa4ca03f6

📥 Commits

Reviewing files that changed from the base of the PR and between 7b6fd71 and 83a1cad.

📒 Files selected for processing (2)
  • src/json/schema.ts
  • src/validators/evm/erc4626/erc4626.validator.ts
✅ Files skipped from review due to trivial changes (1)
  • src/json/schema.ts

@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 30, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​ts-jest@​29.4.4 ⏵ 29.4.697 +110094 +188100

View full report

raiseerco
raiseerco previously approved these changes Mar 31, 2026
Copy link
Copy Markdown

@raiseerco raiseerco left a comment

Choose a reason for hiding this comment

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

just a nit, all other lgtm

}

const receiverAddress = args?.receiverAddress || undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

agree with the bot, receiverAddress looks wired correctly e2e, one small follow-up might be adding a runtime type check here as well, since the schema only covers the JSON path and a truthy no string value could still reach .toLowerCase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, now using isNonEmptyString(args?.receiverAddress) to guard the extraction, which matches the pattern used by the Tron and Solana validators for their args.validatorAddress access. An empty string or non-string value now normalizes to undefined, falling back to userAddress as expected.

Esp since this is a security library good to have clear errors instead of unhandled TypeErrors, thanks for flagging!

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 the current code and only fix it if needed.

Inline comments:
In `@src/validators/evm/erc4626/erc4626.validator.ts`:
- Around line 130-133: Fix Prettier formatting around the receiverAddress
declaration: reflow the ternary so indentation matches project Prettier rules
(align the ? and : with the variable declaration) for the const receiverAddress
= isNonEmptyString(args?.receiverAddress) ? args.receiverAddress : undefined;
expression and remove the extra blank line near the top of the file (around the
earlier imports/definitions). Run Prettier or apply the same formatting to the
erc4626.validator.ts file to ensure lint/CI passes, focusing on the
receiverAddress declaration and the stray blank line near the top.
🪄 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: ffdaad19-9dec-4ceb-bc7e-04551c3af53c

📥 Commits

Reviewing files that changed from the base of the PR and between 83a1cad and 33fcf87.

📒 Files selected for processing (1)
  • src/validators/evm/erc4626/erc4626.validator.ts

@ajag408 ajag408 requested a review from raiseerco March 31, 2026 03:39
Copy link
Copy Markdown

@raiseerco raiseerco left a comment

Choose a reason for hiding this comment

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

lgtm

@ajag408 ajag408 merged commit 9b8f7c3 into main Apr 1, 2026
8 checks 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.

2 participants