Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,45 @@ The CLI reads JSON from stdin and writes JSON to stdout:
### CLI Examples (Bash)

```bash
# List supported yields
echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | npx @yieldxyz/shield

# Check if a yield is supported
echo '{"apiVersion":"1.0","operation":"isSupported","yieldId":"ethereum-eth-lido-staking"}' | npx @yieldxyz/shield

# Validate a transaction
echo '{"apiVersion":"1.0","operation":"validate","yieldId":"ethereum-eth-lido-staking","unsignedTransaction":"{...}","userAddress":"0x..."}' | npx @yieldxyz/shield


# List supported yields
echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | npx @yieldxyz/shield
```

## Supported Yield IDs

- `ethereum-eth-lido-staking`
- `solana-sol-native-multivalidator-staking`
- `tron-trx-native-staking`
- All generic ERC4626 vault yields from: Angle, Curve, Euler, Fluid, Gearbox, Idle Finance, Lista, Morpho, Sky, SummerFi, Venus Flux, Yearn, Yo Protocol

To see the full list:

```bash
echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | npx @yieldxyz/shield
```

> **Note:** Aave, Maple, Spark use non-standard transaction flows and are not yet supported. Protocol-specific validators for these will be added in a future release.

### ERC4626 Vault Operations

Shield validates the following operations for all supported ERC4626 vaults:

| Operation | Transaction Type | Description |
| ----------- | ---------------- | --------------------------------------------- |
| Approve | APPROVAL | ERC20 token approval for vault deposit |
| Deposit | SUPPLY | Deposit assets into vault |
| Mint | SUPPLY | Mint vault shares |
| Withdraw | WITHDRAW | Withdraw assets from vault |
| Redeem | WITHDRAW | Redeem vault shares |
| WETH Wrap | WRAP | Convert native ETH to WETH (WETH vaults only) |
| WETH Unwrap | UNWRAP | Convert WETH to native ETH (WETH vaults only) |

## API Reference

Expand Down Expand Up @@ -211,6 +235,10 @@ Shield is designed with security as a top priority:
- **No Network Access**: The CLI binary has no network capabilities - it only reads stdin and writes stdout
- **Checksum Verification**: All release binaries include SHA256 checksums for integrity verification

### Embedded Vault Registry

ERC4626 vault data is embedded in the package at build time from `vault-registry.json`. The registry includes allocator vault (OAV) addresses for yields with fee configurations. Transactions targeting allocator vaults are validated using the same ERC4626 standard.

### Verifying Binary Integrity

Always verify downloaded binaries:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@yieldxyz/shield",
"version": "1.2.0",
"version": "1.2.1",
"description": "Zero-trust transaction validation library for Yield.xyz integrations.",
"packageManager": "pnpm@10.12.2",
"main": "./dist/index.js",
Expand Down Expand Up @@ -32,6 +32,7 @@
},
"files": [
"dist",
"src/validators/evm/erc4626/vault-registry.json",
"package.json",
"README.md",
"LICENSE"
Expand Down
8 changes: 8 additions & 0 deletions src/validators/evm/erc4626/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const WETH_ADDRESSES: Record<number, string> = {
1: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // Ethereum
42161: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', // Arbitrum
10: '0x4200000000000000000000000000000000000006', // Optimism
8453: '0x4200000000000000000000000000000000000006', // Base
130: '0x4200000000000000000000000000000000000006', // Unichain
56: '0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c', // BSC (WBNB)
};
236 changes: 236 additions & 0 deletions src/validators/evm/erc4626/erc4626.exhaustive-coverage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { ethers } from 'ethers';
import { ERC4626Validator } from './erc4626.validator';
import { loadEmbeddedRegistry } from './vault-config';
import { TransactionType } from '../../../types';
import { WETH_ADDRESSES } from './constants';

import { GENERIC_ERC4626_PROTOCOLS } from '../../index';

const erc20Iface = new ethers.Interface([
'function approve(address spender, uint256 amount) returns (bool)',
]);
const erc4626Iface = new ethers.Interface([
'function deposit(uint256 assets, address receiver) returns (uint256)',
'function redeem(uint256 shares, address receiver, address owner) returns (uint256)',
]);
const wethIface = new ethers.Interface([
'function deposit() payable',
'function withdraw(uint256 wad)',
]);

const USER = '0x742d35cc6634c0532925a3b844bc9e7595f0beb8';

function buildTx(fields: Record<string, unknown>): string {
return JSON.stringify({
from: USER,
nonce: 0,
gasLimit: '0x30d40',
maxFeePerGas: '0x6fc23ac00',
maxPriorityFeePerGas: '0x3b9aca00',
type: 2,
...fields,
});
}

const config = loadEmbeddedRegistry();
const allowedVaults = config.vaults.filter((v) =>
GENERIC_ERC4626_PROTOCOLS.has(v.protocol),
);

describe(`ERC4626 exhaustive coverage — all ${allowedVaults.length} allowed vaults`, () => {
it('should have allowed vaults to test', () => {
expect(allowedVaults.length).toBeGreaterThan(0);
});

for (const vault of allowedVaults) {
const validator = new ERC4626Validator({
vaults: [vault],
lastUpdated: config.lastUpdated,
});
const active = vault.canEnter !== false && vault.canExit !== false;
const label = `${vault.protocol}/${vault.network} — ${vault.yieldId}`;

if (active) {
it(`APPROVAL — ${label}`, () => {
const data = erc20Iface.encodeFunctionData('approve', [
vault.address,
ethers.parseUnits('100', 18),
]);
const tx = buildTx({
to: vault.inputTokenAddress,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.APPROVAL, USER).isValid,
).toBe(true);
});

it(`SUPPLY — ${label}`, () => {
const data = erc4626Iface.encodeFunctionData('deposit', [
ethers.parseUnits('100', 18),
USER,
]);
const tx = buildTx({
to: vault.address,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.SUPPLY, USER).isValid,
).toBe(true);
});

it(`WITHDRAW — ${label}`, () => {
const data = erc4626Iface.encodeFunctionData('redeem', [
ethers.parseUnits('50', 18),
USER,
USER,
]);
const tx = buildTx({
to: vault.address,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.WITHDRAW, USER).isValid,
).toBe(true);
});

if (vault.isWethVault && WETH_ADDRESSES[vault.chainId]) {

Check warning on line 103 in src/validators/evm/erc4626/erc4626.exhaustive-coverage.test.ts

View workflow job for this annotation

GitHub Actions / Test & Build (22.x)

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly

Check warning on line 103 in src/validators/evm/erc4626/erc4626.exhaustive-coverage.test.ts

View workflow job for this annotation

GitHub Actions / Test & Build (20.17.0)

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly
const wethAddr = WETH_ADDRESSES[vault.chainId];

it(`WRAP — ${label}`, () => {
const data = wethIface.encodeFunctionData('deposit', []);
const tx = buildTx({
to: wethAddr,
data,
value: '0xde0b6b3a7640000',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.WRAP, USER).isValid,
).toBe(true);
});

it(`UNWRAP — ${label}`, () => {
const data = wethIface.encodeFunctionData('withdraw', [
ethers.parseEther('1'),
]);
const tx = buildTx({
to: wethAddr,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.UNWRAP, USER).isValid,
).toBe(true);
});
}
} else {
if (vault.canEnter === false) {
it(`SUPPLY blocked (paused) — ${label}`, () => {
const data = erc4626Iface.encodeFunctionData('deposit', [
ethers.parseUnits('100', 18),
USER,
]);
const tx = buildTx({
to: vault.address,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.SUPPLY, USER).isValid,
).toBe(false);
});
}

if (vault.canExit === false) {
it(`WITHDRAW blocked (disabled) — ${label}`, () => {
const data = erc4626Iface.encodeFunctionData('redeem', [
ethers.parseUnits('50', 18),
USER,
USER,
]);
const tx = buildTx({
to: vault.address,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.WITHDRAW, USER).isValid,
).toBe(false);
});
}
}
}
const vaultsWithAllocators = allowedVaults.filter(
(v) => v.allocatorVaults && v.allocatorVaults.length > 0,
);

for (const vault of vaultsWithAllocators) {
const validator = new ERC4626Validator({
vaults: [vault],
lastUpdated: config.lastUpdated,
});

for (const allocAddr of vault.allocatorVaults!) {
const label = `${vault.protocol}/${vault.network} — ${vault.yieldId} → allocator ${allocAddr.slice(0, 10)}`;

it(`SUPPLY (allocator) — ${label}`, () => {
const data = erc4626Iface.encodeFunctionData('deposit', [
ethers.parseUnits('100', 18),
USER,
]);
const tx = buildTx({
to: allocAddr,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.SUPPLY, USER).isValid,
).toBe(true);
});

it(`WITHDRAW (allocator) — ${label}`, () => {
const data = erc4626Iface.encodeFunctionData('redeem', [
ethers.parseUnits('50', 18),
USER,
USER,
]);
const tx = buildTx({
to: allocAddr,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.WITHDRAW, USER).isValid,
).toBe(true);
});

it(`APPROVAL (allocator) — ${label}`, () => {
const data = erc20Iface.encodeFunctionData('approve', [
ethers.getAddress(allocAddr),
ethers.parseUnits('100', 18),
]);
const tx = buildTx({
to: vault.inputTokenAddress,
data,
value: '0x0',
chainId: vault.chainId,
});
expect(
validator.validate(tx, TransactionType.APPROVAL, USER).isValid,
).toBe(true);
});
}
}
});
Loading
Loading