From c7d32a3ded18c854c8c5822e020f39caeb55b1f3 Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Thu, 10 Sep 2026 20:06:05 -0700 Subject: [PATCH 1/2] feat: explicit input token for allocator vault --- src/json/schema.ts | 4 ++ src/types/index.ts | 2 + .../evm/erc4626/erc4626.validator.ts | 71 ++++++++++++------- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/json/schema.ts b/src/json/schema.ts index 52d0138..6fd987b 100644 --- a/src/json/schema.ts +++ b/src/json/schema.ts @@ -74,6 +74,10 @@ export const requestSchema = { depositFeeBps: { type: 'number', minimum: 0, maximum: 10000 }, feeRecipientAddress: { type: 'string', maxLength: 128 }, allocatorVaultAddress: { type: 'string', maxLength: 128 }, + allocatorVaultInputTokenAddress: { + type: 'string', + maxLength: 128, + }, }, }, }, diff --git a/src/types/index.ts b/src/types/index.ts index 81faec3..fb5dd9a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -44,6 +44,8 @@ export interface FeeConfiguration { depositFeeBps?: number; feeRecipientAddress?: string; allocatorVaultAddress?: string; + /** Input token of the injected OAV. Required for injected-OAV APPROVAL. */ + allocatorVaultInputTokenAddress?: string; } export interface ValidationContext { diff --git a/src/validators/evm/erc4626/erc4626.validator.ts b/src/validators/evm/erc4626/erc4626.validator.ts index cfa6bc4..9b79be3 100644 --- a/src/validators/evm/erc4626/erc4626.validator.ts +++ b/src/validators/evm/erc4626/erc4626.validator.ts @@ -265,12 +265,19 @@ export class ERC4626Validator extends BaseEVMValidator { // Validate spender is a whitelisted vault (static registry, then injected OAV) const spenderAddress = spender.toLowerCase(); let vaultInfo = this.vaultInfoMap.get(`${chainId}:${spenderAddress}`); - if ( - !vaultInfo && - this.getInjectedAllocatorVaults(context).has(spenderAddress) - ) { - vaultInfo = this.getBaseVaultForChain(chainId); + if (!vaultInfo) { + const injected = this.getInjectedAllocatorVaults(context); + const injectedInputToken = injected.get(spenderAddress); + if (injectedInputToken) { + vaultInfo = this.synthesizeInjectedVault( + spenderAddress, + chainId, + injectedInputToken, + ); + } } + + if (!vaultInfo) { return this.blocked('Approval spender is not a whitelisted vault', { spender, @@ -665,14 +672,14 @@ export class ERC4626Validator extends BaseEVMValidator { } const staticVault = this.vaultInfoMap.get(`${chainId}:${vaultAddress}`); if (staticVault) return { vaultInfo: staticVault }; - // Runtime, DB-sourced OAV: accept if injected via context - if (this.getInjectedAllocatorVaults(context).has(vaultAddress)) { - const base = this.getBaseVaultForChain(chainId); - if (base) { - return { - vaultInfo: { ...base, address: vaultAddress }, - }; - } + const injected = this.getInjectedAllocatorVaults(context); + if (injected.has(vaultAddress)) { + const synthesized = this.synthesizeInjectedVault( + vaultAddress, + chainId, + injected.get(vaultAddress), + ); + if (synthesized) return { vaultInfo: synthesized }; } return { error: this.blocked('Vault address not whitelisted', { @@ -699,22 +706,38 @@ export class ERC4626Validator extends BaseEVMValidator { return WETH_ADDRESSES[chainId] || null; } - private getInjectedAllocatorVaults(context?: ValidationContext): Set { - const injected = new Set(); + private getInjectedAllocatorVaults( + context?: ValidationContext, + ): Map { + const injected = new Map(); for (const fee of context?.feeConfiguration ?? []) { - if (isNonEmptyString(fee.allocatorVaultAddress)) { - injected.add(fee.allocatorVaultAddress.toLowerCase()); - } + if (!isNonEmptyString(fee.allocatorVaultAddress)) continue; + const address = fee.allocatorVaultAddress.toLowerCase(); + const inputToken = isNonEmptyString(fee.allocatorVaultInputTokenAddress) + ? fee.allocatorVaultInputTokenAddress.toLowerCase() + : undefined; + injected.set(address, inputToken); } return injected; } - - // The instance is yield-scoped to one base vault; use it as the template - // for a context-injected OAV (input token + protocol metadata). - private getBaseVaultForChain(chainId: number): VaultInfo | undefined { + private synthesizeInjectedVault( + vaultAddress: string, + chainId: number, + injectedInputToken?: string, + ): VaultInfo | undefined { + let base: VaultInfo | undefined; for (const vault of this.vaultInfoMap.values()) { - if (vault.chainId === chainId) return vault; + if (vault.chainId === chainId) { + base = vault; + break; + } } - return undefined; + if (!base) return undefined; + return { + ...base, + address: vaultAddress, + inputTokenAddress: injectedInputToken ?? base.inputTokenAddress, + allocatorVaults: [vaultAddress], + }; } } From 34b92e8cb1b42c9ee5b87ab7426ad3571136c520 Mon Sep 17 00:00:00 2001 From: Akash Jag Date: Thu, 10 Sep 2026 22:42:25 -0700 Subject: [PATCH 2/2] feat: tests for injected allocator token --- .../evm/erc4626/erc4626.validator.test.ts | 86 ++++++++++++++++++- .../evm/erc4626/erc4626.validator.ts | 1 - 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/validators/evm/erc4626/erc4626.validator.test.ts b/src/validators/evm/erc4626/erc4626.validator.test.ts index 5dec195..aa456c9 100644 --- a/src/validators/evm/erc4626/erc4626.validator.test.ts +++ b/src/validators/evm/erc4626/erc4626.validator.test.ts @@ -2103,6 +2103,27 @@ describe('ERC4626Validator', () => { }, ], }; + + const runtimeContextWithInputToken = { + feeConfiguration: [ + { + allocatorVaultAddress: INJECTED_ALLOCATOR_VAULT_ADDRESS, + allocatorVaultInputTokenAddress: INPUT_TOKEN, + }, + ], + }; + + const META_OAV_INPUT_TOKEN = '0x82af49447d8a07e3bd95bd0d56f35241523fbab1'; + + const runtimeContextWithMetaInputToken = { + feeConfiguration: [ + { + allocatorVaultAddress: INJECTED_ALLOCATOR_VAULT_ADDRESS, + allocatorVaultInputTokenAddress: META_OAV_INPUT_TOKEN, + }, + ], + }; + it('should validate SUPPLY deposit to a context-injected allocator vault', () => { const data = erc4626Iface.encodeFunctionData('deposit', [ ethers.parseUnits('1000', 6), @@ -2194,7 +2215,7 @@ describe('ERC4626Validator', () => { TransactionType.APPROVAL, USER_ADDRESS, undefined, - runtimeContext, + runtimeContextWithInputToken, ); expect(result.isValid).toBe(true); }); @@ -2208,6 +2229,28 @@ describe('ERC4626Validator', () => { data, value: '0x0', }); + const result = runtimeValidator.validate( + tx, + TransactionType.APPROVAL, + USER_ADDRESS, + undefined, + runtimeContextWithInputToken, + ); + expect(result.isValid).toBe(false); + expect(result.reason).toContain( + 'Approval token does not match vault input token', + ); + }); + it('should reject APPROVAL of an injected OAV when allocatorVaultInputTokenAddress is omitted', () => { + const data = erc20Iface.encodeFunctionData('approve', [ + INJECTED_ALLOCATOR_VAULT_ADDRESS, + ethers.parseUnits('1000', 6), + ]); + const tx = buildTx({ + to: INPUT_TOKEN, + data, + value: '0x0', + }); const result = runtimeValidator.validate( tx, TransactionType.APPROVAL, @@ -2216,10 +2259,51 @@ describe('ERC4626Validator', () => { runtimeContext, ); expect(result.isValid).toBe(false); + expect(result.reason).toContain( + 'Approval spender is not a whitelisted vault', + ); + }); + it('should reject APPROVAL when injected OAV input token does not match the token', () => { + const data = erc20Iface.encodeFunctionData('approve', [ + INJECTED_ALLOCATOR_VAULT_ADDRESS, + ethers.parseUnits('1000', 6), + ]); + const tx = buildTx({ + to: INPUT_TOKEN, + data, + value: '0x0', + }); + const result = runtimeValidator.validate( + tx, + TransactionType.APPROVAL, + USER_ADDRESS, + undefined, + runtimeContextWithMetaInputToken, + ); + expect(result.isValid).toBe(false); expect(result.reason).toContain( 'Approval token does not match vault input token', ); }); + it('should validate APPROVAL when injected OAV input token differs from the base vault token', () => { + const data = erc20Iface.encodeFunctionData('approve', [ + INJECTED_ALLOCATOR_VAULT_ADDRESS, + ethers.parseUnits('1000', 18), + ]); + const tx = buildTx({ + to: META_OAV_INPUT_TOKEN, + data, + value: '0x0', + }); + const result = runtimeValidator.validate( + tx, + TransactionType.APPROVAL, + USER_ADDRESS, + undefined, + runtimeContextWithMetaInputToken, + ); + expect(result.isValid).toBe(true); + }); it('should reject the injected allocator SUPPLY when context is omitted', () => { const data = erc4626Iface.encodeFunctionData('deposit', [ ethers.parseUnits('1000', 6), diff --git a/src/validators/evm/erc4626/erc4626.validator.ts b/src/validators/evm/erc4626/erc4626.validator.ts index 9b79be3..590aae9 100644 --- a/src/validators/evm/erc4626/erc4626.validator.ts +++ b/src/validators/evm/erc4626/erc4626.validator.ts @@ -277,7 +277,6 @@ export class ERC4626Validator extends BaseEVMValidator { } } - if (!vaultInfo) { return this.blocked('Approval spender is not a whitelisted vault', { spender,