From 7e5b1986d13e0e97efa5ea1b5dcfea7528fd80d3 Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Wed, 24 Jun 2026 02:02:20 -0700 Subject: [PATCH 1/2] fix: allow zero amount txs on 4626 --- src/types/index.ts | 1 + src/validators/base.validator.ts | 6 +- .../evm/erc4626/erc4626.validator.test.ts | 65 ++++++++++++++++--- .../evm/erc4626/erc4626.validator.ts | 21 ++---- 4 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 00ed829..881f5b0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,7 @@ export interface ValidationResult { matchedTypes?: TransactionType[]; supportedTypes?: TransactionType[]; warning?: string; + flags?: string[]; // non-blocking signals, e.g. 'ZERO_AMOUNT' attempts?: { type?: TransactionType; reason?: string; diff --git a/src/validators/base.validator.ts b/src/validators/base.validator.ts index d7c5290..51ee9e9 100644 --- a/src/validators/base.validator.ts +++ b/src/validators/base.validator.ts @@ -6,8 +6,10 @@ import { } from '../types'; export abstract class BaseValidator { - protected safe(): ValidationResult { - return { isValid: true }; + protected safe(flags?: string[]): ValidationResult { + return flags && flags.length > 0 + ? { isValid: true, details: { flags } } + : { isValid: true }; } protected blocked( diff --git a/src/validators/evm/erc4626/erc4626.validator.test.ts b/src/validators/evm/erc4626/erc4626.validator.test.ts index a80465c..06f7b08 100644 --- a/src/validators/evm/erc4626/erc4626.validator.test.ts +++ b/src/validators/evm/erc4626/erc4626.validator.test.ts @@ -275,12 +275,12 @@ describe('ERC4626Validator', () => { expect(result.reason).toContain('not to WETH contract'); }); - it('should reject zero ETH value', () => { + it('should accept zero ETH value wrap with ZERO_AMOUNT flag', () => { const data = wethIface.encodeFunctionData('deposit', []); const tx = buildTx({ to: WETH_ARBITRUM, data, value: '0x0' }); const result = validator.validate(tx, TransactionType.WRAP, USER_ADDRESS); - expect(result.isValid).toBe(false); - expect(result.reason).toContain('must send ETH value'); + expect(result.isValid).toBe(true); + expect(result.details?.flags).toContain('ZERO_AMOUNT'); }); it('should reject non-deposit function selector', () => { @@ -501,7 +501,7 @@ describe('ERC4626Validator', () => { expect(result.reason).toContain('should not send ETH'); }); - it('should reject zero-amount deposit', () => { + it('should accept zero-amount deposit with ZERO_AMOUNT flag', () => { const data = erc4626Iface.encodeFunctionData('deposit', [ 0, USER_ADDRESS, @@ -512,8 +512,38 @@ describe('ERC4626Validator', () => { TransactionType.SUPPLY, USER_ADDRESS, ); + expect(result.isValid).toBe(true); + expect(result.details?.flags).toContain('ZERO_AMOUNT'); + }); + + it('should still block zero-amount supply to a non-whitelisted vault', () => { + const data = erc4626Iface.encodeFunctionData('deposit', [ + 0, + USER_ADDRESS, + ]); + const tx = buildTx({ to: MALICIOUS_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.SUPPLY, + USER_ADDRESS, + ); expect(result.isValid).toBe(false); - expect(result.reason).toContain('zero'); + expect(result.reason).toContain('not whitelisted'); + }); + + it('should still block zero-amount supply when receiver != expected', () => { + const data = erc4626Iface.encodeFunctionData('deposit', [ + 0, + 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'); }); }); @@ -645,7 +675,7 @@ describe('ERC4626Validator', () => { expect(result.isValid).toBe(false); }); - it('should reject zero-amount withdraw', () => { + it('should accept zero-amount withdraw with ZERO_AMOUNT flag', () => { const data = erc4626Iface.encodeFunctionData( 'withdraw(uint256,address,address)', [0, USER_ADDRESS, USER_ADDRESS], @@ -656,8 +686,23 @@ describe('ERC4626Validator', () => { TransactionType.WITHDRAW, USER_ADDRESS, ); + expect(result.isValid).toBe(true); + expect(result.details?.flags).toContain('ZERO_AMOUNT'); + }); + + it('should still block zero-amount withdraw when owner != user', () => { + const data = erc4626Iface.encodeFunctionData( + 'withdraw(uint256,address,address)', + [0, USER_ADDRESS, OTHER_ADDRESS], + ); + const tx = buildTx({ to: VAULT_ADDRESS, data, value: '0x0' }); + const result = validator.validate( + tx, + TransactionType.WITHDRAW, + USER_ADDRESS, + ); expect(result.isValid).toBe(false); - expect(result.reason).toContain('zero'); + expect(result.reason).toContain('Owner address does not match'); }); }); @@ -776,7 +821,7 @@ describe('ERC4626Validator', () => { expect(result.isValid).toBe(true); }); - it('should reject zero amount', () => { + it('should accept zero amount with ZERO_AMOUNT flag', () => { const data = wethIface.encodeFunctionData('withdraw', [0]); const tx = buildTx({ to: WETH_ARBITRUM, data, value: '0x0' }); const result = validator.validate( @@ -784,8 +829,8 @@ describe('ERC4626Validator', () => { TransactionType.UNWRAP, USER_ADDRESS, ); - expect(result.isValid).toBe(false); - expect(result.reason).toContain('UNWRAP amount is zero'); + expect(result.isValid).toBe(true); + expect(result.details?.flags).toContain('ZERO_AMOUNT'); }); it('should reject wrong WETH address', () => { diff --git a/src/validators/evm/erc4626/erc4626.validator.ts b/src/validators/evm/erc4626/erc4626.validator.ts index e4275e9..cbd3141 100644 --- a/src/validators/evm/erc4626/erc4626.validator.ts +++ b/src/validators/evm/erc4626/erc4626.validator.ts @@ -233,9 +233,6 @@ export class ERC4626Validator extends BaseEVMValidator { // WRAP must send ETH value const value = BigInt(tx.value ?? '0'); - if (value === 0n) { - return this.blocked('WRAP transaction must send ETH value'); - } // Parse the wrap calldata const result = this.parseAndValidateCalldata( @@ -254,7 +251,7 @@ export class ERC4626Validator extends BaseEVMValidator { }); } - return this.safe(); + return this.safe(value === 0n ? ['ZERO_AMOUNT'] : undefined); } /** @@ -306,9 +303,6 @@ export class ERC4626Validator extends BaseEVMValidator { // Both deposit and mint have receiver as second parameter const [amount, receiver] = parsed.args; const amountBigInt = BigInt(amount); - if (amountBigInt === 0n) { - return this.blocked('Supply amount is zero'); - } // Validate receiver is the intended receiver const expectedReceiver = receiverAddress ?? userAddress; @@ -320,7 +314,7 @@ export class ERC4626Validator extends BaseEVMValidator { }); } - return this.safe(); + return this.safe(amountBigInt === 0n ? ['ZERO_AMOUNT'] : undefined); } /** @@ -372,9 +366,6 @@ export class ERC4626Validator extends BaseEVMValidator { // Both withdraw and redeem have: (amount, receiver, owner) const [amount, receiver, owner] = parsed.args; const amountBigInt = BigInt(amount); - if (amountBigInt === 0n) { - return this.blocked('Withdraw amount is zero'); - } // Validate owner is the user (they must own the shares) if (owner.toLowerCase() !== userAddress.toLowerCase()) { @@ -393,7 +384,7 @@ export class ERC4626Validator extends BaseEVMValidator { }); } - return this.safe(); + return this.safe(amountBigInt === 0n ? ['ZERO_AMOUNT'] : undefined); } /** @@ -451,14 +442,10 @@ export class ERC4626Validator extends BaseEVMValidator { }); } - // Validate amount is not zero const [amount] = parsed.args; const amountBigInt = BigInt(amount); - if (amountBigInt === 0n) { - return this.blocked('UNWRAP amount is zero'); - } - return this.safe(); + return this.safe(amountBigInt === 0n ? ['ZERO_AMOUNT'] : undefined); } private resolveVault( From 150a4a66ede6b4735a0d8bcc4167ab73f3986439 Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Wed, 24 Jun 2026 02:03:45 -0700 Subject: [PATCH 2/2] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7bc1fe..5893d04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yieldxyz/shield", - "version": "1.3.0", + "version": "1.3.1", "description": "Zero-trust transaction validation library for Yield.xyz integrations.", "packageManager": "pnpm@10.33.1", "engines": {