diff --git a/mango_v4.json b/mango_v4.json index d041fa22f..b38b3df22 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -1760,6 +1760,36 @@ } ] }, + { + "name": "sequenceCheck", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "owner" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "expectedSequenceNumber", + "type": "u64" + } + ] + }, { "name": "stubOracleCreate", "accounts": [ @@ -7942,12 +7972,16 @@ ], "type": "u64" }, + { + "name": "sequenceNumber", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 152 + 144 ] } }, @@ -9721,12 +9755,16 @@ "name": "lastCollateralFeeCharge", "type": "u64" }, + { + "name": "sequenceNumber", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 152 + 144 ] } } @@ -11008,6 +11046,9 @@ }, { "name": "TokenForceWithdraw" + }, + { + "name": "SequenceCheck" } ] } @@ -14350,6 +14391,16 @@ "code": 6069, "name": "TokenAssetLiquidationDisabled", "msg": "the asset does not allow liquidation" + }, + { + "code": 6070, + "name": "BorrowsRequireHealthAccountBank", + "msg": "for borrows the bank must be in the health account list" + }, + { + "code": 6071, + "name": "InvalidSequenceNumber", + "msg": "invalid sequence number" } ] } \ No newline at end of file diff --git a/programs/mango-v4/src/accounts_ix/mod.rs b/programs/mango-v4/src/accounts_ix/mod.rs index df8ea1f30..ac5b5639b 100644 --- a/programs/mango-v4/src/accounts_ix/mod.rs +++ b/programs/mango-v4/src/accounts_ix/mod.rs @@ -45,6 +45,7 @@ pub use perp_place_order::*; pub use perp_settle_fees::*; pub use perp_settle_pnl::*; pub use perp_update_funding::*; +pub use sequence_check::*; pub use serum3_cancel_all_orders::*; pub use serum3_cancel_order::*; pub use serum3_close_open_orders::*; @@ -123,6 +124,7 @@ mod perp_place_order; mod perp_settle_fees; mod perp_settle_pnl; mod perp_update_funding; +mod sequence_check; mod serum3_cancel_all_orders; mod serum3_cancel_order; mod serum3_close_open_orders; diff --git a/programs/mango-v4/src/accounts_ix/sequence_check.rs b/programs/mango-v4/src/accounts_ix/sequence_check.rs new file mode 100644 index 000000000..ca4a6f3b9 --- /dev/null +++ b/programs/mango-v4/src/accounts_ix/sequence_check.rs @@ -0,0 +1,20 @@ +use crate::error::*; +use crate::state::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct SequenceCheck<'info> { + #[account( + constraint = group.load()?.is_ix_enabled(IxGate::SequenceCheck) @ MangoError::IxIsDisabled, + )] + pub group: AccountLoader<'info, Group>, + + #[account( + mut, + has_one = group, + has_one = owner, + constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen + )] + pub account: AccountLoader<'info, MangoAccountFixed>, + pub owner: Signer<'info>, +} diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index c7510ba3c..0d283f444 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -147,6 +147,8 @@ pub enum MangoError { TokenAssetLiquidationDisabled, #[msg("for borrows the bank must be in the health account list")] BorrowsRequireHealthAccountBank, + #[msg("invalid sequence number")] + InvalidSequenceNumber, } impl MangoError { diff --git a/programs/mango-v4/src/instructions/ix_gate_set.rs b/programs/mango-v4/src/instructions/ix_gate_set.rs index 413b9ceff..9bae726ce 100644 --- a/programs/mango-v4/src/instructions/ix_gate_set.rs +++ b/programs/mango-v4/src/instructions/ix_gate_set.rs @@ -96,6 +96,7 @@ pub fn ix_gate_set(ctx: Context, ix_gate: u128) -> Result<()> { ); log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2); log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw); + log_if_changed(&group, ix_gate, IxGate::SequenceCheck); group.ix_gate = ix_gate; diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index faa5d8e88..523d1f1f8 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -35,6 +35,7 @@ pub use perp_place_order::*; pub use perp_settle_fees::*; pub use perp_settle_pnl::*; pub use perp_update_funding::*; +pub use sequence_check::*; pub use serum3_cancel_all_orders::*; pub use serum3_cancel_order::*; pub use serum3_cancel_order_by_client_order_id::*; @@ -104,6 +105,7 @@ mod perp_place_order; mod perp_settle_fees; mod perp_settle_pnl; mod perp_update_funding; +mod sequence_check; mod serum3_cancel_all_orders; mod serum3_cancel_order; mod serum3_cancel_order_by_client_order_id; diff --git a/programs/mango-v4/src/instructions/sequence_check.rs b/programs/mango-v4/src/instructions/sequence_check.rs new file mode 100644 index 000000000..e42d042ac --- /dev/null +++ b/programs/mango-v4/src/instructions/sequence_check.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +use crate::accounts_ix::*; +use crate::error::MangoError; +use crate::state::*; + +pub fn sequence_check(ctx: Context, expected_sequence_number: u64) -> Result<()> { + let mut account = ctx.accounts.account.load_full_mut()?; + + require_eq!( + expected_sequence_number, + account.fixed.sequence_number, + MangoError::InvalidSequenceNumber + ); + + account.fixed.sequence_number = account.fixed.sequence_number.wrapping_add(1); + Ok(()) +} diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index b1f4b9102..933ef1efb 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -458,6 +458,15 @@ pub mod mango_v4 { Ok(()) } + pub fn sequence_check( + ctx: Context, + expected_sequence_number: u64, + ) -> Result<()> { + #[cfg(feature = "enable-gpl")] + instructions::sequence_check(ctx, expected_sequence_number)?; + Ok(()) + } + // todo: // ckamm: generally, using an I80F48 arg will make it harder to call // because generic anchor clients won't know how to deal with it diff --git a/programs/mango-v4/src/state/group.rs b/programs/mango-v4/src/state/group.rs index 19fc8db03..e652a8dab 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -246,6 +246,7 @@ pub enum IxGate { TokenConditionalSwapCreateLinearAuction = 70, Serum3PlaceOrderV2 = 71, TokenForceWithdraw = 72, + SequenceCheck = 73, // NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction. } diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 99ea08781..0f6276491 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -157,8 +157,10 @@ pub struct MangoAccount { /// Time at which the last collateral fee was charged pub last_collateral_fee_charge: u64, + pub sequence_number: u64, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 152], + pub reserved: [u8; 144], // dynamic pub header_version: u8, @@ -212,7 +214,8 @@ impl MangoAccount { temporary_delegate: Pubkey::default(), temporary_delegate_expiry: 0, last_collateral_fee_charge: 0, - reserved: [0; 152], + sequence_number: 0, + reserved: [0; 144], header_version: DEFAULT_MANGO_ACCOUNT_VERSION, padding3: Default::default(), padding4: Default::default(), @@ -337,11 +340,12 @@ pub struct MangoAccountFixed { pub temporary_delegate: Pubkey, pub temporary_delegate_expiry: u64, pub last_collateral_fee_charge: u64, - pub reserved: [u8; 152], + pub sequence_number: u64, + pub reserved: [u8; 144], } const_assert_eq!( size_of::(), - 32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 152 + 32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 8 + 144 ); const_assert_eq!(size_of::(), 400); const_assert_eq!(size_of::() % 8, 0); @@ -2909,7 +2913,8 @@ mod tests { temporary_delegate: fixed.temporary_delegate, temporary_delegate_expiry: fixed.temporary_delegate_expiry, last_collateral_fee_charge: fixed.last_collateral_fee_charge, - reserved: [0u8; 152], + sequence_number: 0, + reserved: [0u8; 144], header_version: *zerocopy_reader.header_version(), padding3: Default::default(), diff --git a/programs/mango-v4/tests/cases/test_basic.rs b/programs/mango-v4/tests/cases/test_basic.rs index ee774c080..c20efa8c2 100644 --- a/programs/mango-v4/tests/cases/test_basic.rs +++ b/programs/mango-v4/tests/cases/test_basic.rs @@ -947,3 +947,105 @@ async fn test_withdraw_skip_bank() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_sequence_check() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..1]; + + let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..mango_setup::GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let account = send_tx( + solana, + AccountCreateInstruction { + account_num: 0, + token_count: 6, + serum3_count: 3, + perp_count: 3, + perp_oo_count: 3, + token_conditional_swap_count: 3, + group, + owner, + payer, + }, + ) + .await + .unwrap() + .account; + + let mango_account = get_mango_account(solana, account).await; + assert_eq!(mango_account.fixed.sequence_number, 0); + + // + // TEST: Sequence check with right sequence number + // + + send_tx( + solana, + SequenceCheckInstruction { + account, + owner, + expected_sequence_number: 0, + }, + ) + .await + .unwrap(); + + let mango_account = get_mango_account(solana, account).await; + assert_eq!(mango_account.fixed.sequence_number, 1); + + send_tx( + solana, + SequenceCheckInstruction { + account, + owner, + expected_sequence_number: 1, + }, + ) + .await + .unwrap(); + + let mango_account = get_mango_account(solana, account).await; + assert_eq!(mango_account.fixed.sequence_number, 2); + + // + // TEST: Sequence check with wrong sequence number + // + + send_tx_expect_error!( + solana, + SequenceCheckInstruction { + account, + owner, + expected_sequence_number: 1 + }, + MangoError::InvalidSequenceNumber + ); + + send_tx_expect_error!( + solana, + SequenceCheckInstruction { + account, + owner, + expected_sequence_number: 4 + }, + MangoError::InvalidSequenceNumber + ); + + let mango_account = get_mango_account(solana, account).await; + assert_eq!(mango_account.fixed.sequence_number, 2); + + Ok(()) +} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 6fc904022..4c38cd826 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -5169,3 +5169,42 @@ impl ClientInstruction for HealthAccountSkipping { self.inner.signers() } } + +#[derive(Default)] +pub struct SequenceCheckInstruction { + pub account: Pubkey, + pub owner: TestKeypair, + pub expected_sequence_number: u64, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for SequenceCheckInstruction { + type Accounts = mango_v4::accounts::SequenceCheck; + type Instruction = mango_v4::instruction::SequenceCheck; + async fn to_instruction( + &self, + account_loader: &(impl ClientAccountLoader + 'async_trait), + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction { + expected_sequence_number: self.expected_sequence_number, + }; + + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); + + let accounts = Self::Accounts { + group: account.fixed.group, + account: self.account, + owner: self.owner.pubkey(), + }; + + let instruction = make_instruction(program_id, &accounts, &instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.owner] + } +} diff --git a/ts/client/src/accounts/mangoAccount.spec.ts b/ts/client/src/accounts/mangoAccount.spec.ts index 929c2e4c6..6f92295b7 100644 --- a/ts/client/src/accounts/mangoAccount.spec.ts +++ b/ts/client/src/accounts/mangoAccount.spec.ts @@ -23,6 +23,7 @@ describe('Mango Account', () => { new BN(0), new BN(0), new BN(0), + new BN(0), 0, [], [], diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index c8d4dbad2..a0344e521 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -44,6 +44,7 @@ export class MangoAccount { buybackFeesAccruedCurrent: BN; buybackFeesAccruedPrevious: BN; buybackFeesExpiryTimestamp: BN; + sequenceNumber: BN; headerVersion: number; tokens: unknown; serum3: unknown; @@ -68,6 +69,7 @@ export class MangoAccount { obj.buybackFeesAccruedCurrent, obj.buybackFeesAccruedPrevious, obj.buybackFeesExpiryTimestamp, + obj.sequenceNumber, obj.headerVersion, obj.tokens as TokenPositionDto[], obj.serum3 as Serum3PositionDto[], @@ -94,6 +96,7 @@ export class MangoAccount { public buybackFeesAccruedCurrent: BN, public buybackFeesAccruedPrevious: BN, public buybackFeesExpiryTimestamp: BN, + public sequenceNumber: BN, public headerVersion: number, tokens: TokenPositionDto[], serum3: Serum3PositionDto[], diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 19c764ee6..4f76931bc 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1034,6 +1034,20 @@ export class MangoClient { return await this.sendAndConfirmTransactionForGroup(group, [ix]); } + public async sequenceCheckIx( + group: Group, + mangoAccount: MangoAccount, + ): Promise { + return await this.program.methods + .sequenceCheck(mangoAccount.sequenceNumber) + .accounts({ + group: group.publicKey, + account: mangoAccount.publicKey, + owner: (this.program.provider as AnchorProvider).wallet.publicKey, + }) + .instruction(); + } + public async getMangoAccount( mangoAccountPk: PublicKey, loadSerum3Oo = false, diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts index 0074f7aa6..3baa5a1d2 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -310,6 +310,7 @@ export interface IxGateParams { TokenConditionalSwapCreateLinearAuction: boolean; Serum3PlaceOrderV2: boolean; TokenForceWithdraw: boolean; + SequenceCheck: boolean; } // Default with all ixs enabled, use with buildIxGate @@ -390,6 +391,7 @@ export const TrueIxGateParams: IxGateParams = { TokenConditionalSwapCreateLinearAuction: true, Serum3PlaceOrderV2: true, TokenForceWithdraw: true, + SequenceCheck: true, }; // build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(), @@ -480,6 +482,7 @@ export function buildIxGate(p: IxGateParams): BN { toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70); toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71); toggleIx(ixGate, p, 'TokenForceWithdraw', 72); + toggleIx(ixGate, p, 'SequenceCheck', 73); return ixGate; } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index bcce269fb..ad8cdb421 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1760,6 +1760,36 @@ export type MangoV4 = { } ] }, + { + "name": "sequenceCheck", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "owner" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "expectedSequenceNumber", + "type": "u64" + } + ] + }, { "name": "stubOracleCreate", "accounts": [ @@ -7942,12 +7972,16 @@ export type MangoV4 = { ], "type": "u64" }, + { + "name": "sequenceNumber", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 152 + 144 ] } }, @@ -9721,12 +9755,16 @@ export type MangoV4 = { "name": "lastCollateralFeeCharge", "type": "u64" }, + { + "name": "sequenceNumber", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 152 + 144 ] } } @@ -11008,6 +11046,9 @@ export type MangoV4 = { }, { "name": "TokenForceWithdraw" + }, + { + "name": "SequenceCheck" } ] } @@ -14350,6 +14391,16 @@ export type MangoV4 = { "code": 6069, "name": "TokenAssetLiquidationDisabled", "msg": "the asset does not allow liquidation" + }, + { + "code": 6070, + "name": "BorrowsRequireHealthAccountBank", + "msg": "for borrows the bank must be in the health account list" + }, + { + "code": 6071, + "name": "InvalidSequenceNumber", + "msg": "invalid sequence number" } ] }; @@ -16116,6 +16167,36 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "sequenceCheck", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "owner" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "expectedSequenceNumber", + "type": "u64" + } + ] + }, { "name": "stubOracleCreate", "accounts": [ @@ -22298,12 +22379,16 @@ export const IDL: MangoV4 = { ], "type": "u64" }, + { + "name": "sequenceNumber", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 152 + 144 ] } }, @@ -24077,12 +24162,16 @@ export const IDL: MangoV4 = { "name": "lastCollateralFeeCharge", "type": "u64" }, + { + "name": "sequenceNumber", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 152 + 144 ] } } @@ -25364,6 +25453,9 @@ export const IDL: MangoV4 = { }, { "name": "TokenForceWithdraw" + }, + { + "name": "SequenceCheck" } ] } @@ -28706,6 +28798,16 @@ export const IDL: MangoV4 = { "code": 6069, "name": "TokenAssetLiquidationDisabled", "msg": "the asset does not allow liquidation" + }, + { + "code": 6070, + "name": "BorrowsRequireHealthAccountBank", + "msg": "for borrows the bank must be in the health account list" + }, + { + "code": 6071, + "name": "InvalidSequenceNumber", + "msg": "invalid sequence number" } ] };