Merge branch 'feature/instant-sale' of github.com:atticwip/metaplex into instant-sale

This commit is contained in:
Jordan Prince 2021-08-12 18:33:56 -05:00
commit d13e2a98c7
24 changed files with 1224 additions and 131 deletions

View File

@ -6,4 +6,7 @@ indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
trim_trailing_whitespace = true
[*.rs]
indent_size = 4

View File

@ -14,7 +14,7 @@ This is the bedrock contract of the entire ecosystem. All that you need to inter
Furthermore, if your mint has one token in its supply, you can give it an additional decoration PDA, of type MasterEdition. This PDA denotes the mint as a special type of object that can mint other mints - which we call Editions (as opposed to MasterEditions because they can't print other mints themselves). This makes this mint like the "master records" that record studios used to use to make new copies of records back in the day. The MasterEdition PDA will take away minting and freezing authority from you in the process and will contain information about total supply, maximum possible supply, etc.
The existence of Metadata and its sister PDA MasterEdition makes a very powerful combination for a mint that enables the entire rest of the Metaplex contract stack. Now you can create:
The existence of Metadata and its sister PDA MasterEdition makes a very powerful combination for a mint that enables the entire rest of the Metaplex contract stack. Now you can create:
- Normal mints that just have names (Metadata but no MasterEdition)
- One of a kind NFTs (Metadata + MasterEdition with `max_supply` of 0)
@ -35,7 +35,7 @@ When there are outstanding shares, you cannot, as the vault owner, **Combine** t
### Auction
The Auction Contract represents an auction primitive, and it knows nothing about NFTs, or Metadata, or anything else in the Metaplex ecosystem. All it cares about is that it has a resource address, it has auction mechanics, and it is using those auction mechanics to auction off that resource. It currently supports English Auctions and Open Edition Auctions (no winners but bids are tracked.) Its only purpose is to track who won what place in an auction and to collect money for those wins. When you place bids, or cancel them, you are interacting with this contract. However, when you redeem bids, you are not interacting with this contract, but Metaplex, because while it can provide proof that you did indeed win 4th place, it has no opinion on how the resource being auctioned off is divvied up between 1st, 2nd, 3rd, and 4th place winners, for example.
The Auction Contract represents an auction primitive, and it knows nothing about NFTs, or Metadata, or anything else in the Metaplex ecosystem. All it cares about is that it has a resource address, it has auction mechanics, and it is using those auction mechanics to auction off that resource. It currently supports English Auctions and Open Edition Auctions (no winners but bids are tracked.) Its only purpose is to track who won what place in an auction and to collect money for those wins. When you place bids, or cancel them, you are interacting with this contract. However, when you redeem bids, you are not interacting with this contract, but Metaplex, because while it can provide proof that you did indeed win 4th place, it has no opinion on how the resource being auctioned off is divvied up between 1st, 2nd, 3rd, and 4th place winners, for example.
This contract will be expanded in the future to include other auction types, and better guarantees between that the auctioneer claiming the bid actually has provided the prize by having the winner sign a PDA saying that they received the prize. Right now this primitive contract should *not* be used in isolation, but in companionship with another contract (like Metaplex in our case) that makes such guarantees that prizes are delivered if prizes are won.
@ -48,7 +48,7 @@ This is the granddaddy contract of them all. The primary product of the Metaplex
- Full Rights Transfers (Giving away token + metadata ownership)
- Single Token Transfers (Giving away a token but not metadata ownership)
It orchestrates disbursements of those contents to winners of an auction. An AuctionManager requires both a Vault and an Auction to run, and it requires that the Auction's resource key be set to the Vault.
It orchestrates disbursements of those contents to winners of an auction. An AuctionManager requires both a Vault and an Auction to run, and it requires that the Auction's resource key be set to the Vault.
Due to each type of NFT transfer above requiring slightly different nuanced handling and checking, Metaplex handles knowing about those things, and making the different CPI calls to the Token Metadata contract to make those things happen as required during the redemption phase. It also has full authority over all the objects like Vault and Auction, and handles all royalties payments by collecting funds from the auction into its own central escrow account and then disbursing to artists.
@ -98,11 +98,11 @@ Get ready and grab some aspirin. Here we go!
### Overview
The Token Metadata contract can be used for storing generic metadata about any given mint, whether NFT or not. Metadata allows storage of name, symbol, and URI to an external resource. Additionally, the Metadata allows for the tracking of creators, primary sales, and seller fees. Once the mint has been created, the mint authority can use the SPL Metadata program to create metadata as described in this document.
The Token Metadata contract can be used for storing generic metadata about any given mint, whether NFT or not. Metadata allows storage of name, symbol, and URI to an external resource. Additionally, the Metadata allows for the tracking of creators, primary sales, and seller fees. Once the mint has been created, the mint authority can use the SPL Metadata program to create metadata as described in this document.
Minting an NFT requires creating a new SPL Mint with the supply of one and decimals zero as described [https://spl.solana.com/token#example-create-a-non-fungible-token](https://spl.solana.com/token#example-create-a-non-fungible-token)
Below is the Rust representation of the structs that are stored on-chain.
Below is the Rust representation of the structs that are stored on-chain.
```rust
@ -200,7 +200,7 @@ Master Edition accounts are PDA addresses of `['metaplex', metaplex_program_id,
An edition represents a copy of an NFT, and is created from a Master Edition. Each print has an edition number associated with it. Normally, prints can be created during Open Edition or Limited Edition auction, but they could also be created by the creator manually.
Editions are created by presenting the Master Edition token, along with a new mint that lacks a Metadata account and a token account containing one token from that mint to the `mint_new_edition_from_master_edition_via_token` endpoint. This endpoint will create both an immutable Metadata based on the parent Metadata and a special Edition struct based on the parent Master Edition struct.
Editions are created by presenting the Master Edition token, along with a new mint that lacks a Metadata account and a token account containing one token from that mint to the `mint_new_edition_from_master_edition_via_token` endpoint. This endpoint will create both an immutable Metadata based on the parent Metadata and a special Edition struct based on the parent Master Edition struct.
The Edition has the same PDA as a Master Edition to force collision and prevent a user from having a mint with both, `['metaplex', metaplex_program_id, mint_id, 'edition']`.
@ -208,7 +208,7 @@ The Edition has the same PDA as a Master Edition to force collision and prevent
### Decoration as PDA Extensions
The whole idea of the Token Metadata program is to be a decorator to a Token Mint. Each struct acts as further decoration. The Metadata struct gives a mint a name and a symbol and points to some external URI that can be anything. The Master Edition gives it printing capabilities. The Edition labels it as a child of something.
The whole idea of the Token Metadata program is to be a decorator to a Token Mint. Each struct acts as further decoration. The Metadata struct gives a mint a name and a symbol and points to some external URI that can be anything. The Master Edition gives it printing capabilities. The Edition labels it as a child of something.
This is important to internalize, because it means you as a Rust developer can take it a step further. There is nothing stopping you from building a new contract on top of ours that makes it's own PDAs and and extending this still further. Why not build a CookingRecipes PDA, that has seed `['your-app', your_program_id, mint_id, 'recipes']`? You can require that a Metadata PDA from our contract exists to make a PDA in your program, and then you can further decorate mints on top of our decorations. The idea is to compose mints with further information than they ever had before, and then build clients that can consume that information in new and interesting ways.
@ -245,7 +245,7 @@ The URI resource is compatible with [ERC-1155 JSON Schema](https://github.com/et
},
"seller_fee_basis_points": {
"type": "number",
},
"properties": {
"type": "object",
@ -376,7 +376,7 @@ Safety Deposit Boxes always have PDA addresses of type `['vault', vault_key, min
### External Price Account
The External Price Account is meant to be used as an external oracle. It is provided to a Vault on initialization and doesn't need to be owned or controlled by the vault authority (though it can be.) It can provide data on the `price_per_share` of fractional shares, whether or not the vault authority is currently allowed to **Combine** the vault and reclaim the contents, and what the `price_mint` of the vault is.
The External Price Account is meant to be used as an external oracle. It is provided to a Vault on initialization and doesn't need to be owned or controlled by the vault authority (though it can be.) It can provide data on the `price_per_share` of fractional shares, whether or not the vault authority is currently allowed to **Combine** the vault and reclaim the contents, and what the `price_mint` of the vault is.
ExternalPriceAccounts do not have PDA addresses.
@ -456,6 +456,8 @@ pub struct AuctionDataExtended {
pub tick_size: Option<u64>,
/// gap_tick_size_percentage - two decimal points
pub gap_tick_size_percentage: Option<u8>,
/// auction name
pub name: Option<[u8; 32]>,
}
/// Define valid auction state transitions.
@ -543,11 +545,11 @@ AuctionData accounts always have PDA addresses of `['auction', auction_program_i
### Bid State
Bid State is technically not a top level struct, but an embedded one within AuctionData. I thought it was good to give it a section anyway because it's a complex little beast. It's actually an enum that holds a bid vector and a maximum size denoting how many of those bids are actually "valid winners" vs just placeholders.
Bid State is technically not a top level struct, but an embedded one within AuctionData. I thought it was good to give it a section anyway because it's a complex little beast. It's actually an enum that holds a bid vector and a maximum size denoting how many of those bids are actually "valid winners" vs just placeholders.
It's reversed, which is to say that the number one winner is always at the end of the vec. It's also always bigger generally than the number of winners so that if a bid is cancelled, we have some people who got bumped out of top spots that can be moved back into them without having to cancel and replace their bids. When a bid is placed, it is inserted in the proper position based on it's amount and then the lowest bidder is bumped off the 0th position of the vec if the vec is at max size, so the vec remains sorted at all times.
In the case of open edition, the max is always zero, ie there are never any winners, and we are just accepting bids and creating BidderMetadata tickets and BidderPots to accept payment for (probably) fixed price Participation NFTs.
In the case of open edition, the max is always zero, ie there are never any winners, and we are just accepting bids and creating BidderMetadata tickets and BidderPots to accept payment for (probably) fixed price Participation NFTs.
We would prefer that OpenEdition enum have no bid vector and no max, but unfortunately borsh-js does not support enums with different internal data structures, so all data structures in an enum must be identical (even if unused.) Keep that in mind when designing your own end to end borsh implementations!
@ -567,7 +569,7 @@ BidderPot always has a PDA of `['auction', auction_program_id, auction_id, bidde
If you've read this far, you now get to witness my personal shame. So as it turns out, if you build a complex enough program with enough structs flying around, there is some kind of weird interaction in the Metaplex contract that causes it to blow out with an access violation if you add more than a certain number of keys to one particular struct (AuctionData), and *only* during the redemption endpoint calls. We were unable to discern why this was across 3 days of debugging. We had a theory it was due to some issue with borsh but it is not 100% certain, as we're not experts with that library's internals.
Instead, our work-around was to introduce AuctionDataExtended to add new fields that we needed to AuctionData without breaking this hidden bug that seems to exist. What is odd about the whole thing is adding fields to *other* structs doesn't cause any issues. In the future I'd love to have someone who knows way more than me about these subjects weigh in and tell me what I did wrong here to resolve this split-brain problem! We also don't have reverse lookup capability (Resource key on AuctionData) because of this bug - adding it would cause the blow out I mentioned.
Instead, our work-around was to introduce AuctionDataExtended to add new fields that we needed to AuctionData without breaking this hidden bug that seems to exist. What is odd about the whole thing is adding fields to *other* structs doesn't cause any issues. In the future I'd love to have someone who knows way more than me about these subjects weigh in and tell me what I did wrong here to resolve this split-brain problem! We also don't have reverse lookup capability (Resource key on AuctionData) because of this bug - adding it would cause the blow out I mentioned.
Another note here is `gap_tick_size_percentage` as of the time of this writing has not been implemented yet, it is just a dummy field.
@ -860,7 +862,7 @@ The instruction set for metaplex can be found here: [https://github.com/metaplex
### AuctionManager
This is the top level struct of the entire contract and serves as a container for "all the things." When you make auctions on Metaplex, you are actually really making these ultimately. An AuctionManager has a single authority (you, the auctioneer), a store, which is the storefront struct, an Auction from the auction contract, and a Vault from the vault contract. It also has a token account called `accept_payment` that serves as a central clearing escrow for all tokens that it will collect in the future from the winning bidders and all payments for fixed price participation nfts from all non-winners in the auction.
This is the top level struct of the entire contract and serves as a container for "all the things." When you make auctions on Metaplex, you are actually really making these ultimately. An AuctionManager has a single authority (you, the auctioneer), a store, which is the storefront struct, an Auction from the auction contract, and a Vault from the vault contract. It also has a token account called `accept_payment` that serves as a central clearing escrow for all tokens that it will collect in the future from the winning bidders and all payments for fixed price participation nfts from all non-winners in the auction.
It contains embedded within it a separate `state` and `settings` struct. It is seeded with the `settings` on initialization by the caller, while the `state` is derived from `settings` on initialization. AuctionManager goes through several states:
@ -872,7 +874,7 @@ It contains embedded within it a separate `state` and `settings` struct. It is s
**Disbursing**: The underlying Auction is over and now the AuctionManager is in the business of disbursing royalties to the auctioneer and creators, prizes and participation NFTs to the winners, and possibly participation NFTs to the non-winners.
**Finished:** All funds and prizes disbursed.
**Finished:** All funds and prizes disbursed.
This state is not currently in use as switching to it requires an iteration over prizes to review all items for claimed-ness and this costs CPU that is too precious during the redemption call OR adding new endpoint that is not guaranteed to be called. We will revisit it later to bring it back during a refactoring, for now it is considered a NOOP state.
@ -882,7 +884,7 @@ AuctionManagers always have PDAs of seed `['metaplex', metaplex_program_id, auct
AuctionManagerSettings is an embedded struct inside AuctionManager but is deserving of it's own section. This struct is actually provided by the user in the `init_auction_manager` call to parameterize the AuctionManager with who is winning what and whether or not there is a participation NFT. It is fairly straightforward - for each entry in the WinningConfig vec, it stands for a given winning place in the Auction. The 0th entry is the WinningConfig for the 1st place winner. A WinningConfig has many WinningConfigItems. For each WinningConfigItem in the 0th WinningConfig, it is a mapping to a Vault SafetyDepositBox that the 1st place winner gets items from. You can therefore configure quite arbitrary Auctions this way.
This setup is actually quite redundant and will likely change in the future to a setup where a WinningConfigItem is the top level structure and it simply declares which winners will receive it, because if you wish for multiple winners to receive prints from the same Master Edition, the WinningConfigItem must right now be duplicated across each WinningConfig.
This setup is actually quite redundant and will likely change in the future to a setup where a WinningConfigItem is the top level structure and it simply declares which winners will receive it, because if you wish for multiple winners to receive prints from the same Master Edition, the WinningConfigItem must right now be duplicated across each WinningConfig.
The Participation Config is optional, but has enums describing how it will behave for winners and for non-winners, whether or not it has a price associated with it, and what safety deposit box contains its printing tokens.
@ -902,7 +904,7 @@ BidRedemptionTickets always have PDAs of `['metaplex', auction_id, bidder_metada
### PayoutTicket
For each creator, for each metadata(WinningConfigItem), for each winning place(WinningConfig) in an Auction, a PayoutTicket is created to record the sliver of income generated for that creator. There is also one made for the Auctioneer for every such case. And yes, it really is that specific. This means that a given creator may have quite a few PayoutTickets for a single AuctionManager, but each one represents a slightly different royalty payout.
For each creator, for each metadata(WinningConfigItem), for each winning place(WinningConfig) in an Auction, a PayoutTicket is created to record the sliver of income generated for that creator. There is also one made for the Auctioneer for every such case. And yes, it really is that specific. This means that a given creator may have quite a few PayoutTickets for a single AuctionManager, but each one represents a slightly different royalty payout.
For instance, 1st place may have three items with 3 unique metadata won while 2nd place may have 4 metadata from 4 items, every item with a single unique creator. The split of funds in the 1st place is going to be 3 ways, while in 2nd place would be 4 ways. Even if 1st and 2nd place bids are the same, we want two records to reflect the royalties paid from 1st and 2nd place, because they would be different numbers in this case, and we want to preserve history.
@ -958,9 +960,9 @@ Note that owning the token itself is the *only* requirement for using the `updat
Metadata come locked and stocked with arrays of creators, each with their own `share` and all guaranteed to sum to 100. The Metadata itself has a `seller_fee_basis_points` field that represents the share creators get out of the proceeds in any secondary sale and a `primary_sale_happened` boolean that distinguishes to the world whether or not this particular Metadata has experienced it's first sale or not. With all of this, Metaplex is able to do complete Royalty calculations after an Auction is over. It was mentioned above that on initialization, the Metaplex contract snapshots for each Metadata being sold the `primary_sale_happened` just in case the boolean is flipped during the auction so that royalties are calculated as-of initiation - this is important to note.
At the end of the auction, anybody (permissionless) can cycle through each winning bid in the contract and ask the Metaplex contract to use its authority to call the Auction contract and pump the winning bid monies into the `accept_payment` escrow account via `claim_bid`. Once all winning bids have been settled into here, royalties are eligible to be paid out. We'll cover payouts of fixed price Participation NFTs separately.
At the end of the auction, anybody (permissionless) can cycle through each winning bid in the contract and ask the Metaplex contract to use its authority to call the Auction contract and pump the winning bid monies into the `accept_payment` escrow account via `claim_bid`. Once all winning bids have been settled into here, royalties are eligible to be paid out. We'll cover payouts of fixed price Participation NFTs separately.
Now, anybody (permissionless) can cycle through each creator PLUS the auctioneer on each item in each winning bid and call `empty_payment_account` with an Associated Token Account that is owned by that creator or auctioneer and that action will calculate, using the creator's share or auctioneer's share of that item's metadata, and the fractional percentage of that item of the overall winning basket, to payout the creator or auctioneer from the escrow.
Now, anybody (permissionless) can cycle through each creator PLUS the auctioneer on each item in each winning bid and call `empty_payment_account` with an Associated Token Account that is owned by that creator or auctioneer and that action will calculate, using the creator's share or auctioneer's share of that item's metadata, and the fractional percentage of that item of the overall winning basket, to payout the creator or auctioneer from the escrow.
Our front end implementation immediately calls the `update_primary_sale_happened` endpoint on token metadata for any token once redeemed for users so that if they re-sell, the `primary_sale_happened` boolean is taken into account in the `empty_payment_account` logic and only the basis points given in `seller_fee_basis_points` goes to the creators instead of the whole pie. The remaining part of the pie goes to the auctioneer doing the reselling.
@ -972,9 +974,9 @@ Note because our front end implementation chooses to use SOL instead of a generi
### Validation
Just because you provide a vault to an AuctionManager and an AuctionManagerSettings declaring this vault is filled with wonderful prizes *does not* believe that Metaplex will believe you. For every safety deposit box indexed in a WinningConfigItem, there must be a call to `validate_safety_deposit_box` after initiation where the safety deposit box is provided for inspection to the Metaplex contract so that it can verify that there are enough tokens, and of the right type, to pay off all winners in the auction.
Just because you provide a vault to an AuctionManager and an AuctionManagerSettings declaring this vault is filled with wonderful prizes *does not* believe that Metaplex will believe you. For every safety deposit box indexed in a WinningConfigItem, there must be a call to `validate_safety_deposit_box` after initiation where the safety deposit box is provided for inspection to the Metaplex contract so that it can verify that there are enough tokens, and of the right type, to pay off all winners in the auction.
Given how irritating this process is, we may in the future merge token-vault with metaplex, or simply copy over the parts of it that are relevant, leaving token-vault out for those interested in experimenting with fractionalization.
Given how irritating this process is, we may in the future merge token-vault with metaplex, or simply copy over the parts of it that are relevant, leaving token-vault out for those interested in experimenting with fractionalization.
### Unwon Items

View File

@ -15,7 +15,7 @@ import { findProgramAddress } from '../utils';
export const AUCTION_PREFIX = 'auction';
export const METADATA = 'metadata';
export const EXTENDED = 'extended';
export const MAX_AUCTION_DATA_EXTENDED_SIZE = 8 + 9 + 2 + 200;
export const MAX_AUCTION_DATA_EXTENDED_SIZE = 8 + 9 + 2 + 9 + 33 + 158;
export enum AuctionState {
Created = 0,

View File

@ -1,3 +1,5 @@
use solana_clap_utils::input_parsers::value_of;
use {
clap::{crate_description, crate_name, crate_version, App, Arg, ArgMatches, SubCommand},
rand::Rng,
@ -24,10 +26,16 @@ use {
const PROGRAM_PUBKEY: &str = "HLGetPpEUaagthEtF4px9S24hwJrwz3qvgRZxkWTw4ei";
const TOKEN_PROGRAM_PUBKEY: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
fn string_to_array(value: &str) -> [u8; 32] {
let mut result: [u8; 32] = Default::default();
&result[0..value.len()].copy_from_slice(value.as_bytes());
result
}
fn create_auction(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
use spl_auction::{
instruction,
processor::{CreateAuctionArgs, PriceFloor, WinnerLimit},
processor::{CreateAuctionArgsV2, PriceFloor, WinnerLimit},
PREFIX,
};
@ -57,13 +65,20 @@ fn create_auction(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
}
});
// Optional auction name
let name: &str = app_matches.value_of("name").unwrap_or("");
// Optional instant sale price
let instant_sale_price: Option<u64> = value_of::<u64>(app_matches, "instant_sale_price");
println!(
"Creating Auction:\n\
- Auction: {}\n\
- Payer: {}\n\
- Mint: {}\n\
- Resource: {}\n\
- Salt: {}\n\n\
- Salt: {}\n\
- Name: {}\n\n\
Use the salt when revealing the price.
",
auction_pubkey,
@ -71,8 +86,11 @@ fn create_auction(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
mint.pubkey(),
resource,
salt,
name,
);
let name = string_to_array(name);
let instructions = [
// Create a new mint to test this auction with.
create_account(
@ -105,10 +123,10 @@ fn create_auction(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
let instructions = [
// Create an auction for the auction seller as their own resource.
instruction::create_auction_instruction(
instruction::create_auction_instruction_v2(
program_key,
payer.pubkey(),
CreateAuctionArgs {
CreateAuctionArgsV2 {
authority: payer.pubkey(),
end_auction_at: None,
end_auction_gap: None,
@ -118,6 +136,8 @@ fn create_auction(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
price_floor: floor.unwrap_or(PriceFloor::None([0; 32])),
gap_tick_size_percentage: Some(0),
tick_size: Some(0),
name,
instant_sale_price,
},
),
];
@ -604,6 +624,20 @@ fn main() {
.takes_value(false)
.help("If set, hide the minimum required bid price until the end of the auction."),
)
.arg(
Arg::with_name("name")
.long("name")
.value_name("STRING")
.takes_value(true)
.help("Optional auction name (up to 32 characters long)."),
)
.arg(
Arg::with_name("instant_sale_price")
.long("instant-sale-price")
.value_name("AMOUNT")
.takes_value(true)
.help("Optional instant sale price."),
)
)
.subcommand(
SubCommand::with_name("inspect")

View File

@ -8,7 +8,8 @@ use solana_program::{
pub use crate::processor::{
cancel_bid::CancelBidArgs, claim_bid::ClaimBidArgs, create_auction::CreateAuctionArgs,
end_auction::EndAuctionArgs, place_bid::PlaceBidArgs, start_auction::StartAuctionArgs,
create_auction_v2::CreateAuctionArgsV2, end_auction::EndAuctionArgs, place_bid::PlaceBidArgs,
start_auction::StartAuctionArgs,
};
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
@ -30,8 +31,9 @@ pub enum AuctionInstruction {
/// Create a new auction account bound to a resource, initially in a pending state.
/// 0. `[signer]` The account creating the auction, which is authorised to make changes.
/// 1. `[writable]` Uninitialized auction account.
/// 2. `[]` Rent sysvar
/// 3. `[]` System account
/// 2. `[writable]` Auction extended data account (pda relative to auction of ['auction', program id, vault key, 'extended']).
/// 3. `[]` Rent sysvar
/// 4. `[]` System account
CreateAuction(CreateAuctionArgs),
/// Move SPL tokens from winning bid to the destination account.
@ -44,9 +46,14 @@ pub enum AuctionInstruction {
/// 6. `[]` Token mint of the auction
/// 7. `[]` Clock sysvar
/// 8. `[]` Token program
/// 9. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
ClaimBid(ClaimBidArgs),
/// Ends an auction, regardless of end timing conditions
///
/// 0. `[writable, signer]` Auction authority
/// 1. `[writable]` Auction
/// 6. `[]` Clock sysvar
EndAuction(EndAuctionArgs),
/// Start an inactive auction.
@ -73,6 +80,15 @@ pub enum AuctionInstruction {
/// 11. `[]` System program
/// 12. `[]` SPL Token Program
PlaceBid(PlaceBidArgs),
/// Create a new auction account bound to a resource, initially in a pending state.
/// The only one difference with above instruction it's additional parameters in CreateAuctionArgsV2
/// 0. `[signer]` The account creating the auction, which is authorised to make changes.
/// 1. `[writable]` Uninitialized auction account.
/// 2. `[writable]` Auction extended data account (pda relative to auction of ['auction', program id, vault key, 'extended']).
/// 3. `[]` Rent sysvar
/// 4. `[]` System account
CreateAuctionV2(CreateAuctionArgsV2),
}
/// Creates an CreateAuction instruction.
@ -111,6 +127,42 @@ pub fn create_auction_instruction(
}
}
/// Creates an CreateAuctionV2 instruction.
pub fn create_auction_instruction_v2(
program_id: Pubkey,
creator_pubkey: Pubkey,
args: CreateAuctionArgsV2,
) -> Instruction {
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
args.resource.as_ref(),
];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
];
let (auction_extended_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(creator_pubkey, true),
AccountMeta::new(auction_pubkey, false),
AccountMeta::new(auction_extended_pubkey, false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
],
data: AuctionInstruction::CreateAuctionV2(args)
.try_to_vec()
.unwrap(),
}
}
/// Creates an SetAuthority instruction.
pub fn set_authority_instruction(
program_id: Pubkey,
@ -338,6 +390,14 @@ pub fn claim_bid_instruction(
];
let (bidder_pot_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
];
let (auction_extended_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
@ -350,6 +410,7 @@ pub fn claim_bid_instruction(
AccountMeta::new_readonly(token_mint_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(auction_extended_pubkey, false),
],
data: AuctionInstruction::ClaimBid(args).try_to_vec().unwrap(),
}

View File

@ -1,9 +1,9 @@
#![allow(warnings)]
mod errors;
mod utils;
pub mod entrypoint;
pub mod errors;
pub mod instruction;
pub mod processor;

View File

@ -11,6 +11,7 @@ use std::{cell::Ref, cmp, mem};
pub mod cancel_bid;
pub mod claim_bid;
pub mod create_auction;
pub mod create_auction_v2;
pub mod end_auction;
pub mod place_bid;
pub mod set_authority;
@ -20,6 +21,7 @@ pub mod start_auction;
pub use cancel_bid::*;
pub use claim_bid::*;
pub use create_auction::*;
pub use create_auction_v2::*;
pub use end_auction::*;
pub use place_bid::*;
pub use set_authority::*;
@ -34,7 +36,10 @@ pub fn process_instruction(
match AuctionInstruction::try_from_slice(input)? {
AuctionInstruction::CancelBid(args) => cancel_bid(program_id, accounts, args),
AuctionInstruction::ClaimBid(args) => claim_bid(program_id, accounts, args),
AuctionInstruction::CreateAuction(args) => create_auction(program_id, accounts, args),
AuctionInstruction::CreateAuction(args) => {
create_auction(program_id, accounts, args, None, None)
}
AuctionInstruction::CreateAuctionV2(args) => create_auction_v2(program_id, accounts, args),
AuctionInstruction::EndAuction(args) => end_auction(program_id, accounts, args),
AuctionInstruction::PlaceBid(args) => place_bid(program_id, accounts, args),
AuctionInstruction::SetAuthority => set_authority(program_id, accounts),
@ -90,7 +95,10 @@ pub struct AuctionData {
pub bid_state: BidState,
}
pub const MAX_AUCTION_DATA_EXTENDED_SIZE: usize = 8 + 9 + 2 + 200;
// Alias for auction name.
pub type AuctionName = [u8; 32];
pub const MAX_AUCTION_DATA_EXTENDED_SIZE: usize = 8 + 9 + 2 + 9 + 33 + 158;
// Further storage for more fields. Would like to store more on the main data but due
// to a borsh issue that causes more added fields to inflict "Access violation" errors
// during redemption in main Metaplex app for no reason, we had to add this nasty PDA.
@ -104,6 +112,10 @@ pub struct AuctionDataExtended {
pub tick_size: Option<u64>,
/// gap_tick_size_percentage - two decimal points
pub gap_tick_size_percentage: Option<u8>,
/// Instant sale price
pub instant_sale_price: Option<u64>,
/// Auction name
pub name: Option<AuctionName>,
}
impl AuctionDataExtended {
@ -116,6 +128,37 @@ impl AuctionDataExtended {
Ok(auction_extended)
}
pub fn get_instant_sale_price<'a>(data: &'a Ref<'a, &'a mut [u8]>) -> Option<u64> {
if let Some(idx) = Self::find_instant_sale_beginning(data) {
Some(u64::from_le_bytes(*array_ref![data, idx, 8]))
} else {
None
}
}
fn find_instant_sale_beginning<'a>(data: &'a Ref<'a, &'a mut [u8]>) -> Option<usize> {
// total_uncancelled_bids + tick_size Option
let mut instant_sale_beginning = 8;
// gaps for tick_size and gap_tick_size_percentage
let gaps = [9, 2];
for gap in gaps.iter() {
if data[instant_sale_beginning] == 1 {
instant_sale_beginning += gap;
} else {
instant_sale_beginning += 1;
}
}
// check if instant_sale_price has some value
if data[instant_sale_beginning] == 1 {
Some(instant_sale_beginning + 1)
} else {
None
}
}
}
impl AuctionData {
@ -239,6 +282,37 @@ impl AuctionData {
])
}
pub fn get_winner_bid_amount_at(a: &AccountInfo, idx: usize) -> Option<u64> {
let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
match AuctionData::get_winner_bid_amount_at_inner(
&a.data.borrow(),
idx,
bid_state_beginning,
num_elements,
max,
) {
Some(bid_amount) => Some(bid_amount),
None => None,
}
}
fn get_winner_bid_amount_at_inner<'a>(
data: &'a Ref<'a, &'a mut [u8]>,
idx: usize,
bid_state_beginning: usize,
num_elements: usize,
max: usize,
) -> Option<u64> {
if idx + 1 > num_elements || idx + 1 > max {
return None;
}
Some(u64::from_le_bytes(*array_ref![
data,
bid_state_beginning + (num_elements - idx - 1) * BID_LENGTH + 32,
8
]))
}
pub fn from_account_info(a: &AccountInfo) -> Result<AuctionData, ProgramError> {
if (a.data_len() - BASE_AUCTION_DATA_SIZE) % mem::size_of::<Bid>() != 0 {
return Err(AuctionError::DataTypeMismatch.into());
@ -296,12 +370,26 @@ impl AuctionData {
self.bid_state.winner_at(idx)
}
pub fn consider_instant_bid(&mut self, instant_sale_price: Option<u64>) {
// Check if all the lots were sold with instant_sale_price
if let Some(price) = instant_sale_price {
if self
.bid_state
.lowest_winning_bid_is_instant_bid_price(price)
{
msg!("All the lots were sold with instant_sale_price, auction is ended");
self.state = AuctionState::Ended;
}
}
}
pub fn place_bid(
&mut self,
bid: Bid,
tick_size: Option<u64>,
gap_tick_size_percentage: Option<u8>,
now: UnixTimestamp,
instant_sale_price: Option<u64>,
) -> Result<(), ProgramError> {
let gap_val = match self.ended_at {
Some(end) => {
@ -319,7 +407,19 @@ impl AuctionData {
PriceFloor::MinimumPrice(min) => min[0],
_ => 0,
};
self.bid_state.place_bid(bid, tick_size, gap_val, minimum)
self.bid_state.place_bid(
bid,
tick_size,
gap_val,
minimum,
instant_sale_price,
&mut self.state,
)?;
self.consider_instant_bid(instant_sale_price);
Ok(())
}
}
@ -448,6 +548,8 @@ impl BidState {
tick_size: Option<u64>,
gap_tick_size_percentage: Option<u8>,
minimum: u64,
instant_sale_price: Option<u64>,
auction_state: &mut AuctionState,
) -> Result<(), ProgramError> {
msg!("Placing bid {:?}", &bid.1.to_string());
BidState::assert_valid_tick_size_bid(&bid, tick_size)?;
@ -505,6 +607,7 @@ impl BidState {
break;
}
}
let max_size = BidState::max_array_size_for(*max);
if bids.len() > max_size {
@ -606,6 +709,17 @@ impl BidState {
BidState::OpenEdition { bids, max } => None,
}
}
pub fn lowest_winning_bid_is_instant_bid_price(&self, instant_sale_amount: u64) -> bool {
match self {
// In a capped auction, track the limited number of winners.
BidState::EnglishAuction { bids, max } | BidState::OpenEdition { bids, max } => {
// bids.len() - max = index of the last winner bid
bids.len() >= *max && bids[bids.len() - *max].1 >= instant_sale_amount
}
_ => false,
}
}
}
#[repr(C)]

View File

@ -129,6 +129,10 @@ pub fn cancel_bid(
return Err(AuctionError::IncorrectMint.into());
}
// Load auction extended account to check instant_sale_price
// and update cancelled bids if auction still active
let mut auction_extended = AuctionDataExtended::from_account_info(accounts.auction_extended)?;
// Load the clock, used for various auction timing.
let clock = Clock::from_account_info(accounts.clock_sysvar)?;
@ -175,10 +179,20 @@ pub fn cancel_bid(
}
// Refuse to cancel if the auction ended and this person is a winning account.
if auction.ended(clock.unix_timestamp)? && auction.is_winner(accounts.bidder.key).is_some() {
let winner_bid_index = auction.is_winner(accounts.bidder.key);
if auction.ended(clock.unix_timestamp)? && winner_bid_index.is_some() {
return Err(AuctionError::InvalidState.into());
}
// Refuse to cancel if bidder set price above or equal instant_sale_price
if let Some(bid_index) = winner_bid_index {
if let Some(instant_sale_price) = auction_extended.instant_sale_price {
if auction.bid_state.amount(bid_index) >= instant_sale_price {
return Err(AuctionError::InvalidState.into());
}
}
}
// Confirm we're looking at the real SPL account for this bidder.
let bidder_pot = BidderPot::from_account_info(accounts.bidder_pot)?;
if bidder_pot.bidder_pot != *accounts.bidder_pot_token.key {
@ -219,8 +233,6 @@ pub fn cancel_bid(
EXTENDED.as_bytes(),
],
)?;
let mut auction_extended =
AuctionDataExtended::from_account_info(accounts.auction_extended)?;
msg!("Already cancelled is {:?}", already_cancelled);

View File

@ -3,13 +3,13 @@
use crate::{
errors::AuctionError,
processor::{AuctionData, BidderMetadata, BidderPot},
processor::{AuctionData, AuctionDataExtended, BidderMetadata, BidderPot},
utils::{
assert_derivation, assert_initialized, assert_owned_by, assert_signer,
assert_token_program_matches_package, create_or_allocate_account_raw, spl_token_transfer,
TokenTransferParams,
},
PREFIX,
EXTENDED, PREFIX,
};
use {
@ -44,6 +44,7 @@ struct Accounts<'a, 'b: 'a> {
mint: &'a AccountInfo<'b>,
clock_sysvar: &'a AccountInfo<'b>,
token_program: &'a AccountInfo<'b>,
auction_extended: Option<&'a AccountInfo<'b>>,
}
fn parse_accounts<'a, 'b: 'a>(
@ -61,6 +62,7 @@ fn parse_accounts<'a, 'b: 'a>(
mint: next_account_info(account_iter)?,
clock_sysvar: next_account_info(account_iter)?,
token_program: next_account_info(account_iter)?,
auction_extended: next_account_info(account_iter).ok(),
};
assert_owned_by(accounts.auction, program_id)?;
@ -71,6 +73,10 @@ fn parse_accounts<'a, 'b: 'a>(
assert_signer(accounts.authority)?;
assert_token_program_matches_package(accounts.token_program)?;
if let Some(auction_extended) = accounts.auction_extended {
assert_owned_by(auction_extended, program_id)?;
}
if *accounts.token_program.key != spl_token::id() {
return Err(AuctionError::InvalidTokenProgram.into());
}
@ -120,14 +126,41 @@ pub fn claim_bid(
// User must have won the auction in order to claim their funds. Check early as the rest of the
// checks will be for nothing otherwise.
if auction.is_winner(accounts.bidder.key).is_none() {
let bid_index = auction.is_winner(accounts.bidder.key);
if bid_index.is_none() {
msg!("User {:?} is not winner", accounts.bidder.key);
return Err(AuctionError::InvalidState.into());
}
// Auction must have ended.
let instant_sale_price = accounts.auction_extended.and_then(|info| {
assert_derivation(
program_id,
info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
],
)
.ok()?;
AuctionDataExtended::from_account_info(info)
.ok()?
.instant_sale_price
});
// Auction either must have ended or bidder pay instant_sale_price
if !auction.ended(clock.unix_timestamp)? {
return Err(AuctionError::InvalidState.into());
match instant_sale_price {
Some(instant_sale_price)
if auction.bid_state.amount(bid_index.unwrap()) < instant_sale_price =>
{
return Err(AuctionError::InvalidState.into())
}
None => return Err(AuctionError::InvalidState.into()),
_ => (),
}
}
// The mint provided in this claim must match the one the auction was initialized with.

View File

@ -3,8 +3,8 @@ use mem::size_of;
use crate::{
errors::AuctionError,
processor::{
AuctionData, AuctionDataExtended, AuctionState, Bid, BidState, PriceFloor, WinnerLimit,
BASE_AUCTION_DATA_SIZE, MAX_AUCTION_DATA_EXTENDED_SIZE,
AuctionData, AuctionDataExtended, AuctionName, AuctionState, Bid, BidState, PriceFloor,
WinnerLimit, BASE_AUCTION_DATA_SIZE, MAX_AUCTION_DATA_EXTENDED_SIZE,
},
utils::{assert_derivation, assert_owned_by, create_or_allocate_account_raw},
EXTENDED, PREFIX,
@ -73,6 +73,8 @@ pub fn create_auction(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: CreateAuctionArgs,
instant_sale_price: Option<u64>,
name: Option<AuctionName>,
) -> ProgramResult {
msg!("+ Processing CreateAuction");
let accounts = parse_accounts(program_id, accounts)?;
@ -156,6 +158,8 @@ pub fn create_auction(
total_uncancelled_bids: 0,
tick_size: args.tick_size,
gap_tick_size_percentage: args.gap_tick_size_percentage,
instant_sale_price,
name,
}
.serialize(&mut *accounts.auction_extended.data.borrow_mut())?;

View File

@ -0,0 +1,99 @@
use mem::size_of;
use crate::{
errors::AuctionError,
processor::create_auction::*,
processor::{
AuctionData, AuctionDataExtended, AuctionName, AuctionState, Bid, BidState, PriceFloor,
WinnerLimit, BASE_AUCTION_DATA_SIZE, MAX_AUCTION_DATA_EXTENDED_SIZE,
},
utils::{assert_derivation, assert_owned_by, create_or_allocate_account_raw},
EXTENDED, PREFIX,
};
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
clock::UnixTimestamp,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
},
std::mem,
};
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct CreateAuctionArgsV2 {
/// How many winners are allowed for this auction. See AuctionData.
pub winners: WinnerLimit,
/// End time is the cut-off point that the auction is forced to end by. See AuctionData.
pub end_auction_at: Option<UnixTimestamp>,
/// Gap time is how much time after the previous bid where the auction ends. See AuctionData.
pub end_auction_gap: Option<UnixTimestamp>,
/// Token mint for the SPL token used for bidding.
pub token_mint: Pubkey,
/// Authority
pub authority: Pubkey,
/// The resource being auctioned. See AuctionData.
pub resource: Pubkey,
/// Set a price floor.
pub price_floor: PriceFloor,
/// Add a tick size increment
pub tick_size: Option<u64>,
/// Add a minimum percentage increase each bid must meet.
pub gap_tick_size_percentage: Option<u8>,
/// Auction name
pub name: AuctionName,
/// Add a instant sale price.
pub instant_sale_price: Option<u64>,
}
struct Accounts<'a, 'b: 'a> {
auction: &'a AccountInfo<'b>,
auction_extended: &'a AccountInfo<'b>,
payer: &'a AccountInfo<'b>,
rent: &'a AccountInfo<'b>,
system: &'a AccountInfo<'b>,
}
fn parse_accounts<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
) -> Result<Accounts<'a, 'b>, ProgramError> {
let account_iter = &mut accounts.iter();
let accounts = Accounts {
payer: next_account_info(account_iter)?,
auction: next_account_info(account_iter)?,
auction_extended: next_account_info(account_iter)?,
rent: next_account_info(account_iter)?,
system: next_account_info(account_iter)?,
};
Ok(accounts)
}
pub fn create_auction_v2(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: CreateAuctionArgsV2,
) -> ProgramResult {
create_auction(
program_id,
accounts,
CreateAuctionArgs {
winners: args.winners,
end_auction_at: args.end_auction_at,
end_auction_gap: args.end_auction_gap,
token_mint: args.token_mint,
authority: args.authority,
resource: args.resource,
price_floor: args.price_floor,
tick_size: args.tick_size,
gap_tick_size_percentage: args.gap_tick_size_percentage,
},
args.instant_sale_price,
Some(args.name),
)
}

View File

@ -281,12 +281,21 @@ pub fn place_bid<'r, 'b: 'r>(
.ok_or(AuctionError::NumericalOverflowError)?;
auction_extended.serialize(&mut *accounts.auction_extended.data.borrow_mut())?;
let mut bid_price = args.amount;
if let Some(instant_sale_price) = auction_extended.instant_sale_price {
if args.amount > instant_sale_price {
msg!("Received amount is more than instant_sale_price so it was reduced to instant_sale_price - {:?}", instant_sale_price);
bid_price = instant_sale_price;
}
}
// Confirm payers SPL token balance is enough to pay the bid.
let account: Account = Account::unpack_from_slice(&accounts.bidder_token.data.borrow())?;
if account.amount.saturating_sub(args.amount) < 0 {
if account.amount.saturating_sub(bid_price) < 0 {
msg!(
"Amount is too small: {:?}, compared to account amount of {:?}",
args.amount,
bid_price,
account.amount
);
return Err(AuctionError::BalanceTooLow.into());
@ -299,16 +308,17 @@ pub fn place_bid<'r, 'b: 'r>(
authority: accounts.transfer_authority.clone(),
authority_signer_seeds: bump_authority_seeds,
token_program: accounts.token_program.clone(),
amount: args.amount,
amount: bid_price,
})?;
// Serialize new Auction State
auction.last_bid = Some(clock.unix_timestamp);
auction.place_bid(
Bid(*accounts.bidder.key, args.amount),
Bid(*accounts.bidder.key, bid_price),
auction_extended.tick_size,
auction_extended.gap_tick_size_percentage,
clock.unix_timestamp,
auction_extended.instant_sale_price,
)?;
auction.serialize(&mut *accounts.auction.data.borrow_mut())?;
@ -316,7 +326,7 @@ pub fn place_bid<'r, 'b: 'r>(
BidderMetadata {
bidder_pubkey: *accounts.bidder.key,
auction_pubkey: *accounts.auction.key,
last_bid: args.amount,
last_bid: bid_price,
last_bid_timestamp: clock.unix_timestamp,
cancelled: false,
}

View File

@ -9,11 +9,20 @@ use solana_sdk::{
use spl_auction::{
instruction,
processor::{
CancelBidArgs, ClaimBidArgs, CreateAuctionArgs, EndAuctionArgs, PlaceBidArgs, PriceFloor,
StartAuctionArgs, WinnerLimit,
CancelBidArgs, ClaimBidArgs, CreateAuctionArgs, CreateAuctionArgsV2, EndAuctionArgs,
PlaceBidArgs, PriceFloor, StartAuctionArgs, WinnerLimit,
},
};
fn string_to_array(value: &str) -> Result<[u8; 32], TransportError> {
if value.len() > 32 {
return Err(TransportError::Custom("String too long".to_string()));
}
let mut result: [u8; 32] = Default::default();
&result[0..value.len()].copy_from_slice(value.as_bytes());
Ok(result)
}
pub async fn get_account(banks_client: &mut BanksClient, pubkey: &Pubkey) -> Account {
banks_client
.get_account(*pubkey)
@ -132,6 +141,7 @@ pub async fn get_token_supply(banks_client: &mut BanksClient, mint: &Pubkey) ->
account_info.supply
}
#[allow(clippy::too_many_arguments)]
pub async fn create_auction(
banks_client: &mut BanksClient,
program_id: &Pubkey,
@ -140,27 +150,58 @@ pub async fn create_auction(
resource: &Pubkey,
mint_keypair: &Pubkey,
max_winners: usize,
name: &str,
instant_sale_price: Option<u64>,
price_floor: PriceFloor,
gap_tick_size_percentage: Option<u8>,
tick_size: Option<u64>,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[instruction::create_auction_instruction(
*program_id,
payer.pubkey(),
CreateAuctionArgs {
authority: payer.pubkey(),
end_auction_at: None,
end_auction_gap: None,
resource: *resource,
token_mint: *mint_keypair,
winners: WinnerLimit::Capped(max_winners),
price_floor: PriceFloor::None([0u8; 32]),
gap_tick_size_percentage: Some(0),
tick_size: Some(0),
},
)],
Some(&payer.pubkey()),
&[payer],
*recent_blockhash,
);
let transaction: Transaction;
if instant_sale_price.is_some() {
transaction = Transaction::new_signed_with_payer(
&[instruction::create_auction_instruction_v2(
*program_id,
payer.pubkey(),
CreateAuctionArgsV2 {
authority: payer.pubkey(),
end_auction_at: None,
end_auction_gap: None,
resource: *resource,
token_mint: *mint_keypair,
winners: WinnerLimit::Capped(max_winners),
price_floor,
gap_tick_size_percentage,
tick_size,
name: string_to_array(name)?,
instant_sale_price,
},
)],
Some(&payer.pubkey()),
&[payer],
*recent_blockhash,
);
} else {
transaction = Transaction::new_signed_with_payer(
&[instruction::create_auction_instruction(
*program_id,
payer.pubkey(),
CreateAuctionArgs {
authority: payer.pubkey(),
end_auction_at: None,
end_auction_gap: None,
resource: *resource,
token_mint: *mint_keypair,
winners: WinnerLimit::Capped(max_winners),
price_floor,
gap_tick_size_percentage,
tick_size,
},
)],
Some(&payer.pubkey()),
&[payer],
*recent_blockhash,
);
}
banks_client.process_transaction(transaction).await?;
Ok(())
}
@ -319,8 +360,8 @@ pub async fn claim_bid(
let transaction = Transaction::new_signed_with_payer(
&[instruction::claim_bid_instruction(
*program_id,
authority.pubkey(),
*seller,
authority.pubkey(),
bidder.pubkey(),
bidder_spl_account.pubkey(),
*mint,

View File

@ -1,7 +1,7 @@
#![allow(warnings)]
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::borsh::try_from_slice_unchecked;
use solana_program::{borsh::try_from_slice_unchecked, instruction::InstructionError};
use solana_program_test::*;
use solana_sdk::program_pack::Pack;
use solana_sdk::{
@ -11,10 +11,11 @@ use solana_sdk::{
pubkey::Pubkey,
signature::{Keypair, Signer},
system_instruction, system_program,
transaction::Transaction,
transaction::{Transaction, TransactionError},
transport::TransportError,
};
use spl_auction::{
errors::AuctionError,
instruction,
processor::{
process_instruction, AuctionData, AuctionState, Bid, BidState, BidderPot, CancelBidArgs,
@ -31,6 +32,10 @@ mod helpers;
async fn setup_auction(
start: bool,
max_winners: usize,
instant_sale: Option<u64>,
price_floor: PriceFloor,
gap_tick_size_percentage: Option<u8>,
tick_size: Option<u64>,
) -> (
Pubkey,
BanksClient,
@ -70,6 +75,11 @@ async fn setup_auction(
&resource,
&mint_keypair.pubkey(),
max_winners,
"Some name",
instant_sale,
price_floor,
gap_tick_size_percentage,
tick_size,
)
.await
.unwrap();
@ -182,7 +192,7 @@ enum Action {
Cancel(usize),
End,
}
/* Commenting out for now
#[cfg(feature = "test-bpf")]
#[tokio::test]
async fn test_correct_runs() {
@ -208,9 +218,9 @@ async fn test_correct_runs() {
Action::End,
],
max_winners: 3,
price_floor: PriceFloor::None,
price_floor: PriceFloor::None([0; 32]),
seller_collects: 9000,
expect: vec![(1, 2000), (2, 3000), (3, 4000)],
expect: vec![(3, 4000), (2, 3000), (1, 2000)],
},
// A single bidder should be able to cancel and rebid lower.
Test {
@ -222,7 +232,7 @@ async fn test_correct_runs() {
],
expect: vec![(0, 4000)],
max_winners: 3,
price_floor: PriceFloor::None,
price_floor: PriceFloor::None([0; 32]),
seller_collects: 4000,
},
// The top bidder when cancelling should allow room for lower bidders.
@ -237,9 +247,9 @@ async fn test_correct_runs() {
Action::Cancel(0),
Action::End,
],
expect: vec![(2, 5500), (1, 6000), (3, 7000)],
expect: vec![(3, 7000), (1, 6000), (2, 5500)],
max_winners: 3,
price_floor: PriceFloor::None,
price_floor: PriceFloor::None([0; 32]),
seller_collects: 18500,
},
// An auction where everyone cancels should still succeed, with no winners.
@ -255,7 +265,7 @@ async fn test_correct_runs() {
],
expect: vec![],
max_winners: 3,
price_floor: PriceFloor::None,
price_floor: PriceFloor::None([0; 32]),
seller_collects: 0,
},
// An auction where no one bids should still succeed.
@ -263,7 +273,7 @@ async fn test_correct_runs() {
actions: vec![Action::End],
expect: vec![],
max_winners: 3,
price_floor: PriceFloor::None,
price_floor: PriceFloor::None([0; 32]),
seller_collects: 0,
},
];
@ -280,7 +290,15 @@ async fn test_correct_runs() {
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(true, strategy.max_winners).await;
) = setup_auction(
true,
strategy.max_winners,
None,
strategy.price_floor.clone(),
Some(0),
None,
)
.await;
// Interpret test actions one by one.
for action in strategy.actions.iter() {
@ -417,22 +435,21 @@ async fn test_correct_runs() {
BidState::EnglishAuction { ref bids, .. } => {
// Zip internal bid state with the expected indices this strategy expects winners
// to result in.
let results: Vec<(_, _)> = strategy.expect.iter().zip(bids).collect();
let results: Vec<(_, _)> = strategy.expect.iter().zip(bids.iter().rev()).collect();
for (index, bid) in results.iter() {
let bidder = &bidders[index.0];
let amount = index.1;
// Winners should match the keypair indices we expected.
// bid.0 is the pubkey.
// bidder.2 is the derived potkey we expect Bid.0 to be.
assert_eq!(bid.0, bidder.2);
assert_eq!(bid.0, bidder.0.pubkey());
// Must have bid the amount we expected.
// bid.1 is the amount.
assert_eq!(bid.1, amount);
}
// If the auction has ended, attempt to claim back SPL tokens into a new account.
if auction.ended(0) {
if auction.ended(0).unwrap() {
let collection = Keypair::new();
// Generate Collection Pot.
@ -610,49 +627,40 @@ async fn test_incorrect_runs() {
actions: Vec<Action>,
max_winners: usize,
price_floor: PriceFloor,
gap_tick_size_percentage: Option<u8>,
tick_size: Option<u64>,
}
// A list of auction runs that should succeed. At the end of the run the winning bid state
// should match the expected result.
let strategies = [
Test {
actions: vec![Action::Cancel(0), Action::End],
max_winners: 3,
price_floor: PriceFloor::None,
},
// Cancel a non-existing bid.
// Bidding less than the top bidder should fail.
Test {
actions: vec![
Action::Bid(0, 5000),
Action::Bid(1, 6000),
Action::Bid(2, 5500),
Action::Bid(0, 1000),
Action::Bid(1, 2000),
Action::Bid(2, 3000),
Action::Bid(3, 4000),
Action::Bid(3, 4000),
Action::End,
],
actions: vec![Action::Cancel(0)],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
gap_tick_size_percentage: Some(0),
tick_size: None,
},
// Bidding less than any bidder should fail.
// Bidding not a multiple of tick size should fail.
Test {
actions: vec![
Action::Bid(0, 5000),
Action::Bid(0, 3000),
Action::Bid(1, 6000),
Action::Bid(2, 1000),
Action::End,
],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
gap_tick_size_percentage: Some(0),
tick_size: Some(3),
},
// Bidding after an auction has been explicitly ended should fail.
Test {
actions: vec![Action::Bid(0, 5000), Action::End, Action::Bid(1, 6000)],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
gap_tick_size_percentage: Some(5),
tick_size: None,
},
];
@ -668,29 +676,456 @@ async fn test_incorrect_runs() {
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(true, strategy.max_winners).await;
) = setup_auction(
true,
strategy.max_winners,
None,
strategy.price_floor.clone(),
strategy.gap_tick_size_percentage,
strategy.tick_size,
)
.await;
let mut failed = false;
for action in strategy.actions.iter() {
failed = failed
|| handle_failing_action(
&mut banks_client,
&recent_blockhash,
&program_id,
&bidders,
&mint,
&payer,
&resource,
&auction_pubkey,
action,
)
.await
.is_err();
failed = handle_failing_action(
&mut banks_client,
&recent_blockhash,
&program_id,
&bidders,
&mint,
&payer,
&resource,
&auction_pubkey,
action,
)
.await
.is_err();
}
// Expect to fail.
assert!(failed);
}
}
*/
#[cfg(feature = "test-bpf")]
#[tokio::test]
async fn test_place_instant_sale_bid() {
let instant_sale_price = 5000;
let bid_price = 6000;
let (
program_id,
mut banks_client,
bidders,
payer,
resource,
mint,
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(
true,
1,
Some(instant_sale_price),
PriceFloor::None([0; 32]),
Some(0),
None,
)
.await;
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[0].0.pubkey()).await,
helpers::get_token_balance(&mut banks_client, &bidders[0].1.pubkey()).await,
);
let transfer_authority = Keypair::new();
helpers::approve(
&mut banks_client,
&recent_blockhash,
&payer,
&transfer_authority.pubkey(),
&bidders[0].0,
bid_price,
)
.await
.expect("approve");
// Make bid with price above instant_sale_price to check if it reduce amount
helpers::place_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[0].0,
&bidders[0].1,
&transfer_authority,
&resource,
&mint,
bid_price,
)
.await
.expect("place_bid");
let post_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[0].0.pubkey()).await,
helpers::get_token_balance(&mut banks_client, &bidders[0].1.pubkey()).await,
);
assert_eq!(post_balance.0, pre_balance.0 - instant_sale_price);
assert_eq!(post_balance.1, pre_balance.1 + instant_sale_price);
}
#[cfg(feature = "test-bpf")]
#[tokio::test]
async fn test_all_bids_are_taken_by_instant_sale_price() {
// Local wrapper around a small test description described by actions.
struct Test {
actions: Vec<Action>,
expect: Vec<(usize, u64)>,
max_winners: usize,
price_floor: PriceFloor,
seller_collects: u64,
instant_sale_price: Option<u64>,
}
let strategy = Test {
actions: vec![
Action::Bid(0, 2000),
Action::Bid(1, 3000),
Action::Bid(2, 3000),
Action::Bid(3, 3000),
],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
seller_collects: 9000,
expect: vec![(1, 3000), (2, 3000), (3, 3000)],
instant_sale_price: Some(3000),
};
let (
program_id,
mut banks_client,
bidders,
payer,
resource,
mint,
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(
true,
strategy.max_winners,
strategy.instant_sale_price,
strategy.price_floor,
Some(0),
None,
)
.await;
// Interpret test actions one by one.
for action in strategy.actions.iter() {
println!("Strategy: {} Step {:?}", strategy.actions.len(), action);
match *action {
Action::Bid(bidder, amount) => {
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
let transfer_authority = Keypair::new();
helpers::approve(
&mut banks_client,
&recent_blockhash,
&payer,
&transfer_authority.pubkey(),
&bidders[bidder].0,
amount,
)
.await
.expect("approve");
helpers::place_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[bidder].0,
&bidders[bidder].1,
&transfer_authority,
&resource,
&mint,
amount,
)
.await
.expect("place_bid");
let post_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
assert_eq!(post_balance.0, pre_balance.0 - amount);
assert_eq!(post_balance.1, pre_balance.1 + amount);
}
_ => {}
}
}
let auction: AuctionData = try_from_slice_unchecked(
&banks_client
.get_account(auction_pubkey)
.await
.expect("get_account")
.expect("account not found")
.data,
)
.unwrap();
match auction.bid_state {
BidState::EnglishAuction { ref bids, .. } => {
// Zip internal bid state with the expected indices this strategy expects winners
// to result in.
let results: Vec<(_, _)> = strategy.expect.iter().zip(bids.iter().rev()).collect();
for (index, bid) in results.iter() {
let bidder = &bidders[index.0];
let amount = index.1;
// Winners should match the keypair indices we expected.
// bid.0 is the pubkey.
assert_eq!(bid.0, bidder.0.pubkey());
// Must have bid the amount we expected.
// bid.1 is the amount.
assert_eq!(bid.1, amount);
}
}
_ => {}
}
assert_eq!(auction.state, AuctionState::Ended);
}
#[cfg(feature = "test-bpf")]
#[tokio::test]
async fn test_claim_bid_with_instant_sale_price() {
let instant_sale_price = 5000;
let (
program_id,
mut banks_client,
bidders,
payer,
resource,
mint,
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(
true,
5,
Some(instant_sale_price),
PriceFloor::None([0; 32]),
Some(0),
None,
)
.await;
let transfer_authority = Keypair::new();
helpers::approve(
&mut banks_client,
&recent_blockhash,
&payer,
&transfer_authority.pubkey(),
&bidders[0].0,
instant_sale_price,
)
.await
.expect("approve");
// Make bid with price above instant_sale_price to check if it reduce amount
helpers::place_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[0].0,
&bidders[0].1,
&transfer_authority,
&resource,
&mint,
instant_sale_price,
)
.await
.expect("place_bid");
let collection = Keypair::new();
// Generate Collection Pot.
helpers::create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&collection,
&mint,
&payer.pubkey(),
)
.await
.unwrap();
helpers::claim_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&payer,
&bidders[0].0,
&bidders[0].1,
&collection.pubkey(),
&resource,
&mint,
)
.await
.unwrap();
// Bid pot should be empty
let balance = helpers::get_token_balance(&mut banks_client, &bidders[0].1.pubkey()).await;
assert_eq!(balance, 0);
let balance = helpers::get_token_balance(&mut banks_client, &collection.pubkey()).await;
assert_eq!(balance, instant_sale_price);
}
#[cfg(feature = "test-bpf")]
#[tokio::test]
async fn test_cancel_bid_with_instant_sale_price() {
// Local wrapper around a small test description described by actions.
struct Test {
actions: Vec<Action>,
max_winners: usize,
price_floor: PriceFloor,
instant_sale_price: Option<u64>,
}
let strategy = Test {
actions: vec![
Action::Bid(0, 2000),
Action::Bid(1, 3000),
Action::Cancel(1),
],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
instant_sale_price: Some(3000),
};
let (
program_id,
mut banks_client,
bidders,
payer,
resource,
mint,
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(
true,
strategy.max_winners,
strategy.instant_sale_price,
strategy.price_floor,
Some(0),
None,
)
.await;
// Interpret test actions one by one.
for action in strategy.actions.iter() {
println!("Strategy: {} Step {:?}", strategy.actions.len(), action);
match *action {
Action::Bid(bidder, amount) => {
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
let transfer_authority = Keypair::new();
helpers::approve(
&mut banks_client,
&recent_blockhash,
&payer,
&transfer_authority.pubkey(),
&bidders[bidder].0,
amount,
)
.await
.expect("approve");
helpers::place_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[bidder].0,
&bidders[bidder].1,
&transfer_authority,
&resource,
&mint,
amount,
)
.await
.expect("place_bid");
let post_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
assert_eq!(post_balance.0, pre_balance.0 - amount);
assert_eq!(post_balance.1, pre_balance.1 + amount);
}
Action::Cancel(bidder) => {
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
let err = helpers::cancel_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[bidder].0,
&bidders[bidder].1,
&resource,
&mint,
)
.await
.unwrap_err()
.unwrap();
assert_eq!(
err,
TransactionError::InstructionError(
0,
InstructionError::Custom(AuctionError::InvalidState as u32)
)
);
}
_ => {}
}
}
}

View File

@ -66,6 +66,12 @@ pub struct InitAuctionManagerV2Args {
// validation and have a failed auction.
pub max_ranges: u64,
}
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct EndAuctionArgs {
/// If the auction was blinded, a revealing price must be specified to release the auction
/// winnings.
pub reveal: Option<(u64, u64)>,
}
/// Instructions supported by the Fraction program.
#[derive(BorshSerialize, BorshDeserialize, Clone)]
@ -80,7 +86,7 @@ pub enum MetaplexInstruction {
/// 4. `[signer]` Payer
/// 5. `[]` Accept payment account of same token mint as the auction for taking payment for open editions, owner should be auction manager key
/// 6. `[]` Store that this auction manager will belong to
/// 7. `[]` System sysvar
/// 7. `[]` System sysvar
/// 8. `[]` Rent sysvar
DeprecatedInitAuctionManagerV1(AuctionManagerSettingsV1),
@ -147,7 +153,8 @@ pub enum MetaplexInstruction {
/// relative to token metadata program (if Printing type of WinningConfig)
/// 20. `[]` Safety deposit config pda of ['metaplex', program id, auction manager, safety deposit]
/// This account will only get used AND BE REQUIRED in the event this is an AuctionManagerV2
RedeemBid,
/// 21. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
DeprecatedRedeemBid,
/// Note: This requires that auction manager be in a Running state.
///
@ -185,6 +192,7 @@ pub enum MetaplexInstruction {
/// but please note that this is a PDA relative to the Token Vault program, with the 'vault' prefix
/// 20. `[]` Safety deposit config pda of ['metaplex', program id, auction manager, safety deposit]
/// This account will only get used AND BE REQUIRED in the event this is an AuctionManagerV2
/// 21. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
RedeemFullRightsTransferBid,
/// Note: This requires that auction manager be in a Running state.
@ -223,6 +231,7 @@ pub enum MetaplexInstruction {
/// 18. `[writable]` The accept payment account for the auction manager
/// 19. `[writable]` The token account you will potentially pay for the open edition bid with if necessary
/// 20. `[writable]` Participation NFT printing holding account (present on participation_state)
/// 21. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
DeprecatedRedeemParticipationBid,
/// If the auction manager is in Validated state, it can invoke the start command via calling this command here.
@ -439,6 +448,7 @@ pub enum MetaplexInstruction {
/// where edition_number is NOT the edition number you pass in args but actually edition_number = floor(edition/EDITION_MARKER_BIT_SIZE). PDA is relative to token metadata.
/// 23. `[signer]` Mint authority of new mint - THIS WILL TRANSFER AUTHORITY AWAY FROM THIS KEY
/// 24. `[]` Metadata account of token in vault
/// 25. `[]` Auction extended (pda relative to auction of ['auction', program id, vault key, 'extended'])
RedeemPrintingV2Bid(RedeemPrintingV2BidArgs),
/// Permissionless call to redeem the master edition in a given safety deposit for a PrintingV2 winning config to the
@ -610,6 +620,16 @@ pub enum MetaplexInstruction {
/// 27. `[]` Metadata account of token in vault
// 28. `[]` Auction data extended - pda of ['auction', auction program id, vault key, 'extended'] relative to auction program
RedeemParticipationBidV3(RedeemParticipationBidV3Args),
/// Ends an auction, regardless of end timing conditions.
///
/// 0. `[writable]` Auction manager
/// 1. `[writable]` Auction
/// 2. `[]` Auction extended data account (pda relative to auction of ['auction', program id, vault key, 'extended']).
/// 3. `[signer]` Auction manager authority
/// 4. `[]` Store key
/// 5. `[]` Auction program
/// 6. `[]` Clock sysvar
EndAuction(EndAuctionArgs),
}
/// Creates an DeprecatedInitAuctionManager instruction
@ -853,6 +873,7 @@ pub fn create_redeem_bid_instruction(
vault: Pubkey,
fraction_mint: Pubkey,
auction: Pubkey,
auction_extended: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
@ -880,6 +901,7 @@ pub fn create_redeem_bid_instruction(
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(transfer_authority, false),
AccountMeta::new_readonly(auction_extended, false),
],
data: MetaplexInstruction::RedeemBid.try_to_vec().unwrap(),
}
@ -897,6 +919,7 @@ pub fn create_redeem_full_rights_transfer_bid_instruction(
vault: Pubkey,
fraction_mint: Pubkey,
auction: Pubkey,
auction_extended: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
@ -928,6 +951,7 @@ pub fn create_redeem_full_rights_transfer_bid_instruction(
AccountMeta::new(master_metadata, false),
AccountMeta::new_readonly(new_metadata_authority, false),
AccountMeta::new_readonly(transfer_authority, false),
AccountMeta::new_readonly(auction_extended, false),
],
data: MetaplexInstruction::RedeemFullRightsTransferBid
.try_to_vec()
@ -947,6 +971,7 @@ pub fn create_deprecated_redeem_participation_bid_instruction(
vault: Pubkey,
fraction_mint: Pubkey,
auction: Pubkey,
auction_extended: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
@ -980,6 +1005,7 @@ pub fn create_deprecated_redeem_participation_bid_instruction(
AccountMeta::new(accept_payment, false),
AccountMeta::new(paying_token_account, false),
AccountMeta::new(printing_authorization_token_account, false),
AccountMeta::new_readonly(auction_extended, false),
],
data: MetaplexInstruction::DeprecatedRedeemParticipationBid
.try_to_vec()
@ -1038,6 +1064,7 @@ pub fn create_set_store_instruction(
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_deprecated_populate_participation_printing_account_instruction(
program_id: Pubkey,
safety_deposit_token_store: Pubkey,
@ -1122,6 +1149,7 @@ pub fn create_redeem_printing_v2_bid_instruction(
safety_deposit_box: Pubkey,
vault: Pubkey,
auction: Pubkey,
auction_extended: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
@ -1224,6 +1252,7 @@ pub fn create_redeem_printing_v2_bid_instruction(
AccountMeta::new(edition_mark_pda, false),
AccountMeta::new_readonly(new_mint_authority, true),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(auction_extended, false),
],
data: MetaplexInstruction::RedeemPrintingV2Bid(RedeemPrintingV2BidArgs {
edition_offset,
@ -1312,6 +1341,7 @@ pub fn create_redeem_participation_bid_v3_instruction(
safety_deposit_box: Pubkey,
vault: Pubkey,
auction: Pubkey,
auction_extended: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
@ -1432,6 +1462,7 @@ pub fn create_redeem_participation_bid_v3_instruction(
AccountMeta::new_readonly(new_mint_authority, true),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(extended, false),
AccountMeta::new_readonly(auction_extended, false),
],
data: MetaplexInstruction::RedeemParticipationBidV3(RedeemParticipationBidV3Args {
win_index,
@ -1440,3 +1471,31 @@ pub fn create_redeem_participation_bid_v3_instruction(
.unwrap(),
}
}
/// Creates an EndAuction instruction
#[allow(clippy::too_many_arguments)]
pub fn create_end_auction_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
auction: Pubkey,
auction_data_extended: Pubkey,
auction_manager_authority: Pubkey,
store: Pubkey,
end_auction_args: EndAuctionArgs,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_manager, false),
AccountMeta::new(auction, false),
AccountMeta::new_readonly(auction_data_extended, false),
AccountMeta::new_readonly(auction_manager_authority, true),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(spl_auction::id(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
],
data: MetaplexInstruction::EndAuction(end_auction_args)
.try_to_vec()
.unwrap(),
}
}

View File

@ -9,6 +9,7 @@ use {
deprecated_validate_safety_deposit_box_v1::process_deprecated_validate_safety_deposit_box_v1,
empty_payment_account::process_empty_payment_account,
init_auction_manager_v2::process_init_auction_manager_v2,
end_auction::process_end_auction,
redeem_bid::process_redeem_bid,
redeem_full_rights_transfer_bid::process_full_rights_transfer_bid,
redeem_participation_bid::process_redeem_participation_bid,
@ -30,6 +31,7 @@ pub mod deprecated_validate_participation;
pub mod deprecated_validate_safety_deposit_box_v1;
pub mod empty_payment_account;
pub mod init_auction_manager_v2;
pub mod end_auction;
pub mod redeem_bid;
pub mod redeem_full_rights_transfer_bid;
pub mod redeem_participation_bid;
@ -143,5 +145,9 @@ pub fn process_instruction<'a>(
msg!("Instruction: Redeem Participation Bid V3");
process_redeem_participation_bid(program_id, accounts, false, args.win_index)
}
MetaplexInstruction::EndAuction(args) => {
msg!("Instruction: End auction");
process_end_auction(program_id, accounts, args)
}
}
}

View File

@ -20,6 +20,7 @@ use {
pub fn issue_claim_bid<'a>(
auction_program: AccountInfo<'a>,
auction: AccountInfo<'a>,
auction_extended: AccountInfo<'a>,
accept_payment: AccountInfo<'a>,
authority: AccountInfo<'a>,
bidder: AccountInfo<'a>,
@ -45,6 +46,7 @@ pub fn issue_claim_bid<'a>(
auction_program,
authority,
auction,
auction_extended,
clock,
token_mint,
bidder,
@ -66,6 +68,7 @@ pub fn process_claim_bid(program_id: &Pubkey, accounts: &[AccountInfo]) -> Progr
let bidder_pot_info = next_account_info(account_info_iter)?;
let mut auction_manager_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let auction_extended_info = next_account_info(account_info_iter)?;
let bidder_info = next_account_info(account_info_iter)?;
let token_mint_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
@ -80,6 +83,7 @@ pub fn process_claim_bid(program_id: &Pubkey, accounts: &[AccountInfo]) -> Progr
let token_pot_info = BidderPot::from_account_info(bidder_pot_info)?;
assert_owned_by(auction_info, &store.auction_program)?;
assert_owned_by(auction_extended_info, &store.auction_program)?;
assert_owned_by(auction_manager_info, program_id)?;
assert_owned_by(accept_payment_info, &spl_token::id())?;
assert_owned_by(bidder_pot_token_info, &spl_token::id())?;
@ -138,6 +142,7 @@ pub fn process_claim_bid(program_id: &Pubkey, accounts: &[AccountInfo]) -> Progr
issue_claim_bid(
auction_program_info.clone(),
auction_info.clone(),
auction_extended_info.clone(),
accept_payment_info.clone(),
auction_manager_info.clone(),
bidder_info.clone(),

View File

@ -0,0 +1,124 @@
use {
crate::{
error::MetaplexError,
instruction::EndAuctionArgs as MetaplexEndAuctionArgs,
state::{AuctionManager, AuctionManagerStatus, Store, PREFIX},
utils::{assert_authority_correct, assert_owned_by},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
},
spl_auction::{
instruction::{end_auction_instruction, EndAuctionArgs},
processor::{AuctionData, AuctionDataExtended, BidState},
},
};
pub fn issue_end_auction<'a>(
auction_program: AccountInfo<'a>,
authority: AccountInfo<'a>,
auction: AccountInfo<'a>,
clock: AccountInfo<'a>,
vault: Pubkey,
reveal: Option<(u64, u64)>,
signer_seeds: &[&[u8]],
) -> ProgramResult {
invoke_signed(
&end_auction_instruction(
*auction_program.key,
*authority.key,
EndAuctionArgs {
resource: vault,
reveal,
},
),
&[auction_program, authority, auction, clock],
&[&signer_seeds],
)?;
Ok(())
}
pub fn process_end_auction(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: MetaplexEndAuctionArgs,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let auction_manager_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let auction_data_extended_info = next_account_info(account_info_iter)?;
let authority_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let auction_program_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
let auction = AuctionData::from_account_info(auction_info)?;
let auction_data_extended = AuctionDataExtended::from_account_info(auction_data_extended_info)?;
let store = Store::from_account_info(store_info)?;
if auction.authority != *auction_manager_info.key {
return Err(MetaplexError::AuctionAuthorityMismatch.into());
}
assert_authority_correct(&auction_manager, authority_info)?;
assert_owned_by(auction_info, &store.auction_program)?;
assert_owned_by(auction_manager_info, program_id)?;
assert_owned_by(store_info, program_id)?;
if auction_manager.store != *store_info.key {
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
}
if auction_manager.auction != *auction_info.key {
return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
}
if store.auction_program != *auction_program_info.key {
return Err(MetaplexError::AuctionManagerAuctionProgramMismatch.into());
}
if auction_manager.state.status != AuctionManagerStatus::Validated {
return Err(MetaplexError::AuctionManagerMustBeValidated.into());
}
let seeds = &[PREFIX.as_bytes(), &auction_manager.auction.as_ref()];
let (_, bump_seed) = Pubkey::find_program_address(seeds, &program_id);
let authority_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[bump_seed],
];
issue_end_auction(
auction_program_info.clone(),
auction_manager_info.clone(),
auction_info.clone(),
clock_info.clone(),
auction_manager.vault,
args.reveal,
authority_seeds,
)?;
if auction_data_extended.instant_sale_price.is_some() {
match auction.bid_state {
BidState::EnglishAuction { .. } => {
auction_manager.state.status = AuctionManagerStatus::Disbursing;
}
BidState::OpenEdition { .. } => {
auction_manager.state.status = AuctionManagerStatus::Finished;
}
}
} else {
auction_manager.state.status = AuctionManagerStatus::Disbursing;
}
auction_manager.serialize(&mut *auction_manager_info.data.borrow_mut())?;
Ok(())
}

View File

@ -152,6 +152,7 @@ pub fn process_redeem_bid<'a>(
let transfer_authority_info = next_account_info(account_info_iter)?;
let safety_deposit_config_info = next_account_info(account_info_iter).ok();
let auction_extended_info = next_account_info(account_info_iter).ok();
let CommonRedeemReturn {
auction_manager,
@ -169,6 +170,7 @@ pub fn process_redeem_bid<'a>(
safety_deposit_info,
vault_info,
auction_info,
auction_extended_info,
bidder_metadata_info,
bidder_info,
token_program_info,
@ -226,7 +228,10 @@ pub fn process_redeem_bid<'a>(
Some(val) => val,
None => return Err(ProgramError::NotEnoughAccountKeys)
};
let reservation_list_info = next_account_info(account_info_iter)?;
let reservation_list_info = match auction_extended_info {
Some(val) => val,
None => return Err(ProgramError::NotEnoughAccountKeys)
};
reserve_list_if_needed(
token_metadata_program_info.key,

View File

@ -48,6 +48,7 @@ pub fn process_full_rights_transfer_bid<'a>(
let transfer_authority_info = next_account_info(account_info_iter)?;
let safety_deposit_config_info = next_account_info(account_info_iter).ok();
let auction_extended_info = next_account_info(account_info_iter).ok();
let CommonRedeemReturn {
auction_manager,
@ -65,6 +66,7 @@ pub fn process_full_rights_transfer_bid<'a>(
safety_deposit_info,
vault_info,
auction_info,
auction_extended_info,
bidder_metadata_info,
bidder_info,
token_program_info,

View File

@ -263,11 +263,13 @@ pub fn process_redeem_participation_bid<'a>(
let transfer_authority_info = next_account_info(account_info_iter)?;
let accept_payment_info = next_account_info(account_info_iter)?;
let bidder_token_account_info = next_account_info(account_info_iter)?;
let auction_extended_info: Option<&AccountInfo>;
if legacy {
legacy_accounts = Some(LegacyAccounts {
participation_printing_holding_account_info: next_account_info(account_info_iter)?,
});
auction_extended_info = None;
} else {
v2_accounts = Some(V2Accounts {
prize_tracking_ticket_info: next_account_info(account_info_iter)?,
@ -279,7 +281,8 @@ pub fn process_redeem_participation_bid<'a>(
mint_authority_info: next_account_info(account_info_iter)?,
metadata_account_info: next_account_info(account_info_iter)?,
auction_extended_info: next_account_info(account_info_iter)?,
})
});
auction_extended_info = Some(v2_accounts.unwrap().auction_extended_info)
}
let CommonRedeemReturn {
@ -298,6 +301,7 @@ pub fn process_redeem_participation_bid<'a>(
safety_deposit_info,
vault_info,
auction_info,
auction_extended_info,
bidder_metadata_info,
bidder_info,
token_program_info,

View File

@ -200,6 +200,7 @@ pub fn process_redeem_printing_v2_bid<'a>(
let edition_marker_info = next_account_info(account_info_iter)?;
let mint_authority_info = next_account_info(account_info_iter)?;
let metadata_account_info = next_account_info(account_info_iter)?;
let auction_extended_info = next_account_info(account_info_iter).ok();
let new_edition_account_amount = get_amount_from_token_account(new_edition_token_account_info)?;
@ -230,6 +231,7 @@ pub fn process_redeem_printing_v2_bid<'a>(
safety_deposit_info,
vault_info,
auction_info,
auction_extended_info,
bidder_metadata_info,
bidder_info,
token_program_info,

View File

@ -1,3 +1,4 @@
use {
crate::{
deprecated_state::AuctionManagerV1, error::MetaplexError, utils::try_from_slice_checked,

View File

@ -1,9 +1,10 @@
use {
crate::{
error::MetaplexError,
state::{
get_auction_manager, AuctionManager, AuctionManagerStatus, BidRedemptionTicket, Key,
OriginalAuthorityLookup, Store, WhitelistedCreator, PREFIX,
OriginalAuthorityLookup, Store, WhitelistedCreator, PREFIX
},
},
arrayref::array_ref,
@ -23,7 +24,7 @@ use {
},
spl_auction::{
instruction::end_auction_instruction,
processor::{end_auction::EndAuctionArgs, AuctionData, AuctionState},
processor::{end_auction::EndAuctionArgs, AuctionData, AuctionDataExtended, AuctionState, BidderMetadata},
},
spl_token::instruction::{set_authority, AuthorityType},
spl_token_metadata::{
@ -175,6 +176,37 @@ pub fn assert_authority_correct(
Ok(())
}
pub fn assert_auction_is_ended_or_valid_instant_sale(
auction_info: &AccountInfo,
auction_extended_info: Option<&AccountInfo>,
bidder_metadata_info: &AccountInfo,
win_index: Option<usize>,
) -> ProgramResult {
if AuctionData::get_state(auction_info)? == AuctionState::Ended {
return Ok(());
}
let instant_sale_price = auction_extended_info
.and_then(|info| AuctionDataExtended::get_instant_sale_price(&info.data.borrow()));
match instant_sale_price {
Some(instant_sale_price) => {
let winner_bid_price = win_index
.and_then(|i| AuctionData::get_winner_bid_amount_at(auction_info, i))
// Possible case in an open auction
.unwrap_or(BidderMetadata::from_account_info(bidder_metadata_info)?.last_bid);
if winner_bid_price < instant_sale_price {
return Err(MetaplexError::AuctionHasNotEnded.into());
}
}
None => return Err(MetaplexError::AuctionHasNotEnded.into()),
}
Ok(())
}
/// Create account almost from scratch, lifted from
/// https://github.com/solana-labs/solana-program-library/blob/7d4873c61721aca25464d42cc5ef651a7923ca79/associated-token-account/program/src/processor.rs#L51-L98
#[inline(always)]
@ -364,6 +396,7 @@ pub struct CommonRedeemCheckArgs<'a> {
pub safety_deposit_info: &'a AccountInfo<'a>,
pub vault_info: &'a AccountInfo<'a>,
pub auction_info: &'a AccountInfo<'a>,
pub auction_extended_info: Option<&'a AccountInfo<'a>>,
pub bidder_metadata_info: &'a AccountInfo<'a>,
pub bidder_info: &'a AccountInfo<'a>,
pub token_program_info: &'a AccountInfo<'a>,
@ -486,6 +519,7 @@ pub fn common_redeem_checks(
safety_deposit_info,
vault_info,
auction_info,
auction_extended_info,
bidder_metadata_info,
bidder_info,
token_program_info,
@ -627,9 +661,12 @@ pub fn common_redeem_checks(
return Err(MetaplexError::AuctionManagerTokenMetadataProgramMismatch.into());
}
if AuctionData::get_state(auction_info)? != AuctionState::Ended {
return Err(MetaplexError::AuctionHasNotEnded.into());
}
assert_auction_is_ended_or_valid_instant_sale(
auction_info,
auction_extended_info,
bidder_metadata_info,
win_index,
)?;
// No-op if already set.
auction_manager.set_status(AuctionManagerStatus::Disbursing);