181 lines
4.8 KiB
TypeScript
181 lines
4.8 KiB
TypeScript
|
|
import { Interface, JsonRpcProvider, Wallet, formatEther } from 'ethers';
|
||
|
|
import { readFileSync } from 'fs';
|
||
|
|
|
||
|
|
const SWAP_ABI = [
|
||
|
|
'function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut) external returns (uint256)',
|
||
|
|
];
|
||
|
|
|
||
|
|
export interface ExecutorAllowlist {
|
||
|
|
chainId: number;
|
||
|
|
allowedTo: string[];
|
||
|
|
/** Max msg.value in wei (string). */
|
||
|
|
maxValueWei: string;
|
||
|
|
/** Optional max fee per gas in gwei (omit = no cap). */
|
||
|
|
maxFeePerGasGwei?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function loadAllowlist(path: string): ExecutorAllowlist {
|
||
|
|
const raw = readFileSync(path, 'utf8');
|
||
|
|
const j = JSON.parse(raw) as ExecutorAllowlist;
|
||
|
|
if (!j.chainId || !Array.isArray(j.allowedTo)) {
|
||
|
|
throw new Error('Invalid allowlist: need chainId and allowedTo[]');
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
chainId: j.chainId,
|
||
|
|
allowedTo: j.allowedTo.map((a: string) => a.toLowerCase()),
|
||
|
|
maxValueWei: j.maxValueWei ?? '0',
|
||
|
|
maxFeePerGasGwei: j.maxFeePerGasGwei,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeAddr(a: string): string {
|
||
|
|
return a.trim().toLowerCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Calldata for DODOPMMIntegration.swapExactIn; send `to` = integration address. */
|
||
|
|
export function encodeSwapExactInCalldata(
|
||
|
|
pool: string,
|
||
|
|
tokenIn: string,
|
||
|
|
amountIn: bigint,
|
||
|
|
minAmountOut: bigint
|
||
|
|
): string {
|
||
|
|
const iface = new Interface(SWAP_ABI);
|
||
|
|
return iface.encodeFunctionData('swapExactIn', [pool, tokenIn, amountIn, minAmountOut]);
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface SimulationResult {
|
||
|
|
ok: boolean;
|
||
|
|
error?: string;
|
||
|
|
gasEstimate?: bigint;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function simulateCall(params: {
|
||
|
|
rpcUrl: string;
|
||
|
|
from: string;
|
||
|
|
to: string;
|
||
|
|
data: string;
|
||
|
|
valueWei?: bigint;
|
||
|
|
}): Promise<SimulationResult> {
|
||
|
|
const provider = new JsonRpcProvider(params.rpcUrl);
|
||
|
|
const tx = {
|
||
|
|
from: params.from,
|
||
|
|
to: params.to,
|
||
|
|
data: params.data,
|
||
|
|
value: params.valueWei ?? 0n,
|
||
|
|
};
|
||
|
|
try {
|
||
|
|
await provider.call(tx);
|
||
|
|
} catch (e) {
|
||
|
|
const msg = e instanceof Error ? e.message : String(e);
|
||
|
|
return { ok: false, error: msg };
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const gasEstimate = await provider.estimateGas(tx);
|
||
|
|
return { ok: true, gasEstimate };
|
||
|
|
} catch {
|
||
|
|
return { ok: true, gasEstimate: undefined };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function enforceAllowlistAndSimulate(params: {
|
||
|
|
rpcUrl: string;
|
||
|
|
allowlistPath: string;
|
||
|
|
from: string;
|
||
|
|
to: string;
|
||
|
|
data: string;
|
||
|
|
valueWei?: bigint;
|
||
|
|
}): Promise<SimulationResult & { allowlistOk: boolean; allowlistError?: string }> {
|
||
|
|
let list: ExecutorAllowlist;
|
||
|
|
try {
|
||
|
|
list = loadAllowlist(params.allowlistPath);
|
||
|
|
} catch (e) {
|
||
|
|
return {
|
||
|
|
ok: false,
|
||
|
|
allowlistOk: false,
|
||
|
|
allowlistError: e instanceof Error ? e.message : String(e),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const provider = new JsonRpcProvider(params.rpcUrl);
|
||
|
|
const net = await provider.getNetwork();
|
||
|
|
if (Number(net.chainId) !== list.chainId) {
|
||
|
|
return {
|
||
|
|
ok: false,
|
||
|
|
allowlistOk: false,
|
||
|
|
allowlistError: `chainId mismatch: rpc=${net.chainId} allowlist=${list.chainId}`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!list.allowedTo.includes(normalizeAddr(params.to))) {
|
||
|
|
return {
|
||
|
|
ok: false,
|
||
|
|
allowlistOk: false,
|
||
|
|
allowlistError: `to not in allowlist: ${params.to}`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const maxVal = BigInt(list.maxValueWei);
|
||
|
|
const v = params.valueWei ?? 0n;
|
||
|
|
if (v > maxVal) {
|
||
|
|
return {
|
||
|
|
ok: false,
|
||
|
|
allowlistOk: false,
|
||
|
|
allowlistError: `value ${v} exceeds maxValueWei ${maxVal}`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const sim = await simulateCall(params);
|
||
|
|
return { ...sim, allowlistOk: true };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function broadcastIfApply(params: {
|
||
|
|
rpcUrl: string;
|
||
|
|
privateKey: string;
|
||
|
|
allowlistPath: string;
|
||
|
|
to: string;
|
||
|
|
data: string;
|
||
|
|
valueWei?: bigint;
|
||
|
|
}): Promise<{ hash?: string; error?: string }> {
|
||
|
|
const list = loadAllowlist(params.allowlistPath);
|
||
|
|
const provider = new JsonRpcProvider(params.rpcUrl);
|
||
|
|
const net = await provider.getNetwork();
|
||
|
|
if (Number(net.chainId) !== list.chainId) {
|
||
|
|
return { error: `chainId mismatch: ${net.chainId}` };
|
||
|
|
}
|
||
|
|
if (!list.allowedTo.includes(normalizeAddr(params.to))) {
|
||
|
|
return { error: 'to not allowlisted' };
|
||
|
|
}
|
||
|
|
const maxVal = BigInt(list.maxValueWei);
|
||
|
|
const v = params.valueWei ?? 0n;
|
||
|
|
if (v > maxVal) {
|
||
|
|
return { error: 'value exceeds allowlist' };
|
||
|
|
}
|
||
|
|
|
||
|
|
const wallet = new Wallet(params.privateKey, provider);
|
||
|
|
const tx: Parameters<Wallet['sendTransaction']>[0] = {
|
||
|
|
to: params.to,
|
||
|
|
data: params.data,
|
||
|
|
value: v,
|
||
|
|
};
|
||
|
|
if (list.maxFeePerGasGwei !== undefined) {
|
||
|
|
tx.maxFeePerGas = BigInt(Math.floor(list.maxFeePerGasGwei * 1e9));
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const sent = await wallet.sendTransaction(tx);
|
||
|
|
return { hash: sent.hash };
|
||
|
|
} catch (e) {
|
||
|
|
return { error: e instanceof Error ? e.message : String(e) };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Format gas cost in ETH given estimate and effective gas price (wei). */
|
||
|
|
export function formatGasCostEth(gasEstimate: bigint, gasPriceWei: bigint): string {
|
||
|
|
try {
|
||
|
|
const wei = gasEstimate * gasPriceWei;
|
||
|
|
return formatEther(wei);
|
||
|
|
} catch {
|
||
|
|
return 'n/a';
|
||
|
|
}
|
||
|
|
}
|