Skip to content

feat: validate enter-side amount intent for ERC-4626 vaults (ENG-2418, ENG-2419)#30

Open
ajag408 wants to merge 7 commits into
mainfrom
eng-2418-eng-2419-amount-intent-validation
Open

feat: validate enter-side amount intent for ERC-4626 vaults (ENG-2418, ENG-2419)#30
ajag408 wants to merge 7 commits into
mainfrom
eng-2418-eng-2419-amount-intent-validation

Conversation

@ajag408

@ajag408 ajag408 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the enter-side half of the amount-tampering gap: Shield now cross-references calldata amounts against the caller's declared intent (args.amount) in the ERC-4626 validator. Previously, amounts were only checked for non-zero — calldata could be inflated or mutated while still passing structural validation.

Everything is additive and opt-in: enforcement runs only when args.amount is supplied. Absent that, behavior is byte-for-byte identical to v1.3.0 — this version publishes with zero behavior change for current callers, ahead of the monorepo wiring (ENG-2420).

Validation rules added (ERC-4626 validator only)

Tx type Rule when args.amount is declared
SUPPLY deposit(assets, receiver) assets must exactly match declared amount (both underlying wei)
SUPPLY mint(shares, receiver) Fail-closed: blocked — shares can't be verified against an asset-denominated intent offline, and skipping would let a tamperer bypass the check by rewriting depositmint
APPROVAL approve(spender, amount) Must exactly match declared amount. Declaring maxUint256 sanctions an infinite approval (the useMaxAllowance pattern); approve(maxUint256) against a finite declared amount is blocked. approve(0) stays exempt (USDT allowance-reset; grants no authority — the reset tx is action-declared at the deposit amount, so matching it would false-block)
WRAP (WETH deposit()) tx.value must exactly match declared amount
WITHDRAW / UNWRAP Unchanged — exit validation is deferred (see scope note)

Changes

  • src/types/index.ts — add decimals to ActionArguments (inert context; the comparison is wei-vs-wei)
  • src/json/schema.ts — register decimals (type: 'integer', 0–255)
  • src/utils/amount.ts (new) — MAX_UINT256, matchesDeclaredAmount() (undefined declared → skip)
  • src/validators/evm/erc4626/erc4626.validator.ts — thread declaredAmount into APPROVAL / WRAP / SUPPLY paths with the rules above
  • Tests: src/utils/amount.test.ts (new), amount-intent blocks in erc4626.validator.test.ts, schema boundary tests in json/handler.test.ts

Scope / deferrals

Scope was confirmed against production: all 7 DFNS-enabled yields are ERC-4626 stablecoin vaults (Morpho, Sky, Yearn-family), so this PR touches only the ERC-4626 validator.

  • Exit validation → Phase 2: share-denominated exits (unblocked by monorepo ENG-3325) and asset-exact exits (gated on monorepo ENG-3393 PR 2, withdraw(assets) path). ERC-4626 exits currently convert share↔asset dynamically, which is not verifiable offline.
  • Lido / RocketPool amount validation → Phase 2 (no DFNS stakes on either).
  • Aave/aToken, Solana, Tron → Phase 3+ (validators ship with their own amount validation).

Consumer notes

  • Embedded API: no protection change until ENG-2420 passes amount into validateTransaction() (enter path, both apps). Wiring contract: (incl. the useMaxAllowance → declare-maxUint256 carve-out and the mint exception).
  • Binary / self-hosted consumers: the amount check is only active if you pass args.amount (underlying, wei). Intentional infinite approvals are expressed by declaring maxUint256 — not supported implicitly.

QA Proof

pnpm test --testPathIgnorePatterns=tron — all suites green

CLI smoke test against the baked registry using DFNS's production gtUSDC vault (ethereum-usdc-gtusdc-0xdd0f...490d-4626-vault):

# Scenario Expected Result
0 isSupported sanity supported: true
1 deposit(1 USDC), declared 1 USDC valid, SUPPLY
2 deposit(1000 USDC), declared 1 USDC (inflated calldata) blocked — "Supply amount does not match declared intent"
3 Same inflated tx, no args (back-compat) valid — proves opt-in
4a approve(maxUint256), declared 1 USDC blocked — "Approval amount does not match declared intent"
4b approve(maxUint256), declared maxUint256 (useMaxAllowance) valid, APPROVAL
5 approve(0), declared 1 USDC (USDT reset pattern) valid — zero-exemption
6 mint(...), declared present blocked — "Cannot verify mint (share-denominated) against declared asset amount"
Full CLI output Screen Shot 2026-07-02 at 4 32 36 AM

What Needs to Be QA'd in Staging

Staging QA is gated on ENG-2420 (monorepo bumps this package and passes amount on the enter path). Until then this version is dormant by design — MONITOR soak will confirm.

  • After the ENG-2420 bump: enter flows on the 7 DFNS ERC-4626 yields pass in MONITOR with zero "Shield validation failed" events (Betterstack, projectId = 3a90e48b-25f1-445a-9d34-0d4951dfee65)
  • USDT enter flow with pre-existing allowance (ethereum-usdt-steakusdt) — reset approve(0) step passes
  • A useMaxAllowance: true enter (any client) — approval passes via the declared-maxUint256 carve-out
  • Tamper test: mutated deposit amount in calldata is flagged (MONITOR) / blocked (BLOCK)
  • Regression: clients without amount wiring (all other validators + ERC-4626 callers not passing amount) show zero new failures

QA Team Notification

  • QA team notified to test in staging once ENG-2420 deploys

Note

Medium Risk
Changes tighten validation on security-sensitive ERC-4626 enter flows when args.amount is wired; risk is mitigated by opt-in semantics and broad test coverage, but mis-declared amounts or future monorepo wiring could block legitimate txs.

Overview
v1.3.1 adds opt-in enter-path protection: when callers pass args.amount (underlying wei), the ERC-4626 validator requires calldata amounts to exactly match that intent. Without args.amount (or with an empty string), behavior stays the same as v1.3.0.

SUPPLY deposit assets are checked against the declared amount; mint is rejected if a declared amount is present (share-denominated, fail-closed to block deposit→mint bypass). APPROVAL amounts must match except approve(0) (USDT reset); declaring maxUint256 allows infinite approval. WRAP compares tx.value to the declared amount. WITHDRAW / UNWRAP are unchanged.

API surface: ActionArguments and JSON schema gain optional decimals (0–255); schema tests cover amount/decimals bounds. Shared helper matchesDeclaredAmount lives in src/utils/amount.ts.

Reviewed by Cursor Bugbot for commit 7c02227. Configure here.

@cursor cursor 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.

Stale comment

Risk: medium. Cursor Bugbot and Security Agent completed successfully with no reported findings, but this security-sensitive ERC-4626 amount-intent validation exceeds the low-risk approval threshold. Human review is required; Philippoes, jdomingos, and raiseerco are already assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

dnehl
dnehl previously approved these changes Jul 2, 2026

@dnehl dnehl 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.

looks good - just 2 nits

import { isNonEmptyString } from '../../../utils/validation';
import {
// exceedsDeclaredAmount,
// isInfiniteApproval,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lets cleanup those?

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.

done

Comment thread package.json Outdated
{
"name": "@yieldxyz/shield",
"version": "1.3.0",
"version": "1.3.1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should this be a new minor version instead of a patch, since it's a new feature?

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.

done

@cursor cursor 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.

Risk: medium. Cursor Security Agent completed with no findings; Cursor Bugbot was not present on this run. This ERC-4626 amount-intent validation change exceeds the low-risk approval threshold, so I am not approving. Human reviewers are already assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@ajag408 ajag408 requested a review from dnehl July 3, 2026 06:35
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