Skip to content
This repository was archived by the owner on Jun 16, 2025. It is now read-only.

Commit b43bf25

Browse files
authored
solana: handle non-existent best offer token for settle complete (#173)
Co-authored-by: A5 Pickle <[email protected]>
1 parent 3804864 commit b43bf25

File tree

6 files changed

+233
-73
lines changed

6 files changed

+233
-73
lines changed

solana/programs/matching-engine/src/events/auction_settled.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
use crate::state::MessageProtocol;
22
use anchor_lang::prelude::*;
33

4+
#[derive(Debug, AnchorSerialize, AnchorDeserialize)]
5+
pub struct SettledTokenAccountInfo {
6+
pub key: Pubkey,
7+
pub balance_after: u64,
8+
}
9+
410
#[event]
511
#[derive(Debug)]
612
pub struct AuctionSettled {
713
/// The pubkey of the auction that was settled.
814
pub auction: Pubkey,
915

10-
/// If there was an active auction, this pubkey is the best offer token that was paid back.
11-
pub best_offer_token: Option<Pubkey>,
16+
/// If there was an active auction, this field will have the pubkey of the best offer token that
17+
/// was paid back and its balance after repayment.
18+
pub best_offer_token: Option<SettledTokenAccountInfo>,
1219

13-
/// Token account's new balance. If there was no auction, this balance will be of the fee
14-
/// recipient token account.
15-
pub token_balance_after: u64,
20+
/// Depending on whether there was an active auction, this field will have the pubkey of the
21+
/// executor token (if there was an auction) or fee recipient token (if there was no auction).
22+
pub executor_token: Option<SettledTokenAccountInfo>,
1623

1724
/// This value will only be some if there was no active auction.
1825
pub with_execute: Option<MessageProtocol>,

solana/programs/matching-engine/src/processor/auction/settle/complete.rs

Lines changed: 139 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use crate::{
22
error::MatchingEngineError,
3+
events::SettledTokenAccountInfo,
34
state::{Auction, AuctionStatus, PreparedOrderResponse},
45
};
56
use anchor_lang::prelude::*;
6-
use anchor_spl::{associated_token::get_associated_token_address, token};
7+
use anchor_spl::{
8+
associated_token::get_associated_token_address,
9+
token::{self, TokenAccount},
10+
};
711

812
#[derive(Accounts)]
913
pub struct SettleAuctionComplete<'info> {
@@ -16,21 +20,22 @@ pub struct SettleAuctionComplete<'info> {
1620

1721
#[account(
1822
mut,
19-
token::mint = best_offer_token.mint,
23+
token::mint = common::USDC_MINT,
2024
token::authority = executor,
2125
)]
22-
executor_token: Account<'info, token::TokenAccount>,
26+
executor_token: Account<'info, TokenAccount>,
2327

2428
/// Destination token account, which the redeemer may not own. But because the redeemer is a
2529
/// signer and is the one encoded in the Deposit Fill message, he may have the tokens be sent
2630
/// to any account he chooses (this one).
2731
///
28-
/// CHECK: This token account must already exist.
32+
/// CHECK: This token account may exist. If it doesn't and there is a penalty, we will send all
33+
/// of the tokens to the executor token account.
2934
#[account(
3035
mut,
3136
address = auction.info.as_ref().unwrap().best_offer_token,
3237
)]
33-
best_offer_token: Account<'info, token::TokenAccount>,
38+
best_offer_token: UncheckedAccount<'info>,
3439

3540
#[account(
3641
mut,
@@ -52,7 +57,7 @@ pub struct SettleAuctionComplete<'info> {
5257
],
5358
bump,
5459
)]
55-
prepared_custody_token: Account<'info, token::TokenAccount>,
60+
prepared_custody_token: Account<'info, TokenAccount>,
5661

5762
#[account(
5863
mut,
@@ -101,10 +106,14 @@ fn handle_settle_auction_complete(
101106
let token_program = &ctx.accounts.token_program;
102107
let prepared_custody_token = &ctx.accounts.prepared_custody_token;
103108

104-
// We may deduct from this account if the winning participant was penalized.
105-
let mut repayment = ctx.accounts.prepared_custody_token.amount;
109+
let repayment = ctx.accounts.prepared_custody_token.amount;
110+
111+
struct TokenAccountResult {
112+
balance_before: u64,
113+
amount: u64,
114+
}
106115

107-
match execute_penalty {
116+
let (executor_result, best_offer_result) = match execute_penalty {
108117
None => {
109118
// If there is no penalty, we require that the executor token and best offer token be
110119
// equal. The winning offer should not be penalized for calling this instruction when he
@@ -118,6 +127,19 @@ fn handle_settle_auction_complete(
118127
best_offer_token.key(),
119128
MatchingEngineError::ExecutorTokenMismatch
120129
);
130+
131+
// If the token account happens to not exist anymore, we will revert.
132+
match TokenAccount::try_deserialize(&mut &best_offer_token.data.borrow()[..]) {
133+
Ok(best_offer) => (
134+
None, // executor_result
135+
TokenAccountResult {
136+
balance_before: best_offer.amount,
137+
amount: repayment,
138+
}
139+
.into(),
140+
),
141+
Err(err) => return Err(err),
142+
}
121143
}
122144
_ => {
123145
// If there is a penalty, we want to return the lamports back to the person who paid to
@@ -131,55 +153,121 @@ fn handle_settle_auction_complete(
131153
MatchingEngineError::ExecutorNotPreparedBy
132154
);
133155

134-
if executor_token.key() != best_offer_token.key() {
135-
// Because the auction participant was penalized for executing the order late, he
136-
// will be deducted the base fee. This base fee will be sent to the executor token
137-
// account if it is not the same as the best offer token account.
138-
139-
// We require that the executor token account be an ATA.
140-
require_keys_eq!(
141-
executor_token.key(),
142-
get_associated_token_address(&executor_token.owner, &executor_token.mint),
143-
ErrorCode::AccountNotAssociatedTokenAccount
144-
);
145-
146-
// Transfer base fee to the executor.
147-
token::transfer(
148-
CpiContext::new_with_signer(
149-
token_program.to_account_info(),
150-
token::Transfer {
151-
from: prepared_custody_token.to_account_info(),
152-
to: executor_token.to_account_info(),
153-
authority: prepared_order_response.to_account_info(),
154-
},
155-
&[prepared_order_response_signer_seeds],
156-
),
157-
base_fee,
158-
)?;
159-
160-
repayment = repayment.saturating_sub(base_fee);
156+
// If the token account happens to not exist anymore, we will give everything to the
157+
// executor.
158+
match TokenAccount::try_deserialize(&mut &best_offer_token.data.borrow()[..]) {
159+
Ok(best_offer) => {
160+
if executor_token.key() == best_offer_token.key() {
161+
(
162+
None, // executor_result
163+
TokenAccountResult {
164+
balance_before: best_offer.amount,
165+
amount: repayment,
166+
}
167+
.into(),
168+
)
169+
} else {
170+
// Because the auction participant was penalized for executing the order
171+
// late, he will be deducted the base fee. This base fee will be sent to the
172+
// executor token account if it is not the same as the best offer token
173+
// account.
174+
175+
// We require that the executor token account be an ATA.
176+
require_keys_eq!(
177+
executor_token.key(),
178+
get_associated_token_address(
179+
&executor_token.owner,
180+
&executor_token.mint
181+
),
182+
ErrorCode::AccountNotAssociatedTokenAccount
183+
);
184+
185+
(
186+
TokenAccountResult {
187+
balance_before: executor_token.amount,
188+
amount: base_fee,
189+
}
190+
.into(),
191+
TokenAccountResult {
192+
balance_before: best_offer.amount,
193+
amount: repayment.saturating_sub(base_fee),
194+
}
195+
.into(),
196+
)
197+
}
198+
}
199+
Err(_) => (
200+
TokenAccountResult {
201+
balance_before: executor_token.amount,
202+
amount: repayment,
203+
}
204+
.into(),
205+
None, // best_offer_result
206+
),
207+
}
208+
}
209+
};
210+
211+
// Transfer executor his bounty if there are any.
212+
let settled_executor_result = match executor_result {
213+
Some(TokenAccountResult {
214+
balance_before,
215+
amount,
216+
}) => {
217+
token::transfer(
218+
CpiContext::new_with_signer(
219+
token_program.to_account_info(),
220+
token::Transfer {
221+
from: prepared_custody_token.to_account_info(),
222+
to: executor_token.to_account_info(),
223+
authority: prepared_order_response.to_account_info(),
224+
},
225+
&[prepared_order_response_signer_seeds],
226+
),
227+
amount,
228+
)?;
229+
230+
SettledTokenAccountInfo {
231+
key: executor_token.key(),
232+
balance_after: balance_before.saturating_add(amount),
161233
}
234+
.into()
162235
}
236+
None => None,
163237
};
164238

165-
// Transfer the funds back to the highest bidder.
166-
token::transfer(
167-
CpiContext::new_with_signer(
168-
token_program.to_account_info(),
169-
token::Transfer {
170-
from: prepared_custody_token.to_account_info(),
171-
to: best_offer_token.to_account_info(),
172-
authority: prepared_order_response.to_account_info(),
173-
},
174-
&[prepared_order_response_signer_seeds],
175-
),
176-
repayment,
177-
)?;
239+
// Transfer the funds back to the highest bidder if there are any.
240+
let settled_best_offer_result = match best_offer_result {
241+
Some(TokenAccountResult {
242+
balance_before,
243+
amount,
244+
}) => {
245+
token::transfer(
246+
CpiContext::new_with_signer(
247+
token_program.to_account_info(),
248+
token::Transfer {
249+
from: prepared_custody_token.to_account_info(),
250+
to: best_offer_token.to_account_info(),
251+
authority: prepared_order_response.to_account_info(),
252+
},
253+
&[prepared_order_response_signer_seeds],
254+
),
255+
amount,
256+
)?;
257+
258+
SettledTokenAccountInfo {
259+
key: best_offer_token.key(),
260+
balance_after: balance_before.saturating_add(amount),
261+
}
262+
.into()
263+
}
264+
None => None,
265+
};
178266

179267
emit!(crate::events::AuctionSettled {
180268
auction: ctx.accounts.auction.key(),
181-
best_offer_token: best_offer_token.key().into(),
182-
token_balance_after: best_offer_token.amount.saturating_add(repayment),
269+
best_offer_token: settled_best_offer_result,
270+
executor_token: settled_executor_result,
183271
with_execute: Default::default(),
184272
});
185273

solana/programs/matching-engine/src/processor/auction/settle/none/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ fn settle_none_and_prepare_fill(
9696
emit!(crate::events::AuctionSettled {
9797
auction: auction.key(),
9898
best_offer_token: Default::default(),
99-
token_balance_after: fee_recipient_token.amount.saturating_add(fee),
99+
executor_token: crate::events::SettledTokenAccountInfo {
100+
key: fee_recipient_token.key(),
101+
balance_after: fee_recipient_token.amount.saturating_add(fee)
102+
}
103+
.into(),
100104
with_execute: auction.target_protocol.into(),
101105
});
102106

solana/ts/src/idl/json/matching_engine.json

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1860,7 +1860,8 @@
18601860
"Destination token account, which the redeemer may not own. But because the redeemer is a",
18611861
"signer and is the one encoded in the Deposit Fill message, he may have the tokens be sent",
18621862
"to any account he chooses (this one).",
1863-
""
1863+
"",
1864+
"of the tokens to the executor token account."
18641865
],
18651866
"writable": true
18661867
},
@@ -3459,19 +3460,30 @@
34593460
{
34603461
"name": "best_offer_token",
34613462
"docs": [
3462-
"If there was an active auction, this pubkey is the best offer token that was paid back."
3463+
"If there was an active auction, this field will have the pubkey of the best offer token that",
3464+
"was paid back and its balance after repayment."
34633465
],
34643466
"type": {
3465-
"option": "pubkey"
3467+
"option": {
3468+
"defined": {
3469+
"name": "SettledTokenAccountInfo"
3470+
}
3471+
}
34663472
}
34673473
},
34683474
{
3469-
"name": "token_balance_after",
3475+
"name": "executor_token",
34703476
"docs": [
3471-
"Token account's new balance. If there was no auction, this balance will be of the fee",
3472-
"recipient token account."
3477+
"Depending on whether there was an active auction, this field will have the pubkey of the",
3478+
"executor token (if there was an auction) or fee recipient token (if there was no auction)."
34733479
],
3474-
"type": "u64"
3480+
"type": {
3481+
"option": {
3482+
"defined": {
3483+
"name": "SettledTokenAccountInfo"
3484+
}
3485+
}
3486+
}
34753487
},
34763488
{
34773489
"name": "with_execute",
@@ -4333,6 +4345,22 @@
43334345
}
43344346
]
43354347
}
4348+
},
4349+
{
4350+
"name": "SettledTokenAccountInfo",
4351+
"type": {
4352+
"kind": "struct",
4353+
"fields": [
4354+
{
4355+
"name": "key",
4356+
"type": "pubkey"
4357+
},
4358+
{
4359+
"name": "balance_after",
4360+
"type": "u64"
4361+
}
4362+
]
4363+
}
43364364
}
43374365
]
43384366
}

0 commit comments

Comments
 (0)