diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 7debf1a978..5493a58745 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -609,8 +609,9 @@ fn amm_perp_ref_offset() { let reserve_price = perp_market.amm.reserve_price().unwrap(); let (b1, a1) = perp_market.amm.bid_ask_price(reserve_price).unwrap(); - assert_eq!(b1, 7098048); - assert_eq!(a1, 7105178); + assert_eq!(reserve_price, 7101599); + assert_eq!(b1, 7225876); + assert_eq!(a1, 7233006); assert_eq!( perp_market.amm.historical_oracle_data.last_oracle_price, 7101600 @@ -690,8 +691,8 @@ fn amm_perp_ref_offset() { let r = perp_market.amm.reserve_price().unwrap(); let (b, a) = perp_market.amm.bid_ask_price(r).unwrap(); - assert_eq!(b, 7098048); - assert_eq!(a, 7105178); + assert_eq!(b, 7417620); + assert_eq!(a, 7424750); assert_eq!( perp_market.amm.historical_oracle_data.last_oracle_price, 7101600 diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 94a5ef9e81..a01dd72e57 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -14,10 +14,11 @@ use crate::math::constants::{ }; use crate::math::constants::{ AMM_RESERVE_PRECISION_I128, AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION, - BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, - LIQUIDATION_FEE_PRECISION, LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, LP_FEE_SLICE_DENOMINATOR, - LP_FEE_SLICE_NUMERATOR, MARGIN_PRECISION, MARGIN_PRECISION_U128, MAX_LIQUIDATION_MULTIPLIER, - PEG_PRECISION, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, + BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, + DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, LIQUIDATION_FEE_PRECISION, + LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, LP_FEE_SLICE_DENOMINATOR, LP_FEE_SLICE_NUMERATOR, + MARGIN_PRECISION, MARGIN_PRECISION_U128, MAX_LIQUIDATION_MULTIPLIER, PEG_PRECISION, + PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; use crate::math::helpers::get_proportion_i128; @@ -1410,19 +1411,33 @@ impl AMM { } pub fn bid_price(&self, reserve_price: u64) -> DriftResult { + let adjusted_spread = (-(self + .short_spread + .cast::()?)) + .safe_add(self.reference_price_offset)?; + + let multiplier = BID_ASK_SPREAD_PRECISION_I128.safe_add(adjusted_spread.cast::()?)?; + reserve_price .cast::()? - .safe_mul(BID_ASK_SPREAD_PRECISION_U128.safe_sub(self.short_spread.cast()?)?)? + .safe_mul(multiplier.cast::()?)? .safe_div(BID_ASK_SPREAD_PRECISION_U128)? .cast() } pub fn ask_price(&self, reserve_price: u64) -> DriftResult { + let adjusted_spread = self + .long_spread + .cast::()? + .safe_add(self.reference_price_offset)?; + + let multiplier = BID_ASK_SPREAD_PRECISION_I128.safe_add(adjusted_spread.cast::()?)?; + reserve_price .cast::()? - .safe_mul(BID_ASK_SPREAD_PRECISION_U128.safe_add(self.long_spread.cast()?)?)? + .safe_mul(multiplier.cast::()?)? .safe_div(BID_ASK_SPREAD_PRECISION_U128)? - .cast::() + .cast() } pub fn bid_ask_price(&self, reserve_price: u64) -> DriftResult<(u64, u64)> { diff --git a/sdk/src/bankrun/bankrunConnection.ts b/sdk/src/bankrun/bankrunConnection.ts index 0082b031b8..1e1fe385ed 100644 --- a/sdk/src/bankrun/bankrunConnection.ts +++ b/sdk/src/bankrun/bankrunConnection.ts @@ -121,6 +121,12 @@ export class BankrunContextWrapper { } async moveTimeForward(increment: number): Promise { + + const approxSlots = increment / 0.4; + const slot = await this.connection.getSlot(); + console.log(`warping to slot ${slot} -> ${BigInt(slot) + BigInt(approxSlots)}`) + this.context.warpToSlot(BigInt(Number(slot) + approxSlots)); + const currentClock = await this.context.banksClient.getClock(); const newUnixTimestamp = currentClock.unixTimestamp + BigInt(increment); const newClock = new Clock( @@ -130,6 +136,7 @@ export class BankrunContextWrapper { currentClock.leaderScheduleEpoch, newUnixTimestamp ); + await this.context.setClock(newClock); } @@ -290,7 +297,7 @@ export class BankrunConnection { return signature; } - private async updateSlotAndClock() { + async updateSlotAndClock() { const currentSlot = await this.getSlot(); const nextSlot = currentSlot + BigInt(1); this.context.warpToSlot(nextSlot); diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index aa092abe89..2775260207 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,7 +6,7 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(overwritePerpAccounts.ts) +test_files=(referencePriceOffset.ts) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/referencePriceOffset.ts b/tests/referencePriceOffset.ts new file mode 100644 index 0000000000..9d8c0b58e8 --- /dev/null +++ b/tests/referencePriceOffset.ts @@ -0,0 +1,342 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect } from 'chai'; +import { Program } from '@coral-xyz/anchor'; +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; +import { + BN, + TestClient, + QUOTE_PRECISION, + PRICE_PRECISION, + OracleSource, + PERCENTAGE_PRECISION, + calculateBidAskPrice, + convertToNumber, + OrderType, + BASE_PRECISION, + PositionDirection, + DriftClient, + parseLogs, + parseLogsWithRaw, + isFallbackAvailableLiquiditySource, + calculateBaseAssetAmountForAmmToFulfill, + isFillableByVAMM, +} from '../sdk/src'; +import { + initializeQuoteSpotMarket, + mockUSDCMint, + mockUserUSDCAccount, + overWritePerpMarket, + printTxLogs, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + + +// 1MPEPE-PERP +const marketPubkey = new PublicKey('GsMte91Y1eY9XYtY1nt1Ax77V5hzsj3rr1a7a29mxHZw'); +const marketIndex = 10; +const oraclePubkey = new PublicKey('Eo8x9Y1289GvsuYVwRS2R8HfiWRXxYofL1KYvHK2ZM2o'); +const oracleSource = OracleSource.PYTH_LAZER_1M; +const marketSnapshotBytes = + '0adf0c2c6bf537f7ebc5f713a1eebbe52ad08af6f417aed85122d533728331aaacae7c1cf1fc1cceccf9a3244f7965a75c10c05f359aadbb808523d3d1b7e8cb2e32c9604bc6da08b8579a00000000000000000000000000000000000000000026689a000000000012699a0000000000f71853680000000035d1a5c61d00000000000000000000002836fc9d00000000000000000000000019c19b0727d500000000000000000000000000000000000018b0ff41782207000000000000000000502481ab6227070000000000000000003c6b1200000000000000000000000000c4fdee3020eb0500000000000000000093469bd9ac9f08000000000000000000b1c7840aed2407000000000000000000aaed99000000000000000000000000001f016f8ff1240700000000000000000000d27f92b00b000000000000000000000014582ebff6fffffffffffffffffffffcb99d43700200000000000000000000042c3a7dffffffffffffffffffffffff0080c6a47e8d03000000000000000000c90d0a3bfeffffffffffffffffffffffb1782f4cddffffffffffffffffffffff6ee036a919000000000000000000000021371fb8dcffffffffffffffffffffffee9f3fc61b0000000000000000000000009eff3786030000000000000000000053e103000000000053e103000000000053e1030000000000faf4040000000000bdfe266c4500000000000000000000005be6e8d73800000000000000000000003e4ca2a10c0000000000000000000000e528578d4b0000000000000000000000000000000000000000000000000000008c4d8aa31400000000000000000000000530837a0100000000000000000000002b32817a01000000000000000000000000000000000000000000000000000000527b02fbd31b070000000000000000007b07d1be112e07000000000000000000cb9d314bb52507000000000000000000c69cc2df242407000000000000000000b8579a0000000000000000000000000006289a0000000000a1fd9a0000000000d3929a000000000023bd9a000000000020feb81400000000a9010000000000003463f3f6ffffffffd50c536800000000100e00000000000000ca9a3b00000000640000000000000000f2052a0100000000000000000000007806d8c20d000000b124804a00000000a03db450000000001e07536800000000120e0000000000006e10000000000000f718536800000000e8030000905f01006c1c0000d00d00001d0000002a000000e803320064640e01000000000400000072571d0900000000c8109b93010000004057f0f6ffffffff00000000000000000000000000000000cad66686df3f000000000000000000000000000000000000314d504550452d5045525020202020202020202020202020202020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000c0ed000000000000ec46000000000000231f000000000000ee020000ee020000a861000050c30000c4090000e204000000000000102700007b000000510000000a00010003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; +const oracleSnapshotBytes = + '9f07a1f9225179853d8b010000000000802e6ff8dd3706003efeb81400000000f6ffffff000000000c00000000000000';; + +const usdcMintAmount = new BN(100_000_000).mul(QUOTE_PRECISION); + +describe('Reference Price Offset E2E', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let adminClient: TestClient; + let fillerDriftClient: DriftClient; + let usdcMint: Keypair; + + let userUSDCAccount: Keypair; + + before(async () => { + const context = await startAnchor( + '', + [ + { + name: 'serum_dex', + programId: new PublicKey( + 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX' + ), + }, + ], + [ + { + address: marketPubkey, + info: { + executable: false, + owner: program.programId, + lamports: LAMPORTS_PER_SOL, + data: Buffer.from(marketSnapshotBytes, 'hex'), + } + }, + { + address: oraclePubkey, + info: { + executable: false, + owner: program.programId, + lamports: LAMPORTS_PER_SOL, + data: Buffer.from(oracleSnapshotBytes, 'hex'), + } + } + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + // seed SOL-PERP market and oracle accounts + // bankrunContextWrapper.context.setAccount(marketPubkey, { + // executable: false, + // owner: program.programId, + // lamports: LAMPORTS_PER_SOL, + // data: Buffer.from(marketSnapshotBytes, 'hex'), + // }); + // bankrunContextWrapper.context.setAccount(oraclePubkey, { + // executable: false, + // owner: program.programId, + // lamports: LAMPORTS_PER_SOL, + // data: Buffer.from(oracleSnapshotBytes, 'hex'), + // }); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 50 * LAMPORTS_PER_SOL); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [marketIndex], + spotMarketIndexes: [0, 1, 2], + oracleInfos: [ + { + publicKey: oraclePubkey, + source: oracleSource, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcMintAmount, + bankrunContextWrapper, + keypair.publicKey + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + usdcMintAmount, + userUSDCAccount.publicKey + ); + + /// why have to do this manually and bulk acc lodaer not handle + await adminClient.accountSubscriber.addPerpMarket(0); + await adminClient.accountSubscriber.addOracle({ + publicKey: oraclePubkey, + source: OracleSource.PYTH_LAZER, + }); + await adminClient.accountSubscriber.setPerpOracleMap(); + + const keypair2 = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair2, 50 * LAMPORTS_PER_SOL); + fillerDriftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair2), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + perpMarketIndexes: [marketIndex], + spotMarketIndexes: [0, 1], + // subAccountIds: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oraclePubkey, + source: oracleSource, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await fillerDriftClient.subscribe(); + + await fillerDriftClient.initializeUserAccount(); + }); + + after(async () => { + await adminClient.unsubscribe(); + }); + + it('should overwrite perp accounts', async () => { + await adminClient.fetchAccounts(); + + const oracle = adminClient.getOracleDataForPerpMarket(marketIndex); + + const perpMarket0 = adminClient.getPerpMarketAccount(marketIndex); + expect(perpMarket0.amm.curveUpdateIntensity).to.equal(100); + expect(perpMarket0.amm.referencePriceOffset).to.equal(0); + + const [vBid, vAsk] = calculateBidAskPrice( + perpMarket0.amm, + oracle, + true, + false + ); + const vBidNum = convertToNumber(vBid); + const vAskNum = convertToNumber(vAsk); + const spread = (vAskNum - vBidNum) / ((vAskNum + vBidNum) / 2); + console.log( + `Before ref price: vBid: ${vBidNum}, vAsk: ${vAskNum}, spread: ${spread * 10000}bps` + ); + + + perpMarket0.amm.curveUpdateIntensity = 200; + // perpMarket0.amm.referencePriceOffset = + // PERCENTAGE_PRECISION.toNumber() / 1000; // 10 bps + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket0.pubkey, + perpMarket0 + ); + await adminClient.fetchAccounts(); + + const perpMarket2 = adminClient.getPerpMarketAccount(marketIndex); + expect(perpMarket2.amm.curveUpdateIntensity).to.equal(200); + // expect(perpMarket2.amm.referencePriceOffset).to.equal( + // PERCENTAGE_PRECISION.toNumber() / 1000 + // ); + + const [vBid2, vAsk2] = calculateBidAskPrice( + perpMarket2.amm, + oracle, + true, + false + ); + const vBidNum2 = convertToNumber(vBid2); + const vAskNum2 = convertToNumber(vAsk2); + const spread2 = (vAskNum2 - vBidNum2) / ((vAskNum2 + vBidNum2) / 2); + console.log( + `After ref price: vBid: ${vBidNum2}, vAsk: ${vAskNum2}, spread: ${spread2 * 10000}bps` + ); + + + const adminUser = adminClient.getUser(0); + const adminUserAcc = await adminClient.getUserAccountPublicKey(); + + let now = bankrunContextWrapper.connection.getTime(); + + let tx = await adminClient.placePerpOrder({ + orderType: OrderType.ORACLE, + marketIndex, + baseAssetAmount: new BN(100).mul(BASE_PRECISION), + direction: PositionDirection.LONG, + auctionDuration: 20, + auctionStartPrice: vAsk2, + auctionEndPrice: vAsk2.muln(110).divn(100), + maxTs: new BN(now + 60), + }) + await printTxLogs(bankrunContextWrapper.connection.toConnection(), tx); + let logs = await printTxLogs(bankrunContextWrapper.connection.toConnection(), tx); + let events = parseLogsWithRaw(program, logs); + console.log(events.events.map(e => e.data)); + + await adminClient.fetchAccounts(); + let pos = adminClient.getUser(0).getPerpPosition(marketIndex); + console.log('base: ', convertToNumber(pos.baseAssetAmount, BASE_PRECISION)); + console.log('quote:', convertToNumber(pos.quoteAssetAmount, QUOTE_PRECISION)); + + let openOrders = adminClient.getUser(0).getOpenOrders(); + let order = openOrders.length > 0 ? openOrders[0] : null; + if (!order) { + throw new Error('No open orders found'); + } + + /// check if vamm can fill + let slot = await bankrunContextWrapper.connection.getSlot(); + console.log("slot:", slot); + console.log("now:", now); + now = bankrunContextWrapper.connection.getTime(); + + // console.log("fillableByVamm:", isFillableByVAMM(order, perpMarket2, oracle, Number(slot), now, 0)); + + await bankrunContextWrapper.moveTimeForward(30); + + slot = await bankrunContextWrapper.connection.getSlot(); + now = bankrunContextWrapper.connection.getTime(); + console.log("slot:", slot); + console.log("now:", now); + now = bankrunContextWrapper.connection.getTime(); + console.log("fillableByVamm:", isFillableByVAMM(order, perpMarket2, oracle, Number(slot), now, 0)); + + tx = await fillerDriftClient.fillPerpOrder(adminUserAcc, adminUser.getUserAccount(), order); + logs = await printTxLogs(bankrunContextWrapper.connection.toConnection(), tx); + events = parseLogsWithRaw(program, logs); + console.log(events.events.map(e => e.data)); + + await adminClient.fetchAccounts(); + pos = adminClient.getUser(0).getPerpPosition(marketIndex); + console.log('base: ', convertToNumber(pos.baseAssetAmount, BASE_PRECISION)); + console.log('quote:', convertToNumber(pos.quoteAssetAmount, QUOTE_PRECISION)); + + + + const perpMarket3 = adminClient.getPerpMarketAccount(marketIndex); + expect(perpMarket3.amm.curveUpdateIntensity).to.equal(200); + // expect(perpMarket2.amm.referencePriceOffset).to.equal( + // PERCENTAGE_PRECISION.toNumber() / 1000 + // ); + + const [vBid3, vAsk3] = calculateBidAskPrice( + perpMarket3.amm, + oracle, + true, + false + ); + const vBidNum3 = convertToNumber(vBid3); + const vAskNum3 = convertToNumber(vAsk3); + const spread3 = (vAskNum3 - vBidNum3) / ((vAskNum3 + vBidNum3) / 2); + console.log( + `After ref price: vBid: ${vBidNum3}, vAsk: ${vAskNum3}, spread: ${spread3 * 10000}bps` + ); + }); +}); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index 67a768a61a..65832d4be4 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -550,12 +550,13 @@ export async function createUserWithUSDCAndWSOLAccount( export async function printTxLogs( connection: Connection, txSig: TransactionSignature -): Promise { +): Promise { + const tx = await connection.getTransaction(txSig, { commitment: 'confirmed' }); console.log( 'tx logs', - (await connection.getTransaction(txSig, { commitment: 'confirmed' })).meta - .logMessages + tx.meta.logMessages, ); + return tx.meta.logMessages; } export async function mintToInsuranceFund(