From f31aeb07f51abd4e0c6ea398ed223258a8017779 Mon Sep 17 00:00:00 2001 From: Yaroslav Khodakovskij Date: Mon, 1 Apr 2024 17:04:25 +0300 Subject: [PATCH] Add squads integration logic --- packages/js/package.json | 1 + .../operations/createVault.ts | 48 ++- packages/js/tests/integration/squads.spec.ts | 307 ++++++++++++++++++ packages/validator/helpers.ts | 10 + 4 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 packages/js/tests/integration/squads.spec.ts diff --git a/packages/js/package.json b/packages/js/package.json index 53398e8d2..48ee6c3e9 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -69,6 +69,7 @@ "@project-serum/anchor": "^0.26.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.87.6", + "@sqds/multisig": "^2.1.1", "@types/uuid": "^9.0.1", "big.js": "^6.2.1", "bignumber.js": "^9.0.2", diff --git a/packages/js/src/plugins/vaultOperatorModule/operations/createVault.ts b/packages/js/src/plugins/vaultOperatorModule/operations/createVault.ts index 89a0426a1..c9b1d0487 100644 --- a/packages/js/src/plugins/vaultOperatorModule/operations/createVault.ts +++ b/packages/js/src/plugins/vaultOperatorModule/operations/createVault.ts @@ -1,6 +1,7 @@ import { createCreateRfqInstruction } from '@convergence-rfq/vault-operator'; import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; import BN from 'bn.js'; +import * as multisig from '@sqds/multisig'; import { SendAndConfirmTransactionResponse } from '../../rpcModule'; import { Convergence } from '../../../Convergence'; @@ -8,6 +9,7 @@ import { Operation, OperationHandler, OperationScope, + Signer, useOperation, } from '../../../types'; import { @@ -83,7 +85,9 @@ export const createVaultOperationHandler: OperationHandler }, }; -export type CreateVaultBuilderParams = CreateVaultInput; +export type CreateVaultBuilderParams = CreateVaultInput & { + squads?: { vaultPda: PublicKey; transactionPda: PublicKey }; +}; export type CreateVaultBuilderResult = { builder: TransactionBuilder; @@ -105,15 +109,33 @@ export const createVaultBuilder = async ( orderDetails, activeWindow, settlingWindow, + squads, } = params; const leg = await SpotLegInstrument.create(cvg, legMint, 1, 'long'); const quote = await SpotQuoteInstrument.create(cvg, quoteMint); - const vaultProgram = cvg.programs().getVaultOperator(programs).address; const creator = cvg.identity(); - const vaultParams = Keypair.generate(); - const operator = cvg.vaultOperator().pdas().operator(vaultParams.publicKey); + + let signers: Signer[]; + let vaultParamsKey: PublicKey; + let executorKey: PublicKey; + if (squads === undefined) { + const vaultParamsSigner = Keypair.generate(); + signers = [creator, vaultParamsSigner]; + vaultParamsKey = vaultParamsSigner.publicKey; + executorKey = creator.publicKey; + } else { + signers = []; + vaultParamsKey = multisig.getEphemeralSignerPda({ + ephemeralSignerIndex: 0, + transactionPda: squads.transactionPda, + })[0]; + executorKey = squads.vaultPda; + } + + const vaultProgram = cvg.programs().getVaultOperator(programs).address; + const operator = cvg.vaultOperator().pdas().operator(vaultParamsKey); const protocol = await cvg.protocol().get(); const sendMint = @@ -138,9 +160,9 @@ export const createVaultBuilder = async ( operator, programs ); - const creatorTokens = cvg.tokens().pdas().associatedTokenAccount({ + const executorTokens = cvg.tokens().pdas().associatedTokenAccount({ mint: sendMint, - owner: creator.publicKey, + owner: executorKey, programs, }); @@ -185,11 +207,11 @@ export const createVaultBuilder = async ( const lamportsForOperator = 14288880; const transferLamportIx = { instruction: SystemProgram.transfer({ - fromPubkey: creator.publicKey, + fromPubkey: executorKey, toPubkey: operator, lamports: lamportsForOperator, }), - signers: [creator], + signers: [], key: 'sendLamportsToOperator', }; const acceptablePriceLimitWithDecimals = addDecimals( @@ -208,13 +230,13 @@ export const createVaultBuilder = async ( .add(transferLamportIx, { instruction: createCreateRfqInstruction( { - creator: creator.publicKey, - vaultParams: vaultParams.publicKey, + creator: executorKey, + vaultParams: vaultParamsKey, operator, sendMint, receiveMint, vault: vaultTokens, - vaultTokensSource: creatorTokens, + vaultTokensSource: executorTokens, protocol: cvg.protocol().pdas().protocol(), rfq: rfqPda, whitelist: vaultProgram, @@ -240,7 +262,7 @@ export const createVaultBuilder = async ( }, vaultProgram ), - signers: [creator, vaultParams], + signers, key: 'createVault', }); @@ -255,7 +277,7 @@ export const createVaultBuilder = async ( return { builder, ataBuilder, - vaultAddress: vaultParams.publicKey, + vaultAddress: vaultParamsKey, rfqAddress: rfqPda, }; }; diff --git a/packages/js/tests/integration/squads.spec.ts b/packages/js/tests/integration/squads.spec.ts new file mode 100644 index 000000000..246a82577 --- /dev/null +++ b/packages/js/tests/integration/squads.spec.ts @@ -0,0 +1,307 @@ +import * as multisig from '@sqds/multisig'; +import { + ComputeBudgetProgram, + Keypair, + Signer, + SystemProgram, + TransactionMessage, +} from '@solana/web3.js'; +import expect from 'expect'; +import { + CreateVaultInput, + Mint, + TransactionBuilder, + addDecimals, + createVaultBuilder, +} from '../../src'; +import { createUserCvg } from '../helpers'; +import { BASE_MINT_BTC_PK, QUOTE_MINT_PK } from '../constants'; + +const { Permission, Permissions } = multisig.types; + +describe('integration.squads', () => { + const cvg = createUserCvg('taker'); + const cvgSecond = createUserCvg('dao'); + const cvgMaker = createUserCvg('maker'); + const { connection } = cvg; + const creator = cvg.identity(); + let transactionIndex = BigInt(0); + + const createKey = Keypair.generate(); + + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + }); + + const [squadsVault] = multisig.getVaultPda({ + multisigPda, + index: 0, + }); + + let baseMintBTC: Mint; + let quoteMint: Mint; + + before(async () => { + baseMintBTC = await cvg + .tokens() + .findMintByAddress({ address: BASE_MINT_BTC_PK }); + quoteMint = await cvg + .tokens() + .findMintByAddress({ address: QUOTE_MINT_PK }); + + await createAndFundSquads(); + }); + + const createAndFundSquads = async () => { + const programConfigPda = multisig.getProgramConfigPda({})[0]; + const programConfig = + await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + const configTreasury = programConfig.treasury; + const signature = await multisig.rpc.multisigCreateV2({ + connection, + createKey, + creator, + multisigPda, + configAuthority: null, + timeLock: 0, + members: [ + { + key: creator.publicKey, + permissions: Permissions.all(), + }, + { + key: cvgSecond.identity().publicKey, + permissions: Permissions.fromPermissions([ + Permission.Vote, + Permission.Execute, + ]), + }, + ], + threshold: 2, + rentCollector: null, + treasury: configTreasury, + }); + await connection.confirmTransaction(signature); + + await Promise.all([ + cvg.tokens().send({ + amount: { + basisPoints: addDecimals(100, baseMintBTC.decimals), + currency: baseMintBTC.currency, + }, + mintAddress: baseMintBTC.address, + toOwner: squadsVault, + }), + new TransactionBuilder() + .add({ + instruction: SystemProgram.transfer({ + fromPubkey: cvg.identity().publicKey, + toPubkey: squadsVault, + lamports: addDecimals(10, 9), + }), + signers: [cvg.identity()], + }) + .sendAndConfirm(cvg), + cvg.tokens().createToken({ mint: quoteMint.address, owner: squadsVault }), + ]); + }; + + const createProposal = async (vaultInput: CreateVaultInput) => { + transactionIndex += BigInt(1); + const transactionPda = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + })[0]; + + const { + builder: vaultBuilder, + ataBuilder, + vaultAddress, + } = await createVaultBuilder(cvg, { + ...vaultInput, + squads: { transactionPda, vaultPda: squadsVault }, + }); + await ataBuilder.sendAndConfirm(cvg); + + const message = new TransactionMessage({ + payerKey: squadsVault, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [...vaultBuilder.getInstructions()], + }); + const signature1 = await multisig.rpc.vaultTransactionCreate({ + connection, + feePayer: creator, + multisigPda, + transactionIndex, + creator: creator.publicKey, + vaultIndex: 0, + ephemeralSigners: 1, + transactionMessage: message, + }); + + await connection.confirmTransaction(signature1); + const signature2 = await multisig.rpc.proposalCreate({ + connection, + feePayer: creator, + multisigPda, + transactionIndex, + creator, + }); + + await connection.confirmTransaction(signature2); + + return vaultAddress; + }; + + const approveProposal = async (member: Signer) => { + const signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: member, + multisigPda, + transactionIndex: BigInt(transactionIndex), + member, + }); + + await connection.confirmTransaction(signature); + }; + + const executeProposal = async () => { + const { instruction } = await multisig.instructions.vaultTransactionExecute( + { + connection, + multisigPda, + transactionIndex, + member: cvgSecond.identity().publicKey, + } + ); + const builder = new TransactionBuilder() + .add({ + instruction: ComputeBudgetProgram.setComputeUnitLimit({ + units: 1400000, + }), + signers: [], + }) + .add({ + instruction, + signers: [cvgSecond.identity()], + }); + await builder.sendAndConfirm(cvgSecond); + }; + + const measureTokenDiff = async () => { + const measure = () => + Promise.all([ + cvg.tokens().getTokenBalance({ + mintAddress: baseMintBTC.address, + mintDecimals: baseMintBTC.decimals, + owner: squadsVault, + }), + cvg.tokens().getTokenBalance({ + mintAddress: quoteMint.address, + mintDecimals: quoteMint.decimals, + owner: squadsVault, + }), + ]); + + const [{ tokenBalance: legBefore }, { tokenBalance: quoteBefore }] = + await measure(); + + return async () => { + const [{ tokenBalance: legAfter }, { tokenBalance: quoteAfter }] = + await measure(); + + return { leg: legAfter - legBefore, quote: quoteAfter - quoteBefore }; + }; + }; + + it('Settle sell proposals through vault', async () => { + const measurer = await measureTokenDiff(); + + const vaultAddress = await createProposal({ + acceptablePriceLimit: 40000, + quoteMint, + legMint: baseMintBTC, + orderDetails: { + type: 'sell', + legAmount: 2, + }, + activeWindow: 600, + settlingWindow: 600, + }); + + await approveProposal(creator); + await approveProposal(cvgSecond.identity()); + await executeProposal(); + + const { vault, rfq } = await cvg + .vaultOperator() + .findByAddress({ address: vaultAddress }); + + const { rfqResponse: response } = await cvgMaker + .rfqs() + .respond({ rfq: rfq.address, bid: { price: 40000 } }); + const { vault: updatedVault } = await cvgMaker + .vaultOperator() + .confirmAndPrepare({ rfq, vault, response }); + await cvgMaker.rfqs().prepareSettlement({ + rfq: rfq.address, + response: response.address, + legAmountToPrepare: 1, + }); + await cvgMaker.rfqs().settle({ response: response.address }); + await cvgMaker.rfqs().cleanUpResponse({ response: response.address }); + await cvgMaker.vaultOperator().withdrawTokens({ rfq, vault: updatedVault }); + + expect(await measurer()).toMatchObject({ + leg: -2, + quote: 80000 * (1 - 0.01), + }); + }); + + it('Settle buy proposals through vault', async () => { + const measurer = await measureTokenDiff(); + + const vaultAddress = await createProposal({ + acceptablePriceLimit: 5000, + quoteMint, + legMint: baseMintBTC, + orderDetails: { + type: 'buy', + quoteAmount: 75000, + }, + activeWindow: 600, + settlingWindow: 600, + }); + + await approveProposal(creator); + await approveProposal(cvgSecond.identity()); + await executeProposal(); + + const { vault, rfq } = await cvg + .vaultOperator() + .findByAddress({ address: vaultAddress }); + + const { rfqResponse: response } = await cvgMaker + .rfqs() + .respond({ rfq: rfq.address, ask: { price: 5000 } }); + const { vault: updatedVault } = await cvgMaker + .vaultOperator() + .confirmAndPrepare({ rfq, vault, response }); + await cvgMaker.rfqs().prepareSettlement({ + rfq: rfq.address, + response: response.address, + legAmountToPrepare: 1, + }); + await cvgMaker.rfqs().settle({ response: response.address }); + await cvgMaker.rfqs().cleanUpResponse({ response: response.address }); + await cvgMaker.vaultOperator().withdrawTokens({ rfq, vault: updatedVault }); + + expect(await measurer()).toMatchObject({ + leg: 15, + quote: -75000, + }); + }); +}); diff --git a/packages/validator/helpers.ts b/packages/validator/helpers.ts index 9eb4841f9..e281b4c44 100644 --- a/packages/validator/helpers.ts +++ b/packages/validator/helpers.ts @@ -102,6 +102,16 @@ const getBaseArgs = () => [ '--account-dir', path.join(HXRO_DEPS, 'accounts'), + // squads fixtures + '--url', + 'm', + '-c', + 'BSTq9w3kZwNwpBXJEvTZz2G9ZTNyKBvoSeXMvwb4cNZr', + '-c', + 'SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf', + '-c', + 'Fy3YMJCvwbAXUgUM5b91ucUVA3jYzwWLHL3MwBqKsh8n', + '--ledger', './test-ledger', '--reset',