ywpl_plex/processor/
empty_payment_account.rs

1use solana_program::msg;
2
3use crate::{
4    error::MetaplexError,
5    instruction::EmptyPaymentAccountArgs,
6    state::{
7        get_auction_manager, AuctionManager, Key, PayoutTicket, Store, MAX_PAYOUT_TICKET_SIZE,
8        PREFIX, TOTALS,
9    },
10    utils::{
11        assert_derivation, assert_initialized, assert_is_ata, assert_owned_by, assert_rent_exempt,
12        assert_safety_deposit_config_valid, create_or_allocate_account_raw, spl_token_transfer,
13    },
14};
15use borsh::BorshSerialize;
16use ywpl_auction::processor::AuctionData;
17use mpl_token_metadata::state::{MasterEditionV1, Metadata};
18use ywpl_token_vault::state::SafetyDepositBox;
19use solana_program::{
20    account_info::{next_account_info, AccountInfo},
21    entrypoint::ProgramResult,
22    program_error::ProgramError,
23    program_option::COption,
24    pubkey::Pubkey,
25    rent::Rent,
26    sysvar::Sysvar,
27};
28use spl_token::state::Account;
29
30fn assert_destination_ownership_validity(
31    auction_manager: &Box<dyn AuctionManager>,
32    metadata: &Metadata,
33    destination_info: &AccountInfo,
34    destination: &Account,
35    _store: &Store,
36    creator_index: Option<u8>,
37) -> ProgramResult {
38    if let Some(creators) = &metadata.data.creators {
39        if let Some(index) = creator_index {
40            if (index as usize) < creators.len() {
41                let creator = &creators[index as usize];
42                if destination.owner != creator.address {
43                    return Err(MetaplexError::IncorrectOwner.into());
44                }
45
46                // Let's avoid importing the entire ATA library here just to get a helper and an ID.
47                // Assert destination is, in fact, an ATA.
48                assert_is_ata(destination_info, &creator.address, &destination.mint)?;
49            } else {
50                return Err(MetaplexError::InvalidCreatorIndex.into());
51            }
52        } else if destination.owner != auction_manager.authority() {
53            return Err(MetaplexError::IncorrectOwner.into());
54        }
55    } else if destination.owner != auction_manager.authority() {
56        return Err(MetaplexError::IncorrectOwner.into());
57    }
58
59    if destination.delegate != COption::None {
60        return Err(MetaplexError::DelegateShouldBeNone.into());
61    }
62
63    if destination.close_authority != COption::None {
64        return Err(MetaplexError::CloseAuthorityShouldBeNone.into());
65    }
66
67    Ok(())
68}
69
70fn calculate_owed_amount(
71    auction_token_tracker_info: Option<&AccountInfo>,
72    safety_deposit_config_info: Option<&AccountInfo>,
73    auction_manager: &Box<dyn AuctionManager>,
74    auction: &AuctionData,
75    metadata: &Metadata,
76    winning_config_index: &Option<u8>,
77    winning_config_item_index: &Option<u8>,
78    creator_index: &Option<u8>,
79) -> Result<u64, ProgramError> {
80    let primary_sale_happened = auction_manager.get_primary_sale_happened(
81        metadata,
82        *winning_config_index,
83        *winning_config_item_index,
84    )?;
85
86    let mut amount_available_to_split: u128 = match winning_config_index {
87        Some(index) => auction.bid_state.amount(*index as usize) as u128,
88        None => {
89            // this means the amount owed is the amount collected from participation nft bids.
90            auction_manager.get_collected_to_accept_payment(safety_deposit_config_info)?
91        }
92    };
93
94    if winning_config_index.is_some() {
95        msg!("Winning config index {:?}", winning_config_index.unwrap());
96    }
97    if winning_config_item_index.is_some() {
98        msg!(
99            "Winning config item index {:?}",
100            winning_config_item_index.unwrap()
101        );
102    }
103    if creator_index.is_some() {
104        msg!("Creator index {:?}", creator_index.unwrap());
105    }
106
107    msg!("Amount available to split {:?}", amount_available_to_split);
108    let numerator: u128 = match creator_index {
109        Some(_) => {
110            if primary_sale_happened {
111                // during secondary sale, artists get a percentage of the proceeds
112                metadata.data.seller_fee_basis_points as u128
113            } else {
114                // during primary sale, artists get all of the proceeds
115                10000
116            }
117        }
118        None => {
119            if primary_sale_happened {
120                // during secondary sale, auctioneer gets whats left after artists get their cut
121                (10000 - metadata.data.seller_fee_basis_points) as u128
122            } else {
123                // during primary sale, auctioneer (creator index not provided)
124                // get none of the proceeds
125                0u128
126            }
127        }
128    };
129
130    msg!("Numerator {:?}", numerator);
131
132    // Each artist gets a cut of the overall share all artists get. IE if 2 artists contributed and one
133    // did 70% and the other 30%, the artist further multiplier of A is 7000 and the other is 3000,
134    // because we convert their shares of 70 and 30 to basis point units of 7000 and 3000.
135    let artist_further_multiplier = match creator_index {
136        Some(index) => match &metadata.data.creators {
137            Some(creators) => (creators[*index as usize].share as u128) * 100u128,
138            None => return Err(MetaplexError::CreatorIndexExpected.into()),
139        },
140        None => 10000,
141    };
142
143    msg!("Artist further multiplier {:?}", artist_further_multiplier);
144
145    // Numerator represents the whittling to cut the artist or auctioneer's piece  off of the
146    // total amount available. So if it's the auctioneer and they get 90% in a secondary sale, this would
147    // be (9000/10000) * bid amount, numerator is 9000. Or if it's the artists collective cut, this would
148    // be 1000.
149    amount_available_to_split = amount_available_to_split
150        .checked_mul(numerator)
151        .ok_or(MetaplexError::NumericalOverflowError)?;
152
153    msg!(
154        "Amount available to split after numerator mult {:?}",
155        amount_available_to_split,
156    );
157
158    // Artist further multiplier is the numerator of the fraction that is multiplied for the specific
159    // artist involved. So if artist A gets 70% of the total artist cut then we'd multiply the
160    // artist contribution by a further 7/10, so this would be 7000 basis points, so we're doing *7000
161    // here.
162    amount_available_to_split = amount_available_to_split
163        .checked_mul(artist_further_multiplier)
164        .ok_or(MetaplexError::NumericalOverflowError)?;
165
166    msg!(
167        "Amount available to split after artist further multiplier mult {:?}",
168        amount_available_to_split,
169    );
170    if amount_available_to_split == 0 {
171        // cant do checked_ceil_div on 0
172        return Ok(0u64);
173    }
174
175    let proportion_divisor = match winning_config_index {
176        Some(val) => auction_manager.get_number_of_unique_token_types_for_this_winner(
177            *val as usize,
178            auction_token_tracker_info,
179        )?,
180        None => 1,
181    };
182
183    // Since we have multiple prizes need to split each prize's contribution by it's portion of config
184    let proportional_amount_available_to_split = amount_available_to_split
185        .checked_div(proportion_divisor)
186        .ok_or(MetaplexError::NumericalOverflowError)?;
187
188    msg!(
189        "Divided the amount by {:?} to get {:?} due to sharing reward with other prizes",
190        proportion_divisor,
191        proportional_amount_available_to_split
192    );
193
194    // We do two 10000's - one for the first numerator/10000 fraction and one for the artist contribution
195    // For the auctioneer's case, the second 10000 cancels out to 1 because there is no further
196    // whittling there (auctioneer shares with nobody) but for the artist they may be sharing
197    // with another artist, say a 70/30 split, so we need to further multiply the amount available by
198    // 7/10ths or something.
199    let final_amount_available_to_split = proportional_amount_available_to_split
200        .checked_div(10000 * 10000)
201        .ok_or(MetaplexError::NumericalOverflowError)?;
202    msg!("Final amount mult {:?}", final_amount_available_to_split);
203
204    Ok(final_amount_available_to_split as u64)
205}
206
207pub fn process_empty_payment_account(
208    program_id: &Pubkey,
209    accounts: &[AccountInfo],
210    args: EmptyPaymentAccountArgs,
211) -> ProgramResult {
212    let account_info_iter = &mut accounts.iter();
213    let accept_payment_info = next_account_info(account_info_iter)?;
214    let destination_info = next_account_info(account_info_iter)?;
215    let auction_manager_info = next_account_info(account_info_iter)?;
216    let payout_ticket_info = next_account_info(account_info_iter)?;
217    let payer_info = next_account_info(account_info_iter)?;
218    let metadata_info = next_account_info(account_info_iter)?;
219    let master_edition_info = next_account_info(account_info_iter)?;
220    let safety_deposit_info = next_account_info(account_info_iter)?;
221    let store_info = next_account_info(account_info_iter)?;
222    let vault_info = next_account_info(account_info_iter)?;
223    let auction_info = next_account_info(account_info_iter)?;
224    let token_program_info = next_account_info(account_info_iter)?;
225    let system_info = next_account_info(account_info_iter)?;
226    let rent_info = next_account_info(account_info_iter)?;
227    let auction_token_tracker_info = next_account_info(account_info_iter).ok();
228    let safety_deposit_config_info = next_account_info(account_info_iter).ok();
229
230    if let Some(tracker_info) = auction_token_tracker_info {
231        assert_derivation(
232            program_id,
233            tracker_info,
234            &[
235                PREFIX.as_bytes(),
236                &program_id.as_ref(),
237                auction_manager_info.key.as_ref(),
238                TOTALS.as_bytes(),
239            ],
240        )?;
241    }
242
243    let rent = &Rent::from_account_info(&rent_info)?;
244
245    let auction_manager = get_auction_manager(auction_manager_info)?;
246    let store = Store::from_account_info(store_info)?;
247    let safety_deposit = SafetyDepositBox::from_account_info(safety_deposit_info)?;
248    let metadata = Metadata::from_account_info(metadata_info)?;
249    let auction = AuctionData::from_account_info(auction_info)?;
250    let destination: Account = assert_initialized(destination_info)?;
251    let accept_payment: Account = assert_initialized(accept_payment_info)?;
252
253    if auction_manager.store() != *store_info.key {
254        return Err(MetaplexError::AuctionManagerStoreMismatch.into());
255    }
256
257    msg!(
258        "At this point, accept payment has {:?} in it",
259        accept_payment.amount
260    );
261
262    // Before continuing further, assert all bid monies have been pushed to the main escrow
263    // account so that we have a complete (less the unredeemed participation nft bids) accounting
264    // to work with
265    auction_manager.assert_all_bids_claimed(&auction)?;
266
267    if *token_program_info.key != store.token_program {
268        return Err(MetaplexError::AuctionManagerTokenProgramMismatch.into());
269    }
270
271    assert_owned_by(auction_manager_info, program_id)?;
272    if !payout_ticket_info.data_is_empty() {
273        assert_owned_by(payout_ticket_info, program_id)?;
274    }
275    assert_owned_by(destination_info, token_program_info.key)?;
276    assert_owned_by(accept_payment_info, token_program_info.key)?;
277    assert_owned_by(metadata_info, &store.token_metadata_program)?;
278    if *master_edition_info.key != solana_program::system_program::id() {
279        assert_owned_by(master_edition_info, &store.token_metadata_program)?;
280    }
281    assert_owned_by(safety_deposit_info, &store.token_vault_program)?;
282    assert_owned_by(store_info, program_id)?;
283    assert_owned_by(vault_info, &store.token_vault_program)?;
284    assert_owned_by(auction_info, &store.auction_program)?;
285    assert_rent_exempt(rent, destination_info)?;
286
287    // Assert the winning config points to the safety deposit you sent up
288    auction_manager.assert_winning_config_safety_deposit_validity(
289        &safety_deposit,
290        args.winning_config_index,
291        args.winning_config_item_index,
292    )?;
293
294    assert_safety_deposit_config_valid(
295        program_id,
296        auction_manager_info,
297        safety_deposit_info,
298        safety_deposit_config_info,
299        &auction_manager.key(),
300    )?;
301
302    // assert the destination account matches the ownership expected to creator or auction manager authority
303    // given in the argument's creator index
304    assert_destination_ownership_validity(
305        &auction_manager,
306        &metadata,
307        destination_info,
308        &destination,
309        &store,
310        args.creator_index,
311    )?;
312
313    // further assert that the vault and safety deposit are correctly matched to the auction manager
314    if auction_manager.vault() != *vault_info.key {
315        return Err(MetaplexError::AuctionManagerVaultMismatch.into());
316    }
317
318    if auction_manager.auction() != *auction_info.key {
319        return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
320    }
321
322    if safety_deposit.vault != *vault_info.key {
323        return Err(MetaplexError::SafetyDepositBoxVaultMismatch.into());
324    }
325
326    // assert that the metadata sent up is the metadata in the safety deposit
327    if metadata.mint != safety_deposit.token_mint {
328        if master_edition_info.data.borrow()[0]
329            == mpl_token_metadata::state::Key::MasterEditionV1 as u8
330        {
331            // Could be a limited edition, in which case printing tokens or auth tokens were offered, not the original.
332            let master_edition: MasterEditionV1 =
333                MasterEditionV1::from_account_info(master_edition_info)?;
334            if master_edition.printing_mint != safety_deposit.token_mint
335                && master_edition.one_time_printing_authorization_mint != safety_deposit.token_mint
336            {
337                return Err(MetaplexError::SafetyDepositBoxMetadataMismatch.into());
338            }
339        } else {
340            return Err(MetaplexError::SafetyDepositBoxMetadataMismatch.into());
341        }
342    }
343
344    // make sure the accept payment account is right
345    if auction_manager.accept_payment() != *accept_payment_info.key {
346        return Err(MetaplexError::AcceptPaymentMismatch.into());
347    }
348
349    if destination.mint != accept_payment.mint {
350        return Err(MetaplexError::AcceptPaymentMintMismatch.into());
351    }
352
353    let winning_config_index_key: String = match args.winning_config_index {
354        Some(val) => val.to_string(),
355        None => "participation".to_owned(),
356    };
357
358    let winning_config_item_index_key: String = match args.winning_config_item_index {
359        Some(val) => val.to_string(),
360        None => "0".to_owned(),
361    };
362
363    let creator_index_key: String = match args.creator_index {
364        Some(val) => val.to_string(),
365        None => "auctioneer".to_owned(),
366    };
367
368    let payout_bump = assert_derivation(
369        program_id,
370        payout_ticket_info,
371        &[
372            PREFIX.as_bytes(),
373            auction_manager_info.key.as_ref(),
374            winning_config_index_key.as_bytes(),
375            winning_config_item_index_key.as_bytes(),
376            creator_index_key.as_bytes(),
377            &safety_deposit_info.key.as_ref(),
378            &destination.owner.as_ref(),
379        ],
380    )?;
381
382    let payout_seeds = &[
383        PREFIX.as_bytes(),
384        auction_manager_info.key.as_ref(),
385        winning_config_index_key.as_bytes(),
386        winning_config_item_index_key.as_bytes(),
387        creator_index_key.as_bytes(),
388        &safety_deposit_info.key.as_ref(),
389        &destination.owner.as_ref(),
390        &[payout_bump],
391    ];
392
393    if payout_ticket_info.data_is_empty() {
394        create_or_allocate_account_raw(
395            *program_id,
396            payout_ticket_info,
397            rent_info,
398            system_info,
399            payer_info,
400            MAX_PAYOUT_TICKET_SIZE,
401            payout_seeds,
402        )?;
403    }
404
405    let mut payout_ticket = PayoutTicket::from_account_info(payout_ticket_info)?;
406    payout_ticket.recipient = destination.owner;
407    payout_ticket.key = Key::PayoutTicketV1;
408
409    let amount = calculate_owed_amount(
410        auction_token_tracker_info,
411        safety_deposit_config_info,
412        &auction_manager,
413        &auction,
414        &metadata,
415        &args.winning_config_index,
416        &args.winning_config_item_index,
417        &args.creator_index,
418    )?;
419
420    let final_amount = amount
421        .checked_sub(payout_ticket.amount_paid)
422        .ok_or(MetaplexError::NumericalOverflowError)?;
423
424    if final_amount > 0 {
425        payout_ticket.amount_paid = payout_ticket
426            .amount_paid
427            .checked_add(final_amount)
428            .ok_or(MetaplexError::NumericalOverflowError)?;
429
430        let auction_key = auction_manager.auction();
431
432        let bump_seed = assert_derivation(
433            program_id,
434            auction_manager_info,
435            &[PREFIX.as_bytes(), auction_key.as_ref()],
436        )?;
437
438        let authority_seeds = &[PREFIX.as_bytes(), auction_key.as_ref(), &[bump_seed]];
439
440        spl_token_transfer(
441            accept_payment_info.clone(),
442            destination_info.clone(),
443            final_amount,
444            auction_manager_info.clone(),
445            authority_seeds,
446            token_program_info.clone(),
447        )?;
448    }
449
450    payout_ticket.serialize(&mut *payout_ticket_info.data.borrow_mut())?;
451
452    Ok(())
453}