Skip to content

Commit 37813c8

Browse files
authored
Feat: Generic ERC4626 Shield validator + comprehensive test suite (#8)
* feat(erc4626): setup * feat(erc4626-validator): 4626 generic validator * feat(erc4626-validator): supported tx specific validations * feat(erc4626-validator): vault config fix and vault discovery v1 * stash * feat: 4626 generic validator and tests * feat: additional tests and cleanup * fix: lint * chore: address feeback and refactoring * fix: lint * fix: rabbit * fix: add back unichain weth vault * fix: lint * feat: register generic ERC4626 validators with embedded vault registry (#9) * feat: bootstrapped vault registry and 4626 validator registry, tests * feat: protocol and exhaustive tests * fix: lint * chore: refactor weth addresses * fix: lint * feat: allocator vault support * feat: unit tests for allocator vault support * fix: lint * feat: updated vault registry with allocator vaults + venus-flux support * fix: 4626 unit test fixture * fix: add wrap unwrap for wbnb vaults
1 parent 3034a69 commit 37813c8

13 files changed

Lines changed: 2352 additions & 4 deletions

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,45 @@ The CLI reads JSON from stdin and writes JSON to stdout:
140140
### CLI Examples (Bash)
141141

142142
```bash
143-
# List supported yields
144-
echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | npx @yieldxyz/shield
145-
146143
# Check if a yield is supported
147144
echo '{"apiVersion":"1.0","operation":"isSupported","yieldId":"ethereum-eth-lido-staking"}' | npx @yieldxyz/shield
148145

149146
# Validate a transaction
150147
echo '{"apiVersion":"1.0","operation":"validate","yieldId":"ethereum-eth-lido-staking","unsignedTransaction":"{...}","userAddress":"0x..."}' | npx @yieldxyz/shield
148+
149+
150+
# List supported yields
151+
echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | npx @yieldxyz/shield
151152
```
152153

153154
## Supported Yield IDs
154155

155156
- `ethereum-eth-lido-staking`
156157
- `solana-sol-native-multivalidator-staking`
157158
- `tron-trx-native-staking`
159+
- All generic ERC4626 vault yields from: Angle, Curve, Euler, Fluid, Gearbox, Idle Finance, Lista, Morpho, Sky, SummerFi, Venus Flux, Yearn, Yo Protocol
160+
161+
To see the full list:
162+
163+
```bash
164+
echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | npx @yieldxyz/shield
165+
```
166+
167+
> **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.
168+
169+
### ERC4626 Vault Operations
170+
171+
Shield validates the following operations for all supported ERC4626 vaults:
172+
173+
| Operation | Transaction Type | Description |
174+
| ----------- | ---------------- | --------------------------------------------- |
175+
| Approve | APPROVAL | ERC20 token approval for vault deposit |
176+
| Deposit | SUPPLY | Deposit assets into vault |
177+
| Mint | SUPPLY | Mint vault shares |
178+
| Withdraw | WITHDRAW | Withdraw assets from vault |
179+
| Redeem | WITHDRAW | Redeem vault shares |
180+
| WETH Wrap | WRAP | Convert native ETH to WETH (WETH vaults only) |
181+
| WETH Unwrap | UNWRAP | Convert WETH to native ETH (WETH vaults only) |
158182

159183
## API Reference
160184

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

238+
### Embedded Vault Registry
239+
240+
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.
241+
214242
### Verifying Binary Integrity
215243

216244
Always verify downloaded binaries:

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@yieldxyz/shield",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"description": "Zero-trust transaction validation library for Yield.xyz integrations.",
55
"packageManager": "pnpm@10.12.2",
66
"main": "./dist/index.js",
@@ -32,6 +32,7 @@
3232
},
3333
"files": [
3434
"dist",
35+
"src/validators/evm/erc4626/vault-registry.json",
3536
"package.json",
3637
"README.md",
3738
"LICENSE"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const WETH_ADDRESSES: Record<number, string> = {
2+
1: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // Ethereum
3+
42161: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', // Arbitrum
4+
10: '0x4200000000000000000000000000000000000006', // Optimism
5+
8453: '0x4200000000000000000000000000000000000006', // Base
6+
130: '0x4200000000000000000000000000000000000006', // Unichain
7+
56: '0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c', // BSC (WBNB)
8+
};
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { ethers } from 'ethers';
2+
import { ERC4626Validator } from './erc4626.validator';
3+
import { loadEmbeddedRegistry } from './vault-config';
4+
import { TransactionType } from '../../../types';
5+
import { WETH_ADDRESSES } from './constants';
6+
7+
import { GENERIC_ERC4626_PROTOCOLS } from '../../index';
8+
9+
const erc20Iface = new ethers.Interface([
10+
'function approve(address spender, uint256 amount) returns (bool)',
11+
]);
12+
const erc4626Iface = new ethers.Interface([
13+
'function deposit(uint256 assets, address receiver) returns (uint256)',
14+
'function redeem(uint256 shares, address receiver, address owner) returns (uint256)',
15+
]);
16+
const wethIface = new ethers.Interface([
17+
'function deposit() payable',
18+
'function withdraw(uint256 wad)',
19+
]);
20+
21+
const USER = '0x742d35cc6634c0532925a3b844bc9e7595f0beb8';
22+
23+
function buildTx(fields: Record<string, unknown>): string {
24+
return JSON.stringify({
25+
from: USER,
26+
nonce: 0,
27+
gasLimit: '0x30d40',
28+
maxFeePerGas: '0x6fc23ac00',
29+
maxPriorityFeePerGas: '0x3b9aca00',
30+
type: 2,
31+
...fields,
32+
});
33+
}
34+
35+
const config = loadEmbeddedRegistry();
36+
const allowedVaults = config.vaults.filter((v) =>
37+
GENERIC_ERC4626_PROTOCOLS.has(v.protocol),
38+
);
39+
40+
describe(`ERC4626 exhaustive coverage — all ${allowedVaults.length} allowed vaults`, () => {
41+
it('should have allowed vaults to test', () => {
42+
expect(allowedVaults.length).toBeGreaterThan(0);
43+
});
44+
45+
for (const vault of allowedVaults) {
46+
const validator = new ERC4626Validator({
47+
vaults: [vault],
48+
lastUpdated: config.lastUpdated,
49+
});
50+
const active = vault.canEnter !== false && vault.canExit !== false;
51+
const label = `${vault.protocol}/${vault.network}${vault.yieldId}`;
52+
53+
if (active) {
54+
it(`APPROVAL — ${label}`, () => {
55+
const data = erc20Iface.encodeFunctionData('approve', [
56+
vault.address,
57+
ethers.parseUnits('100', 18),
58+
]);
59+
const tx = buildTx({
60+
to: vault.inputTokenAddress,
61+
data,
62+
value: '0x0',
63+
chainId: vault.chainId,
64+
});
65+
expect(
66+
validator.validate(tx, TransactionType.APPROVAL, USER).isValid,
67+
).toBe(true);
68+
});
69+
70+
it(`SUPPLY — ${label}`, () => {
71+
const data = erc4626Iface.encodeFunctionData('deposit', [
72+
ethers.parseUnits('100', 18),
73+
USER,
74+
]);
75+
const tx = buildTx({
76+
to: vault.address,
77+
data,
78+
value: '0x0',
79+
chainId: vault.chainId,
80+
});
81+
expect(
82+
validator.validate(tx, TransactionType.SUPPLY, USER).isValid,
83+
).toBe(true);
84+
});
85+
86+
it(`WITHDRAW — ${label}`, () => {
87+
const data = erc4626Iface.encodeFunctionData('redeem', [
88+
ethers.parseUnits('50', 18),
89+
USER,
90+
USER,
91+
]);
92+
const tx = buildTx({
93+
to: vault.address,
94+
data,
95+
value: '0x0',
96+
chainId: vault.chainId,
97+
});
98+
expect(
99+
validator.validate(tx, TransactionType.WITHDRAW, USER).isValid,
100+
).toBe(true);
101+
});
102+
103+
if (vault.isWethVault && WETH_ADDRESSES[vault.chainId]) {
104+
const wethAddr = WETH_ADDRESSES[vault.chainId];
105+
106+
it(`WRAP — ${label}`, () => {
107+
const data = wethIface.encodeFunctionData('deposit', []);
108+
const tx = buildTx({
109+
to: wethAddr,
110+
data,
111+
value: '0xde0b6b3a7640000',
112+
chainId: vault.chainId,
113+
});
114+
expect(
115+
validator.validate(tx, TransactionType.WRAP, USER).isValid,
116+
).toBe(true);
117+
});
118+
119+
it(`UNWRAP — ${label}`, () => {
120+
const data = wethIface.encodeFunctionData('withdraw', [
121+
ethers.parseEther('1'),
122+
]);
123+
const tx = buildTx({
124+
to: wethAddr,
125+
data,
126+
value: '0x0',
127+
chainId: vault.chainId,
128+
});
129+
expect(
130+
validator.validate(tx, TransactionType.UNWRAP, USER).isValid,
131+
).toBe(true);
132+
});
133+
}
134+
} else {
135+
if (vault.canEnter === false) {
136+
it(`SUPPLY blocked (paused) — ${label}`, () => {
137+
const data = erc4626Iface.encodeFunctionData('deposit', [
138+
ethers.parseUnits('100', 18),
139+
USER,
140+
]);
141+
const tx = buildTx({
142+
to: vault.address,
143+
data,
144+
value: '0x0',
145+
chainId: vault.chainId,
146+
});
147+
expect(
148+
validator.validate(tx, TransactionType.SUPPLY, USER).isValid,
149+
).toBe(false);
150+
});
151+
}
152+
153+
if (vault.canExit === false) {
154+
it(`WITHDRAW blocked (disabled) — ${label}`, () => {
155+
const data = erc4626Iface.encodeFunctionData('redeem', [
156+
ethers.parseUnits('50', 18),
157+
USER,
158+
USER,
159+
]);
160+
const tx = buildTx({
161+
to: vault.address,
162+
data,
163+
value: '0x0',
164+
chainId: vault.chainId,
165+
});
166+
expect(
167+
validator.validate(tx, TransactionType.WITHDRAW, USER).isValid,
168+
).toBe(false);
169+
});
170+
}
171+
}
172+
}
173+
const vaultsWithAllocators = allowedVaults.filter(
174+
(v) => v.allocatorVaults && v.allocatorVaults.length > 0,
175+
);
176+
177+
for (const vault of vaultsWithAllocators) {
178+
const validator = new ERC4626Validator({
179+
vaults: [vault],
180+
lastUpdated: config.lastUpdated,
181+
});
182+
183+
for (const allocAddr of vault.allocatorVaults!) {
184+
const label = `${vault.protocol}/${vault.network}${vault.yieldId} → allocator ${allocAddr.slice(0, 10)}`;
185+
186+
it(`SUPPLY (allocator) — ${label}`, () => {
187+
const data = erc4626Iface.encodeFunctionData('deposit', [
188+
ethers.parseUnits('100', 18),
189+
USER,
190+
]);
191+
const tx = buildTx({
192+
to: allocAddr,
193+
data,
194+
value: '0x0',
195+
chainId: vault.chainId,
196+
});
197+
expect(
198+
validator.validate(tx, TransactionType.SUPPLY, USER).isValid,
199+
).toBe(true);
200+
});
201+
202+
it(`WITHDRAW (allocator) — ${label}`, () => {
203+
const data = erc4626Iface.encodeFunctionData('redeem', [
204+
ethers.parseUnits('50', 18),
205+
USER,
206+
USER,
207+
]);
208+
const tx = buildTx({
209+
to: allocAddr,
210+
data,
211+
value: '0x0',
212+
chainId: vault.chainId,
213+
});
214+
expect(
215+
validator.validate(tx, TransactionType.WITHDRAW, USER).isValid,
216+
).toBe(true);
217+
});
218+
219+
it(`APPROVAL (allocator) — ${label}`, () => {
220+
const data = erc20Iface.encodeFunctionData('approve', [
221+
ethers.getAddress(allocAddr),
222+
ethers.parseUnits('100', 18),
223+
]);
224+
const tx = buildTx({
225+
to: vault.inputTokenAddress,
226+
data,
227+
value: '0x0',
228+
chainId: vault.chainId,
229+
});
230+
expect(
231+
validator.validate(tx, TransactionType.APPROVAL, USER).isValid,
232+
).toBe(true);
233+
});
234+
}
235+
}
236+
});

0 commit comments

Comments
 (0)