Skip to content

Commit

Permalink
Add eol with reward detector script
Browse files Browse the repository at this point in the history
  • Loading branch information
prevostc committed Feb 4, 2024
1 parent c35227e commit 205cfc4
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ dist/
typings/
old
.DS_Store
compile.json
compile.json
*.store.json
8 changes: 8 additions & 0 deletions src/lib/addressbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import * as addressbook from 'blockchain-addressbook';
import { Chain } from './chain';
import { Hex } from 'viem';

export function getChainWNativeTokenDecimals(chain: Chain): number {
if (chain === 'linea') {
return 18;
}
const tokens = addressbook.addressBook[chain].tokens;
return tokens.WNATIVE.decimals;
}

export function getChainWNativeTokenSymbol(chain: Chain): string {
if (chain === 'linea') {
return 'ETH';
Expand Down
302 changes: 302 additions & 0 deletions src/script/eol-with-rewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
import { Chain, allChainIds } from '../lib/chain';
import { BeefyVault } from '../lib/vault';
import { getVaultsToMonitorByChain } from '../lib/vault-list';
import { runMain } from '../util/process';
import * as fs from 'fs';
import { rootLogger } from '../util/logger';
import yargs from 'yargs';
import { getReadOnlyRpcClient, getWalletAccount } from '../lib/rpc-client';
import { RPC_CONFIG } from '../lib/config';
import { BeefyHarvestLensABI } from '../abi/BeefyHarvestLensABI';
import { getChainWNativeTokenAddress, getChainWNativeTokenDecimals } from '../lib/addressbook';
import { AItem, AKey, AVal, reportOnMultipleAsyncCall, serializeReport } from '../lib/reports';
import { Hex } from 'viem';
import { Async } from '../util/async';

class JsonFileKVStore<T> {
private path: string;
public data: {
[k: string]: T;
};

constructor(path: string) {
this.path = path;
this.data = {};
}

async load() {
if (!fs.existsSync(this.path)) {
return;
}
const dataIfExists = await fs.promises.readFile(this.path, { encoding: 'utf-8' });
this.data = JSON.parse(dataIfExists);
}

async persist() {
await fs.promises.writeFile(this.path, serializeReport(this.data, true), { encoding: 'utf-8' });
}

get(key: string) {
return this.data[key];
}

set(key: string, data: T) {
this.data[key] = data;
}

has(key: string) {
return key in this.data;
}
}

interface EolWithRewardsReportItem {
vault: BeefyVault;
simulation: Async<{
estimatedCallRewardsWei: bigint;
harvestWillSucceed: boolean;
lastHarvest: Date;
hoursSinceLastHarvest: number;
isLastHarvestRecent: boolean;
paused: boolean;
blockNumber: bigint;
harvestResultData: Hex;
gasUsed: bigint;
}> | null;
}

const logger = rootLogger.child({ module: 'eol-with-rewards-main' });

type CmdOptions = {
chain: Chain[];
storePath: string;
mode: 'fetch' | 'report-summary';
};

async function main() {
const argv = await yargs.usage('$0 <cmd> [args]').options({
chain: {
type: 'array',
choices: [...allChainIds, 'all'],
alias: 'c',
demand: false,
default: 'all',
describe: 'only harest these chains. eol chains will be ignored',
},
'store-path': {
type: 'string',
alias: 's',
demand: false,
default: 'eol-with-rewards.store.json',
describe: 'path to store the result and restart from in case of crash',
},
mode: {
type: 'string',
alias: 'm',
demand: false,
default: 'fetch',
choices: ['fetch', 'report-summary'],
describe: 'fetch the data or report the summary',
},
}).argv;

const options: CmdOptions = {
chain: argv.chain.includes('all') ? allChainIds : (argv.chain as Chain[]),
storePath: argv['store-path'] as string,
mode: argv.mode as 'fetch' | 'report-summary',
};
logger.trace({ msg: 'running with options', data: options });

const store = new JsonFileKVStore<EolWithRewardsReportItem>(options.storePath);
await store.load();

if (options.mode === 'fetch') {
const vaultsByChain = await getVaultsToMonitorByChain({ chains: options.chain, strategyAddress: null });

const processPromises = Object.entries(vaultsByChain).map(async ([chain, vaults]) => {
const rpcConfig = RPC_CONFIG[chain as Chain];
if (rpcConfig.eol) {
logger.debug({ msg: 'skipping eol chain', data: { chain } });
return;
}
if (!rpcConfig.harvest.enabled) {
logger.debug({ msg: 'skipping chain with harvest disabled', data: { chain } });
return;
}

logger.debug({ msg: 'processing chain', data: { chain, vaults: vaults.length } });

const vaultsToProcess: BeefyVault[] = [];
for (const vault of vaults) {
if (!vault.eol) {
logger.trace({ msg: 'vault is not eol', data: { vaultId: vault.id } });
continue;
}
if (store.has(vault.id)) {
const report = store.get(vault.id);
if (report.simulation && report.simulation.status === 'fulfilled') {
logger.trace({ msg: 'vault already simulated successfully', data: { vaultId: vault.id } });
continue;
}
}
vaultsToProcess.push(vault);
}

logger.debug({ msg: 'processing vaults', data: { chain, vaultsToProcessCount: vaultsToProcess.length } });

const result = await fetchLensResult(chain as Chain, vaultsToProcess);

for (const item of result) {
store.set(item.vault.id, item);
}

await store.persist();
});

await Promise.allSettled(processPromises);
} else if (options.mode === 'report-summary') {
const summary: {
chain: Chain;
vaultId: string;
status: 'not-simulated' | 'unsuccessful-simulation' | 'no-rewards-found' | 'found-eol-rewards';
rewards?: string;
}[] = [];

for (const [vaultId, item] of Object.entries(store.data)) {
logger.trace({ msg: 'reporting summary', data: { vaultId } });

if (item.simulation === null) {
summary.push({ chain: item.vault.chain, vaultId, status: 'not-simulated' });
continue;
}
if (item.simulation.status === 'rejected') {
summary.push({ chain: item.vault.chain, vaultId, status: 'unsuccessful-simulation' });
continue;
}

const rewards = BigInt(item.simulation.value.estimatedCallRewardsWei);
if (rewards === 0n) {
summary.push({ chain: item.vault.chain, vaultId, status: 'no-rewards-found' });
continue;
}

summary.push({
chain: item.vault.chain,
vaultId,
status: 'found-eol-rewards',
rewards: rewards.toString(),
});
}

//console.log(JSON.stringify(summary, null, 2));

const countsByChainAndStatus = summary.reduce(
(acc, cur) => {
if (!acc[cur.chain]) {
acc[cur.chain] = {};
}
if (!acc[cur.chain][cur.status]) {
acc[cur.chain][cur.status] = 0;
}
acc[cur.chain][cur.status]++;
return acc;
},
{} as { [k: string]: { [k: string]: number } }
);
console.log(JSON.stringify(countsByChainAndStatus, null, 2));

const vaultSummaryByChain = summary.reduce(
(acc, cur) => {
if (!acc[cur.chain]) {
acc[cur.chain] = [];
}
const rewards = BigInt(cur.rewards || '0');
const wnativeDecimals = getChainWNativeTokenDecimals(cur.chain);
const divisor = BigInt('1' + '0'.repeat(wnativeDecimals));
const rewardsEth =
(rewards / divisor).toString() + '.' + rewards.toString().padStart(wnativeDecimals, '0');
acc[cur.chain].push({ vaultId: cur.vaultId, rewards, rewardsEth });
return acc;
},
{} as { [k: string]: { vaultId: string; rewards: BigInt; rewardsEth: string }[] }
);

const top3VaultsByRewardsAndChain = Object.entries(vaultSummaryByChain).reduce(
(acc, [chain, vaults]) => {
acc[chain] = vaults.sort((a, b) => (a.rewards > b.rewards ? -1 : 0)).slice(0, 3);
return acc;
},
{} as { [k: string]: { vaultId: string; rewards: BigInt; rewardsEth: string }[] }
);
console.log(serializeReport(top3VaultsByRewardsAndChain, true));

const totalTvlByChain = Object.entries(store.data).reduce(
(acc, [vaultId, item]) => {
if (!acc[item.vault.chain]) {
acc[item.vault.chain] = 0;
}
acc[item.vault.chain] += item.vault.tvlUsd;
return acc;
},
{} as { [k: string]: number }
);

totalTvlByChain['__all__'] = Object.values(totalTvlByChain).reduce((acc, cur) => acc + cur, 0);
console.log(JSON.stringify(totalTvlByChain, null, 2));
}
}

function reportOnMultipleEolRewardsAsyncCall<
TKey extends AKey<EolWithRewardsReportItem>,
TItem extends AItem<EolWithRewardsReportItem>,
TVal extends AVal<EolWithRewardsReportItem, TKey>,
>(...args: Parameters<typeof reportOnMultipleAsyncCall<EolWithRewardsReportItem, TKey, TItem, TVal>>) {
return reportOnMultipleAsyncCall<EolWithRewardsReportItem, TKey, TItem, TVal>(...args);
}

async function fetchLensResult(chain: Chain, vaults: BeefyVault[]) {
const wnative = getChainWNativeTokenAddress(chain);
const publicClient = getReadOnlyRpcClient({ chain });
const walletAccount = getWalletAccount({ chain });

// we need the harvest lense
const rpcConfig = RPC_CONFIG[chain];
if (!rpcConfig.contracts.harvestLens) {
throw new Error(`Missing harvest lens address for chain ${chain}`);
}
const harvestLensContract = { abi: BeefyHarvestLensABI, address: rpcConfig.contracts.harvestLens };

const items = vaults.map(vault => ({ vault, report: { vault, simulation: null } as EolWithRewardsReportItem }));

await reportOnMultipleEolRewardsAsyncCall(items, 'simulation', 'parallel', async item => {
const {
result: { callReward, gasUsed, lastHarvest, paused, success, blockNumber, harvestResult },
} = await publicClient.simulateContract({
...harvestLensContract,
functionName: 'harvest',
args: [item.vault.strategyAddress, wnative] as const,
account: walletAccount,
});

const now = new Date();
const lastHarvestDate = new Date(Number(lastHarvest) * 1000);
const timeSinceLastHarvestMs = now.getTime() - lastHarvestDate.getTime();
const isLastHarvestRecent = timeSinceLastHarvestMs < rpcConfig.harvest.targetTimeBetweenHarvestsMs;

//await new Promise(resolve => setTimeout(resolve, 1000));
return {
estimatedCallRewardsWei: callReward,
harvestWillSucceed: success,
lastHarvest: lastHarvestDate,
hoursSinceLastHarvest: timeSinceLastHarvestMs / 1000 / 60 / 60,
isLastHarvestRecent,
paused,
blockNumber,
gasUsed,
harvestResultData: harvestResult,
};
});

return items.map(i => i.report);
}

runMain(main);

0 comments on commit 205cfc4

Please sign in to comment.