From 0a0eb2c0ae87162231969d7bd440ce6ad1ab5c6a Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Wed, 21 Jul 2021 22:44:35 -0700 Subject: [PATCH] Prune instruction (#1) --- Cargo.lock | 25 ++-- deps/serum-dex | 2 +- programs/permissioned-markets/src/lib.rs | 78 ++++++++++++ tests/permissioned-markets.js | 147 ++++++++++++++++++++--- tests/utils/market-lister.js | 9 +- tests/utils/market-proxy.js | 28 ++++- 6 files changed, 259 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9573f83..18a5b99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ checksum = "6b2d54853319fd101b8dd81de382bcbf3e03410a64d8928bbee85a3e7dcde483" [[package]] name = "anchor-attribute-access-control" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "anyhow", @@ -37,7 +37,7 @@ dependencies = [ [[package]] name = "anchor-attribute-account" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "anyhow", @@ -49,7 +49,7 @@ dependencies = [ [[package]] name = "anchor-attribute-error" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "proc-macro2", @@ -60,7 +60,7 @@ dependencies = [ [[package]] name = "anchor-attribute-event" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "anyhow", @@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "anchor-attribute-interface" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "anyhow", @@ -85,7 +85,7 @@ dependencies = [ [[package]] name = "anchor-attribute-program" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "anyhow", @@ -97,7 +97,7 @@ dependencies = [ [[package]] name = "anchor-attribute-state" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "anyhow", @@ -109,7 +109,7 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-syn", "anyhow", @@ -121,7 +121,7 @@ dependencies = [ [[package]] name = "anchor-lang" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -136,12 +136,13 @@ dependencies = [ "bytemuck", "solana-program", "thiserror", + "zeroize", ] [[package]] name = "anchor-spl" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anchor-lang", "lazy_static", @@ -153,7 +154,7 @@ dependencies = [ [[package]] name = "anchor-syn" version = "0.11.1" -source = "git+https://github.com/project-serum/anchor#615764b9c85fe1507b946ee1f5c3714f0ff1ad64" +source = "git+https://github.com/project-serum/anchor#0998422348fa5f8ebdf907006f5334ad14c4e452" dependencies = [ "anyhow", "bs58", @@ -880,7 +881,7 @@ dependencies = [ [[package]] name = "serum_dex" version = "0.3.1" -source = "git+https://github.com/project-serum/serum-dex?branch=armani/auth#2037a646f82e689f8e7a00c8a34b30e20253ba11" +source = "git+https://github.com/project-serum/serum-dex?branch=armani/auth#814c1fd05b00ae99d68d8f9617cc3868b7aceae1" dependencies = [ "arrayref", "bincode", diff --git a/deps/serum-dex b/deps/serum-dex index 86a413a..814c1fd 160000 --- a/deps/serum-dex +++ b/deps/serum-dex @@ -1 +1 @@ -Subproject commit 86a413a0f99eb7b2195f797e60183878d82e7fed +Subproject commit 814c1fd05b00ae99d68d8f9617cc3868b7aceae1 diff --git a/programs/permissioned-markets/src/lib.rs b/programs/permissioned-markets/src/lib.rs index 2d4e83d..ba36db3 100644 --- a/programs/permissioned-markets/src/lib.rs +++ b/programs/permissioned-markets/src/lib.rs @@ -68,6 +68,14 @@ pub mod permissioned_markets { /// The identity token must be given as the fist account. struct Identity; +impl Identity { + fn prepare_pda<'info>(acc_info: &AccountInfo<'info>) -> AccountInfo<'info> { + let mut acc_info = acc_info.clone(); + acc_info.is_signer = true; + acc_info + } +} + impl MarketMiddleware for Identity { /// Accounts: /// @@ -117,6 +125,25 @@ impl MarketMiddleware for Identity { verify_and_strip_auth(ctx) } + /// Accounts: + /// + /// 0. Authorization token (revoked). + /// .. + fn prune(&self, ctx: &mut Context) -> ProgramResult { + verify_revoked_and_strip_auth(ctx)?; + + // Sign with the prune authority. + let market = &ctx.accounts[0]; + ctx.seeds.push(prune_authority! { + program = ctx.program_id, + dex_program = ctx.dex_program_id, + market = market.key + }); + + ctx.accounts[3] = Self::prepare_pda(&ctx.accounts[3]); + Ok(()) + } + /// Accounts: /// /// 0. Authorization token. @@ -139,12 +166,63 @@ fn verify_and_strip_auth(ctx: &mut Context) -> ProgramResult { Ok(()) } +fn verify_revoked_and_strip_auth(ctx: &mut Context) -> ProgramResult { + // The rent sysvar is used as a dummy example of an identity token. + let auth = &ctx.accounts[0]; + require!(auth.key != &rent::ID, TokenNotRevoked); + + // Strip off the account before possing on the message. + ctx.accounts = (&ctx.accounts[1..]).to_vec(); + + Ok(()) +} + +// Macros. + +/// Returns the seeds used for the prune authority. +#[macro_export] +macro_rules! prune_authority { + ( + program = $program:expr, + dex_program = $dex_program:expr, + market = $market:expr, + bump = $bump:expr + ) => { + vec![ + b"prune".to_vec(), + $dex_program.as_ref().to_vec(), + $market.as_ref().to_vec(), + vec![$bump], + ] + }; + ( + program = $program:expr, + dex_program = $dex_program:expr, + market = $market:expr + ) => { + vec![ + b"prune".to_vec(), + $dex_program.as_ref().to_vec(), + $market.as_ref().to_vec(), + vec![ + Pubkey::find_program_address( + &[b"prune".as_ref(), $dex_program.as_ref(), $market.as_ref()], + $program, + ) + .1, + ], + ] + }; +} + // Error. #[error] pub enum ErrorCode { #[msg("Invalid auth token provided")] InvalidAuth, + #[msg("Auth token not revoked")] + TokenNotRevoked, } // Constants. diff --git a/tests/permissioned-markets.js b/tests/permissioned-markets.js index afcaf08..e43c019 100644 --- a/tests/permissioned-markets.js +++ b/tests/permissioned-markets.js @@ -164,21 +164,6 @@ describe("permissioned-markets", () => { assert.ok(afterOoAccount.quoteTokenTotal.eq(usdcPosted)); }); - // Need to crank the cancel so that we can close later. - it("Cranks the cancel transaction", async () => { - // TODO: can do this in a single transaction if we covert the pubkey bytes - // into a [u64; 4] array and sort. I'm lazy though. - let eq = await marketProxy.market.loadEventQueue(provider.connection); - while (eq.length > 0) { - const tx = new Transaction(); - tx.add( - marketProxy.market.makeConsumeEventsInstruction([eq[0].openOrders], 1) - ); - await provider.send(tx); - eq = await marketProxy.market.loadEventQueue(provider.connection); - } - }); - it("Settles funds on the orderbook", async () => { // Given. const beforeTokenAccount = await usdcClient.getAccountInfo(usdcAccount); @@ -204,6 +189,124 @@ describe("permissioned-markets", () => { ); }); + // Need to crank the cancel so that we can close later. + it("Cranks the cancel transaction", async () => { + await crankEventQueue(provider, marketProxy); + }); + + it("Closes an open orders account", async () => { + // Given. + const beforeAccount = await program.provider.connection.getAccountInfo( + program.provider.wallet.publicKey + ); + + // When. + const tx = new Transaction(); + tx.add( + marketProxy.instruction.closeOpenOrders( + openOrders, + provider.wallet.publicKey, + provider.wallet.publicKey + ) + ); + await provider.send(tx); + + // Then. + const afterAccount = await program.provider.connection.getAccountInfo( + program.provider.wallet.publicKey + ); + const closedAccount = await program.provider.connection.getAccountInfo( + openOrders + ); + assert.ok(23352768 === afterAccount.lamports - beforeAccount.lamports); + assert.ok(closedAccount === null); + }); + + it("Re-opens an open orders account", async () => { + const tx = new Transaction(); + tx.add( + await marketProxy.instruction.initOpenOrders( + program.provider.wallet.publicKey, + marketProxy.market.address, + marketProxy.market.address, // Dummy. Replaced by middleware. + marketProxy.market.address // Dummy. Replaced by middleware. + ) + ); + await provider.send(tx); + + const account = await provider.connection.getAccountInfo(openOrders); + assert.ok(account.owner.toString() === DEX_PID.toString()); + }); + + it("Posts several bids and asks on the orderbook", async () => { + const size = 10; + const price = 2; + for (let k = 0; k < 10; k += 1) { + const tx = new Transaction(); + tx.add( + marketProxy.instruction.newOrderV3({ + owner: program.provider.wallet.publicKey, + payer: usdcAccount, + side: "buy", + price, + size, + orderType: "postOnly", + clientId: new BN(999), + openOrdersAddressKey: openOrders, + selfTradeBehavior: "abortTransaction", + }) + ); + await provider.send(tx); + } + + const sizeAsk = 10; + const priceAsk = 10; + + for (let k = 0; k < 10; k += 1) { + const txAsk = new Transaction(); + txAsk.add( + marketProxy.instruction.newOrderV3({ + owner: program.provider.wallet.publicKey, + payer: tokenAccount, + side: "sell", + price: priceAsk, + size: sizeAsk, + orderType: "postOnly", + clientId: new BN(1000), + openOrdersAddressKey: openOrders, + selfTradeBehavior: "abortTransaction", + }) + ); + await provider.send(txAsk); + } + }); + + it("Prunes the orderbook", async () => { + const tx = new Transaction(); + tx.add( + marketProxy.instruction.prune(openOrders, provider.wallet.publicKey) + ); + await provider.send(tx); + }); + + it("Settles the account", async () => { + const tx = new Transaction(); + tx.add( + await marketProxy.instruction.settleFunds( + openOrders, + provider.wallet.publicKey, + tokenAccount, + usdcAccount, + referral + ) + ); + await provider.send(tx); + }); + + it("Cranks the prune transaction", async () => { + await crankEventQueue(provider, marketProxy); + }); + it("Closes an open orders account", async () => { // Given. const beforeAccount = await program.provider.connection.getAccountInfo( @@ -232,3 +335,17 @@ describe("permissioned-markets", () => { assert.ok(closedAccount === null); }); }); + +async function crankEventQueue(provider, marketProxy) { + // TODO: can do this in a single transaction if we covert the pubkey bytes + // into a [u64; 4] array and sort. I'm lazy though. + let eq = await marketProxy.market.loadEventQueue(provider.connection); + while (eq.length > 0) { + const tx = new Transaction(); + tx.add( + marketProxy.market.makeConsumeEventsInstruction([eq[0].openOrders], 1) + ); + await provider.send(tx); + eq = await marketProxy.market.loadEventQueue(provider.connection); + } +} diff --git a/tests/utils/market-lister.js b/tests/utils/market-lister.js index 99ed16d..634ad92 100644 --- a/tests/utils/market-lister.js +++ b/tests/utils/market-lister.js @@ -7,12 +7,14 @@ const { SystemProgram, } = require("@project-serum/anchor").web3; const { TOKEN_PROGRAM_ID } = require("@solana/spl-token"); +const serum = require("@project-serum/serum"); const { DexInstructions, TokenInstructions, OpenOrdersPda, MARKET_STATE_LAYOUT_V3, -} = require("@project-serum/serum"); +} = serum; +const { Identity } = require("./market-proxy"); const { DEX_PID } = require("./common"); // Creates a market on the dex. @@ -129,6 +131,11 @@ async function list({ DEX_PID, proxyProgramId ), + pruneAuthority: await Identity.pruneAuthority( + market.publicKey, + DEX_PID, + proxyProgramId + ), }) ); diff --git a/tests/utils/market-proxy.js b/tests/utils/market-proxy.js index f5e1e14..2b5b960 100644 --- a/tests/utils/market-proxy.js +++ b/tests/utils/market-proxy.js @@ -1,4 +1,9 @@ -const { SYSVAR_RENT_PUBKEY } = require("@solana/web3.js"); +const anchor = require("@project-serum/anchor"); +const { + PublicKey, + SYSVAR_RENT_PUBKEY, + SYSVAR_CLOCK_PUBKEY, +} = require("@solana/web3.js"); const { OpenOrders, OpenOrdersPda, @@ -51,14 +56,35 @@ class Identity { closeOpenOrders(ix) { this.proxy(ix); } + prune(ix) { + this.proxyRevoked(ix); + } proxy(ix) { ix.keys = [ { pubkey: SYSVAR_RENT_PUBKEY, isWritable: false, isSigner: false }, ...ix.keys, ]; } + proxyRevoked(ix) { + ix.keys = [ + { pubkey: SYSVAR_CLOCK_PUBKEY, isWritable: false, isSigner: false }, + ...ix.keys, + ]; + } + static async pruneAuthority(market, dexProgramId, proxyProgramId) { + const [addr] = await PublicKey.findProgramAddress( + [ + anchor.utils.bytes.utf8.encode("prune"), + dexProgramId.toBuffer(), + market.toBuffer(), + ], + proxyProgramId + ); + return addr; + } } module.exports = { load, + Identity, };