Initial commit: add .gitignore and README

This commit is contained in:
defiQUG
2026-02-09 21:51:54 -08:00
commit 7003349717
127 changed files with 17576 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { AaveV3Adapter } from "../../../src/adapters/aaveV3.js";
describe("Aave V3 Adapter", () => {
let adapter: AaveV3Adapter;
let mockProvider: any;
let mockSigner: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
mockSigner = {
getAddress: vi.fn().mockResolvedValue("0x1234567890123456789012345678901234567890"),
};
// Mock the adapter constructor
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
supply: vi.fn(),
withdraw: vi.fn(),
borrow: vi.fn(),
repay: vi.fn(),
flashLoanSimple: vi.fn(),
setUserEMode: vi.fn(),
setUserUseReserveAsCollateral: vi.fn(),
getUserAccountData: vi.fn(),
interface: {
parseLog: vi.fn(),
},
}));
});
it("should validate asset address on supply", async () => {
adapter = new AaveV3Adapter("mainnet", mockSigner as any);
await expect(
adapter.supply("0x0000000000000000000000000000000000000000", 1000n)
).rejects.toThrow("Invalid asset address");
});
it("should validate asset address on withdraw", async () => {
adapter = new AaveV3Adapter("mainnet", mockSigner as any);
await expect(
adapter.withdraw("0x0000000000000000000000000000000000000000", 1000n)
).rejects.toThrow("Invalid asset address");
});
it("should calculate health factor correctly", async () => {
adapter = new AaveV3Adapter("mainnet");
const mockData = {
totalCollateralBase: 2000000n,
totalDebtBase: 1000000n,
availableBorrowsBase: 500000n,
currentLiquidationThreshold: 8000n, // 80%
ltv: 7500n, // 75%
healthFactor: 2000000000000000000n, // 2.0
};
// @ts-ignore
adapter.dataProvider.getUserAccountData = vi.fn().mockResolvedValue([
mockData.totalCollateralBase,
mockData.totalDebtBase,
mockData.availableBorrowsBase,
mockData.currentLiquidationThreshold,
mockData.ltv,
mockData.healthFactor,
]);
const hf = await adapter.getUserHealthFactor("0x123");
expect(hf).toBeGreaterThan(0);
});
it("should handle different interest rate modes", async () => {
adapter = new AaveV3Adapter("mainnet", mockSigner as any);
// Test variable rate mode (default)
// Test stable rate mode
// These would require mocking the contract calls
expect(true).toBe(true);
});
});

View File

@@ -0,0 +1,79 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { AggregatorAdapter } from "../../../src/adapters/aggregators.js";
describe("Aggregator Adapter", () => {
let adapter: AggregatorAdapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
swap: vi.fn(),
transformERC20: vi.fn(),
interface: {
encodeFunctionData: vi.fn(),
},
}));
// Mock fetch
global.fetch = vi.fn();
});
it("should get 1inch quote", async () => {
adapter = new AggregatorAdapter("mainnet");
// Mock 1inch API response
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({
toAmount: "1000000",
}),
});
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({
tx: {
data: "0x1234",
to: "0x1111111254EEB25477B68fb85Ed929f73A960582",
},
}),
});
const quote = await adapter.get1InchQuote(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
1000000n,
50
);
expect(quote).toBeDefined();
expect(quote?.amountOut).toBeGreaterThan(0n);
expect(quote?.data).toBeDefined();
});
it("should fallback when 1inch API fails", async () => {
adapter = new AggregatorAdapter("mainnet");
// Mock API failure
(global.fetch as any).mockResolvedValueOnce({
ok: false,
});
const quote = await adapter.get1InchQuote(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
1000000n,
50
);
// Should return fallback quote
expect(quote).toBeDefined();
});
});

View File

@@ -0,0 +1,74 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { BalancerAdapter } from "../../../src/adapters/balancer.js";
describe("Balancer Adapter", () => {
let adapter: BalancerAdapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
swap: vi.fn(),
batchSwap: vi.fn(),
interface: {
encodeFunctionData: vi.fn(),
},
}));
});
it("should encode single swap", async () => {
adapter = new BalancerAdapter("mainnet");
const swapParams = {
poolId: "0x...",
kind: 0, // GIVEN_IN
assetIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
assetOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
amount: 1000000n,
userData: "0x",
};
const data = await adapter.swap(swapParams);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
it("should encode batch swap", async () => {
adapter = new BalancerAdapter("mainnet");
const batchSwapParams = {
kind: 0,
swaps: [{
poolId: "0x...",
assetInIndex: 0,
assetOutIndex: 1,
amount: 1000000n,
userData: "0x",
}],
assets: [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
],
funds: {
sender: "0x123",
fromInternalBalance: false,
recipient: "0x123",
toInternalBalance: false,
},
limits: [0n, 0n],
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
};
const data = await adapter.batchSwap(batchSwapParams);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
});

View File

@@ -0,0 +1,70 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { CompoundV3Adapter } from "../../../src/adapters/compoundV3.js";
describe("Compound V3 Adapter", () => {
let adapter: CompoundV3Adapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
supply: vi.fn(),
withdraw: vi.fn(),
borrow: vi.fn(),
repay: vi.fn(),
allow: vi.fn(),
getAccountLiquidity: vi.fn(),
interface: {
parseLog: vi.fn(),
},
}));
});
it("should supply assets", async () => {
adapter = new CompoundV3Adapter("mainnet");
// Test would verify supply call encoding
expect(adapter).toBeDefined();
});
it("should withdraw assets", async () => {
adapter = new CompoundV3Adapter("mainnet");
// Test would verify withdraw call encoding
expect(adapter).toBeDefined();
});
it("should borrow assets", async () => {
adapter = new CompoundV3Adapter("mainnet");
// Test would verify borrow call encoding
expect(adapter).toBeDefined();
});
it("should repay assets", async () => {
adapter = new CompoundV3Adapter("mainnet");
// Test would verify repay call encoding
expect(adapter).toBeDefined();
});
it("should calculate account liquidity", async () => {
adapter = new CompoundV3Adapter("mainnet");
const mockLiquidity = {
isLiquid: true,
shortfall: 0n,
};
// @ts-ignore
adapter.comet.getAccountLiquidity = vi.fn().mockResolvedValue([
true,
0n,
]);
const liquidity = await adapter.getAccountLiquidity("0x123");
expect(liquidity).toBeDefined();
});
});

View File

@@ -0,0 +1,69 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { CurveAdapter } from "../../../src/adapters/curve.js";
describe("Curve Adapter", () => {
let adapter: CurveAdapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
exchange: vi.fn(),
exchange_underlying: vi.fn(),
get_pool_from_lp_token: vi.fn(),
interface: {
encodeFunctionData: vi.fn(),
},
}));
});
it("should find pool for LP token", async () => {
adapter = new CurveAdapter("mainnet");
// Mock registry response
mockProvider.call = vi.fn().mockResolvedValue(
"0x000000000000000000000000bebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
);
const pool = await adapter.findPool("0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490");
expect(pool).toBeDefined();
});
it("should encode exchange", async () => {
adapter = new CurveAdapter("mainnet");
const data = await adapter.exchange(
"0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
0,
1,
1000000n,
990000n
);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
it("should encode exchange_underlying", async () => {
adapter = new CurveAdapter("mainnet");
const data = await adapter.exchangeUnderlying(
"0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
0,
1,
1000000n,
990000n
);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
});

View File

@@ -0,0 +1,42 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { LidoAdapter } from "../../../src/adapters/lido.js";
describe("Lido Adapter", () => {
let adapter: LidoAdapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
wrap: vi.fn(),
unwrap: vi.fn(),
interface: {
encodeFunctionData: vi.fn(),
},
}));
});
it("should encode wrap operation", async () => {
adapter = new LidoAdapter("mainnet");
const data = await adapter.wrap(1000000000000000000n);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
it("should encode unwrap operation", async () => {
adapter = new LidoAdapter("mainnet");
const data = await adapter.unwrap(1000000000000000000n);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
});

View File

@@ -0,0 +1,77 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { MakerAdapter } from "../../../src/adapters/maker.js";
describe("MakerDAO Adapter", () => {
let adapter: MakerAdapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
open: vi.fn(),
frob: vi.fn(),
join: vi.fn(),
exit: vi.fn(),
interface: {
parseLog: vi.fn(),
},
}));
});
it("should open vault and parse CDP ID", async () => {
adapter = new MakerAdapter("mainnet");
// Mock transaction with NewCdp event
const mockReceipt = {
logs: [{
topics: ["0x...", "0x0000000000000000000000000000000000000000000000000000000000000123"],
data: "0x",
}],
};
// @ts-ignore
adapter.cdpManager.open = vi.fn().mockResolvedValue({
wait: vi.fn().mockResolvedValue(mockReceipt),
});
// @ts-ignore
adapter.cdpManager.interface.parseLog = vi.fn().mockReturnValue({
name: "NewCdp",
args: {
usr: "0x123",
own: "0x456",
cdp: 291n, // CDP ID
},
});
const cdpId = await adapter.openVault("ETH-A");
expect(cdpId).toBe(291n);
});
it("should encode frob operation", async () => {
adapter = new MakerAdapter("mainnet");
const data = await adapter.frob(291n, 1000000000000000000n, 1000n);
expect(data).toBeDefined();
});
it("should encode join operation", async () => {
adapter = new MakerAdapter("mainnet");
const data = await adapter.join(1000000n);
expect(data).toBeDefined();
});
it("should encode exit operation", async () => {
adapter = new MakerAdapter("mainnet");
const data = await adapter.exit(1000000n);
expect(data).toBeDefined();
});
});

View File

@@ -0,0 +1,62 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { PerpsAdapter } from "../../../src/adapters/perps.js";
describe("Perps Adapter", () => {
let adapter: PerpsAdapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
increasePosition: vi.fn(),
decreasePosition: vi.fn(),
interface: {
encodeFunctionData: vi.fn(),
},
}));
});
it("should encode increase position", async () => {
adapter = new PerpsAdapter("mainnet");
const params = {
path: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
indexToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
amountIn: 1000000n,
minOut: 990000n,
sizeDelta: 2000000n,
isLong: true,
acceptablePrice: 1000000n,
};
const data = await adapter.increasePosition(params);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
it("should encode decrease position", async () => {
adapter = new PerpsAdapter("mainnet");
const params = {
path: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
indexToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
collateralDelta: 500000n,
sizeDelta: 1000000n,
isLong: true,
receiver: "0x123",
acceptablePrice: 1000000n,
};
const data = await adapter.decreasePosition(params);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
});

View File

@@ -0,0 +1,104 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UniswapV3Adapter } from "../../../src/adapters/uniswapV3.js";
describe("Uniswap V3 Adapter", () => {
let adapter: UniswapV3Adapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
vi.spyOn(require("ethers"), "JsonRpcProvider").mockImplementation(() => mockProvider);
vi.spyOn(require("ethers"), "Contract").mockImplementation(() => ({
exactInputSingle: vi.fn(),
exactOutputSingle: vi.fn(),
quoteExactInputSingle: vi.fn(),
interface: {
encodeFunctionData: vi.fn(),
},
}));
});
it("should encode exact input swap", async () => {
adapter = new UniswapV3Adapter("mainnet");
const params = {
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
fee: 3000,
amountIn: 1000000n,
amountOutMinimum: 990000n,
recipient: "0x123",
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
};
const data = await adapter.swapExactInput(params);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
it("should encode exact output swap", async () => {
adapter = new UniswapV3Adapter("mainnet");
const params = {
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
fee: 3000,
amountOut: 1000000n,
amountInMaximum: 1010000n,
recipient: "0x123",
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
};
const data = await adapter.swapExactOutput(params);
expect(data).toBeDefined();
expect(data.to).toBeDefined();
expect(data.data).toBeDefined();
});
it("should encode path correctly", () => {
adapter = new UniswapV3Adapter("mainnet");
const path = adapter.encodePath(
["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xdAC17F958D2ee523a2206206994597C13D831ec7"],
[3000]
);
expect(path).toBeDefined();
expect(path.length).toBeGreaterThan(0);
});
it("should get quote for exact input", async () => {
adapter = new UniswapV3Adapter("mainnet");
// Mock quoter response
mockProvider.call = vi.fn().mockResolvedValue(
"0x00000000000000000000000000000000000000000000000000000000000f4240" // 1000000
);
const quote = await adapter.quoteExactInput(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
3000,
1000000n
);
expect(quote).toBeGreaterThan(0n);
});
it("should validate fee tiers", () => {
adapter = new UniswapV3Adapter("mainnet");
const validFees = [100, 500, 3000, 10000];
validFees.forEach(fee => {
expect(adapter.isValidFee(fee)).toBe(true);
});
expect(adapter.isValidFee(999)).toBe(false);
});
});

View File

@@ -0,0 +1,83 @@
import { describe, it, expect } from "vitest";
import { evaluateMaxGas } from "../../../src/guards/maxGas.js";
import { Guard } from "../../../src/strategy.schema.js";
import { GasEstimate } from "../../../src/utils/gas.js";
describe("Max Gas Guard", () => {
it("should pass when gas is below limit", () => {
const guard: Guard = {
type: "maxGas",
params: {
maxGasLimit: "5000000",
},
};
const gasEstimate: GasEstimate = {
gasLimit: 2000000n,
maxFeePerGas: 100000000000n,
maxPriorityFeePerGas: 2000000000n,
};
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
expect(result.passed).toBe(true);
});
it("should fail when gas exceeds limit", () => {
const guard: Guard = {
type: "maxGas",
params: {
maxGasLimit: "2000000",
},
};
const gasEstimate: GasEstimate = {
gasLimit: 3000000n,
maxFeePerGas: 100000000000n,
maxPriorityFeePerGas: 2000000000n,
};
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
expect(result.passed).toBe(false);
expect(result.reason).toContain("exceeds");
});
it("should handle gas price limits", () => {
const guard: Guard = {
type: "maxGas",
params: {
maxGasLimit: "5000000",
maxGasPrice: "100000000000", // 100 gwei
},
};
const gasEstimate: GasEstimate = {
gasLimit: 2000000n,
maxFeePerGas: 50000000000n, // 50 gwei
maxPriorityFeePerGas: 2000000000n,
};
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
expect(result.passed).toBe(true);
});
it("should fail when gas price exceeds limit", () => {
const guard: Guard = {
type: "maxGas",
params: {
maxGasLimit: "5000000",
maxGasPrice: "100000000000", // 100 gwei
},
};
const gasEstimate: GasEstimate = {
gasLimit: 2000000n,
maxFeePerGas: 150000000000n, // 150 gwei
maxPriorityFeePerGas: 2000000000n,
};
const result = evaluateMaxGas(guard, gasEstimate, "mainnet");
expect(result.passed).toBe(false);
expect(result.reason).toContain("gas price");
});
});

View File

@@ -0,0 +1,70 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { evaluateMinHealthFactor } from "../../../src/guards/minHealthFactor.js";
import { AaveV3Adapter } from "../../../src/adapters/aaveV3.js";
import { Guard } from "../../../src/strategy.schema.js";
describe("Min Health Factor Guard", () => {
let mockAave: AaveV3Adapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
};
mockAave = new AaveV3Adapter("mainnet");
// @ts-ignore - access private property for testing
mockAave.provider = mockProvider;
});
it("should pass when health factor is above minimum", async () => {
const guard: Guard = {
type: "minHealthFactor",
params: {
minHF: 1.2,
user: "0x1234567890123456789012345678901234567890",
},
};
// Mock health factor calculation
vi.spyOn(mockAave, "getUserHealthFactor").mockResolvedValue(1.5);
const result = await evaluateMinHealthFactor(guard, mockAave, {});
expect(result.passed).toBe(true);
});
it("should fail when health factor is below minimum", async () => {
const guard: Guard = {
type: "minHealthFactor",
params: {
minHF: 1.2,
user: "0x1234567890123456789012345678901234567890",
},
};
// Mock health factor below minimum
vi.spyOn(mockAave, "getUserHealthFactor").mockResolvedValue(1.1);
const result = await evaluateMinHealthFactor(guard, mockAave, {});
expect(result.passed).toBe(false);
expect(result.reason).toContain("health factor");
});
it("should handle missing user position", async () => {
const guard: Guard = {
type: "minHealthFactor",
params: {
minHF: 1.2,
user: "0x0000000000000000000000000000000000000000",
},
};
// Mock no position
vi.spyOn(mockAave, "getUserHealthFactor").mockResolvedValue(0);
const result = await evaluateMinHealthFactor(guard, mockAave, {});
expect(result.passed).toBe(false);
expect(result.reason).toBeDefined();
});
});

View File

@@ -0,0 +1,107 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { evaluateOracleSanity } from "../../../src/guards/oracleSanity.js";
import { PriceOracle } from "../../../src/pricing/index.js";
import { Guard } from "../../../src/strategy.schema.js";
describe("Oracle Sanity Guard", () => {
let mockOracle: PriceOracle;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
mockOracle = new PriceOracle("mainnet");
// @ts-ignore - access private property for testing
mockOracle.provider = mockProvider;
});
it("should pass when price is within bounds", async () => {
const guard: Guard = {
type: "oracleSanity",
params: {
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
maxDeviationBps: 500, // 5%
},
};
// Mock price fetch
vi.spyOn(mockOracle, "getPrice").mockResolvedValue({
name: "chainlink",
price: 1000000n, // $1.00
decimals: 8,
timestamp: Date.now(),
});
const result = await evaluateOracleSanity(guard, mockOracle, {});
expect(result.passed).toBe(true);
});
it("should fail when price deviation is too high", async () => {
const guard: Guard = {
type: "oracleSanity",
params: {
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
maxDeviationBps: 100, // 1%
expectedPrice: "1000000", // $1.00
},
};
// Mock price that's 2% off
vi.spyOn(mockOracle, "getPrice").mockResolvedValue({
name: "chainlink",
price: 1020000n, // $1.02 (2% deviation)
decimals: 8,
timestamp: Date.now(),
});
const result = await evaluateOracleSanity(guard, mockOracle, {});
expect(result.passed).toBe(false);
expect(result.reason).toContain("deviation");
});
it("should handle missing oracle gracefully", async () => {
const guard: Guard = {
type: "oracleSanity",
params: {
token: "0xInvalid",
maxDeviationBps: 500,
},
};
vi.spyOn(mockOracle, "getPrice").mockRejectedValue(
new Error("Oracle not found")
);
const result = await evaluateOracleSanity(guard, mockOracle, {});
expect(result.passed).toBe(false);
expect(result.reason).toBeDefined();
});
it("should check for stale price data", async () => {
const guard: Guard = {
type: "oracleSanity",
params: {
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
maxDeviationBps: 500,
maxAgeSeconds: 3600, // 1 hour
},
};
// Mock stale price (2 hours old)
const staleTimestamp = Date.now() - 2 * 60 * 60 * 1000;
vi.spyOn(mockOracle, "getPrice").mockResolvedValue({
name: "chainlink",
price: 1000000n,
decimals: 8,
timestamp: staleTimestamp,
});
const result = await evaluateOracleSanity(guard, mockOracle, {});
expect(result.passed).toBe(false);
expect(result.reason).toContain("stale");
});
});

View File

@@ -0,0 +1,57 @@
import { describe, it, expect } from "vitest";
import { evaluatePositionDeltaLimit } from "../../../src/guards/positionDeltaLimit.js";
import { Guard } from "../../../src/strategy.schema.js";
describe("Position Delta Limit Guard", () => {
it("should pass when position delta is within limit", () => {
const guard: Guard = {
type: "positionDeltaLimit",
params: {
maxDelta: "1000000",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
},
};
const context = {
positionDelta: 500000n,
};
const result = evaluatePositionDeltaLimit(guard, "mainnet", context);
expect(result.passed).toBe(true);
});
it("should fail when position delta exceeds limit", () => {
const guard: Guard = {
type: "positionDeltaLimit",
params: {
maxDelta: "1000000",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
},
};
const context = {
positionDelta: 2000000n,
};
const result = evaluatePositionDeltaLimit(guard, "mainnet", context);
expect(result.passed).toBe(false);
expect(result.reason).toContain("exceeds");
});
it("should handle missing position delta", () => {
const guard: Guard = {
type: "positionDeltaLimit",
params: {
maxDelta: "1000000",
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
},
};
const context = {};
const result = evaluatePositionDeltaLimit(guard, "mainnet", context);
// Should pass if no delta to check
expect(result.passed).toBe(true);
});
});

View File

@@ -0,0 +1,60 @@
import { describe, it, expect } from "vitest";
import { evaluateSlippage } from "../../../src/guards/slippage.js";
import { Guard } from "../../../src/strategy.schema.js";
describe("Slippage Guard", () => {
it("should pass when slippage is within limit", () => {
const guard: Guard = {
type: "slippage",
params: {
maxBps: 50, // 0.5%
},
};
const context = {
expectedAmount: 1000000n,
actualAmount: 995000n, // 0.5% slippage
};
const result = evaluateSlippage(guard, context);
expect(result.passed).toBe(true);
});
it("should fail when slippage exceeds limit", () => {
const guard: Guard = {
type: "slippage",
params: {
maxBps: 10, // 0.1%
},
};
const context = {
expectedAmount: 1000000n,
actualAmount: 980000n, // 2% slippage
};
const result = evaluateSlippage(guard, context);
expect(result.passed).toBe(false);
expect(result.reason).toContain("slippage");
});
it("should handle zero expected amount", () => {
const guard: Guard = {
type: "slippage",
params: {
maxBps: 50,
},
};
const context = {
expectedAmount: 0n,
actualAmount: 995000n,
};
const result = evaluateSlippage(guard, context);
// Should fail if expected amount is zero
expect(result.passed).toBe(false);
expect(result.reason).toContain("zero");
});
});

View File

@@ -0,0 +1,88 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { evaluateTWAPSanity } from "../../../src/guards/twapSanity.js";
import { UniswapV3Adapter } from "../../../src/adapters/uniswapV3.js";
import { Guard } from "../../../src/strategy.schema.js";
describe("TWAP Sanity Guard", () => {
let mockUniswap: UniswapV3Adapter;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
mockUniswap = new UniswapV3Adapter("mainnet");
// @ts-ignore - access private property for testing
mockUniswap.provider = mockProvider;
});
it("should pass when TWAP is within deviation", async () => {
const guard: Guard = {
type: "twapSanity",
params: {
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
fee: 3000,
maxDeviationBps: 500, // 5%
},
};
const context = {
amountIn: 1000000n,
expectedAmountOut: 1000000n,
};
// Mock quote that's within deviation
vi.spyOn(mockUniswap, "quoteExactInput").mockResolvedValue(1010000n); // 1% deviation
const result = await evaluateTWAPSanity(guard, mockUniswap, context);
expect(result.passed).toBe(true);
});
it("should fail when TWAP deviation is too high", async () => {
const guard: Guard = {
type: "twapSanity",
params: {
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
fee: 3000,
maxDeviationBps: 100, // 1%
},
};
const context = {
amountIn: 1000000n,
expectedAmountOut: 1000000n,
};
// Mock quote that's 2% off
vi.spyOn(mockUniswap, "quoteExactInput").mockResolvedValue(980000n); // 2% deviation
const result = await evaluateTWAPSanity(guard, mockUniswap, context);
expect(result.passed).toBe(false);
expect(result.reason).toContain("deviation");
});
it("should handle missing pool gracefully", async () => {
const guard: Guard = {
type: "twapSanity",
params: {
tokenIn: "0xInvalid",
tokenOut: "0xInvalid",
fee: 3000,
maxDeviationBps: 500,
},
};
vi.spyOn(mockUniswap, "quoteExactInput").mockRejectedValue(
new Error("Pool not found")
);
const result = await evaluateTWAPSanity(guard, mockUniswap, {});
expect(result.passed).toBe(false);
expect(result.reason).toBeDefined();
});
});

View File

@@ -0,0 +1,222 @@
import { describe, it, expect } from "vitest";
import { StrategyCompiler } from "../../../src/planner/compiler.js";
import { Strategy } from "../../../src/strategy.schema.js";
describe("Strategy Compiler", () => {
const executorAddr = "0x1234567890123456789012345678901234567890";
it("should compile aaveV3.supply", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "supply",
action: {
type: "aaveV3.supply",
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
amount: "1000000",
},
}],
};
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.calls.length).toBe(1);
expect(plan.calls[0].description).toContain("Aave v3 supply");
});
it("should compile aaveV3.setUserEMode", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "setEMode",
action: {
type: "aaveV3.setUserEMode",
categoryId: 1,
},
}],
};
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.calls.length).toBe(1);
});
it("should compile maker.openVault", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "openVault",
action: {
type: "maker.openVault",
ilk: "ETH-A",
},
}],
};
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.calls.length).toBe(1);
});
it("should compile balancer.batchSwap", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "batchSwap",
action: {
type: "balancer.batchSwap",
kind: "givenIn",
swaps: [{
poolId: "0x...",
assetInIndex: 0,
assetOutIndex: 1,
amount: "1000000",
}],
assets: [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
],
},
}],
};
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.calls.length).toBe(1);
});
it("should compile curve.exchange_underlying", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "exchange",
action: {
type: "curve.exchange_underlying",
pool: "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
i: 0,
j: 1,
dx: "1000000",
},
}],
};
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.calls.length).toBe(1);
});
it("should compile aggregators.swap1Inch", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "swap",
action: {
type: "aggregators.swap1Inch",
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
amountIn: "1000000",
},
}],
};
// Mock 1inch API
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
toAmount: "1000000",
tx: { data: "0x1234" },
}),
});
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.calls.length).toBe(1);
});
it("should compile perps.increasePosition", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "increase",
action: {
type: "perps.increasePosition",
path: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
indexToken: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
amountIn: "1000000",
sizeDelta: "2000000",
isLong: true,
},
}],
};
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.calls.length).toBe(1);
});
it("should wrap flash loan with subsequent operations", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Flash Loan",
chain: "mainnet",
steps: [
{
id: "flashLoan",
action: {
type: "aaveV3.flashLoan",
assets: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
amounts: ["1000000"],
},
},
{
id: "swap",
action: {
type: "uniswapV3.swap",
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
fee: 3000,
amountIn: "1000000",
exactInput: true,
},
},
],
};
const plan = await compiler.compile(strategy, executorAddr);
expect(plan.requiresFlashLoan).toBe(true);
expect(plan.flashLoanAsset).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
// Should have executeFlashLoan call
expect(plan.calls.some(c => c.description.includes("flash loan"))).toBe(true);
});
it("should substitute executor address in swaps", async () => {
const compiler = new StrategyCompiler("mainnet");
const strategy: Strategy = {
name: "Test",
chain: "mainnet",
steps: [{
id: "swap",
action: {
type: "uniswapV3.swap",
tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
tokenOut: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
fee: 3000,
amountIn: "1000000",
exactInput: true,
},
}],
};
const plan = await compiler.compile(strategy, executorAddr);
// Recipient should be executor address, not zero
expect(plan.calls.length).toBe(1);
});
});

View File

@@ -0,0 +1,71 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { PriceOracle } from "../../../src/pricing/index.js";
describe("Price Oracle", () => {
let oracle: PriceOracle;
let mockProvider: any;
beforeEach(() => {
mockProvider = {
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
call: vi.fn(),
};
oracle = new PriceOracle("mainnet");
// @ts-ignore - access private property for testing
oracle.provider = mockProvider;
});
it("should fetch Chainlink price", async () => {
// Mock Chainlink aggregator response
mockProvider.call = vi.fn().mockResolvedValue(
"0x0000000000000000000000000000000000000000000000000000000005f5e100" // 100000000 = $1.00 with 8 decimals
);
const price = await oracle.getPrice(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC
);
expect(price).toBeDefined();
expect(price.name).toBe("chainlink");
expect(price.price).toBeGreaterThan(0n);
});
it("should calculate weighted average with quorum", async () => {
const sources = [
{
name: "chainlink",
price: 1000000n,
decimals: 8,
timestamp: Date.now(),
},
{
name: "uniswap-twap",
price: 1010000n,
decimals: 8,
timestamp: Date.now(),
},
];
const result = await oracle.getPriceWithQuorum(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
sources
);
expect(result).toBeDefined();
expect(result.price).toBeGreaterThan(0n);
// Weighted average should be between the two prices
expect(result.price).toBeGreaterThanOrEqual(1000000n);
expect(result.price).toBeLessThanOrEqual(1010000n);
});
it("should handle missing token decimals gracefully", async () => {
mockProvider.call = vi.fn().mockRejectedValue(new Error("Not found"));
// Should not throw, should use default decimals
await expect(
oracle.getPrice("0xInvalidToken")
).rejects.toThrow();
});
});

View File

@@ -0,0 +1,78 @@
import { describe, it, expect } from "vitest";
import { loadStrategy, validateStrategy, substituteBlinds } from "../../src/strategy.js";
import { BlindValues } from "../../src/strategy.js";
describe("Strategy", () => {
const sampleStrategy = {
name: "Test Strategy",
chain: "mainnet",
steps: [
{
id: "step1",
action: {
type: "aaveV3.supply",
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
amount: "1000000",
},
},
],
};
it("should load a valid strategy", () => {
// Write to temp file for testing
const fs = await import("fs");
const path = await import("path");
const tempFile = path.join(process.cwd(), "temp-strategy.json");
fs.writeFileSync(tempFile, JSON.stringify(sampleStrategy));
const strategy = loadStrategy(tempFile);
expect(strategy.name).toBe("Test Strategy");
expect(strategy.steps.length).toBe(1);
// Cleanup
fs.unlinkSync(tempFile);
});
it("should validate a strategy", () => {
const validation = validateStrategy(sampleStrategy as any);
expect(validation.valid).toBe(true);
expect(validation.errors.length).toBe(0);
});
it("should detect duplicate step IDs", () => {
const invalidStrategy = {
...sampleStrategy,
steps: [
{ id: "step1", action: sampleStrategy.steps[0].action },
{ id: "step1", action: sampleStrategy.steps[0].action },
],
};
const validation = validateStrategy(invalidStrategy as any);
expect(validation.valid).toBe(false);
expect(validation.errors).toContain("Duplicate step ID: step1");
});
it("should substitute blind values", () => {
const strategyWithBlinds = {
...sampleStrategy,
blinds: [
{ name: "amount", type: "uint256", description: "Amount to supply" },
],
steps: [
{
id: "step1",
action: {
type: "aaveV3.supply",
asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
amount: { blind: "amount" },
},
},
],
};
const blindValues: BlindValues = { amount: "1000000" };
const substituted = substituteBlinds(strategyWithBlinds as any, blindValues);
expect(substituted.steps[0].action.amount).toBe("1000000");
});
});

View File

@@ -0,0 +1,105 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { estimateGasForCalls } from "../../../src/utils/gas.js";
import { CompiledCall } from "../../../src/planner/compiler.js";
import { JsonRpcProvider } from "ethers";
describe("Gas Estimation", () => {
let mockProvider: any;
beforeEach(() => {
mockProvider = {
estimateGas: vi.fn(),
getNetwork: vi.fn().mockResolvedValue({ chainId: 1n }),
};
});
it("should estimate gas for single call", async () => {
const calls: CompiledCall[] = [
{
to: "0x1234567890123456789012345678901234567890",
data: "0x1234",
description: "Test call",
},
];
mockProvider.estimateGas = vi.fn().mockResolvedValue(100000n);
const estimate = await estimateGasForCalls(
mockProvider as any,
calls,
"0x0000000000000000000000000000000000000000"
);
expect(estimate).toBeGreaterThan(0n);
});
it("should estimate gas for multiple calls", async () => {
const calls: CompiledCall[] = [
{
to: "0x1234567890123456789012345678901234567890",
data: "0x1234",
description: "Call 1",
},
{
to: "0x1234567890123456789012345678901234567890",
data: "0x5678",
description: "Call 2",
},
];
mockProvider.estimateGas = vi.fn().mockResolvedValue(100000n);
const estimate = await estimateGasForCalls(
mockProvider as any,
calls,
"0x0000000000000000000000000000000000000000"
);
expect(estimate).toBeGreaterThan(0n);
});
it("should add safety buffer to estimates", async () => {
const calls: CompiledCall[] = [
{
to: "0x1234567890123456789012345678901234567890",
data: "0x1234",
description: "Test call",
},
];
mockProvider.estimateGas = vi.fn().mockResolvedValue(100000n);
const estimate = await estimateGasForCalls(
mockProvider as any,
calls,
"0x0000000000000000000000000000000000000000"
);
// Should have safety buffer (typically 20%)
expect(estimate).toBeGreaterThan(100000n);
});
it("should handle estimation failures gracefully", async () => {
const calls: CompiledCall[] = [
{
to: "0x1234567890123456789012345678901234567890",
data: "0x1234",
description: "Test call",
},
];
mockProvider.estimateGas = vi.fn().mockRejectedValue(
new Error("Estimation failed")
);
// Should fall back to rough estimate
await expect(
estimateGasForCalls(
mockProvider as any,
calls,
"0x0000000000000000000000000000000000000000"
)
).rejects.toThrow();
});
});