Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/validators/base.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
65 changes: 55 additions & 10 deletions src/validators/evm/erc4626/erc4626.validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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,
Expand All @@ -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');
});
});

Expand Down Expand Up @@ -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],
Expand All @@ -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');
});
});

Expand Down Expand Up @@ -776,16 +821,16 @@ 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(
tx,
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', () => {
Expand Down
21 changes: 4 additions & 17 deletions src/validators/evm/erc4626/erc4626.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@

// Get and validate chain ID from transaction
const chainId = this.getNumericChainId(tx);
if (!chainId) {

Check warning on line 120 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (20.19.0)

Unexpected nullable number value in conditional. Please handle the nullish/zero/NaN cases explicitly

Check warning on line 120 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (24.x)

Unexpected nullable number value in conditional. Please handle the nullish/zero/NaN cases explicitly

Check warning on line 120 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (22.x)

Unexpected nullable number value in conditional. Please handle the nullish/zero/NaN cases explicitly
return this.blocked('Chain ID not found in transaction');
}

// Ensure destination address exists
if (!tx.to) {

Check warning on line 125 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (20.19.0)

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

Check warning on line 125 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (24.x)

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

Check warning on line 125 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (22.x)

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
return this.blocked('Transaction has no destination address');
}

Expand Down Expand Up @@ -210,7 +210,7 @@
private validateWrap(tx: EVMTransaction, chainId: number): ValidationResult {
// Get WETH address for this chain
const wethAddress = this.getWethAddress(chainId);
if (!wethAddress) {

Check warning on line 213 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (20.19.0)

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

Check warning on line 213 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (24.x)

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

Check warning on line 213 in src/validators/evm/erc4626/erc4626.validator.ts

View workflow job for this annotation

GitHub Actions / Test & Build (22.x)

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
return this.blocked('WETH address not configured for chain', { chainId });
}

Expand All @@ -233,9 +233,6 @@

// 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(
Expand All @@ -254,7 +251,7 @@
});
}

return this.safe();
return this.safe(value === 0n ? ['ZERO_AMOUNT'] : undefined);
}

/**
Expand Down Expand Up @@ -306,9 +303,6 @@
// 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;
Expand All @@ -320,7 +314,7 @@
});
}

return this.safe();
return this.safe(amountBigInt === 0n ? ['ZERO_AMOUNT'] : undefined);
}

/**
Expand Down Expand Up @@ -372,9 +366,6 @@
// 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()) {
Expand All @@ -393,7 +384,7 @@
});
}

return this.safe();
return this.safe(amountBigInt === 0n ? ['ZERO_AMOUNT'] : undefined);
}

/**
Expand Down Expand Up @@ -451,14 +442,10 @@
});
}

// 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(
Expand Down
Loading