program: add a sequence check IX (#908)

Add a sequence check IX

This new IX `SequenceCheck` can be used to avoid having multiple concurrent TX in flight causing unexpected result (multiple borrow for example)
This commit is contained in:
Serge Farny 2024-03-07 14:59:05 +01:00 committed by GitHub
parent 81f05b32c7
commit 494835631b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 386 additions and 11 deletions

View File

@ -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"
}
]
}

View File

@ -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;

View File

@ -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>,
}

View File

@ -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 {

View File

@ -96,6 +96,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, 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;

View File

@ -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;

View File

@ -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<SequenceCheck>, 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(())
}

View File

@ -458,6 +458,15 @@ pub mod mango_v4 {
Ok(())
}
pub fn sequence_check(
ctx: Context<SequenceCheck>,
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

View File

@ -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.
}

View File

@ -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::<MangoAccountFixed>(),
32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 152
32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 8 + 144
);
const_assert_eq!(size_of::<MangoAccountFixed>(), 400);
const_assert_eq!(size_of::<MangoAccountFixed>() % 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(),

View File

@ -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(())
}

View File

@ -5169,3 +5169,42 @@ impl<T: ClientInstruction> ClientInstruction for HealthAccountSkipping<T> {
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<TestKeypair> {
vec![self.owner]
}
}

View File

@ -23,6 +23,7 @@ describe('Mango Account', () => {
new BN(0),
new BN(0),
new BN(0),
new BN(0),
0,
[],
[],

View File

@ -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[],

View File

@ -1034,6 +1034,20 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async sequenceCheckIx(
group: Group,
mangoAccount: MangoAccount,
): Promise<TransactionInstruction> {
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,

View File

@ -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;
}

View File

@ -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"
}
]
};