import { LISTING_PRESETS, MidPriceImpact, getMidPriceImpacts, } 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 { LiqorPriceImpact, buildGroupGrid, findLargestAssetBatchUi, getEquityForMangoAccounts, } from '../src/risk'; import { buildFetch, toNative, 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; 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)}`, ); } 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; 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 (!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; // } // } try { // 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, // ); if (!bank.areBorrowsReduceOnly()) { // 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 (tier) { console.log( `${bank.name}, ${tier.preset_name}, ${( params.liquidationFee! * 100 ).toFixed(2)} ${(params.platformLiquidationFee! * 100).toFixed( 2, )}`, ); } const 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(); 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(`....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; console.log(DRY_RUN); if (!DRY_RUN) { const proposalAddress = await createProposal( client.connection, walletSigner, MANGO_DAO_WALLET_GOVERNANCE, tokenOwnerRecord, PROPOSAL_TITLE ? PROPOSAL_TITLE : 'Update liquidation and platform liquidation fee 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); }