import { LISTING_PRESETS, MidPriceImpact, } from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools'; import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; import { BN } from '@project-serum/anchor'; import { getAllProposals, getTokenOwnerRecord, getTokenOwnerRecordAddress, } from '@solana/spl-governance'; import { Builder } from '../src/builder'; import { AccountMeta, Connection, Keypair, PublicKey, Transaction, TransactionInstruction, } from '@solana/web3.js'; import fs from 'fs'; import { Bank } from '../src/accounts/bank'; import { Group } from '../src/accounts/group'; import { MangoAccount } from '../src/accounts/mangoAccount'; import { MangoClient } from '../src/client'; import { NullTokenEditParams } from '../src/clientIxParamBuilder'; import { MANGO_V4_MAIN_GROUP as MANGO_V4_PRIMARY_GROUP } from '../src/constants'; import { getEquityForMangoAccounts } from '../src/risk'; import { buildFetch, toNativeI80F48ForQuote, toUiDecimalsForQuote, } from '../src/utils'; import { MANGO_DAO_WALLET_GOVERNANCE, MANGO_GOVERNANCE_PROGRAM, MANGO_MINT, MANGO_REALM_PK, } from './governanceInstructions/constants'; import { createProposal } from './governanceInstructions/createProposal'; import { DEFAULT_VSR_ID, VsrClient, } from './governanceInstructions/voteStakeRegistryClient'; const { MB_CLUSTER_URL, PROPOSAL_TITLE, PROPOSAL_LINK, VSR_DELEGATE_KEYPAIR, VSR_DELEGATE_FROM_PK, DRY_RUN, } = process.env; function wrapWithForwarder( ix: TransactionInstruction, signer: PublicKey, timeout: BN, ): TransactionInstruction { return new TransactionInstruction({ keys: [ { pubkey: signer, isSigner: true, isWritable: false, }, { pubkey: ix.programId, isSigner: false, isWritable: false, }, ...ix.keys, ], programId: new PublicKey('ixFPGCPYEp5GzhoahhHFVL8VVzkq1kc2eeFZh3qpYca'), data: Buffer.concat([timeout.toArrayLike(Buffer, 'le', 8), ix.data]), }); } const getApiTokenName = (bankName: string): string => { if (bankName === 'ETH (Portal)') { return 'ETH'; } return bankName; }; async function buildClient(): Promise { return await MangoClient.connectDefault(MB_CLUSTER_URL!); } async function setupWallet(): Promise { const clientKeypair = Keypair.fromSecretKey( Buffer.from(JSON.parse(fs.readFileSync(VSR_DELEGATE_KEYPAIR!, 'utf-8'))), ); const clientWallet = new Wallet(clientKeypair); return clientWallet; } async function setupVsr( connection: Connection, clientWallet: Wallet, ): Promise { const options = AnchorProvider.defaultOptions(); const provider = new AnchorProvider(connection, clientWallet, options); const vsrClient = await VsrClient.connect(provider, DEFAULT_VSR_ID); return vsrClient; } async function getTotalLiqorEquity( client: MangoClient, group: Group, mangoAccounts: MangoAccount[], ): Promise { const liqors = ( await ( await ( await buildFetch() )( `https://api.mngo.cloud/data/v4/stats/liqors-over_period?over_period=1MONTH`, { mode: 'cors', headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }, }, ) ).json() ).map((data) => new PublicKey(data['liqor'])); const ttlLiqorEquity = ( await getEquityForMangoAccounts(client, group, liqors, mangoAccounts) ).reduce((partialSum, ae) => partialSum + ae.Equity.val, 0); return ttlLiqorEquity; } function getPriceImpactForBank( midPriceImpacts: MidPriceImpact[], bank: Bank, priceImpactPercent = 1, ): MidPriceImpact { const tokenToPriceImpact = midPriceImpacts .filter((x) => x.avg_price_impact_percent < priceImpactPercent) .reduce((acc: { [key: string]: MidPriceImpact }, val: MidPriceImpact) => { if ( !acc[val.symbol] || val.target_amount > acc[val.symbol].target_amount ) { acc[val.symbol] = val; } return acc; }, {}); const priceImpact = tokenToPriceImpact[getApiTokenName(bank.name)]; return priceImpact; } async function updateTokenParams(): Promise { const [client, wallet] = await Promise.all([buildClient(), setupWallet()]); const vsrClient = await setupVsr(client.connection, wallet); const group = await client.getGroup(MANGO_V4_PRIMARY_GROUP); const instructions: TransactionInstruction[] = []; // const allMangoAccounts = await client.getAllMangoAccounts(group, true); // const stepSize = 1; // const ttlLiqorEquityUi = await getTotalLiqorEquity( // client, // group, // allMangoAccounts, // ); // const midPriceImpacts = getMidPriceImpacts(group.pis); // const pisForLiqor: LiqorPriceImpact[][] = []; // // eslint-disable-next-line no-constant-condition // if (false) { // const pisForLiqor: LiqorPriceImpact[][] = await buildGroupGrid( // group, // allMangoAccounts, // stepSize, // ); // } // eslint-disable-next-line no-constant-condition // if (false) { // // Deposit limits header // console.log( // `${'name'.padStart(20)} ${'maxLiqBatchUi'.padStart( // 15, // )} ${'maxLiqBatchUi'.padStart(15)} ${'sellImpact'.padStart( // 12, // )}$ ${'pi %'.padStart(12)}% ${'aNDUi'.padStart( // 20, // )}${'aNDQuoteUi'.padStart(20)} ${'uiDeposits'.padStart( // 12, // )} ${'uiDeposits'.padStart(12)} ${'depositLimitsUi'.padStart(12)}`, // ); // } console.log(Array.from(group.banksMapByTokenIndex.values()).length); Array.from(group.banksMapByTokenIndex.values()) .map((banks) => banks[0]) .sort((a, b) => a.name.localeCompare(b.name)) .forEach(async (bank) => { const builder = Builder(NullTokenEditParams); let change = false; // try { const tier = Object.values(LISTING_PRESETS).find((x) => x.initLiabWeight.toFixed(1) === '1.8' ? x.initLiabWeight.toFixed(1) === bank?.initLiabWeight.toNumber().toFixed(1) && x.reduceOnly === bank.reduceOnly : x.initLiabWeight.toFixed(1) === bank?.initLiabWeight.toNumber().toFixed(1), ); // eslint-disable-next-line no-constant-condition if (true) { if (bank.uiBorrows() == 0 && bank.reduceOnly == 1) { console.log(` - ${bank.name}`); builder.forceWithdraw(true); // builder.disableAssetLiquidation(true); change = true; } } // const maybeSbOracle = SB_ON_DEMAND_LST_FALLBACK_ORACLES.filter( // (x) => // x[0].replace('/USD', '').toLocaleUpperCase() == // bank.name.toLocaleUpperCase(), // ); // if (maybeSbOracle.length > 0) { // console.log(` - ${bank.name} ${maybeSbOracle[0][0]}`); // builder.fallbackOracle(PublicKey.default); // change = true; // } else { // return; // } // const maybeSbOracle = SB_FEEDS_TO_MIGRATE.filter( // (x) => x.name.replace('/USD', '') === bank.name.toLocaleUpperCase(), // ); // if (maybeSbOracle.length > 0) { // console.log(` - ${bank.name} ${maybeSbOracle[0].name}`); // builder.oracle(new PublicKey(maybeSbOracle[0].newPk)); // change = true; // } else { // return; // } // if (bank.oracleProvider != OracleProvider.Pyth) { // console.log(`Skipping ${bank.name}, since not pyth`); // return; // } // if (bank.reduceOnly == 1) { // console.log(`Skipping ${bank.name}, since reduceOnly`); // return; // } // const maybePythV2Feed = PYTH_SPONSORED_ORACLES.filter( // (x) => // x[0].replace('/USD', '') == // (bank.name.includes('BTC') // ? 'BTC' // : bank.name.includes('ETH') // ? 'ETH' // : bank.name.toUpperCase()), // ); // if (maybePythV2Feed.length > 0) { // console.log(` - ${bank.name} ${bank.oracle} ${maybePythV2Feed[0][0]}`); // builder.oracle(new PublicKey(maybePythV2Feed[0][1])); // change = true; // } else { // console.log(`Skipping ${bank.name}, cant find pyth feed`); // } // if ( // bank.reduceOnly != 1 && // maybePythV2Feed.length == 0 && // bank.oracleProvider == OracleProvider.Pyth && // !['CHAI', 'DAI', 'BLZE', 'MNGO', 'RENDER'].some( // (item) => item == bank.name, // ) // ) { // throw new Error(`No pyth feed for ${bank.name}`); // } // eslint-disable-next-line no-constant-condition // if (true) { // if ( // bank.uiBorrows() == 0 && // bank.reduceOnly == 2 && // bank.initAssetWeight.toNumber() == 0 && // bank.maintAssetWeight.toNumber() == 0 // ) { // builder.disableAssetLiquidation(true); // builder.oracleConfig({ // confFilter: 1000, // maxStalenessSlots: -1, // }); // change = true; // console.log( // ` - ${bank.name}, ${( // bank.uiDeposits() * bank.uiPrice // ).toLocaleString()} disabled asset liquidation`, // ); // } // } // // eslint-disable-next-line no-constant-condition // if (true) { // if (!tier) { // console.log(`${bank.name}, no tier found`); // } else if (tier.preset_name != 'C') { // if (tier.preset_name.includes('A')) { // builder.liquidationFee(bank.liquidationFee.toNumber() * 0.2); // builder.platformLiquidationFee( // bank.liquidationFee.toNumber() * 0.8, // ); // } else if (tier.preset_name.includes('B')) { // builder.liquidationFee(bank.liquidationFee.toNumber() * 0.4); // builder.platformLiquidationFee( // bank.liquidationFee.toNumber() * 0.6, // ); // } // change = true; // } // } // eslint-disable-next-line no-constant-condition // if (true) { // if (!tier) { // console.log(`${bank.name}, no tier found`); // } else { // console.log( // `${bank.name.padStart(10)}, ${bank.loanFeeRate // .mul(I80F48.fromNumber(100)) // .toFixed(2)}, ${bank.loanOriginationFeeRate // .mul(I80F48.fromNumber(100)) // .toFixed(2)}, ${tier?.preset_name.padStart(5)}, ${( // tier.loanFeeRate * 100 // ).toFixed(2)}, ${(tier!.loanOriginationFeeRate * 100).toFixed(2)}`, // ); // builder.loanFeeRate(tier!.loanFeeRate); // builder.loanOriginationFeeRate(tier!.loanOriginationFeeRate); // builder.flashLoanSwapFeeRate(tier!.loanOriginationFeeRate); // change = true; // } // } // formulas are sourced from here // https://www.notion.so/mango-markets/Mango-v4-Risk-parameter-recommendations-d309cdf5faac4aeea7560356e68532ab // const priceImpact = getPriceImpactForBank(midPriceImpacts, bank); // const scaleStartQuoteUi = Math.min( // 50 * ttlLiqorEquityUi, // 4 * priceImpact.target_amount, // ); // eslint-disable-next-line no-constant-condition if (false) { // Net borrow limits const netBorrowLimitPerWindowQuote = Math.max( 10_000, Math.min(bank.uiDeposits() * bank.uiPrice, 300_000) / 3 + Math.max(0, bank.uiDeposits() * bank.uiPrice - 300_000) / 5, ); builder.netBorrowLimitPerWindowQuote( toNativeI80F48ForQuote(netBorrowLimitPerWindowQuote).toNumber(), ); change = true; if ( netBorrowLimitPerWindowQuote != toUiDecimalsForQuote(bank.netBorrowLimitPerWindowQuote) ) { console.log( `${ bank.name } new - ${netBorrowLimitPerWindowQuote.toLocaleString()}, old - ${toUiDecimalsForQuote( bank.netBorrowLimitPerWindowQuote, ).toLocaleString()}`, ); } } // // Deposit limits // // eslint-disable-next-line no-constant-condition // if (false) { // if (bank.maintAssetWeight.toNumber() > 0) { // { // // Find asset's largest batch in $ we would need to liquidate, batches are extreme points of a range of price drop, // // range is constrained by leverage provided // // i.e. how much volatility we expect // const r = findLargestAssetBatchUi( // pisForLiqor, // bank.name, // Math.round(bank.maintAssetWeight.toNumber() * 100), // 100 - Math.round(bank.maintAssetWeight.toNumber() * 100), // stepSize, // ); // const maxLiqBatchQuoteUi = r[0]; // const maxLiqBatchUi = r[1]; // const sellImpact = getPriceImpactForBank( // midPriceImpacts, // bank, // (bank.liquidationFee.toNumber() * 100) / 2, // ); // // Deposit limit = sell impact - largest batch // const allowedNewDepositsQuoteUi = // sellImpact.target_amount - maxLiqBatchQuoteUi; // const allowedNewDepositsUi = // sellImpact.target_amount / bank.uiPrice - // maxLiqBatchQuoteUi / bank.uiPrice; // const depositLimitUi = bank.uiDeposits() + allowedNewDepositsUi; // // LOG // // console.log( // // `${bank.name.padStart(20)} ${maxLiqBatchUi // // .toLocaleString() // // .padStart(15)} ${maxLiqBatchQuoteUi // // .toLocaleString() // // .padStart(15)}$ ${sellImpact.target_amount // // .toLocaleString() // // .padStart(12)}$ ${sellImpact.avg_price_impact_percent // // .toLocaleString() // // .padStart(12)}% ${allowedNewDepositsUi // // .toLocaleString() // // .padStart(20)}${allowedNewDepositsQuoteUi // // .toLocaleString() // // .padStart(20)}$ ${bank // // .uiDeposits() // // .toLocaleString() // // .padStart(12)} ${(bank.uiDeposits() * bank.uiPrice) // // .toLocaleString() // // .padStart(12)}$ ${depositLimitUi // // .toLocaleString() // // .padStart(12)}`, // // ); // builder.depositLimit(toNative(depositLimitUi, bank.mintDecimals)); // change = true; // } // } // } const params = builder.build(); if (change) { // console.log( // `${bank.name}, ${params.disableAssetLiquidation} ${params.oracleConfig?.maxStalenessSlots} ${params.oracleConfig?.confFilter}`, // ); // console.log(`${bank.name}, ${bank.oracle} ${params.oracle}`); } let ix = await client.program.methods .tokenEdit( params.oracle, params.oracleConfig, params.groupInsuranceFund, params.interestRateParams, params.loanFeeRate, params.loanOriginationFeeRate, params.maintAssetWeight, params.initAssetWeight, params.maintLiabWeight, params.initLiabWeight, params.liquidationFee, params.stablePriceDelayIntervalSeconds, params.stablePriceDelayGrowthLimit, params.stablePriceGrowthLimit, params.minVaultToDepositsRatio, params.netBorrowLimitPerWindowQuote !== null ? new BN(params.netBorrowLimitPerWindowQuote) : null, params.netBorrowLimitWindowSizeTs !== null ? new BN(params.netBorrowLimitWindowSizeTs) : null, params.borrowWeightScaleStartQuote, params.depositWeightScaleStartQuote, params.resetStablePrice ?? false, params.resetNetBorrowLimit ?? false, params.reduceOnly, params.name, params.forceClose, params.tokenConditionalSwapTakerFeeRate, params.tokenConditionalSwapMakerFeeRate, params.flashLoanSwapFeeRate, params.interestCurveScaling, params.interestTargetUtilization, params.maintWeightShiftStart, params.maintWeightShiftEnd, params.maintWeightShiftAssetTarget, params.maintWeightShiftLiabTarget, params.maintWeightShiftAbort ?? false, false, // setFallbackOracle, unused params.depositLimit, params.zeroUtilRate, params.platformLiquidationFee, params.disableAssetLiquidation, params.collateralFeePerDay, params.forceWithdraw, ) .accounts({ group: group.publicKey, oracle: bank.oracle, admin: group.admin, mintInfo: group.mintInfosMapByTokenIndex.get(bank.tokenIndex) ?.publicKey, fallbackOracle: PublicKey.default, }) .remainingAccounts([ { pubkey: bank.publicKey, isWritable: true, isSigner: false, } as AccountMeta, ]) .instruction(); ix = wrapWithForwarder( ix, new PublicKey('8SSLjXBEVk9nesbhi9UMCA32uijbVBUqWoKPPQPTekzt'), new BN(new Date().getTime() / 1000 + 60 * 60 * 24 * 2 * 7), // 2 weeks ); const tx = new Transaction({ feePayer: wallet.publicKey }).add(ix); const simulated = await client.connection.simulateTransaction(tx); if (simulated.value.err) { console.log('error', simulated.value.logs); throw simulated.value.logs; } if (change) { instructions.push(ix); } // } catch (error) { // // console.log(error.stack); // console.log(`....Skipping ${bank.name}, ${error}`); // } }); const tokenOwnerRecordPk = await getTokenOwnerRecordAddress( MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK, MANGO_MINT, new PublicKey(VSR_DELEGATE_FROM_PK!), ); const [tokenOwnerRecord, proposals] = await Promise.all([ getTokenOwnerRecord(client.connection, tokenOwnerRecordPk), getAllProposals( client.connection, MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK, ), ]); const walletSigner = wallet as never; if (!DRY_RUN) { const proposalAddress = await createProposal( client.connection, walletSigner, MANGO_DAO_WALLET_GOVERNANCE, tokenOwnerRecord, PROPOSAL_TITLE ? PROPOSAL_TITLE : 'Reset sb on demand oracles as fallback oracles in mango-v4', PROPOSAL_LINK ?? '', Object.values(proposals).length, instructions, vsrClient!, false, ); console.log(proposalAddress.toBase58()); } } async function main(): Promise { try { await updateTokenParams(); } catch (error) { console.log(error); } } try { main(); } catch (error) { console.log(error); }