145 lines
4.6 KiB
TypeScript
145 lines
4.6 KiB
TypeScript
import type { Scenario, ScenarioStep } from '../types.js';
|
|
import { ScenarioRunner } from './scenario-runner.js';
|
|
import type { ForkOrchestrator } from './fork-orchestrator.js';
|
|
import type { ProtocolAdapter, Network } from '../types.js';
|
|
import type { RunReport } from '../types.js';
|
|
|
|
/**
|
|
* Fuzzer
|
|
* Runs scenarios with parameterized inputs
|
|
*/
|
|
export class Fuzzer {
|
|
constructor(
|
|
private fork: ForkOrchestrator,
|
|
private adapters: Map<string, ProtocolAdapter>,
|
|
private network: Network
|
|
) {}
|
|
|
|
/**
|
|
* Fuzz test a scenario with parameterized inputs
|
|
*/
|
|
async fuzz(
|
|
scenario: Scenario,
|
|
options: {
|
|
iterations: number;
|
|
seed?: number;
|
|
parameterRanges?: Record<string, { min: number; max: number; step?: number }>;
|
|
}
|
|
): Promise<RunReport[]> {
|
|
const results: RunReport[] = [];
|
|
const runner = new ScenarioRunner(this.fork, this.adapters, this.network);
|
|
|
|
// Simple seeded RNG
|
|
let rngSeed = options.seed || Math.floor(Math.random() * 1000000);
|
|
const rng = () => {
|
|
rngSeed = (rngSeed * 9301 + 49297) % 233280;
|
|
return rngSeed / 233280;
|
|
};
|
|
|
|
console.log(`Fuzzing scenario with ${options.iterations} iterations (seed: ${options.seed || 'random'})`);
|
|
|
|
for (let i = 0; i < options.iterations; i++) {
|
|
console.log(`\n=== Iteration ${i + 1}/${options.iterations} ===`);
|
|
|
|
// Create a mutated scenario
|
|
const mutatedScenario = this.mutateScenario(scenario, options.parameterRanges || {}, rng);
|
|
|
|
try {
|
|
// Create a snapshot before running
|
|
const snapshotId = await this.fork.snapshot(`fuzz_${i}`);
|
|
|
|
// Run the scenario
|
|
const report = await runner.run(mutatedScenario);
|
|
results.push(report);
|
|
|
|
// Revert to snapshot for next iteration
|
|
await this.fork.revert(snapshotId);
|
|
|
|
console.log(` Result: ${report.passed ? 'PASSED' : 'FAILED'}`);
|
|
if (!report.passed) {
|
|
console.log(` Error: ${report.error}`);
|
|
}
|
|
} catch (error: any) {
|
|
console.error(` Error in iteration ${i + 1}: ${error.message}`);
|
|
// Continue with next iteration
|
|
}
|
|
}
|
|
|
|
// Summary
|
|
const passed = results.filter(r => r.passed).length;
|
|
const failed = results.filter(r => !r.passed).length;
|
|
console.log(`\n=== Fuzzing Summary ===`);
|
|
console.log(`Total iterations: ${options.iterations}`);
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
console.log(`Success rate: ${((passed / options.iterations) * 100).toFixed(2)}%`);
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Mutate a scenario with random parameter values
|
|
*/
|
|
private mutateScenario(
|
|
scenario: Scenario,
|
|
parameterRanges: Record<string, { min: number; max: number; step?: number }>,
|
|
rng: () => number
|
|
): Scenario {
|
|
const mutated = JSON.parse(JSON.stringify(scenario)) as Scenario;
|
|
|
|
// Mutate step arguments
|
|
for (const step of mutated.steps) {
|
|
this.mutateStep(step, parameterRanges, rng);
|
|
}
|
|
|
|
return mutated;
|
|
}
|
|
|
|
/**
|
|
* Mutate a single step
|
|
*/
|
|
private mutateStep(
|
|
step: ScenarioStep,
|
|
parameterRanges: Record<string, { min: number; max: number; step?: number }>,
|
|
rng: () => number
|
|
): void {
|
|
// Mutate amount parameters
|
|
if (step.args.amount && typeof step.args.amount === 'string') {
|
|
const amountNum = parseFloat(step.args.amount);
|
|
if (!isNaN(amountNum)) {
|
|
// Apply random variation (±20%)
|
|
const variation = (rng() - 0.5) * 0.4; // -0.2 to 0.2
|
|
const newAmount = amountNum * (1 + variation);
|
|
step.args.amount = newAmount.toFixed(6);
|
|
}
|
|
}
|
|
|
|
// Mutate percentage-based parameters
|
|
if (step.args.pctDelta !== undefined && typeof step.args.pctDelta === 'number') {
|
|
if (parameterRanges.pctDelta) {
|
|
const { min, max, step: stepSize } = parameterRanges.pctDelta;
|
|
const range = max - min;
|
|
const steps = stepSize ? Math.floor(range / stepSize) : 100;
|
|
const randomStep = Math.floor(rng() * steps);
|
|
step.args.pctDelta = min + (stepSize || (range / steps)) * randomStep;
|
|
} else {
|
|
// Default: vary between -20% and 20%
|
|
step.args.pctDelta = (rng() - 0.5) * 40;
|
|
}
|
|
}
|
|
|
|
// Mutate fee parameters (for Uniswap)
|
|
if (step.args.fee !== undefined && typeof step.args.fee === 'number') {
|
|
const fees = [100, 500, 3000, 10000]; // Common Uniswap fees
|
|
step.args.fee = fees[Math.floor(rng() * fees.length)];
|
|
}
|
|
|
|
// Mutate slippage
|
|
if (step.args.slippageBps !== undefined && typeof step.args.slippageBps === 'number') {
|
|
// Vary slippage between 10 and 100 bps (0.1% to 1%)
|
|
step.args.slippageBps = Math.floor(10 + rng() * 90);
|
|
}
|
|
}
|
|
}
|
|
|