feat: validate enter-side amount intent for ERC-4626 vaults (ENG-2418, ENG-2419)#30
feat: validate enter-side amount intent for ERC-4626 vaults (ENG-2418, ENG-2419)#30ajag408 wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
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.
Sent by Cursor Approval Agent: Pull Request Router and Approver
| import { isNonEmptyString } from '../../../utils/validation'; | ||
| import { | ||
| // exceedsDeclaredAmount, | ||
| // isInfiniteApproval, |
| { | ||
| "name": "@yieldxyz/shield", | ||
| "version": "1.3.0", | ||
| "version": "1.3.1", |
There was a problem hiding this comment.
Should this be a new minor version instead of a patch, since it's a new feature?
There was a problem hiding this comment.
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.
Sent by Cursor Approval Agent: Pull Request Router and Approver


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.amountis 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)
args.amountis declareddeposit(assets, receiver)assetsmust exactly match declared amount (both underlying wei)mint(shares, receiver)deposit→mintapprove(spender, amount)maxUint256sanctions an infinite approval (theuseMaxAllowancepattern);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)deposit())tx.valuemust exactly match declared amountChanges
src/types/index.ts— adddecimalstoActionArguments(inert context; the comparison is wei-vs-wei)src/json/schema.ts— registerdecimals(type: 'integer', 0–255)src/utils/amount.ts(new) —MAX_UINT256,matchesDeclaredAmount()(undefined declared → skip)src/validators/evm/erc4626/erc4626.validator.ts— threaddeclaredAmountinto APPROVAL / WRAP / SUPPLY paths with the rules abovesrc/utils/amount.test.ts(new), amount-intent blocks inerc4626.validator.test.ts, schema boundary tests injson/handler.test.tsScope / 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.
withdraw(assets)path). ERC-4626 exits currently convert share↔asset dynamically, which is not verifiable offline.Consumer notes
amountintovalidateTransaction()(enter path, both apps). Wiring contract: (incl. theuseMaxAllowance→ declare-maxUint256carve-out and the mint exception).args.amount(underlying, wei). Intentional infinite approvals are expressed by declaringmaxUint256— not supported implicitly.QA Proof
pnpm test --testPathIgnorePatterns=tron— all suites greenCLI smoke test against the baked registry using DFNS's production gtUSDC vault (
ethereum-usdc-gtusdc-0xdd0f...490d-4626-vault):isSupportedsanitysupported: truedeposit(1 USDC), declared 1 USDCSUPPLYdeposit(1000 USDC), declared 1 USDC (inflated calldata)"Supply amount does not match declared intent"args(back-compat)approve(maxUint256), declared 1 USDC"Approval amount does not match declared intent"approve(maxUint256), declaredmaxUint256(useMaxAllowance)APPROVALapprove(0), declared 1 USDC (USDT reset pattern)mint(...), declared present"Cannot verify mint (share-denominated) against declared asset amount"Full CLI output
What Needs to Be QA'd in Staging
"Shield validation failed"events (Betterstack,projectId = 3a90e48b-25f1-445a-9d34-0d4951dfee65)ethereum-usdt-steakusdt) — resetapprove(0)step passesuseMaxAllowance: trueenter (any client) — approval passes via the declared-maxUint256carve-outamount) show zero new failuresQA Team Notification
Note
Medium Risk
Changes tighten validation on security-sensitive ERC-4626 enter flows when
args.amountis 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. Withoutargs.amount(or with an empty string), behavior stays the same as v1.3.0.SUPPLY
depositassets are checked against the declared amount;mintis rejected if a declared amount is present (share-denominated, fail-closed to block deposit→mint bypass). APPROVAL amounts must match exceptapprove(0)(USDT reset); declaringmaxUint256allows infinite approval. WRAP comparestx.valueto the declared amount. WITHDRAW / UNWRAP are unchanged.API surface:
ActionArgumentsand JSON schema gain optionaldecimals(0–255); schema tests coveramount/decimalsbounds. Shared helpermatchesDeclaredAmountlives insrc/utils/amount.ts.Reviewed by Cursor Bugbot for commit 7c02227. Configure here.