From c1f9035128f51290c459d30c65e1c038f7131756 Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Mon, 30 Mar 2026 15:19:43 -0700 Subject: [PATCH 1/5] fix: receiverAddress override and zero amount approval --- package.json | 2 +- src/json/schema.ts | 1 + src/types/index.ts | 1 + .../evm/erc4626/erc4626.validator.test.ts | 104 +++++++++++++++++- .../evm/erc4626/erc4626.validator.ts | 37 ++++--- 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 62e882b..4ea5ce0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yieldxyz/shield", - "version": "1.2.4", + "version": "1.2.5", "description": "Zero-trust transaction validation library for Yield.xyz integrations.", "packageManager": "pnpm@10.12.2", "main": "./dist/index.js", diff --git a/src/json/schema.ts b/src/json/schema.ts index 4cc3872..5fb19b7 100644 --- a/src/json/schema.ts +++ b/src/json/schema.ts @@ -50,6 +50,7 @@ export const requestSchema = { cosmosPubKey: { type: 'string', maxLength: 256 }, tezosPubKey: { type: 'string', maxLength: 256 }, nominatorAddress: { type: 'string', maxLength: 128 }, + receiverAddress: { type: 'string', maxLength: 128 }, nftIds: { type: 'array', items: { type: 'string', maxLength: 256 }, diff --git a/src/types/index.ts b/src/types/index.ts index 17321cd..00ed829 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,6 +27,7 @@ export type ActionArguments = { cosmosPubKey?: string; tezosPubKey?: string; nominatorAddress?: string; + receiverAddress?: string; nftIds?: string[]; }; diff --git a/src/validators/evm/erc4626/erc4626.validator.test.ts b/src/validators/evm/erc4626/erc4626.validator.test.ts index d29ec22..a80465c 100644 --- a/src/validators/evm/erc4626/erc4626.validator.test.ts +++ b/src/validators/evm/erc4626/erc4626.validator.test.ts @@ -17,6 +17,7 @@ const MALICIOUS_ADDRESS = '0x000000000000000000000000000000000000bad1'; const PAUSED_VAULT_ADDRESS = '0xDEAD000000000000000000000000000000000001'; const ALLOCATOR_VAULT_ADDRESS = '0xa110ca7040000000000000000000000000000001'; const MORPHO_VAULT_ADDRESS = '0x00000000000000000000000000000000000face2'; +const RECEIVER_ADDRESS = '0x2222222222222222222222222222222222222222'; const CHAIN_ID = 42161; // Arbitrum // --------------------------------------------------------------------------- @@ -158,7 +159,7 @@ describe('ERC4626Validator', () => { expect(result.reason).toContain('not a whitelisted vault'); }); - it('should reject zero approval amount', () => { + it('should accept zero approval amount (USDT reset pattern)', () => { const data = erc20Iface.encodeFunctionData('approve', [VAULT_ADDRESS, 0]); const tx = buildTx({ to: INPUT_TOKEN, data, value: '0x0' }); const result = validator.validate( @@ -166,8 +167,7 @@ describe('ERC4626Validator', () => { TransactionType.APPROVAL, USER_ADDRESS, ); - expect(result.isValid).toBe(false); - expect(result.reason).toContain('zero'); + expect(result.isValid).toBe(true); }); it('should reject when ETH value is attached', () => { @@ -661,6 +661,104 @@ describe('ERC4626Validator', () => { }); }); + // ========================================================================= + // receiverAddress override (args.receiverAddress) + // ========================================================================= + describe('receiverAddress override via args', () => { + // ---- SUPPLY ---- + it('SUPPLY: should accept when args.receiverAddress matches calldata receiver', () => { + const data = erc4626Iface.encodeFunctionData('deposit', [ + ethers.parseUnits('1000', 6), + RECEIVER_ADDRESS, + ]); + const tx = buildTx({ to: VAULT_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.SUPPLY, + USER_ADDRESS, + { receiverAddress: RECEIVER_ADDRESS }, + ); + expect(result.isValid).toBe(true); + }); + + it('SUPPLY: should block when args.receiverAddress does NOT match calldata receiver', () => { + const data = erc4626Iface.encodeFunctionData('deposit', [ + ethers.parseUnits('1000', 6), + MALICIOUS_ADDRESS, + ]); + const tx = buildTx({ to: VAULT_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.SUPPLY, + USER_ADDRESS, + { receiverAddress: RECEIVER_ADDRESS }, + ); + expect(result.isValid).toBe(false); + expect(result.reason).toContain('does not match expected address'); + }); + + it('SUPPLY: without args.receiverAddress, receiver != user is blocked (default behavior)', () => { + const data = erc4626Iface.encodeFunctionData('deposit', [ + ethers.parseUnits('1000', 6), + OTHER_ADDRESS, + ]); + const tx = buildTx({ to: VAULT_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.SUPPLY, + USER_ADDRESS, + ); + expect(result.isValid).toBe(false); + expect(result.reason).toContain('does not match expected address'); + }); + + it('SUPPLY: without args.receiverAddress, receiver == user is safe (default behavior)', () => { + const data = erc4626Iface.encodeFunctionData('deposit', [ + ethers.parseUnits('1000', 6), + USER_ADDRESS, + ]); + const tx = buildTx({ to: VAULT_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.SUPPLY, + USER_ADDRESS, + ); + expect(result.isValid).toBe(true); + }); + + // ---- WITHDRAW ---- + it('WITHDRAW: should accept when args.receiverAddress matches calldata receiver', () => { + const data = erc4626Iface.encodeFunctionData( + 'withdraw(uint256,address,address)', + [ethers.parseUnits('1000', 6), RECEIVER_ADDRESS, USER_ADDRESS], + ); + const tx = buildTx({ to: VAULT_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.WITHDRAW, + USER_ADDRESS, + { receiverAddress: RECEIVER_ADDRESS }, + ); + expect(result.isValid).toBe(true); + }); + + it('WITHDRAW: should block when args.receiverAddress does NOT match calldata receiver', () => { + const data = erc4626Iface.encodeFunctionData( + 'withdraw(uint256,address,address)', + [ethers.parseUnits('1000', 6), MALICIOUS_ADDRESS, USER_ADDRESS], + ); + const tx = buildTx({ to: VAULT_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.WITHDRAW, + USER_ADDRESS, + { receiverAddress: RECEIVER_ADDRESS }, + ); + expect(result.isValid).toBe(false); + expect(result.reason).toContain('does not match expected address'); + }); + }); + // ========================================================================= // UNWRAP // ========================================================================= diff --git a/src/validators/evm/erc4626/erc4626.validator.ts b/src/validators/evm/erc4626/erc4626.validator.ts index 9c5f372..268aeac 100644 --- a/src/validators/evm/erc4626/erc4626.validator.ts +++ b/src/validators/evm/erc4626/erc4626.validator.ts @@ -98,7 +98,7 @@ export class ERC4626Validator extends BaseEVMValidator { unsignedTransaction: string, transactionType: TransactionType, userAddress: string, - _args?: ActionArguments, + args?: ActionArguments, _context?: ValidationContext, ): ValidationResult { const decoded = this.decodeEVMTransaction(unsignedTransaction); @@ -125,6 +125,8 @@ export class ERC4626Validator extends BaseEVMValidator { return this.blocked('Transaction has no destination address'); } + const receiverAddress = args?.receiverAddress; + // Route to appropriate validation based on transaction type switch (transactionType) { case TransactionType.APPROVAL: @@ -132,9 +134,9 @@ export class ERC4626Validator extends BaseEVMValidator { case TransactionType.WRAP: return this.validateWrap(tx, chainId); case TransactionType.SUPPLY: - return this.validateSupply(tx, userAddress, chainId); + return this.validateSupply(tx, userAddress, chainId, receiverAddress); case TransactionType.WITHDRAW: - return this.validateWithdraw(tx, userAddress, chainId); + return this.validateWithdraw(tx, userAddress, chainId, receiverAddress); case TransactionType.UNWRAP: return this.validateUnwrap(tx, chainId); default: @@ -177,7 +179,7 @@ export class ERC4626Validator extends BaseEVMValidator { } // Get spender (should be vault address) - const [spender, amount] = parsed.args; + const [spender] = parsed.args; // Validate spender is a whitelisted vault const vaultInfo = this.vaultInfoMap.get( @@ -196,12 +198,6 @@ export class ERC4626Validator extends BaseEVMValidator { }); } - // Validate amount is not zero - const amountBigInt = BigInt(amount); - if (amountBigInt === 0n) { - return this.blocked('Approval amount is zero'); - } - return this.safe(); } @@ -265,6 +261,7 @@ export class ERC4626Validator extends BaseEVMValidator { tx: EVMTransaction, userAddress: string, chainId: number, + receiverAddress?: string, ): ValidationResult { const resolved = this.resolveVault(tx, chainId); if ('error' in resolved) return resolved.error; @@ -310,10 +307,12 @@ export class ERC4626Validator extends BaseEVMValidator { return this.blocked('Supply amount is zero'); } - // Validate receiver is the user - if (receiver.toLowerCase() !== userAddress.toLowerCase()) { - return this.blocked('Receiver address does not match user address', { - expected: userAddress, + // Validate receiver is the intended receiver + const expectedReceiver = receiverAddress ?? userAddress; + + if (receiver.toLowerCase() !== expectedReceiver.toLowerCase()) { + return this.blocked('Receiver address does not match expected address', { + expected: expectedReceiver, actual: receiver, }); } @@ -328,6 +327,7 @@ export class ERC4626Validator extends BaseEVMValidator { tx: EVMTransaction, userAddress: string, chainId: number, + receiverAddress?: string, ): ValidationResult { const resolved = this.resolveVault(tx, chainId); if ('error' in resolved) return resolved.error; @@ -381,10 +381,11 @@ export class ERC4626Validator extends BaseEVMValidator { }); } - // Validate receiver is the user (for safety) - if (receiver.toLowerCase() !== userAddress.toLowerCase()) { - return this.blocked('Receiver address does not match user address', { - expected: userAddress, + const expectedReceiver = receiverAddress ?? userAddress; + + if (receiver.toLowerCase() !== expectedReceiver.toLowerCase()) { + return this.blocked('Receiver address does not match expected address', { + expected: expectedReceiver, actual: receiver, }); } From 7b6fd71087519158b12a317bb4079496672682fc Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Mon, 30 Mar 2026 15:31:55 -0700 Subject: [PATCH 2/5] fix: dev dependency fix --- package.json | 2 +- pnpm-lock.yaml | 31 +++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 4ea5ce0..d0ec433 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "jest": "^29.0.0", "postject": "1.0.0-alpha.6", "prettier": "^3.2.5", - "ts-jest": "^29.0.0", + "ts-jest": "^29.4.6", "typescript": "^5.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db86bfd..0197efa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,8 +58,8 @@ importers: specifier: ^3.2.5 version: 3.6.2 ts-jest: - specifier: ^29.0.0 - version: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.27.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.19))(typescript@5.9.3) + specifier: ^29.4.6 + version: 29.4.6(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.27.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.19))(typescript@5.9.3) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -1370,8 +1370,8 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true @@ -1980,6 +1980,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2091,8 +2096,8 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-jest@29.4.4: - resolution: {integrity: sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==} + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -3744,7 +3749,7 @@ snapshots: graphemer@1.4.0: {} - handlebars@4.7.8: + handlebars@4.7.9: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -3842,7 +3847,7 @@ snapshots: '@babel/parser': 7.28.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -4255,7 +4260,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 make-error@1.3.6: {} @@ -4476,6 +4481,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4582,16 +4589,16 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.27.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.19))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.27.2)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.19))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 + handlebars: 4.7.9 jest: 29.7.0(@types/node@20.19.19) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.3 + semver: 7.7.4 type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 From 83a1cada28e7e3b22f41b1a8c6d7737c768cfb2b Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Mon, 30 Mar 2026 15:35:50 -0700 Subject: [PATCH 3/5] fix: rabbit --- src/json/schema.ts | 2 +- src/validators/evm/erc4626/erc4626.validator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/json/schema.ts b/src/json/schema.ts index 5fb19b7..641fdb9 100644 --- a/src/json/schema.ts +++ b/src/json/schema.ts @@ -50,7 +50,7 @@ export const requestSchema = { cosmosPubKey: { type: 'string', maxLength: 256 }, tezosPubKey: { type: 'string', maxLength: 256 }, nominatorAddress: { type: 'string', maxLength: 128 }, - receiverAddress: { type: 'string', maxLength: 128 }, + receiverAddress: { type: 'string', minLength: 1, maxLength: 128 }, nftIds: { type: 'array', items: { type: 'string', maxLength: 256 }, diff --git a/src/validators/evm/erc4626/erc4626.validator.ts b/src/validators/evm/erc4626/erc4626.validator.ts index 268aeac..5f1d02e 100644 --- a/src/validators/evm/erc4626/erc4626.validator.ts +++ b/src/validators/evm/erc4626/erc4626.validator.ts @@ -125,7 +125,7 @@ export class ERC4626Validator extends BaseEVMValidator { return this.blocked('Transaction has no destination address'); } - const receiverAddress = args?.receiverAddress; + const receiverAddress = args?.receiverAddress || undefined; // Route to appropriate validation based on transaction type switch (transactionType) { From 33fcf87a3efc9cb4297a4240d7e04c71790081e9 Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Mon, 30 Mar 2026 20:28:54 -0700 Subject: [PATCH 4/5] fix: receiverAddress runtime type validation --- src/validators/evm/erc4626/erc4626.validator.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/validators/evm/erc4626/erc4626.validator.ts b/src/validators/evm/erc4626/erc4626.validator.ts index 5f1d02e..7c32eeb 100644 --- a/src/validators/evm/erc4626/erc4626.validator.ts +++ b/src/validators/evm/erc4626/erc4626.validator.ts @@ -8,6 +8,8 @@ import { import { BaseEVMValidator, EVMTransaction } from '../base.validator'; import { VaultInfo, VaultConfiguration } from './types'; import { WETH_ADDRESSES } from './constants'; +import { isNonEmptyString } from '../../../utils/validation'; + /** * Standard ERC4626 ABI - only the functions we need to validate @@ -125,7 +127,9 @@ export class ERC4626Validator extends BaseEVMValidator { return this.blocked('Transaction has no destination address'); } - const receiverAddress = args?.receiverAddress || undefined; + const receiverAddress = isNonEmptyString(args?.receiverAddress) + ? args.receiverAddress + : undefined; // Route to appropriate validation based on transaction type switch (transactionType) { From 58ef47ef849d7e8664fb03ccbf7b68b6b394a8b2 Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Mon, 30 Mar 2026 20:31:46 -0700 Subject: [PATCH 5/5] fix: lint --- src/validators/evm/erc4626/erc4626.validator.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/validators/evm/erc4626/erc4626.validator.ts b/src/validators/evm/erc4626/erc4626.validator.ts index 7c32eeb..e4275e9 100644 --- a/src/validators/evm/erc4626/erc4626.validator.ts +++ b/src/validators/evm/erc4626/erc4626.validator.ts @@ -10,7 +10,6 @@ import { VaultInfo, VaultConfiguration } from './types'; import { WETH_ADDRESSES } from './constants'; import { isNonEmptyString } from '../../../utils/validation'; - /** * Standard ERC4626 ABI - only the functions we need to validate */ @@ -128,8 +127,8 @@ export class ERC4626Validator extends BaseEVMValidator { } const receiverAddress = isNonEmptyString(args?.receiverAddress) - ? args.receiverAddress - : undefined; + ? args.receiverAddress + : undefined; // Route to appropriate validation based on transaction type switch (transactionType) {