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
4 changes: 4 additions & 0 deletions src/json/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
86 changes: 85 additions & 1 deletion src/validators/evm/erc4626/erc4626.validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -2194,7 +2215,7 @@ describe('ERC4626Validator', () => {
TransactionType.APPROVAL,
USER_ADDRESS,
undefined,
runtimeContext,
runtimeContextWithInputToken,
);
expect(result.isValid).toBe(true);
});
Expand All @@ -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,
Expand All @@ -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),
Expand Down
70 changes: 46 additions & 24 deletions src/validators/evm/erc4626/erc4626.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@

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

Check warning on line 130 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 130 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 130 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 135 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 135 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 135 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 @@ -265,12 +265,18 @@
// 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) {

Check warning on line 271 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 271 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 271 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
vaultInfo = this.synthesizeInjectedVault(
spenderAddress,
chainId,
injectedInputToken,
);
}
}

if (!vaultInfo) {
return this.blocked('Approval spender is not a whitelisted vault', {
spender,
Expand Down Expand Up @@ -665,14 +671,14 @@
}
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', {
Expand All @@ -699,22 +705,38 @@
return WETH_ADDRESSES[chainId] || null;
}

private getInjectedAllocatorVaults(context?: ValidationContext): Set<string> {
const injected = new Set<string>();
private getInjectedAllocatorVaults(
context?: ValidationContext,
): Map<string, string | undefined> {
const injected = new Map<string, string | undefined>();
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],
};
}
}
Loading