├── .gitignore ├── .prettierignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── DISCLAIMER.md ├── LICENSE ├── README.md ├── migrations └── deploy.ts ├── package-lock.json ├── package.json ├── programs └── launchpad │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ ├── error.rs │ ├── instructions.rs │ ├── instructions │ ├── add_tokens.rs │ ├── cancel_bid.rs │ ├── delete_auction.rs │ ├── disable_auction.rs │ ├── enable_auction.rs │ ├── get_auction_amount.rs │ ├── get_auction_price.rs │ ├── init.rs │ ├── init_auction.rs │ ├── init_custody.rs │ ├── place_bid.rs │ ├── remove_tokens.rs │ ├── set_admin_signers.rs │ ├── set_fees.rs │ ├── set_oracle_config.rs │ ├── set_permissions.rs │ ├── set_test_oracle_price.rs │ ├── set_test_time.rs │ ├── test_init.rs │ ├── update_auction.rs │ ├── whitelist_add.rs │ ├── whitelist_remove.rs │ ├── withdraw_fees.rs │ └── withdraw_funds.rs │ ├── lib.rs │ ├── math.rs │ ├── state.rs │ └── state │ ├── auction.rs │ ├── bid.rs │ ├── custody.rs │ ├── launchpad.rs │ ├── multisig.rs │ ├── oracle.rs │ └── seller_balance.rs ├── tests ├── basic.ts └── launchpad_tester.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | node_modules 6 | dist 7 | build 8 | test-ledger 9 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | [programs.localnet] 4 | launchpad = "LPD1BCWvd499Rk7aG5zG8uieUTTqba1JaYkUpXjUN9q" 5 | [programs.devnet] 6 | launchpad = "LPD1BCWvd499Rk7aG5zG8uieUTTqba1JaYkUpXjUN9q" 7 | 8 | [registry] 9 | url = "https://anchor.projectserum.com" 10 | 11 | [provider] 12 | cluster = "localnet" 13 | wallet = "~/.config/solana/id.json" 14 | 15 | [scripts] 16 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | -------------------------------------------------------------------------------- /DISCLAIMER.md: -------------------------------------------------------------------------------- 1 | # Disclaimer 2 | 3 | All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the good faith efforts Solana Labs, Inc. and its affiliates ("SL"). It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment. 4 | Any content produced by SL or developer resources that SL provides have not been subject to audit and are for educational and inspiration purposes only. SL does not encourage, induce or sanction the deployment, integration or use of any such applications (including the code comprising the Solana blockchain protocol) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. This includes use of any such applications by the reader (a) in violation of export control or sanctions laws of the United States or any other applicable jurisdiction, (b) if the reader is located in or ordinarily resident in a country or territory subject to comprehensive sanctions administered by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the reader is or is working on behalf of a Specially Designated National (SDN) or a person subject to similar blocking or denied party prohibitions. 5 | The reader should be aware that U.S. export control and sanctions laws prohibit U.S. persons (and other persons that are subject to such laws) from transacting with persons in certain countries and territories or that are on the SDN list. As a project based primarily on open-source software, it is possible that such sanctioned persons may nevertheless bypass prohibitions, obtain the code comprising the Solana blockchain protocol (or other project code or applications) and deploy, integrate, or otherwise use it. Accordingly, there is a risk to individuals that other persons using the Solana blockchain protocol may be sanctioned persons and that transactions with such persons would be a violation of U.S. export controls and sanctions law. This risk applies to individuals, organizations, and other ecosystem participants that deploy, integrate, or use the Solana blockchain protocol code directly (e.g., as a node operator), and individuals that transact on the Solana blockchain through light clients, third party interfaces, and/or wallet software. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Solana Labs, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Launchpad 2 | 3 | ## Introduction 4 | 5 | Solana Launchpad is a trustless and decentralized token distribution platform. It can assist you in launching a new token or selling already existing tokens via flexible price discovery mechanisms. Launchpad program initiates the token sale process by locking tokens provided by a seller and then offering them at the dynamic price, set by one of the pricing models. 6 | 7 | Main Goals: 8 | 9 | - Maximize funds raised with token sales. 10 | - Provide transparency and trust to the sale process. 11 | - Provide flexibility and full control to the sellers. 12 | 13 | ## Contributing 14 | 15 | Contributions are very welcome. Please refer to the [Contributing](https://github.com/solana-labs/solana/blob/master/CONTRIBUTING.md) guidelines for more information. 16 | 17 | ## License 18 | 19 | Solana Launchpad is a part of the Solana Program Library, which is released under this [License](https://github.com/solana-labs/solana-program-library/blob/master/LICENSE). 20 | 21 | ## Disclaimer 22 | 23 | By accessing or using Solana Launchpad or any of its components, you accept and agree with the [Disclaimer](DISCLAIMER.md). 24 | -------------------------------------------------------------------------------- /migrations/deploy.ts: -------------------------------------------------------------------------------- 1 | // Migrations are an early feature. Currently, they're nothing more than this 2 | // single deploy script that's invoked from the CLI, injecting a provider 3 | // configured from the workspace's Anchor.toml. 4 | 5 | const anchor = require("@project-serum/anchor"); 6 | 7 | module.exports = async function (provider) { 8 | // Configure client to use the provider. 9 | anchor.setProvider(provider); 10 | 11 | // Add your deploy script here. 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" 5 | }, 6 | "dependencies": { 7 | "@project-serum/anchor": "^0.24.2", 8 | "@solana/web3.js": "^1.47.3", 9 | "@solana/spl-token": "^0.3.4", 10 | "ts-node": "^10.4.0" 11 | }, 12 | "devDependencies": { 13 | "chai": "^4.3.4", 14 | "mocha": "^9.0.3", 15 | "ts-mocha": "^10.0.0", 16 | "@types/bn.js": "^5.1.0", 17 | "@types/chai": "^4.3.0", 18 | "@types/mocha": "^9.0.0", 19 | "typescript": "^4.3.5", 20 | "prettier": "^2.6.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /programs/launchpad/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-launchpad" 3 | version = "0.1.0" 4 | description = "Solana Launchpad" 5 | authors = ["Solana Maintainers "] 6 | repository = "https://github.com/solana-labs/solana-program-library/launchpad" 7 | license = "Apache-2.0" 8 | homepage = "https://solana.com/" 9 | edition = "2021" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "lib"] 13 | name = "solana_launchpad" 14 | 15 | [features] 16 | no-entrypoint = [] 17 | no-idl = [] 18 | no-log-ix-name = [] 19 | cpi = ["no-entrypoint"] 20 | test = [] 21 | default = [] 22 | 23 | [profile.release] 24 | overflow-checks = true 25 | 26 | [dependencies] 27 | anchor-lang = {version = "0.25.0", features = ["init-if-needed"]} 28 | anchor-spl = "0.25.0" 29 | solana-program = "1.10.41" 30 | solana-address-lookup-table-program = "1.10.41" 31 | solana-security-txt = "1.0.2" 32 | pyth-sdk-solana = "0.6.1" 33 | ahash = "0.7.6" 34 | num-traits = "0.2.15" 35 | num = "0.4.0" -------------------------------------------------------------------------------- /programs/launchpad/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/launchpad/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use anchor_lang::prelude::*; 4 | 5 | #[error_code] 6 | pub enum LaunchpadError { 7 | #[msg("Account is not authorized to sign this instruction")] 8 | MultisigAccountNotAuthorized, 9 | #[msg("Account has already signed this instruction")] 10 | MultisigAlreadySigned, 11 | #[msg("This instruction has already been executed")] 12 | MultisigAlreadyExecuted, 13 | #[msg("Invalid launchpad config")] 14 | InvalidLaunchpadConfig, 15 | #[msg("Invalid custody config")] 16 | InvalidCustodyConfig, 17 | #[msg("Invalid auction config")] 18 | InvalidAuctionConfig, 19 | #[msg("Invalid pricing config")] 20 | InvalidPricingConfig, 21 | #[msg("Invalid token amount")] 22 | InvalidTokenAmount, 23 | #[msg("Too many remaining accounts")] 24 | TooManyAccountKeys, 25 | #[msg("Invalid bid account address")] 26 | InvalidBidAddress, 27 | #[msg("Invalid receiving account address")] 28 | InvalidReceivingAddress, 29 | #[msg("Invalid dispensing account address")] 30 | InvalidDispenserAddress, 31 | #[msg("Dispensing accounts should have the same decimals")] 32 | InvalidDispenserDecimals, 33 | #[msg("Invalid seller's balance address")] 34 | InvalidSellerBalanceAddress, 35 | #[msg("New auctions are not allowed at this time")] 36 | NewAuctionsNotAllowed, 37 | #[msg("Auction updates are not allowed at this time")] 38 | AuctionUpdatesNotAllowed, 39 | #[msg("Auction refills are not allowed at this time")] 40 | AuctionRefillsNotAllowed, 41 | #[msg("Auction pull-outs are not allowed at this time")] 42 | AuctionPullOutsNotAllowed, 43 | #[msg("Bids are not allowed at this time")] 44 | BidsNotAllowed, 45 | #[msg("Withdrawals are not allowed at this time")] 46 | WithdrawalsNotAllowed, 47 | #[msg("Instruction is not allowed in production")] 48 | InvalidEnvironment, 49 | #[msg("Auction hasn't started")] 50 | AuctionNotStarted, 51 | #[msg("Auction has been ended")] 52 | AuctionEnded, 53 | #[msg("Auction is empty")] 54 | AuctionEmpty, 55 | #[msg("Auction is not empty")] 56 | AuctionNotEmpty, 57 | #[msg("Auction is not updatable")] 58 | AuctionNotUpdatable, 59 | #[msg("Auction with fixed amount")] 60 | AuctionWithFixedAmount, 61 | #[msg("Auction is still in progress")] 62 | AuctionInProgress, 63 | #[msg("Overflow in arithmetic operation")] 64 | MathOverflow, 65 | #[msg("Unsupported price oracle")] 66 | UnsupportedOracle, 67 | #[msg("Invalid oracle account")] 68 | InvalidOracleAccount, 69 | #[msg("Invalid oracle state")] 70 | InvalidOracleState, 71 | #[msg("Stale oracle price")] 72 | StaleOraclePrice, 73 | #[msg("Invalid oracle price")] 74 | InvalidOraclePrice, 75 | #[msg("Insufficient amount available at the given price")] 76 | InsufficientAmount, 77 | #[msg("Bid amount is too large")] 78 | BidAmountTooLarge, 79 | #[msg("Bid price is too small")] 80 | BidPriceTooSmall, 81 | #[msg("Fill limit exceeded")] 82 | FillAmountLimit, 83 | #[msg("Unexpected price calculation error")] 84 | PriceCalcError, 85 | #[msg("This instruction must be all alone in the transaction")] 86 | MustBeSingleInstruction, 87 | } 88 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions.rs: -------------------------------------------------------------------------------- 1 | // admin instructions 2 | pub mod delete_auction; 3 | pub mod init; 4 | pub mod init_custody; 5 | pub mod set_admin_signers; 6 | pub mod set_fees; 7 | pub mod set_oracle_config; 8 | pub mod set_permissions; 9 | pub mod withdraw_fees; 10 | 11 | // test instructions 12 | pub mod set_test_oracle_price; 13 | pub mod set_test_time; 14 | pub mod test_init; 15 | 16 | // seller instructions 17 | pub mod add_tokens; 18 | pub mod disable_auction; 19 | pub mod enable_auction; 20 | pub mod init_auction; 21 | pub mod remove_tokens; 22 | pub mod update_auction; 23 | pub mod whitelist_add; 24 | pub mod whitelist_remove; 25 | pub mod withdraw_funds; 26 | 27 | // buyer instructions 28 | pub mod cancel_bid; 29 | pub mod get_auction_amount; 30 | pub mod get_auction_price; 31 | pub mod place_bid; 32 | 33 | // bring everything in scope 34 | pub use add_tokens::*; 35 | pub use cancel_bid::*; 36 | pub use delete_auction::*; 37 | pub use disable_auction::*; 38 | pub use enable_auction::*; 39 | pub use get_auction_amount::*; 40 | pub use get_auction_price::*; 41 | pub use init::*; 42 | pub use init_auction::*; 43 | pub use init_custody::*; 44 | pub use place_bid::*; 45 | pub use remove_tokens::*; 46 | pub use set_admin_signers::*; 47 | pub use set_fees::*; 48 | pub use set_oracle_config::*; 49 | pub use set_permissions::*; 50 | pub use set_test_oracle_price::*; 51 | pub use set_test_time::*; 52 | pub use test_init::*; 53 | pub use update_auction::*; 54 | pub use whitelist_add::*; 55 | pub use whitelist_remove::*; 56 | pub use withdraw_fees::*; 57 | pub use withdraw_funds::*; 58 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/add_tokens.rs: -------------------------------------------------------------------------------- 1 | //! AddTokens instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{auction::Auction, launchpad::Launchpad}, 7 | }, 8 | anchor_lang::prelude::*, 9 | anchor_spl::token::{Mint, Token, TokenAccount}, 10 | }; 11 | 12 | #[derive(Accounts)] 13 | pub struct AddTokens<'info> { 14 | #[account(mut)] 15 | pub owner: Signer<'info>, 16 | 17 | #[account( 18 | mut, 19 | constraint = funding_account.mint == dispensing_custody.mint, 20 | has_one = owner 21 | )] 22 | pub funding_account: Box>, 23 | 24 | /// CHECK: empty PDA, authority for token accounts 25 | #[account( 26 | seeds = [b"transfer_authority"], 27 | bump = launchpad.transfer_authority_bump 28 | )] 29 | pub transfer_authority: AccountInfo<'info>, 30 | 31 | #[account( 32 | seeds = [b"launchpad"], 33 | bump = launchpad.launchpad_bump 34 | )] 35 | pub launchpad: Box>, 36 | 37 | #[account( 38 | has_one = owner, 39 | seeds = [b"auction", 40 | auction.common.name.as_bytes()], 41 | bump = auction.bump 42 | )] 43 | pub auction: Box>, 44 | 45 | pub dispensing_custody_mint: Box>, 46 | 47 | #[account( 48 | init_if_needed, 49 | payer = owner, 50 | constraint = dispensing_custody_mint.key() == dispensing_custody.mint, 51 | token::mint = dispensing_custody_mint, 52 | token::authority = transfer_authority, 53 | seeds = [b"dispense", 54 | dispensing_custody_mint.key().as_ref(), 55 | auction.key().as_ref()], 56 | bump 57 | )] 58 | pub dispensing_custody: Box>, 59 | 60 | system_program: Program<'info, System>, 61 | token_program: Program<'info, Token>, 62 | rent: Sysvar<'info, Rent>, 63 | } 64 | 65 | #[derive(AnchorSerialize, AnchorDeserialize)] 66 | pub struct AddTokensParams { 67 | pub amount: u64, 68 | } 69 | 70 | pub fn add_tokens(ctx: Context, params: &AddTokensParams) -> Result<()> { 71 | if ctx 72 | .accounts 73 | .auction 74 | .is_started(ctx.accounts.auction.get_time()?, true) 75 | { 76 | require!( 77 | ctx.accounts.launchpad.permissions.allow_auction_refills, 78 | LaunchpadError::AuctionRefillsNotAllowed 79 | ); 80 | } 81 | require!( 82 | !ctx.accounts.auction.fixed_amount, 83 | LaunchpadError::AuctionWithFixedAmount 84 | ); 85 | 86 | ctx.accounts.launchpad.transfer_tokens( 87 | ctx.accounts.funding_account.to_account_info(), 88 | ctx.accounts.dispensing_custody.to_account_info(), 89 | ctx.accounts.owner.to_account_info(), 90 | ctx.accounts.token_program.to_account_info(), 91 | params.amount, 92 | )?; 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/cancel_bid.rs: -------------------------------------------------------------------------------- 1 | //! CancelBid instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{auction::Auction, bid::Bid}, 7 | }, 8 | anchor_lang::{prelude::*, AccountsClose}, 9 | }; 10 | 11 | #[derive(Accounts)] 12 | pub struct CancelBid<'info> { 13 | #[account(mut)] 14 | pub initializer: Signer<'info>, 15 | 16 | #[account( 17 | seeds = [b"auction", 18 | auction.common.name.as_bytes()], 19 | bump = auction.bump 20 | )] 21 | pub auction: Box>, 22 | 23 | #[account( 24 | mut, 25 | seeds = [b"bid", 26 | bid.owner.key().as_ref(), 27 | auction.key().as_ref()], 28 | bump = bid.bump 29 | )] 30 | pub bid: Box>, 31 | } 32 | 33 | #[derive(AnchorSerialize, AnchorDeserialize)] 34 | pub struct CancelBidParams {} 35 | 36 | pub fn cancel_bid(ctx: Context, _params: &CancelBidParams) -> Result<()> { 37 | require!( 38 | ctx.accounts 39 | .auction 40 | .is_ended(ctx.accounts.auction.get_time()?, true), 41 | LaunchpadError::AuctionInProgress 42 | ); 43 | 44 | let bid = ctx.accounts.bid.as_mut(); 45 | if (!bid.seller_initialized && ctx.accounts.initializer.key() == bid.owner) 46 | || (bid.seller_initialized && ctx.accounts.initializer.key() == ctx.accounts.auction.owner) 47 | { 48 | bid.close(ctx.accounts.initializer.to_account_info()) 49 | } else { 50 | Err(ProgramError::IllegalOwner.into()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/delete_auction.rs: -------------------------------------------------------------------------------- 1 | //! DeleteAuction instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | self, 8 | auction::Auction, 9 | launchpad::Launchpad, 10 | multisig::{AdminInstruction, Multisig}, 11 | }, 12 | }, 13 | anchor_lang::prelude::*, 14 | anchor_spl::token::{Token, TokenAccount}, 15 | }; 16 | 17 | #[derive(Accounts)] 18 | pub struct DeleteAuction<'info> { 19 | #[account()] 20 | pub admin: Signer<'info>, 21 | 22 | #[account( 23 | mut, 24 | seeds = [b"multisig"], 25 | bump = multisig.load()?.bump 26 | )] 27 | pub multisig: AccountLoader<'info, Multisig>, 28 | 29 | /// CHECK: empty PDA, authority for token accounts 30 | #[account( 31 | mut, 32 | seeds = [b"transfer_authority"], 33 | bump = launchpad.transfer_authority_bump 34 | )] 35 | pub transfer_authority: AccountInfo<'info>, 36 | 37 | #[account( 38 | seeds = [b"launchpad"], 39 | bump = launchpad.launchpad_bump 40 | )] 41 | pub launchpad: Box>, 42 | 43 | #[account( 44 | mut, 45 | seeds = [b"auction", 46 | auction.common.name.as_bytes()], 47 | bump = auction.bump, 48 | close = transfer_authority 49 | )] 50 | pub auction: Box>, 51 | 52 | token_program: Program<'info, Token>, 53 | // remaining accounts: 54 | // 1 to Auction::MAX_TOKENS dispensing custody addresses (write, unsigned) 55 | // with seeds = [b"dispense", mint.key().as_ref(), auction.key().as_ref()], 56 | } 57 | 58 | #[derive(AnchorSerialize, AnchorDeserialize)] 59 | pub struct DeleteAuctionParams {} 60 | 61 | pub fn delete_auction<'info>( 62 | ctx: Context<'_, '_, '_, 'info, DeleteAuction<'info>>, 63 | params: &DeleteAuctionParams, 64 | ) -> Result { 65 | if !cfg!(feature = "test") { 66 | return err!(LaunchpadError::InvalidEnvironment); 67 | } 68 | 69 | // validate signatures 70 | let mut multisig = ctx.accounts.multisig.load_mut()?; 71 | 72 | let signatures_left = multisig.sign_multisig( 73 | &ctx.accounts.admin, 74 | &Multisig::get_account_infos(&ctx)[1..], 75 | &Multisig::get_instruction_data(AdminInstruction::DeleteAuction, params)?, 76 | )?; 77 | if signatures_left > 0 { 78 | msg!( 79 | "Instruction has been signed but more signatures are required: {}", 80 | signatures_left 81 | ); 82 | return Ok(signatures_left); 83 | } 84 | 85 | let auction = &ctx.accounts.auction; 86 | if !ctx.remaining_accounts.is_empty() && auction.num_tokens > 0 { 87 | if ctx.remaining_accounts.len() > auction.num_tokens.into() { 88 | return err!(LaunchpadError::TooManyAccountKeys); 89 | } 90 | if ctx.remaining_accounts.len() < auction.num_tokens.into() { 91 | return Err(ProgramError::NotEnoughAccountKeys.into()); 92 | } 93 | let dispensers = state::load_accounts::( 94 | &ctx.remaining_accounts[..auction.num_tokens.into()], 95 | &Token::id(), 96 | )?; 97 | for (i, dispenser) in dispensers.iter().enumerate() { 98 | require_keys_eq!( 99 | dispenser.key(), 100 | auction.tokens[i].account, 101 | LaunchpadError::InvalidDispenserAddress 102 | ); 103 | if dispenser.amount > 0 { 104 | msg!("Non-empty dispensing account: {}", dispenser.key()); 105 | return err!(LaunchpadError::AuctionNotEmpty); 106 | } 107 | state::close_token_account( 108 | ctx.accounts.transfer_authority.to_account_info(), 109 | ctx.remaining_accounts[i].clone(), 110 | ctx.accounts.token_program.to_account_info(), 111 | ctx.accounts.transfer_authority.to_account_info(), 112 | &[&[ 113 | b"transfer_authority", 114 | &[ctx.accounts.launchpad.transfer_authority_bump], 115 | ]], 116 | )?; 117 | } 118 | } 119 | 120 | Ok(0) 121 | } 122 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/disable_auction.rs: -------------------------------------------------------------------------------- 1 | //! DisableAuction instruction handler 2 | 3 | use {crate::state::auction::Auction, anchor_lang::prelude::*}; 4 | 5 | #[derive(Accounts)] 6 | pub struct DisableAuction<'info> { 7 | #[account()] 8 | pub owner: Signer<'info>, 9 | 10 | #[account( 11 | mut, 12 | has_one = owner, 13 | seeds = [b"auction", 14 | auction.common.name.as_bytes()], 15 | bump = auction.bump 16 | )] 17 | pub auction: Box>, 18 | } 19 | 20 | #[derive(AnchorSerialize, AnchorDeserialize)] 21 | pub struct DisableAuctionParams {} 22 | 23 | pub fn disable_auction(ctx: Context, _params: &DisableAuctionParams) -> Result<()> { 24 | let auction = ctx.accounts.auction.as_mut(); 25 | auction.enabled = false; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/enable_auction.rs: -------------------------------------------------------------------------------- 1 | //! EnableAuction instruction handler 2 | 3 | use {crate::state::auction::Auction, anchor_lang::prelude::*}; 4 | 5 | #[derive(Accounts)] 6 | pub struct EnableAuction<'info> { 7 | #[account()] 8 | pub owner: Signer<'info>, 9 | 10 | #[account( 11 | mut, 12 | has_one = owner, 13 | seeds = [b"auction", 14 | auction.common.name.as_bytes()], 15 | bump = auction.bump 16 | )] 17 | pub auction: Box>, 18 | } 19 | 20 | #[derive(AnchorSerialize, AnchorDeserialize)] 21 | pub struct EnableAuctionParams {} 22 | 23 | pub fn enable_auction(ctx: Context, _params: &EnableAuctionParams) -> Result<()> { 24 | let auction = ctx.accounts.auction.as_mut(); 25 | auction.enabled = true; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/get_auction_amount.rs: -------------------------------------------------------------------------------- 1 | //! GetAuctionAmount instruction handler 2 | 3 | use { 4 | crate::state::{auction::Auction, launchpad::Launchpad}, 5 | anchor_lang::prelude::*, 6 | }; 7 | 8 | #[derive(Accounts)] 9 | pub struct GetAuctionAmount<'info> { 10 | #[account()] 11 | pub user: Signer<'info>, 12 | 13 | #[account( 14 | seeds = [b"launchpad"], 15 | bump = launchpad.launchpad_bump 16 | )] 17 | pub launchpad: Box>, 18 | 19 | #[account( 20 | seeds = [b"auction", 21 | auction.common.name.as_bytes()], 22 | bump = auction.bump 23 | )] 24 | pub auction: Box>, 25 | } 26 | 27 | #[derive(AnchorSerialize, AnchorDeserialize)] 28 | pub struct GetAuctionAmountParams { 29 | price: u64, 30 | } 31 | 32 | pub fn get_auction_amount( 33 | ctx: Context, 34 | params: &GetAuctionAmountParams, 35 | ) -> Result { 36 | ctx.accounts 37 | .auction 38 | .get_auction_amount(params.price, ctx.accounts.auction.get_time()?) 39 | } 40 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/get_auction_price.rs: -------------------------------------------------------------------------------- 1 | //! GetAuctionPrice instruction handler 2 | 3 | use { 4 | crate::state::{auction::Auction, launchpad::Launchpad}, 5 | anchor_lang::prelude::*, 6 | }; 7 | 8 | #[derive(Accounts)] 9 | pub struct GetAuctionPrice<'info> { 10 | #[account()] 11 | pub user: Signer<'info>, 12 | 13 | #[account( 14 | seeds = [b"launchpad"], 15 | bump = launchpad.launchpad_bump 16 | )] 17 | pub launchpad: Box>, 18 | 19 | #[account( 20 | seeds = [b"auction", 21 | auction.common.name.as_bytes()], 22 | bump = auction.bump 23 | )] 24 | pub auction: Box>, 25 | } 26 | 27 | #[derive(AnchorSerialize, AnchorDeserialize)] 28 | pub struct GetAuctionPriceParams { 29 | amount: u64, 30 | } 31 | 32 | pub fn get_auction_price( 33 | ctx: Context, 34 | params: &GetAuctionPriceParams, 35 | ) -> Result { 36 | ctx.accounts 37 | .auction 38 | .get_auction_price(params.amount, ctx.accounts.auction.get_time()?) 39 | } 40 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/init.rs: -------------------------------------------------------------------------------- 1 | //! Init instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | launchpad::{Fee, Launchpad}, 8 | multisig::Multisig, 9 | }, 10 | }, 11 | anchor_lang::prelude::*, 12 | anchor_spl::token::Token, 13 | solana_address_lookup_table_program as saltp, 14 | solana_program::{program, program_error::ProgramError, sysvar}, 15 | }; 16 | 17 | #[derive(Accounts)] 18 | pub struct Init<'info> { 19 | #[account(mut)] 20 | pub upgrade_authority: Signer<'info>, 21 | 22 | #[account( 23 | init, 24 | payer = upgrade_authority, 25 | space = Multisig::LEN, 26 | seeds = [b"multisig"], 27 | bump 28 | )] 29 | pub multisig: AccountLoader<'info, Multisig>, 30 | 31 | /// CHECK: empty PDA, will be set as authority for token accounts 32 | #[account( 33 | init, 34 | payer = upgrade_authority, 35 | space = 0, 36 | seeds = [b"transfer_authority"], 37 | bump 38 | )] 39 | pub transfer_authority: AccountInfo<'info>, 40 | 41 | #[account( 42 | init, 43 | payer = upgrade_authority, 44 | space = Launchpad::LEN, 45 | seeds = [b"launchpad"], 46 | bump 47 | )] 48 | pub launchpad: Box>, 49 | 50 | #[account( 51 | constraint = launchpad_program.programdata_address()? == Some(launchpad_program_data.key()) 52 | )] 53 | pub launchpad_program: Program<'info, Launchpad>, 54 | 55 | #[account( 56 | constraint = launchpad_program_data.upgrade_authority_address == Some(upgrade_authority.key()) 57 | )] 58 | pub launchpad_program_data: Account<'info, ProgramData>, 59 | 60 | /// CHECK: lookup table account 61 | #[account(mut)] 62 | pub lookup_table: AccountInfo<'info>, 63 | 64 | /// CHECK: account constraints checked in account trait 65 | #[account( 66 | address = sysvar::slot_hashes::id() 67 | )] 68 | recent_slothashes: UncheckedAccount<'info>, 69 | 70 | /// CHECK: account constraints checked in account trait 71 | #[account( 72 | address = sysvar::instructions::id() 73 | )] 74 | instructions: UncheckedAccount<'info>, 75 | 76 | /// CHECK: lookup table program 77 | lookup_table_program: AccountInfo<'info>, 78 | 79 | system_program: Program<'info, System>, 80 | token_program: Program<'info, Token>, 81 | // remaining accounts: 1 to Multisig::MAX_SIGNERS admin signers (read-only, unsigned) 82 | } 83 | 84 | #[derive(AnchorSerialize, AnchorDeserialize)] 85 | pub struct InitParams { 86 | pub min_signatures: u8, 87 | pub allow_new_auctions: bool, 88 | pub allow_auction_updates: bool, 89 | pub allow_auction_refills: bool, 90 | pub allow_auction_pullouts: bool, 91 | pub allow_new_bids: bool, 92 | pub allow_withdrawals: bool, 93 | pub new_auction_fee: u64, 94 | pub auction_update_fee: u64, 95 | pub invalid_bid_fee: Fee, 96 | pub trade_fee: Fee, 97 | pub recent_slot: u64, 98 | } 99 | 100 | pub fn init(ctx: Context, params: &InitParams) -> Result<()> { 101 | // initialize multisig, this will fail if account is already initialized 102 | let mut multisig = ctx.accounts.multisig.load_init()?; 103 | 104 | multisig.set_signers(ctx.remaining_accounts, params.min_signatures)?; 105 | 106 | // record multisig PDA bump 107 | multisig.bump = *ctx 108 | .bumps 109 | .get("multisig") 110 | .ok_or(ProgramError::InvalidSeeds)?; 111 | 112 | // record launchpad 113 | let launchpad = ctx.accounts.launchpad.as_mut(); 114 | launchpad.permissions.allow_new_auctions = params.allow_new_auctions; 115 | launchpad.permissions.allow_auction_updates = params.allow_auction_updates; 116 | launchpad.permissions.allow_auction_refills = params.allow_auction_refills; 117 | launchpad.permissions.allow_auction_pullouts = params.allow_auction_pullouts; 118 | launchpad.permissions.allow_new_bids = params.allow_new_bids; 119 | launchpad.permissions.allow_withdrawals = params.allow_withdrawals; 120 | launchpad.fees.new_auction = params.new_auction_fee; 121 | launchpad.fees.auction_update = params.auction_update_fee; 122 | launchpad.fees.invalid_bid = params.invalid_bid_fee; 123 | launchpad.fees.trade = params.trade_fee; 124 | launchpad.collected_fees.new_auction_sol = 0; 125 | launchpad.collected_fees.auction_update_sol = 0; 126 | launchpad.collected_fees.invalid_bid_usdc = 0; 127 | launchpad.collected_fees.trade_usdc = 0; 128 | launchpad.transfer_authority_bump = *ctx 129 | .bumps 130 | .get("transfer_authority") 131 | .ok_or(ProgramError::InvalidSeeds)?; 132 | launchpad.launchpad_bump = *ctx 133 | .bumps 134 | .get("launchpad") 135 | .ok_or(ProgramError::InvalidSeeds)?; 136 | 137 | if !launchpad.validate() { 138 | return err!(LaunchpadError::InvalidLaunchpadConfig); 139 | } 140 | 141 | // initialize lookup-table 142 | let transfer_authority = ctx.accounts.transfer_authority.key(); 143 | let payer = ctx.accounts.upgrade_authority.key(); 144 | let (init_table_ix, table_address) = 145 | saltp::instruction::create_lookup_table(transfer_authority, payer, params.recent_slot); 146 | require_keys_eq!(table_address, ctx.accounts.lookup_table.key()); 147 | require_keys_eq!(ctx.accounts.lookup_table_program.key(), saltp::ID); 148 | 149 | let authority_seeds: &[&[&[u8]]] = 150 | &[&[b"transfer_authority", &[launchpad.transfer_authority_bump]]]; 151 | program::invoke_signed( 152 | &init_table_ix, 153 | &[ 154 | ctx.accounts.lookup_table.to_account_info(), 155 | ctx.accounts.transfer_authority.to_account_info(), 156 | ctx.accounts.upgrade_authority.to_account_info(), 157 | ctx.accounts.system_program.to_account_info(), 158 | ], 159 | authority_seeds, 160 | )?; 161 | 162 | // add addresses to the lookup table 163 | let extend_table_ix = saltp::instruction::extend_lookup_table( 164 | table_address, 165 | transfer_authority, 166 | Some(payer), 167 | vec![ 168 | transfer_authority, 169 | ctx.accounts.launchpad.key(), 170 | ctx.accounts.recent_slothashes.key(), 171 | ctx.accounts.instructions.key(), 172 | ctx.accounts.system_program.key(), 173 | ctx.accounts.token_program.key(), 174 | ], 175 | ); 176 | program::invoke_signed( 177 | &extend_table_ix, 178 | &[ 179 | ctx.accounts.lookup_table.to_account_info(), 180 | ctx.accounts.transfer_authority.to_account_info(), 181 | ctx.accounts.upgrade_authority.to_account_info(), 182 | ctx.accounts.system_program.to_account_info(), 183 | ], 184 | authority_seeds, 185 | )?; 186 | 187 | Ok(()) 188 | } 189 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/init_auction.rs: -------------------------------------------------------------------------------- 1 | //! InitAuction instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | self, 8 | auction::{ 9 | Auction, AuctionStats, AuctionToken, CommonParams, PaymentParams, PricingParams, 10 | }, 11 | custody::Custody, 12 | launchpad::Launchpad, 13 | }, 14 | }, 15 | anchor_lang::prelude::*, 16 | anchor_spl::token::Token, 17 | }; 18 | 19 | #[derive(Accounts)] 20 | #[instruction(params: InitAuctionParams)] 21 | pub struct InitAuction<'info> { 22 | #[account(mut)] 23 | pub owner: Signer<'info>, 24 | 25 | /// CHECK: empty PDA, authority for token accounts 26 | #[account( 27 | mut, 28 | seeds = [b"transfer_authority"], 29 | bump = launchpad.transfer_authority_bump 30 | )] 31 | pub transfer_authority: AccountInfo<'info>, 32 | 33 | #[account( 34 | mut, 35 | seeds = [b"launchpad"], 36 | bump = launchpad.launchpad_bump 37 | )] 38 | pub launchpad: Box>, 39 | 40 | #[account( 41 | init, 42 | payer = owner, 43 | space = Auction::LEN, 44 | seeds = [b"auction", 45 | params.common.name.as_bytes()], 46 | bump 47 | )] 48 | pub auction: Box>, 49 | 50 | #[account( 51 | seeds = [b"custody", 52 | pricing_custody.mint.as_ref()], 53 | bump = pricing_custody.bump 54 | )] 55 | pub pricing_custody: Box>, 56 | 57 | system_program: Program<'info, System>, 58 | token_program: Program<'info, Token>, 59 | rent: Sysvar<'info, Rent>, 60 | // remaining accounts: 61 | // 1 to Auction::MAX_TOKENS dispensing custody addresses (write, unsigned) 62 | // with seeds = [b"dispense", mint.key().as_ref(), auction.key().as_ref()], 63 | // 1 to Auction::MAX_TOKENS dispensing custody mints (read-only, unsigned) 64 | } 65 | 66 | #[derive(AnchorSerialize, AnchorDeserialize)] 67 | pub struct InitAuctionParams { 68 | pub enabled: bool, 69 | pub updatable: bool, 70 | pub fixed_amount: bool, 71 | pub common: CommonParams, 72 | pub payment: PaymentParams, 73 | pub pricing: PricingParams, 74 | pub token_ratios: Vec, 75 | } 76 | 77 | pub fn init_auction<'info>( 78 | ctx: Context<'_, '_, '_, 'info, InitAuction<'info>>, 79 | params: &InitAuctionParams, 80 | ) -> Result<()> { 81 | require!( 82 | ctx.accounts.launchpad.permissions.allow_new_auctions, 83 | LaunchpadError::NewAuctionsNotAllowed 84 | ); 85 | 86 | // collect fee 87 | let launchpad = ctx.accounts.launchpad.as_mut(); 88 | state::transfer_sol( 89 | ctx.accounts.owner.to_account_info(), 90 | ctx.accounts.transfer_authority.to_account_info(), 91 | ctx.accounts.system_program.to_account_info(), 92 | launchpad.fees.new_auction, 93 | )?; 94 | launchpad.collected_fees.new_auction_sol = launchpad 95 | .collected_fees 96 | .new_auction_sol 97 | .wrapping_add(launchpad.fees.new_auction); 98 | 99 | // create dispensing accounts 100 | if ctx.remaining_accounts.is_empty() || ctx.remaining_accounts.len() % 2 != 0 { 101 | return Err(ProgramError::NotEnoughAccountKeys.into()); 102 | } 103 | let accounts_half_len = ctx.remaining_accounts.len() / 2; 104 | require!( 105 | accounts_half_len <= Auction::MAX_TOKENS, 106 | LaunchpadError::TooManyAccountKeys 107 | ); 108 | let dispensers = state::create_token_accounts( 109 | &ctx.remaining_accounts[..accounts_half_len], 110 | &ctx.remaining_accounts[accounts_half_len..], 111 | ctx.accounts.transfer_authority.to_account_info(), 112 | ctx.accounts.owner.to_account_info(), 113 | &ctx.accounts.auction.key(), 114 | ctx.accounts.system_program.to_account_info(), 115 | ctx.accounts.token_program.to_account_info(), 116 | ctx.accounts.rent.to_account_info(), 117 | )?; 118 | 119 | require_keys_eq!( 120 | ctx.accounts.pricing_custody.key(), 121 | params.pricing.custody, 122 | LaunchpadError::InvalidPricingConfig 123 | ); 124 | 125 | // record auction data 126 | let auction = ctx.accounts.auction.as_mut(); 127 | 128 | auction.owner = ctx.accounts.owner.key(); 129 | auction.enabled = params.enabled; 130 | auction.updatable = params.updatable; 131 | auction.fixed_amount = params.fixed_amount; 132 | auction.common = params.common.clone(); 133 | auction.payment = params.payment; 134 | auction.pricing = params.pricing; 135 | auction.stats = AuctionStats::default(); 136 | auction.stats.wl_bidders.min_fill_price = u64::MAX; 137 | auction.stats.reg_bidders.min_fill_price = u64::MAX; 138 | auction.tokens = [AuctionToken::default(); Auction::MAX_TOKENS]; 139 | auction.num_tokens = dispensers.len() as u8; 140 | 141 | for (n, dispenser) in dispensers.iter().enumerate() { 142 | auction.tokens[n].ratio = params.token_ratios[n]; 143 | auction.tokens[n].account = dispenser.key(); 144 | } 145 | 146 | auction.bump = *ctx.bumps.get("auction").ok_or(ProgramError::InvalidSeeds)?; 147 | 148 | auction.creation_time = if cfg!(feature = "test") { 149 | 0 150 | } else { 151 | auction.get_time()? 152 | }; 153 | auction.update_time = auction.creation_time; 154 | 155 | if !auction.validate()? { 156 | err!(LaunchpadError::InvalidAuctionConfig) 157 | } else { 158 | Ok(()) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/init_custody.rs: -------------------------------------------------------------------------------- 1 | //! InitCustody instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | custody::Custody, 8 | multisig::{AdminInstruction, Multisig}, 9 | oracle::OracleType, 10 | }, 11 | }, 12 | anchor_lang::prelude::*, 13 | anchor_spl::{ 14 | associated_token::AssociatedToken, 15 | token::{Mint, Token, TokenAccount}, 16 | }, 17 | }; 18 | 19 | #[derive(Accounts)] 20 | pub struct InitCustody<'info> { 21 | #[account(mut)] 22 | pub admin: Signer<'info>, 23 | 24 | #[account( 25 | mut, 26 | seeds = [b"multisig"], 27 | bump = multisig.load()?.bump 28 | )] 29 | pub multisig: AccountLoader<'info, Multisig>, 30 | 31 | /// CHECK: empty PDA, will be set as authority for token accounts 32 | #[account( 33 | seeds = [b"transfer_authority"], 34 | bump 35 | )] 36 | pub transfer_authority: AccountInfo<'info>, 37 | 38 | // instruction can be called multiple times due to multisig use, hence init_if_needed 39 | // instead of init. On the first call account is zero initialized and filled out when 40 | // all signatures are collected. When account is in zeroed state it can't be used in other 41 | // instructions because seeds are computed with recorded mints. Uniqueness is enforced 42 | // manually in the instruction handler. 43 | #[account( 44 | init_if_needed, 45 | payer = admin, 46 | space = Custody::LEN, 47 | seeds = [b"custody", 48 | custody_token_mint.key().as_ref()], 49 | bump 50 | )] 51 | pub custody: Box>, 52 | 53 | pub custody_token_mint: Box>, 54 | 55 | // token custodies are shared between multiple auctions 56 | #[account( 57 | init_if_needed, 58 | payer = admin, 59 | constraint = custody_token_mint.key() == custody_token_account.mint, 60 | associated_token::mint = custody_token_mint, 61 | associated_token::authority = transfer_authority 62 | )] 63 | pub custody_token_account: Box>, 64 | 65 | system_program: Program<'info, System>, 66 | token_program: Program<'info, Token>, 67 | associated_token_program: Program<'info, AssociatedToken>, 68 | rent: Sysvar<'info, Rent>, 69 | } 70 | 71 | #[derive(AnchorSerialize, AnchorDeserialize)] 72 | pub struct InitCustodyParams { 73 | pub max_oracle_price_error: f64, 74 | pub max_oracle_price_age_sec: u32, 75 | pub oracle_type: OracleType, 76 | pub oracle_account: Pubkey, 77 | } 78 | 79 | pub fn init_custody<'info>( 80 | ctx: Context<'_, '_, '_, 'info, InitCustody<'info>>, 81 | params: &InitCustodyParams, 82 | ) -> Result { 83 | // validate signatures 84 | let mut multisig = ctx.accounts.multisig.load_mut()?; 85 | 86 | let signatures_left = multisig.sign_multisig( 87 | &ctx.accounts.admin, 88 | &Multisig::get_account_infos(&ctx)[1..], 89 | &Multisig::get_instruction_data(AdminInstruction::InitCustody, params)?, 90 | )?; 91 | if signatures_left > 0 { 92 | msg!( 93 | "Instruction has been signed but more signatures are required: {}", 94 | signatures_left 95 | ); 96 | return Ok(signatures_left); 97 | } 98 | 99 | // record custody data 100 | let custody = ctx.accounts.custody.as_mut(); 101 | if custody.mint != Pubkey::default() { 102 | // return error if custody is already initialized 103 | return Err(ProgramError::AccountAlreadyInitialized.into()); 104 | } 105 | 106 | custody.token_account = ctx.accounts.custody_token_account.key(); 107 | custody.collected_fees = 0; 108 | custody.mint = ctx.accounts.custody_token_mint.key(); 109 | custody.decimals = ctx.accounts.custody_token_mint.decimals; 110 | custody.max_oracle_price_error = params.max_oracle_price_error; 111 | custody.max_oracle_price_age_sec = params.max_oracle_price_age_sec; 112 | custody.oracle_type = params.oracle_type; 113 | custody.oracle_account = params.oracle_account; 114 | custody.bump = *ctx.bumps.get("custody").ok_or(ProgramError::InvalidSeeds)?; 115 | 116 | if !custody.validate() { 117 | err!(LaunchpadError::InvalidCustodyConfig) 118 | } else { 119 | Ok(0) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/place_bid.rs: -------------------------------------------------------------------------------- 1 | //! PlaceBid instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | math, 7 | state::{ 8 | self, 9 | auction::Auction, 10 | bid::{BadBidType, Bid, BidType}, 11 | custody::Custody, 12 | launchpad::Launchpad, 13 | oracle::OraclePrice, 14 | seller_balance::SellerBalance, 15 | }, 16 | }, 17 | anchor_lang::prelude::*, 18 | anchor_spl::token::{Token, TokenAccount, Transfer}, 19 | solana_program::sysvar, 20 | }; 21 | 22 | #[derive(Accounts)] 23 | pub struct PlaceBid<'info> { 24 | #[account(mut)] 25 | pub owner: Signer<'info>, 26 | 27 | #[account( 28 | mut, 29 | constraint = funding_account.mint == payment_custody.mint, 30 | has_one = owner 31 | )] 32 | pub funding_account: Box>, 33 | 34 | /// CHECK: empty PDA, authority for token accounts 35 | #[account( 36 | seeds = [b"transfer_authority"], 37 | bump = launchpad.transfer_authority_bump 38 | )] 39 | pub transfer_authority: AccountInfo<'info>, 40 | 41 | #[account( 42 | mut, 43 | seeds = [b"launchpad"], 44 | bump = launchpad.launchpad_bump 45 | )] 46 | pub launchpad: Box>, 47 | 48 | #[account( 49 | mut, 50 | seeds = [b"auction", 51 | auction.common.name.as_bytes()], 52 | bump = auction.bump 53 | )] 54 | pub auction: Box>, 55 | 56 | #[account( 57 | init_if_needed, 58 | payer = owner, 59 | space = SellerBalance::LEN, 60 | seeds = [b"seller_balance", 61 | auction.owner.as_ref(), 62 | payment_custody.key().as_ref()], 63 | bump 64 | )] 65 | pub seller_balance: Box>, 66 | 67 | #[account( 68 | init_if_needed, 69 | payer = owner, 70 | space = Bid::LEN, 71 | seeds = [b"bid", 72 | owner.key().as_ref(), 73 | auction.key().as_ref()], 74 | bump 75 | )] 76 | pub bid: Box>, 77 | 78 | #[account( 79 | constraint = pricing_custody.key() == auction.pricing.custody, 80 | seeds = [b"custody", 81 | pricing_custody.mint.as_ref()], 82 | bump = pricing_custody.bump 83 | )] 84 | pub pricing_custody: Box>, 85 | 86 | /// CHECK: oracle account for the pricing token 87 | #[account( 88 | constraint = pricing_oracle_account.key() == pricing_custody.oracle_account 89 | )] 90 | pub pricing_oracle_account: AccountInfo<'info>, 91 | 92 | #[account( 93 | mut, 94 | seeds = [b"custody", 95 | payment_custody.mint.as_ref()], 96 | bump = payment_custody.bump 97 | )] 98 | pub payment_custody: Box>, 99 | 100 | /// CHECK: oracle account for the payment token 101 | #[account( 102 | constraint = payment_oracle_account.key() == payment_custody.oracle_account 103 | )] 104 | pub payment_oracle_account: AccountInfo<'info>, 105 | 106 | #[account( 107 | mut, 108 | constraint = payment_token_account.key() == payment_custody.token_account.key() 109 | )] 110 | pub payment_token_account: Box>, 111 | 112 | /// CHECK: account constraints checked in account trait 113 | #[account( 114 | address = sysvar::slot_hashes::id() 115 | )] 116 | recent_slothashes: UncheckedAccount<'info>, 117 | 118 | /// CHECK: account constraints checked in account trait 119 | #[account( 120 | address = sysvar::instructions::id() 121 | )] 122 | instructions: UncheckedAccount<'info>, 123 | 124 | system_program: Program<'info, System>, 125 | token_program: Program<'info, Token>, 126 | // remaining accounts: 127 | // 1 to Auction::MAX_TOKENS user's token receiving accounts (write, unsigned) 128 | // 1 to Auction::MAX_TOKENS dispensing custody addresses (write, unsigned) 129 | } 130 | 131 | #[derive(AnchorSerialize, AnchorDeserialize)] 132 | pub struct PlaceBidParams { 133 | price: u64, 134 | amount: u64, 135 | bid_type: BidType, 136 | } 137 | 138 | pub fn place_bid<'info>( 139 | ctx: Context<'_, '_, '_, 'info, PlaceBid<'info>>, 140 | params: &PlaceBidParams, 141 | ) -> Result<()> { 142 | require!( 143 | ctx.accounts.launchpad.permissions.allow_new_bids, 144 | LaunchpadError::BidsNotAllowed 145 | ); 146 | 147 | // check if this instruction is the only instruction in the transaction 148 | require!( 149 | sysvar::instructions::load_current_index_checked( 150 | &ctx.accounts.instructions.to_account_info() 151 | )? == 0 152 | && sysvar::instructions::load_instruction_at_checked( 153 | 1, 154 | &ctx.accounts.instructions.to_account_info() 155 | ) 156 | .is_err(), 157 | LaunchpadError::MustBeSingleInstruction 158 | ); 159 | 160 | // load accounts 161 | msg!("Load accounts"); 162 | let launchpad = ctx.accounts.launchpad.as_mut(); 163 | let auction = ctx.accounts.auction.as_mut(); 164 | let bid = ctx.accounts.bid.as_mut(); 165 | let seller_balance = ctx.accounts.seller_balance.as_mut(); 166 | let payment_custody = ctx.accounts.payment_custody.as_mut(); 167 | 168 | if ctx.remaining_accounts.is_empty() || ctx.remaining_accounts.len() % 2 != 0 { 169 | return Err(ProgramError::NotEnoughAccountKeys.into()); 170 | } 171 | let accounts_half_len = ctx.remaining_accounts.len() / 2; 172 | if accounts_half_len > auction.num_tokens.into() { 173 | return err!(LaunchpadError::TooManyAccountKeys); 174 | } 175 | if accounts_half_len < auction.num_tokens.into() { 176 | return Err(ProgramError::NotEnoughAccountKeys.into()); 177 | } 178 | let receiving_accounts = state::load_accounts::( 179 | &ctx.remaining_accounts[..accounts_half_len], 180 | &Token::id(), 181 | )?; 182 | let dispensing_custodies = state::load_accounts::( 183 | &ctx.remaining_accounts[accounts_half_len..], 184 | &Token::id(), 185 | )?; 186 | 187 | // validate inputs 188 | msg!("Validate inputs"); 189 | require_gt!(params.amount, 0u64, LaunchpadError::InvalidTokenAmount); 190 | let order_amount_limit = if bid.whitelisted { 191 | std::cmp::max( 192 | auction.common.order_limit_wl_address, 193 | auction.common.order_limit_reg_address, 194 | ) 195 | } else { 196 | auction.common.order_limit_reg_address 197 | }; 198 | require_gte!( 199 | order_amount_limit, 200 | params.amount, 201 | LaunchpadError::BidAmountTooLarge 202 | ); 203 | require_gte!( 204 | params.price, 205 | auction.pricing.min_price, 206 | LaunchpadError::BidPriceTooSmall 207 | ); 208 | 209 | // check if auction is active 210 | let curtime = auction.get_time()?; 211 | let mut bad_bid_type = BadBidType::None; 212 | 213 | if !auction.is_started(curtime, bid.whitelisted) { 214 | bad_bid_type = BadBidType::TooEarly; 215 | } 216 | 217 | require!( 218 | !auction.is_ended(curtime, bid.whitelisted), 219 | LaunchpadError::AuctionEnded 220 | ); 221 | 222 | // validate dispensing and receiving accounts 223 | // all accounts needs to be validated, not the only selected to dispense, 224 | // so the user can't game the process 225 | msg!("Validate dispensing and receiving accounts"); 226 | for token in 0..auction.num_tokens as usize { 227 | if receiving_accounts[token].owner != ctx.accounts.owner.key() { 228 | msg!("Invalid owner of the receiving token account"); 229 | return Err(ProgramError::IllegalOwner.into()); 230 | } 231 | require_keys_eq!( 232 | dispensing_custodies[token].key(), 233 | auction.tokens[token].account, 234 | LaunchpadError::InvalidDispenserAddress 235 | ); 236 | require_keys_eq!( 237 | dispensing_custodies[token].mint, 238 | receiving_accounts[token].mint, 239 | LaunchpadError::InvalidReceivingAddress 240 | ) 241 | } 242 | 243 | // pick a random token to dispense 244 | msg!("Select token to dispense"); 245 | let token_num = if auction.num_tokens == 1 { 246 | 0 247 | } else { 248 | let slothashes_data = ctx.accounts.recent_slothashes.data.borrow(); 249 | if slothashes_data.len() < 20 { 250 | return Err(ProgramError::InvalidAccountData.into()); 251 | } 252 | let rand_seed = usize::from_le_bytes(slothashes_data[12..20].try_into().unwrap()); 253 | rand_seed % dispensing_custodies.len() 254 | }; 255 | let max_amount_to_dispense = math::checked_div( 256 | dispensing_custodies[token_num].amount, 257 | auction.pricing.unit_size, 258 | )?; 259 | 260 | // get available amount at the given price 261 | msg!("Compute available amount"); 262 | let avail_amount = std::cmp::min( 263 | auction.get_auction_amount(params.price, curtime)?, 264 | max_amount_to_dispense, 265 | ); 266 | 267 | if avail_amount == 0 || (params.bid_type == BidType::Fok && avail_amount < params.amount) { 268 | return err!(LaunchpadError::InsufficientAmount); 269 | } 270 | let fill_amount = std::cmp::min(avail_amount, params.amount); 271 | 272 | let fill_price = auction.get_auction_price(fill_amount, curtime)?; 273 | require_gte!(params.price, fill_price, LaunchpadError::PriceCalcError); 274 | 275 | // check for malicious bid 276 | let fill_amount_limit = if bid.whitelisted { 277 | std::cmp::max( 278 | auction.common.fill_limit_wl_address, 279 | auction.common.fill_limit_reg_address, 280 | ) 281 | } else { 282 | auction.common.fill_limit_reg_address 283 | }; 284 | if fill_amount_limit < bid.filled { 285 | bad_bid_type = BadBidType::FillLimit; 286 | } 287 | 288 | if bad_bid_type != BadBidType::None { 289 | if launchpad.fees.invalid_bid.is_zero() { 290 | if bad_bid_type == BadBidType::TooEarly { 291 | return err!(LaunchpadError::AuctionNotStarted); 292 | } else { 293 | return err!(LaunchpadError::FillAmountLimit); 294 | } 295 | } else { 296 | return collect_bad_bid_fee( 297 | launchpad, 298 | payment_custody, 299 | ctx.accounts.token_program.to_account_info(), 300 | ctx.accounts.funding_account.to_account_info(), 301 | ctx.accounts.payment_token_account.to_account_info(), 302 | ctx.accounts.pricing_oracle_account.to_account_info(), 303 | ctx.accounts.owner.to_account_info(), 304 | std::cmp::min(fill_amount, ctx.accounts.funding_account.amount), 305 | curtime, 306 | ); 307 | } 308 | } 309 | 310 | // compute payment amount 311 | let mut payment_amount = 0; 312 | if fill_price > 0 { 313 | msg!("Compute payment amount"); 314 | let payment_token_price = if !launchpad.fees.trade.is_zero() 315 | || payment_custody.key() != ctx.accounts.pricing_custody.key() 316 | { 317 | OraclePrice::new_from_oracle( 318 | payment_custody.oracle_type, 319 | &ctx.accounts.payment_oracle_account.to_account_info(), 320 | payment_custody.max_oracle_price_error, 321 | payment_custody.max_oracle_price_age_sec, 322 | curtime, 323 | )? 324 | } else { 325 | OraclePrice::new(0, 0) 326 | }; 327 | 328 | if payment_custody.key() == ctx.accounts.pricing_custody.key() { 329 | payment_amount = math::checked_mul(fill_price, fill_amount)?; 330 | } else { 331 | let pricing_custody = &ctx.accounts.pricing_custody; 332 | let auction_token_price = OraclePrice::new_from_oracle( 333 | pricing_custody.oracle_type, 334 | &ctx.accounts.pricing_oracle_account.to_account_info(), 335 | pricing_custody.max_oracle_price_error, 336 | pricing_custody.max_oracle_price_age_sec, 337 | curtime, 338 | )?; 339 | 340 | let token_pair_price = auction_token_price.checked_div(&payment_token_price)?; 341 | let price_per_token = math::checked_decimal_ceil_mul( 342 | fill_price, 343 | -(pricing_custody.decimals as i32), 344 | token_pair_price.price, 345 | token_pair_price.exponent, 346 | -(payment_custody.decimals as i32), 347 | )?; 348 | 349 | payment_amount = math::checked_mul(price_per_token, fill_amount)?; 350 | } 351 | 352 | // compute fee 353 | let fee_amount = launchpad.fees.trade.get_fee_amount(payment_amount)?; 354 | 355 | // collect payment and fee 356 | msg!("Collect payment {} and fee {}", payment_amount, fee_amount); 357 | let total_amount = math::checked_add(payment_amount, fee_amount)?; 358 | let context = CpiContext::new( 359 | ctx.accounts.token_program.to_account_info(), 360 | Transfer { 361 | from: ctx.accounts.funding_account.to_account_info(), 362 | to: ctx.accounts.payment_token_account.to_account_info(), 363 | authority: ctx.accounts.owner.to_account_info(), 364 | }, 365 | ); 366 | anchor_spl::token::transfer(context, total_amount)?; 367 | 368 | if fee_amount > 0 { 369 | payment_custody.collected_fees = 370 | math::checked_add(payment_custody.collected_fees, fee_amount)?; 371 | 372 | let fees_in_usdc = math::to_token_amount( 373 | payment_token_price.get_asset_value_usd(fee_amount, payment_custody.decimals)?, 374 | 6, 375 | )?; 376 | 377 | launchpad.collected_fees.trade_usdc = launchpad 378 | .collected_fees 379 | .trade_usdc 380 | .wrapping_add(fees_in_usdc); 381 | } 382 | } 383 | 384 | // update user's bid 385 | msg!("Update user's bid"); 386 | if bid.bump == 0 { 387 | bid.owner = ctx.accounts.owner.key(); 388 | bid.auction = auction.key(); 389 | bid.whitelisted = false; 390 | bid.seller_initialized = false; 391 | bid.bump = *ctx.bumps.get("bid").ok_or(ProgramError::InvalidSeeds)?; 392 | } else if bid.owner != ctx.accounts.owner.key() || bid.auction != auction.key() { 393 | return err!(LaunchpadError::InvalidBidAddress); 394 | } 395 | 396 | bid.bid_time = auction.get_time()?; 397 | bid.bid_price = params.price; 398 | bid.bid_amount = params.amount; 399 | bid.bid_type = params.bid_type; 400 | bid.filled = math::checked_add(bid.filled, fill_amount)?; 401 | bid.fill_time = bid.bid_time; 402 | bid.fill_price = fill_price; 403 | bid.fill_amount = fill_amount; 404 | 405 | // update seller's balance 406 | msg!("Update seller's balance"); 407 | if seller_balance.bump == 0 { 408 | seller_balance.owner = auction.owner; 409 | seller_balance.custody = ctx.accounts.payment_custody.key(); 410 | seller_balance.bump = *ctx 411 | .bumps 412 | .get("seller_balance") 413 | .ok_or(ProgramError::InvalidSeeds)?; 414 | } else if seller_balance.owner != auction.owner 415 | || seller_balance.custody == ctx.accounts.payment_custody.key() 416 | { 417 | return err!(LaunchpadError::InvalidSellerBalanceAddress); 418 | } 419 | seller_balance.balance = math::checked_add(seller_balance.balance, payment_amount)?; 420 | 421 | // update auction stats 422 | msg!("Update auction stats"); 423 | let curtime = auction.get_time()?; 424 | if auction.stats.first_trade_time == 0 { 425 | auction.stats.first_trade_time = curtime; 426 | } 427 | auction.stats.last_trade_time = curtime; 428 | auction.stats.last_amount = fill_amount; 429 | auction.stats.last_price = fill_price; 430 | 431 | let bidder_stats = if bid.whitelisted { 432 | &mut auction.stats.wl_bidders 433 | } else { 434 | &mut auction.stats.reg_bidders 435 | }; 436 | bidder_stats.fills_volume = math::checked_add(bidder_stats.fills_volume, fill_amount)?; 437 | bidder_stats.weighted_fills_sum = math::checked_add( 438 | bidder_stats.weighted_fills_sum, 439 | math::checked_mul(fill_amount as u128, fill_price as u128)?, 440 | )?; 441 | if fill_price < bidder_stats.min_fill_price { 442 | bidder_stats.min_fill_price = fill_price; 443 | } 444 | if fill_price > bidder_stats.max_fill_price { 445 | bidder_stats.max_fill_price = fill_price; 446 | } 447 | bidder_stats.num_trades = bidder_stats.num_trades.wrapping_add(1); 448 | 449 | // transfer purchased tokens to the user 450 | let transfer_amount = math::checked_mul(fill_amount, auction.pricing.unit_size)?; 451 | msg!("Transfer {} tokens to the user", transfer_amount); 452 | ctx.accounts.launchpad.transfer_tokens( 453 | dispensing_custodies[token_num].to_account_info(), 454 | receiving_accounts[token_num].to_account_info(), 455 | ctx.accounts.transfer_authority.to_account_info(), 456 | ctx.accounts.token_program.to_account_info(), 457 | transfer_amount, 458 | )?; 459 | 460 | Ok(()) 461 | } 462 | 463 | #[allow(clippy::too_many_arguments)] 464 | fn collect_bad_bid_fee<'info>( 465 | launchpad: &mut Account<'info, Launchpad>, 466 | custody: &mut Account<'info, Custody>, 467 | token_program: AccountInfo<'info>, 468 | funding_account: AccountInfo<'info>, 469 | destination_account: AccountInfo<'info>, 470 | oracle_account: AccountInfo<'info>, 471 | authority: AccountInfo<'info>, 472 | bid_amount: u64, 473 | curtime: i64, 474 | ) -> Result<()> { 475 | let fee_amount = launchpad.fees.invalid_bid.get_fee_amount(bid_amount)?; 476 | if fee_amount == 0 { 477 | return Ok(()); 478 | } 479 | 480 | msg!("Collect bad bid fee {}", fee_amount); 481 | let context = CpiContext::new( 482 | token_program, 483 | Transfer { 484 | from: funding_account, 485 | to: destination_account, 486 | authority, 487 | }, 488 | ); 489 | anchor_spl::token::transfer(context, fee_amount)?; 490 | 491 | custody.collected_fees = math::checked_add(custody.collected_fees, fee_amount)?; 492 | 493 | let oracle_price = OraclePrice::new_from_oracle( 494 | custody.oracle_type, 495 | &oracle_account, 496 | custody.max_oracle_price_error, 497 | custody.max_oracle_price_age_sec, 498 | curtime, 499 | )?; 500 | let fees_in_usdc = math::to_token_amount( 501 | oracle_price.get_asset_value_usd(fee_amount, custody.decimals)?, 502 | 6, 503 | )?; 504 | 505 | launchpad.collected_fees.invalid_bid_usdc = launchpad 506 | .collected_fees 507 | .invalid_bid_usdc 508 | .wrapping_add(fees_in_usdc); 509 | 510 | Ok(()) 511 | } 512 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/remove_tokens.rs: -------------------------------------------------------------------------------- 1 | //! RemoveTokens instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{auction::Auction, launchpad::Launchpad}, 7 | }, 8 | anchor_lang::prelude::*, 9 | anchor_spl::token::{Token, TokenAccount}, 10 | }; 11 | 12 | #[derive(Accounts)] 13 | pub struct RemoveTokens<'info> { 14 | #[account()] 15 | pub owner: Signer<'info>, 16 | 17 | #[account( 18 | mut, 19 | constraint = receiving_account.mint == dispensing_custody.mint, 20 | has_one = owner 21 | )] 22 | pub receiving_account: Box>, 23 | 24 | /// CHECK: empty PDA, authority for token accounts 25 | #[account( 26 | seeds = [b"transfer_authority"], 27 | bump = launchpad.transfer_authority_bump 28 | )] 29 | pub transfer_authority: AccountInfo<'info>, 30 | 31 | #[account( 32 | seeds = [b"launchpad"], 33 | bump = launchpad.launchpad_bump 34 | )] 35 | pub launchpad: Box>, 36 | 37 | #[account( 38 | has_one = owner, 39 | seeds = [b"auction", 40 | auction.common.name.as_bytes()], 41 | bump = auction.bump 42 | )] 43 | pub auction: Box>, 44 | 45 | #[account( 46 | mut, 47 | seeds = [b"dispense", 48 | dispensing_custody.mint.as_ref(), 49 | auction.key().as_ref()], 50 | bump 51 | )] 52 | pub dispensing_custody: Box>, 53 | 54 | token_program: Program<'info, Token>, 55 | } 56 | 57 | #[derive(AnchorSerialize, AnchorDeserialize)] 58 | pub struct RemoveTokensParams { 59 | pub amount: u64, 60 | } 61 | 62 | pub fn remove_tokens(ctx: Context, params: &RemoveTokensParams) -> Result<()> { 63 | let curtime = ctx.accounts.auction.get_time()?; 64 | if ctx.accounts.auction.is_started(curtime, true) 65 | && !ctx.accounts.auction.is_ended(curtime, true) 66 | { 67 | require!( 68 | ctx.accounts.launchpad.permissions.allow_auction_pullouts, 69 | LaunchpadError::AuctionPullOutsNotAllowed 70 | ); 71 | } 72 | 73 | require!( 74 | !ctx.accounts.auction.fixed_amount, 75 | LaunchpadError::AuctionWithFixedAmount 76 | ); 77 | 78 | ctx.accounts.launchpad.transfer_tokens( 79 | ctx.accounts.dispensing_custody.to_account_info(), 80 | ctx.accounts.receiving_account.to_account_info(), 81 | ctx.accounts.transfer_authority.to_account_info(), 82 | ctx.accounts.token_program.to_account_info(), 83 | params.amount, 84 | )?; 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/set_admin_signers.rs: -------------------------------------------------------------------------------- 1 | //! SetAdminSigners instruction handler 2 | 3 | use { 4 | crate::state::multisig::{AdminInstruction, Multisig}, 5 | anchor_lang::prelude::*, 6 | }; 7 | 8 | #[derive(Accounts)] 9 | pub struct SetAdminSigners<'info> { 10 | #[account()] 11 | pub admin: Signer<'info>, 12 | 13 | #[account( 14 | mut, 15 | seeds = [b"multisig"], 16 | bump = multisig.load()?.bump 17 | )] 18 | pub multisig: AccountLoader<'info, Multisig>, 19 | // remaining accounts: 1 to Multisig::MAX_SIGNERS admin signers (read-only, unsigned) 20 | } 21 | 22 | #[derive(AnchorSerialize, AnchorDeserialize)] 23 | pub struct SetAdminSignersParams { 24 | pub min_signatures: u8, 25 | } 26 | 27 | pub fn set_admin_signers<'info>( 28 | ctx: Context<'_, '_, '_, 'info, SetAdminSigners<'info>>, 29 | params: &SetAdminSignersParams, 30 | ) -> Result { 31 | // validate signatures 32 | let mut multisig = ctx.accounts.multisig.load_mut()?; 33 | 34 | let signatures_left = multisig.sign_multisig( 35 | &ctx.accounts.admin, 36 | &Multisig::get_account_infos(&ctx)[1..], 37 | &Multisig::get_instruction_data(AdminInstruction::SetAdminSigners, params)?, 38 | )?; 39 | if signatures_left > 0 { 40 | msg!( 41 | "Instruction has been signed but more signatures are required: {}", 42 | signatures_left 43 | ); 44 | return Ok(signatures_left); 45 | } 46 | 47 | // set new admin signers 48 | multisig.set_signers(ctx.remaining_accounts, params.min_signatures)?; 49 | 50 | Ok(0) 51 | } 52 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/set_fees.rs: -------------------------------------------------------------------------------- 1 | //! SetFees instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | launchpad::{Fee, Launchpad}, 8 | multisig::{AdminInstruction, Multisig}, 9 | }, 10 | }, 11 | anchor_lang::prelude::*, 12 | }; 13 | 14 | #[derive(Accounts)] 15 | pub struct SetFees<'info> { 16 | #[account()] 17 | pub admin: Signer<'info>, 18 | 19 | #[account( 20 | mut, 21 | seeds = [b"multisig"], 22 | bump = multisig.load()?.bump 23 | )] 24 | pub multisig: AccountLoader<'info, Multisig>, 25 | 26 | #[account( 27 | mut, 28 | seeds = [b"launchpad"], 29 | bump = launchpad.launchpad_bump 30 | )] 31 | pub launchpad: Box>, 32 | } 33 | 34 | #[derive(AnchorSerialize, AnchorDeserialize)] 35 | pub struct SetFeesParams { 36 | pub new_auction: u64, 37 | pub auction_update: u64, 38 | pub invalid_bid: Fee, 39 | pub trade: Fee, 40 | } 41 | 42 | pub fn set_fees<'info>( 43 | ctx: Context<'_, '_, '_, 'info, SetFees<'info>>, 44 | params: &SetFeesParams, 45 | ) -> Result { 46 | // validate signatures 47 | let mut multisig = ctx.accounts.multisig.load_mut()?; 48 | 49 | let signatures_left = multisig.sign_multisig( 50 | &ctx.accounts.admin, 51 | &Multisig::get_account_infos(&ctx)[1..], 52 | &Multisig::get_instruction_data(AdminInstruction::SetFees, params)?, 53 | )?; 54 | if signatures_left > 0 { 55 | msg!( 56 | "Instruction has been signed but more signatures are required: {}", 57 | signatures_left 58 | ); 59 | return Ok(signatures_left); 60 | } 61 | 62 | // update permissions 63 | let launchpad = ctx.accounts.launchpad.as_mut(); 64 | launchpad.fees.new_auction = params.new_auction; 65 | launchpad.fees.auction_update = params.auction_update; 66 | launchpad.fees.invalid_bid = params.invalid_bid; 67 | launchpad.fees.trade = params.trade; 68 | 69 | if !launchpad.validate() { 70 | err!(LaunchpadError::InvalidLaunchpadConfig) 71 | } else { 72 | Ok(0) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/set_oracle_config.rs: -------------------------------------------------------------------------------- 1 | //! SetOracleConfig instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | custody::Custody, 8 | multisig::{AdminInstruction, Multisig}, 9 | oracle::OracleType, 10 | }, 11 | }, 12 | anchor_lang::prelude::*, 13 | }; 14 | 15 | #[derive(Accounts)] 16 | pub struct SetOracleConfig<'info> { 17 | #[account()] 18 | pub admin: Signer<'info>, 19 | 20 | #[account( 21 | mut, 22 | seeds = [b"multisig"], 23 | bump = multisig.load()?.bump 24 | )] 25 | pub multisig: AccountLoader<'info, Multisig>, 26 | 27 | #[account( 28 | mut, 29 | seeds = [b"custody", 30 | custody.mint.as_ref()], 31 | bump = custody.bump 32 | )] 33 | pub custody: Box>, 34 | } 35 | 36 | #[derive(AnchorSerialize, AnchorDeserialize)] 37 | pub struct SetOracleConfigParams { 38 | pub max_oracle_price_error: f64, 39 | pub max_oracle_price_age_sec: u32, 40 | pub oracle_type: OracleType, 41 | pub oracle_account: Pubkey, 42 | } 43 | 44 | pub fn set_oracle_config<'info>( 45 | ctx: Context<'_, '_, '_, 'info, SetOracleConfig<'info>>, 46 | params: &SetOracleConfigParams, 47 | ) -> Result { 48 | // validate signatures 49 | let mut multisig = ctx.accounts.multisig.load_mut()?; 50 | 51 | let signatures_left = multisig.sign_multisig( 52 | &ctx.accounts.admin, 53 | &Multisig::get_account_infos(&ctx)[1..], 54 | &Multisig::get_instruction_data(AdminInstruction::SetOracleConfig, params)?, 55 | )?; 56 | if signatures_left > 0 { 57 | msg!( 58 | "Instruction has been signed but more signatures are required: {}", 59 | signatures_left 60 | ); 61 | return Ok(signatures_left); 62 | } 63 | 64 | // update custody data 65 | let custody = ctx.accounts.custody.as_mut(); 66 | custody.max_oracle_price_error = params.max_oracle_price_error; 67 | custody.max_oracle_price_age_sec = params.max_oracle_price_age_sec; 68 | custody.oracle_type = params.oracle_type; 69 | custody.oracle_account = params.oracle_account; 70 | 71 | if !custody.validate() { 72 | err!(LaunchpadError::InvalidCustodyConfig) 73 | } else { 74 | Ok(0) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/set_permissions.rs: -------------------------------------------------------------------------------- 1 | //! SetPermissions instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | launchpad::Launchpad, 8 | multisig::{AdminInstruction, Multisig}, 9 | }, 10 | }, 11 | anchor_lang::prelude::*, 12 | }; 13 | 14 | #[derive(Accounts)] 15 | pub struct SetPermissions<'info> { 16 | #[account()] 17 | pub admin: Signer<'info>, 18 | 19 | #[account( 20 | mut, 21 | seeds = [b"multisig"], 22 | bump = multisig.load()?.bump 23 | )] 24 | pub multisig: AccountLoader<'info, Multisig>, 25 | 26 | #[account( 27 | mut, 28 | seeds = [b"launchpad"], 29 | bump = launchpad.launchpad_bump 30 | )] 31 | pub launchpad: Box>, 32 | } 33 | 34 | #[derive(AnchorSerialize, AnchorDeserialize)] 35 | pub struct SetPermissionsParams { 36 | pub allow_new_auctions: bool, 37 | pub allow_auction_updates: bool, 38 | pub allow_auction_refills: bool, 39 | pub allow_auction_pullouts: bool, 40 | pub allow_new_bids: bool, 41 | pub allow_withdrawals: bool, 42 | } 43 | 44 | pub fn set_permissions<'info>( 45 | ctx: Context<'_, '_, '_, 'info, SetPermissions<'info>>, 46 | params: &SetPermissionsParams, 47 | ) -> Result { 48 | // validate signatures 49 | let mut multisig = ctx.accounts.multisig.load_mut()?; 50 | 51 | let signatures_left = multisig.sign_multisig( 52 | &ctx.accounts.admin, 53 | &Multisig::get_account_infos(&ctx)[1..], 54 | &Multisig::get_instruction_data(AdminInstruction::SetPermissions, params)?, 55 | )?; 56 | if signatures_left > 0 { 57 | msg!( 58 | "Instruction has been signed but more signatures are required: {}", 59 | signatures_left 60 | ); 61 | return Ok(signatures_left); 62 | } 63 | 64 | // update permissions 65 | let launchpad = ctx.accounts.launchpad.as_mut(); 66 | launchpad.permissions.allow_new_auctions = params.allow_new_auctions; 67 | launchpad.permissions.allow_auction_updates = params.allow_auction_updates; 68 | launchpad.permissions.allow_auction_refills = params.allow_auction_refills; 69 | launchpad.permissions.allow_auction_pullouts = params.allow_auction_pullouts; 70 | launchpad.permissions.allow_new_bids = params.allow_new_bids; 71 | launchpad.permissions.allow_withdrawals = params.allow_withdrawals; 72 | 73 | if !launchpad.validate() { 74 | err!(LaunchpadError::InvalidLaunchpadConfig) 75 | } else { 76 | Ok(0) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/set_test_oracle_price.rs: -------------------------------------------------------------------------------- 1 | //! SetTestOraclePrice instruction handler 2 | 3 | use { 4 | crate::state::{ 5 | auction::Auction, 6 | custody::Custody, 7 | multisig::{AdminInstruction, Multisig}, 8 | oracle::TestOracle, 9 | }, 10 | anchor_lang::prelude::*, 11 | }; 12 | 13 | #[derive(Accounts)] 14 | pub struct SetTestOraclePrice<'info> { 15 | #[account(mut)] 16 | pub admin: Signer<'info>, 17 | 18 | #[account( 19 | mut, 20 | seeds = [b"multisig"], 21 | bump = multisig.load()?.bump 22 | )] 23 | pub multisig: AccountLoader<'info, Multisig>, 24 | 25 | #[account( 26 | seeds = [b"auction", 27 | auction.common.name.as_bytes()], 28 | bump = auction.bump 29 | )] 30 | pub auction: Box>, 31 | 32 | #[account( 33 | seeds = [b"custody", 34 | custody.mint.as_ref()], 35 | bump = custody.bump 36 | )] 37 | pub custody: Box>, 38 | 39 | #[account( 40 | init_if_needed, 41 | payer = admin, 42 | space = TestOracle::LEN, 43 | constraint = oracle_account.key() == custody.oracle_account, 44 | seeds = [b"oracle_account", 45 | custody.mint.as_ref(), 46 | auction.key().as_ref()], 47 | bump 48 | )] 49 | pub oracle_account: Box>, 50 | 51 | system_program: Program<'info, System>, 52 | } 53 | 54 | #[derive(AnchorSerialize, AnchorDeserialize)] 55 | pub struct SetTestOraclePriceParams { 56 | pub price: u64, 57 | pub expo: i32, 58 | pub conf: u64, 59 | pub publish_time: i64, 60 | } 61 | 62 | pub fn set_test_oracle_price<'info>( 63 | ctx: Context<'_, '_, '_, 'info, SetTestOraclePrice<'info>>, 64 | params: &SetTestOraclePriceParams, 65 | ) -> Result { 66 | // validate signatures 67 | let mut multisig = ctx.accounts.multisig.load_mut()?; 68 | 69 | let signatures_left = multisig.sign_multisig( 70 | &ctx.accounts.admin, 71 | &Multisig::get_account_infos(&ctx)[1..], 72 | &Multisig::get_instruction_data(AdminInstruction::SetTestOraclePrice, params)?, 73 | )?; 74 | if signatures_left > 0 { 75 | msg!( 76 | "Instruction has been signed but more signatures are required: {}", 77 | signatures_left 78 | ); 79 | return Ok(signatures_left); 80 | } 81 | 82 | // update oracle data 83 | let oracle_account = ctx.accounts.oracle_account.as_mut(); 84 | oracle_account.price = params.price; 85 | oracle_account.expo = params.expo; 86 | oracle_account.conf = params.conf; 87 | oracle_account.publish_time = params.publish_time; 88 | 89 | Ok(0) 90 | } 91 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/set_test_time.rs: -------------------------------------------------------------------------------- 1 | //! SetTestTime instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | auction::Auction, 8 | multisig::{AdminInstruction, Multisig}, 9 | }, 10 | }, 11 | anchor_lang::prelude::*, 12 | }; 13 | 14 | #[derive(Accounts)] 15 | pub struct SetTestTime<'info> { 16 | #[account()] 17 | pub admin: Signer<'info>, 18 | 19 | #[account( 20 | mut, 21 | seeds = [b"multisig"], 22 | bump = multisig.load()?.bump 23 | )] 24 | pub multisig: AccountLoader<'info, Multisig>, 25 | 26 | #[account( 27 | mut, 28 | seeds = [b"auction", 29 | auction.common.name.as_bytes()], 30 | bump = auction.bump 31 | )] 32 | pub auction: Box>, 33 | } 34 | 35 | #[derive(AnchorSerialize, AnchorDeserialize)] 36 | pub struct SetTestTimeParams { 37 | pub time: i64, 38 | } 39 | 40 | pub fn set_test_time<'info>( 41 | ctx: Context<'_, '_, '_, 'info, SetTestTime<'info>>, 42 | params: &SetTestTimeParams, 43 | ) -> Result { 44 | if !cfg!(feature = "test") { 45 | return err!(LaunchpadError::InvalidEnvironment); 46 | } 47 | 48 | // validate signatures 49 | let mut multisig = ctx.accounts.multisig.load_mut()?; 50 | 51 | let signatures_left = multisig.sign_multisig( 52 | &ctx.accounts.admin, 53 | &Multisig::get_account_infos(&ctx)[1..], 54 | &Multisig::get_instruction_data(AdminInstruction::SetTestTime, params)?, 55 | )?; 56 | if signatures_left > 0 { 57 | msg!( 58 | "Instruction has been signed but more signatures are required: {}", 59 | signatures_left 60 | ); 61 | return Ok(signatures_left); 62 | } 63 | 64 | // update time data 65 | if cfg!(feature = "test") { 66 | ctx.accounts.auction.creation_time = params.time; 67 | } 68 | 69 | Ok(0) 70 | } 71 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/test_init.rs: -------------------------------------------------------------------------------- 1 | //! TestInit instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | launchpad::{Fee, Launchpad}, 8 | multisig::Multisig, 9 | }, 10 | }, 11 | anchor_lang::prelude::*, 12 | anchor_spl::token::Token, 13 | solana_address_lookup_table_program as saltp, 14 | solana_program::{program, program_error::ProgramError, sysvar}, 15 | }; 16 | 17 | #[derive(Accounts)] 18 | pub struct TestInit<'info> { 19 | #[account(mut)] 20 | pub upgrade_authority: Signer<'info>, 21 | 22 | #[account( 23 | init, 24 | payer = upgrade_authority, 25 | space = Multisig::LEN, 26 | seeds = [b"multisig"], 27 | bump 28 | )] 29 | pub multisig: AccountLoader<'info, Multisig>, 30 | 31 | /// CHECK: empty PDA, will be set as authority for token accounts 32 | #[account( 33 | init, 34 | payer = upgrade_authority, 35 | space = 0, 36 | seeds = [b"transfer_authority"], 37 | bump 38 | )] 39 | pub transfer_authority: AccountInfo<'info>, 40 | 41 | #[account( 42 | init, 43 | payer = upgrade_authority, 44 | space = Launchpad::LEN, 45 | seeds = [b"launchpad"], 46 | bump 47 | )] 48 | pub launchpad: Box>, 49 | 50 | /// CHECK: lookup table account 51 | #[account(mut)] 52 | pub lookup_table: AccountInfo<'info>, 53 | 54 | /// CHECK: account constraints checked in account trait 55 | #[account( 56 | address = sysvar::slot_hashes::id() 57 | )] 58 | recent_slothashes: UncheckedAccount<'info>, 59 | 60 | /// CHECK: account constraints checked in account trait 61 | #[account( 62 | address = sysvar::instructions::id() 63 | )] 64 | instructions: UncheckedAccount<'info>, 65 | 66 | /// CHECK: lookup table program 67 | lookup_table_program: AccountInfo<'info>, 68 | 69 | system_program: Program<'info, System>, 70 | token_program: Program<'info, Token>, 71 | // remaining accounts: 1 to Multisig::MAX_SIGNERS admin signers (read-only, unsigned) 72 | } 73 | 74 | #[derive(AnchorSerialize, AnchorDeserialize)] 75 | pub struct TestInitParams { 76 | pub min_signatures: u8, 77 | pub allow_new_auctions: bool, 78 | pub allow_auction_updates: bool, 79 | pub allow_auction_refills: bool, 80 | pub allow_auction_pullouts: bool, 81 | pub allow_new_bids: bool, 82 | pub allow_withdrawals: bool, 83 | pub new_auction_fee: u64, 84 | pub auction_update_fee: u64, 85 | pub invalid_bid_fee: Fee, 86 | pub trade_fee: Fee, 87 | pub recent_slot: u64, 88 | } 89 | 90 | pub fn test_init(ctx: Context, params: &TestInitParams) -> Result<()> { 91 | if !cfg!(feature = "test") { 92 | return err!(LaunchpadError::InvalidEnvironment); 93 | } 94 | 95 | // initialize multisig, this will fail if account is already initialized 96 | let mut multisig = ctx.accounts.multisig.load_init()?; 97 | 98 | multisig.set_signers(ctx.remaining_accounts, params.min_signatures)?; 99 | 100 | // record multisig PDA bump 101 | multisig.bump = *ctx 102 | .bumps 103 | .get("multisig") 104 | .ok_or(ProgramError::InvalidSeeds)?; 105 | 106 | // record launchpad 107 | let launchpad = ctx.accounts.launchpad.as_mut(); 108 | launchpad.permissions.allow_new_auctions = params.allow_new_auctions; 109 | launchpad.permissions.allow_auction_updates = params.allow_auction_updates; 110 | launchpad.permissions.allow_auction_refills = params.allow_auction_refills; 111 | launchpad.permissions.allow_auction_pullouts = params.allow_auction_pullouts; 112 | launchpad.permissions.allow_new_bids = params.allow_new_bids; 113 | launchpad.permissions.allow_withdrawals = params.allow_withdrawals; 114 | launchpad.fees.new_auction = params.new_auction_fee; 115 | launchpad.fees.auction_update = params.auction_update_fee; 116 | launchpad.fees.invalid_bid = params.invalid_bid_fee; 117 | launchpad.fees.trade = params.trade_fee; 118 | launchpad.collected_fees.new_auction_sol = 0; 119 | launchpad.collected_fees.auction_update_sol = 0; 120 | launchpad.collected_fees.invalid_bid_usdc = 0; 121 | launchpad.collected_fees.trade_usdc = 0; 122 | launchpad.transfer_authority_bump = *ctx 123 | .bumps 124 | .get("transfer_authority") 125 | .ok_or(ProgramError::InvalidSeeds)?; 126 | launchpad.launchpad_bump = *ctx 127 | .bumps 128 | .get("launchpad") 129 | .ok_or(ProgramError::InvalidSeeds)?; 130 | 131 | if !launchpad.validate() { 132 | return err!(LaunchpadError::InvalidLaunchpadConfig); 133 | } 134 | 135 | // initialize lookup-table 136 | let transfer_authority = ctx.accounts.transfer_authority.key(); 137 | let payer = ctx.accounts.upgrade_authority.key(); 138 | let (init_table_ix, table_address) = 139 | saltp::instruction::create_lookup_table(transfer_authority, payer, params.recent_slot); 140 | require_keys_eq!(table_address, ctx.accounts.lookup_table.key()); 141 | require_keys_eq!(ctx.accounts.lookup_table_program.key(), saltp::ID); 142 | 143 | let authority_seeds: &[&[&[u8]]] = 144 | &[&[b"transfer_authority", &[launchpad.transfer_authority_bump]]]; 145 | program::invoke_signed( 146 | &init_table_ix, 147 | &[ 148 | ctx.accounts.lookup_table.to_account_info(), 149 | ctx.accounts.transfer_authority.to_account_info(), 150 | ctx.accounts.upgrade_authority.to_account_info(), 151 | ctx.accounts.system_program.to_account_info(), 152 | ], 153 | authority_seeds, 154 | )?; 155 | 156 | // add addresses to the lookup table 157 | let extend_table_ix = saltp::instruction::extend_lookup_table( 158 | table_address, 159 | transfer_authority, 160 | Some(payer), 161 | vec![ 162 | transfer_authority, 163 | ctx.accounts.launchpad.key(), 164 | ctx.accounts.recent_slothashes.key(), 165 | ctx.accounts.instructions.key(), 166 | ctx.accounts.system_program.key(), 167 | ctx.accounts.token_program.key(), 168 | ], 169 | ); 170 | program::invoke_signed( 171 | &extend_table_ix, 172 | &[ 173 | ctx.accounts.lookup_table.to_account_info(), 174 | ctx.accounts.transfer_authority.to_account_info(), 175 | ctx.accounts.upgrade_authority.to_account_info(), 176 | ctx.accounts.system_program.to_account_info(), 177 | ], 178 | authority_seeds, 179 | )?; 180 | 181 | Ok(()) 182 | } 183 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/update_auction.rs: -------------------------------------------------------------------------------- 1 | //! UpdateAuction instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{ 7 | self, 8 | auction::{Auction, CommonParams, PaymentParams, PricingParams}, 9 | launchpad::Launchpad, 10 | }, 11 | }, 12 | anchor_lang::prelude::*, 13 | }; 14 | 15 | #[derive(Accounts)] 16 | pub struct UpdateAuction<'info> { 17 | #[account(mut)] 18 | pub owner: Signer<'info>, 19 | 20 | /// CHECK: empty PDA, authority for token accounts 21 | #[account( 22 | mut, 23 | seeds = [b"transfer_authority"], 24 | bump = launchpad.transfer_authority_bump 25 | )] 26 | pub transfer_authority: AccountInfo<'info>, 27 | 28 | #[account( 29 | mut, 30 | seeds = [b"launchpad"], 31 | bump = launchpad.launchpad_bump 32 | )] 33 | pub launchpad: Box>, 34 | 35 | #[account( 36 | mut, 37 | has_one = owner, 38 | seeds = [b"auction", 39 | auction.common.name.as_bytes()], 40 | bump = auction.bump 41 | )] 42 | pub auction: Box>, 43 | 44 | system_program: Program<'info, System>, 45 | } 46 | 47 | #[derive(AnchorSerialize, AnchorDeserialize)] 48 | pub struct UpdateAuctionParams { 49 | pub common: CommonParams, 50 | pub payment: PaymentParams, 51 | pub pricing: PricingParams, 52 | pub token_ratios: Vec, 53 | } 54 | 55 | pub fn update_auction(ctx: Context, params: &UpdateAuctionParams) -> Result<()> { 56 | require!( 57 | ctx.accounts.launchpad.permissions.allow_auction_updates, 58 | LaunchpadError::AuctionUpdatesNotAllowed 59 | ); 60 | 61 | // collect fee 62 | let launchpad = ctx.accounts.launchpad.as_mut(); 63 | state::transfer_sol( 64 | ctx.accounts.owner.to_account_info(), 65 | ctx.accounts.transfer_authority.to_account_info(), 66 | ctx.accounts.system_program.to_account_info(), 67 | launchpad.fees.auction_update, 68 | )?; 69 | launchpad.collected_fees.auction_update_sol = launchpad 70 | .collected_fees 71 | .auction_update_sol 72 | .wrapping_add(launchpad.fees.auction_update); 73 | 74 | // update auction data 75 | let auction = ctx.accounts.auction.as_mut(); 76 | 77 | require!(auction.updatable, LaunchpadError::AuctionNotUpdatable); 78 | 79 | auction.common = params.common.clone(); 80 | auction.payment = params.payment; 81 | auction.pricing = params.pricing; 82 | 83 | for n in 0..(auction.num_tokens as usize) { 84 | auction.tokens[n].ratio = params.token_ratios[n]; 85 | } 86 | 87 | auction.update_time = auction.get_time()?; 88 | 89 | if !auction.validate()? { 90 | err!(LaunchpadError::InvalidAuctionConfig) 91 | } else { 92 | Ok(()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/whitelist_add.rs: -------------------------------------------------------------------------------- 1 | //! WhitelistAdd instruction handler 2 | 3 | use { 4 | crate::state::{self, auction::Auction}, 5 | anchor_lang::prelude::*, 6 | }; 7 | 8 | #[derive(Accounts)] 9 | pub struct WhitelistAdd<'info> { 10 | #[account(mut)] 11 | pub owner: Signer<'info>, 12 | 13 | #[account( 14 | has_one = owner, 15 | seeds = [b"auction", 16 | auction.common.name.as_bytes()], 17 | bump = auction.bump 18 | )] 19 | pub auction: Box>, 20 | 21 | system_program: Program<'info, System>, 22 | // remaining accounts: 23 | // Bid accounts for addresses to be whitelisted (write, unsigned) 24 | // seeds = [b"bid", address, auction.key().as_ref()] 25 | } 26 | 27 | #[derive(AnchorSerialize, AnchorDeserialize)] 28 | pub struct WhitelistAddParams { 29 | addresses: Vec, 30 | } 31 | 32 | pub fn whitelist_add<'info>( 33 | ctx: Context<'_, '_, '_, 'info, WhitelistAdd<'info>>, 34 | params: &WhitelistAddParams, 35 | ) -> Result<()> { 36 | if ctx.remaining_accounts.is_empty() || ctx.remaining_accounts.len() != params.addresses.len() { 37 | return Err(ProgramError::NotEnoughAccountKeys.into()); 38 | } 39 | 40 | // load or initialize bid accounts 41 | let mut bid_accounts = state::create_bid_accounts( 42 | ctx.remaining_accounts, 43 | ¶ms.addresses, 44 | ctx.accounts.owner.to_account_info(), 45 | &ctx.accounts.auction.key(), 46 | ctx.accounts.system_program.to_account_info(), 47 | )?; 48 | 49 | // add to white-list 50 | for bid in bid_accounts.iter_mut() { 51 | bid.whitelisted = true; 52 | } 53 | 54 | state::save_accounts(&bid_accounts)?; 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/whitelist_remove.rs: -------------------------------------------------------------------------------- 1 | //! WhitelistRemove instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | state::{self, auction::Auction, bid::Bid}, 7 | }, 8 | anchor_lang::{prelude::*, AccountsClose}, 9 | }; 10 | 11 | #[derive(Accounts)] 12 | pub struct WhitelistRemove<'info> { 13 | #[account(mut)] 14 | pub owner: Signer<'info>, 15 | 16 | #[account( 17 | has_one = owner, 18 | seeds = [b"auction", 19 | auction.common.name.as_bytes()], 20 | bump = auction.bump 21 | )] 22 | pub auction: Box>, 23 | // remaining accounts: 24 | // Bid accounts to be removed from the whitelist (write, unsigned) 25 | // seeds = [b"bid", address, auction.key().as_ref()] 26 | } 27 | 28 | #[derive(AnchorSerialize, AnchorDeserialize)] 29 | pub struct WhitelistRemoveParams {} 30 | 31 | pub fn whitelist_remove<'info>( 32 | ctx: Context<'_, '_, '_, 'info, WhitelistRemove<'info>>, 33 | _params: &WhitelistRemoveParams, 34 | ) -> Result<()> { 35 | if ctx.remaining_accounts.is_empty() { 36 | return Err(ProgramError::NotEnoughAccountKeys.into()); 37 | } 38 | 39 | let auction_ended = ctx 40 | .accounts 41 | .auction 42 | .is_ended(ctx.accounts.auction.get_time()?, true); 43 | let mut bid_accounts = state::load_accounts::(ctx.remaining_accounts, &crate::ID)?; 44 | for bid in bid_accounts.iter_mut() { 45 | // validate bid address 46 | let expected_bid_key = Pubkey::create_program_address( 47 | &[ 48 | b"bid", 49 | bid.owner.as_ref(), 50 | ctx.accounts.auction.key().as_ref(), 51 | &[bid.bump], 52 | ], 53 | &crate::ID, 54 | ) 55 | .map_err(|_| LaunchpadError::InvalidBidAddress)?; 56 | require_keys_eq!( 57 | bid.key(), 58 | expected_bid_key, 59 | LaunchpadError::InvalidBidAddress 60 | ); 61 | 62 | // remove from white-list or close the account 63 | if auction_ended && bid.seller_initialized { 64 | bid.close(ctx.accounts.owner.to_account_info())?; 65 | } else { 66 | bid.whitelisted = false; 67 | bid.exit(&crate::ID)?; 68 | } 69 | } 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/withdraw_fees.rs: -------------------------------------------------------------------------------- 1 | //! WithdrawFees instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | math, 7 | state::{ 8 | self, 9 | custody::Custody, 10 | launchpad::Launchpad, 11 | multisig::{AdminInstruction, Multisig}, 12 | }, 13 | }, 14 | anchor_lang::prelude::*, 15 | anchor_spl::token::{Token, TokenAccount}, 16 | solana_program::sysvar, 17 | }; 18 | 19 | #[derive(Accounts)] 20 | pub struct WithdrawFees<'info> { 21 | #[account()] 22 | pub admin: Signer<'info>, 23 | 24 | #[account( 25 | mut, 26 | seeds = [b"multisig"], 27 | bump = multisig.load()?.bump 28 | )] 29 | pub multisig: AccountLoader<'info, Multisig>, 30 | 31 | /// CHECK: empty PDA, authority for token accounts 32 | #[account( 33 | mut, 34 | seeds = [b"transfer_authority"], 35 | bump = launchpad.transfer_authority_bump 36 | )] 37 | pub transfer_authority: AccountInfo<'info>, 38 | 39 | #[account( 40 | seeds = [b"launchpad"], 41 | bump = launchpad.launchpad_bump 42 | )] 43 | pub launchpad: Box>, 44 | 45 | #[account( 46 | mut, 47 | seeds = [b"custody", 48 | custody.mint.key().as_ref()], 49 | bump = custody.bump 50 | )] 51 | pub custody: Box>, 52 | 53 | #[account( 54 | mut, 55 | constraint = custody_token_account.key() == custody.token_account.key() 56 | )] 57 | pub custody_token_account: Box>, 58 | 59 | #[account( 60 | mut, 61 | constraint = receiving_token_account.mint == custody_token_account.mint 62 | )] 63 | pub receiving_token_account: Box>, 64 | 65 | /// CHECK: SOL fees receiving account 66 | #[account( 67 | mut, 68 | constraint = receiving_sol_account.data_is_empty() 69 | )] 70 | pub receiving_sol_account: AccountInfo<'info>, 71 | 72 | token_program: Program<'info, Token>, 73 | } 74 | 75 | #[derive(AnchorSerialize, AnchorDeserialize)] 76 | pub struct WithdrawFeesParams { 77 | pub token_amount: u64, 78 | pub sol_amount: u64, 79 | } 80 | 81 | pub fn withdraw_fees<'info>( 82 | ctx: Context<'_, '_, '_, 'info, WithdrawFees<'info>>, 83 | params: &WithdrawFeesParams, 84 | ) -> Result { 85 | // validate inputs 86 | require!( 87 | params.token_amount > 0 || params.sol_amount > 0, 88 | LaunchpadError::InvalidTokenAmount 89 | ); 90 | 91 | // validate signatures 92 | let mut multisig = ctx.accounts.multisig.load_mut()?; 93 | 94 | let signatures_left = multisig.sign_multisig( 95 | &ctx.accounts.admin, 96 | &Multisig::get_account_infos(&ctx)[1..], 97 | &Multisig::get_instruction_data(AdminInstruction::WithdrawFees, params)?, 98 | )?; 99 | if signatures_left > 0 { 100 | msg!( 101 | "Instruction has been signed but more signatures are required: {}", 102 | signatures_left 103 | ); 104 | return Ok(signatures_left); 105 | } 106 | 107 | // transfer token fees from the custody to the receiver 108 | if params.token_amount > 0 { 109 | let custody = ctx.accounts.custody.as_mut(); 110 | msg!( 111 | "Withdraw token fees: {} / {}", 112 | params.token_amount, 113 | custody.collected_fees 114 | ); 115 | if custody.collected_fees < params.token_amount { 116 | return Err(ProgramError::InsufficientFunds.into()); 117 | } 118 | custody.collected_fees = math::checked_sub(custody.collected_fees, params.token_amount)?; 119 | 120 | ctx.accounts.launchpad.transfer_tokens( 121 | ctx.accounts.custody_token_account.to_account_info(), 122 | ctx.accounts.receiving_token_account.to_account_info(), 123 | ctx.accounts.transfer_authority.to_account_info(), 124 | ctx.accounts.token_program.to_account_info(), 125 | params.token_amount, 126 | )?; 127 | } 128 | 129 | // transfer sol fees from the custody to the receiver 130 | if params.sol_amount > 0 { 131 | let balance = ctx.accounts.transfer_authority.try_lamports()?; 132 | let min_balance = sysvar::rent::Rent::get().unwrap().minimum_balance(0); 133 | let available_balance = if balance > min_balance { 134 | math::checked_sub(balance, min_balance)? 135 | } else { 136 | 0 137 | }; 138 | msg!( 139 | "Withdraw SOL fees: {} / {}", 140 | params.sol_amount, 141 | available_balance 142 | ); 143 | if available_balance < params.sol_amount { 144 | return Err(ProgramError::InsufficientFunds.into()); 145 | } 146 | 147 | state::transfer_sol_from_owned( 148 | ctx.accounts.transfer_authority.to_account_info(), 149 | ctx.accounts.receiving_sol_account.to_account_info(), 150 | params.sol_amount, 151 | )?; 152 | } 153 | 154 | Ok(0) 155 | } 156 | -------------------------------------------------------------------------------- /programs/launchpad/src/instructions/withdraw_funds.rs: -------------------------------------------------------------------------------- 1 | //! WithdrawFunds instruction handler 2 | 3 | use { 4 | crate::{ 5 | error::LaunchpadError, 6 | math, 7 | state::{ 8 | auction::Auction, custody::Custody, launchpad::Launchpad, seller_balance::SellerBalance, 9 | }, 10 | }, 11 | anchor_lang::prelude::*, 12 | anchor_spl::token::{Token, TokenAccount}, 13 | }; 14 | 15 | #[derive(Accounts)] 16 | pub struct WithdrawFunds<'info> { 17 | #[account()] 18 | pub owner: Signer<'info>, 19 | 20 | /// CHECK: empty PDA, authority for token accounts 21 | #[account( 22 | seeds = [b"transfer_authority"], 23 | bump = launchpad.transfer_authority_bump 24 | )] 25 | pub transfer_authority: AccountInfo<'info>, 26 | 27 | #[account( 28 | seeds = [b"launchpad"], 29 | bump = launchpad.launchpad_bump 30 | )] 31 | pub launchpad: Box>, 32 | 33 | #[account( 34 | mut, 35 | has_one = owner, 36 | seeds = [b"auction", 37 | auction.common.name.as_bytes()], 38 | bump = auction.bump 39 | )] 40 | pub auction: Box>, 41 | 42 | #[account( 43 | seeds = [b"custody", 44 | custody.mint.as_ref()], 45 | bump = custody.bump 46 | )] 47 | pub custody: Box>, 48 | 49 | #[account( 50 | mut, 51 | constraint = custody_token_account.key() == custody.token_account.key() 52 | )] 53 | pub custody_token_account: Box>, 54 | 55 | #[account( 56 | mut, 57 | has_one = owner, 58 | constraint = seller_balance.custody == custody.key(), 59 | seeds = [b"seller_balance", 60 | auction.owner.as_ref(), 61 | seller_balance.custody.as_ref()], 62 | bump = seller_balance.bump 63 | )] 64 | pub seller_balance: Box>, 65 | 66 | #[account( 67 | mut, 68 | constraint = receiving_account.mint == custody_token_account.mint, 69 | has_one = owner 70 | )] 71 | pub receiving_account: Box>, 72 | 73 | token_program: Program<'info, Token>, 74 | } 75 | 76 | #[derive(AnchorSerialize, AnchorDeserialize)] 77 | pub struct WithdrawFundsParams { 78 | pub amount: u64, 79 | } 80 | 81 | pub fn withdraw_funds(ctx: Context, params: &WithdrawFundsParams) -> Result<()> { 82 | require!( 83 | ctx.accounts.launchpad.permissions.allow_withdrawals, 84 | LaunchpadError::WithdrawalsNotAllowed 85 | ); 86 | 87 | // validate inputs 88 | require_gt!(params.amount, 0u64, LaunchpadError::InvalidTokenAmount); 89 | 90 | // transfer fees from the custody to the receiver 91 | msg!( 92 | "Withdraw funds: {} / {}", 93 | params.amount, 94 | ctx.accounts.custody_token_account.amount 95 | ); 96 | 97 | let seller_balance = ctx.accounts.seller_balance.as_mut(); 98 | if seller_balance.balance < params.amount { 99 | return Err(ProgramError::InsufficientFunds.into()); 100 | } 101 | seller_balance.balance = math::checked_sub(seller_balance.balance, params.amount)?; 102 | 103 | ctx.accounts.launchpad.transfer_tokens( 104 | ctx.accounts.custody_token_account.to_account_info(), 105 | ctx.accounts.receiving_account.to_account_info(), 106 | ctx.accounts.transfer_authority.to_account_info(), 107 | ctx.accounts.token_program.to_account_info(), 108 | params.amount, 109 | )?; 110 | 111 | Ok(()) 112 | } 113 | -------------------------------------------------------------------------------- /programs/launchpad/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Launchpad program entrypoint 2 | 3 | #![allow(clippy::result_large_err)] 4 | 5 | mod error; 6 | mod instructions; 7 | mod math; 8 | mod state; 9 | 10 | use {anchor_lang::prelude::*, instructions::*}; 11 | 12 | solana_security_txt::security_txt! { 13 | name: "Launchpad", 14 | project_url: "https://github.com/solana-labs/solana-program-library/tree/master/launchpad", 15 | contacts: "email:solana.farms@protonmail.com", 16 | policy: "", 17 | preferred_languages: "en", 18 | auditors: "" 19 | } 20 | 21 | declare_id!("LPD1BCWvd499Rk7aG5zG8uieUTTqba1JaYkUpXjUN9q"); 22 | 23 | #[program] 24 | pub mod launchpad { 25 | use super::*; 26 | 27 | // admin instructions 28 | 29 | pub fn delete_auction<'info>( 30 | ctx: Context<'_, '_, '_, 'info, DeleteAuction<'info>>, 31 | params: DeleteAuctionParams, 32 | ) -> Result { 33 | instructions::delete_auction(ctx, ¶ms) 34 | } 35 | 36 | pub fn init(ctx: Context, params: InitParams) -> Result<()> { 37 | instructions::init(ctx, ¶ms) 38 | } 39 | 40 | pub fn init_custody<'info>( 41 | ctx: Context<'_, '_, '_, 'info, InitCustody<'info>>, 42 | params: InitCustodyParams, 43 | ) -> Result { 44 | instructions::init_custody(ctx, ¶ms) 45 | } 46 | 47 | pub fn set_admin_signers<'info>( 48 | ctx: Context<'_, '_, '_, 'info, SetAdminSigners<'info>>, 49 | params: SetAdminSignersParams, 50 | ) -> Result { 51 | instructions::set_admin_signers(ctx, ¶ms) 52 | } 53 | 54 | pub fn set_fees<'info>( 55 | ctx: Context<'_, '_, '_, 'info, SetFees<'info>>, 56 | params: SetFeesParams, 57 | ) -> Result { 58 | instructions::set_fees(ctx, ¶ms) 59 | } 60 | 61 | pub fn set_oracle_config<'info>( 62 | ctx: Context<'_, '_, '_, 'info, SetOracleConfig<'info>>, 63 | params: SetOracleConfigParams, 64 | ) -> Result { 65 | instructions::set_oracle_config(ctx, ¶ms) 66 | } 67 | 68 | pub fn set_permissions<'info>( 69 | ctx: Context<'_, '_, '_, 'info, SetPermissions<'info>>, 70 | params: SetPermissionsParams, 71 | ) -> Result { 72 | instructions::set_permissions(ctx, ¶ms) 73 | } 74 | 75 | pub fn withdraw_fees<'info>( 76 | ctx: Context<'_, '_, '_, 'info, WithdrawFees<'info>>, 77 | params: WithdrawFeesParams, 78 | ) -> Result { 79 | instructions::withdraw_fees(ctx, ¶ms) 80 | } 81 | 82 | // test instructions 83 | 84 | pub fn set_test_oracle_price<'info>( 85 | ctx: Context<'_, '_, '_, 'info, SetTestOraclePrice<'info>>, 86 | params: SetTestOraclePriceParams, 87 | ) -> Result { 88 | instructions::set_test_oracle_price(ctx, ¶ms) 89 | } 90 | 91 | pub fn set_test_time<'info>( 92 | ctx: Context<'_, '_, '_, 'info, SetTestTime<'info>>, 93 | params: SetTestTimeParams, 94 | ) -> Result { 95 | instructions::set_test_time(ctx, ¶ms) 96 | } 97 | 98 | pub fn test_init(ctx: Context, params: TestInitParams) -> Result<()> { 99 | instructions::test_init(ctx, ¶ms) 100 | } 101 | 102 | // seller instructions 103 | 104 | pub fn add_tokens(ctx: Context, params: AddTokensParams) -> Result<()> { 105 | instructions::add_tokens(ctx, ¶ms) 106 | } 107 | 108 | pub fn disable_auction( 109 | ctx: Context, 110 | params: DisableAuctionParams, 111 | ) -> Result<()> { 112 | instructions::disable_auction(ctx, ¶ms) 113 | } 114 | 115 | pub fn enable_auction(ctx: Context, params: EnableAuctionParams) -> Result<()> { 116 | instructions::enable_auction(ctx, ¶ms) 117 | } 118 | 119 | pub fn init_auction<'info>( 120 | ctx: Context<'_, '_, '_, 'info, InitAuction<'info>>, 121 | params: InitAuctionParams, 122 | ) -> Result<()> { 123 | instructions::init_auction(ctx, ¶ms) 124 | } 125 | 126 | pub fn remove_tokens(ctx: Context, params: RemoveTokensParams) -> Result<()> { 127 | instructions::remove_tokens(ctx, ¶ms) 128 | } 129 | 130 | pub fn update_auction(ctx: Context, params: UpdateAuctionParams) -> Result<()> { 131 | instructions::update_auction(ctx, ¶ms) 132 | } 133 | 134 | pub fn whitelist_add<'info>( 135 | ctx: Context<'_, '_, '_, 'info, WhitelistAdd<'info>>, 136 | params: WhitelistAddParams, 137 | ) -> Result<()> { 138 | instructions::whitelist_add(ctx, ¶ms) 139 | } 140 | 141 | pub fn whitelist_remove<'info>( 142 | ctx: Context<'_, '_, '_, 'info, WhitelistRemove<'info>>, 143 | params: WhitelistRemoveParams, 144 | ) -> Result<()> { 145 | instructions::whitelist_remove(ctx, ¶ms) 146 | } 147 | 148 | pub fn withdraw_funds(ctx: Context, params: WithdrawFundsParams) -> Result<()> { 149 | instructions::withdraw_funds(ctx, ¶ms) 150 | } 151 | 152 | // buyer instructions 153 | 154 | pub fn cancel_bid(ctx: Context, params: CancelBidParams) -> Result<()> { 155 | instructions::cancel_bid(ctx, ¶ms) 156 | } 157 | 158 | pub fn get_auction_amount( 159 | ctx: Context, 160 | params: GetAuctionAmountParams, 161 | ) -> Result { 162 | instructions::get_auction_amount(ctx, ¶ms) 163 | } 164 | 165 | pub fn get_auction_price( 166 | ctx: Context, 167 | params: GetAuctionPriceParams, 168 | ) -> Result { 169 | instructions::get_auction_price(ctx, ¶ms) 170 | } 171 | 172 | pub fn place_bid<'info>( 173 | ctx: Context<'_, '_, '_, 'info, PlaceBid<'info>>, 174 | params: PlaceBidParams, 175 | ) -> Result<()> { 176 | instructions::place_bid(ctx, ¶ms) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /programs/launchpad/src/math.rs: -------------------------------------------------------------------------------- 1 | //! Common math routines. 2 | 3 | #![allow(dead_code)] 4 | 5 | use {crate::error::LaunchpadError, anchor_lang::prelude::*, std::fmt::Display}; 6 | 7 | pub fn checked_add(arg1: T, arg2: T) -> Result 8 | where 9 | T: num_traits::PrimInt + Display, 10 | { 11 | if let Some(res) = arg1.checked_add(&arg2) { 12 | Ok(res) 13 | } else { 14 | msg!("Error: Overflow in {} + {}", arg1, arg2); 15 | err!(LaunchpadError::MathOverflow) 16 | } 17 | } 18 | 19 | pub fn checked_sub(arg1: T, arg2: T) -> Result 20 | where 21 | T: num_traits::PrimInt + Display, 22 | { 23 | if let Some(res) = arg1.checked_sub(&arg2) { 24 | Ok(res) 25 | } else { 26 | msg!("Error: Overflow in {} - {}", arg1, arg2); 27 | err!(LaunchpadError::MathOverflow) 28 | } 29 | } 30 | 31 | pub fn checked_div(arg1: T, arg2: T) -> Result 32 | where 33 | T: num_traits::PrimInt + Display, 34 | { 35 | if let Some(res) = arg1.checked_div(&arg2) { 36 | Ok(res) 37 | } else { 38 | msg!("Error: Overflow in {} / {}", arg1, arg2); 39 | err!(LaunchpadError::MathOverflow) 40 | } 41 | } 42 | 43 | pub fn checked_float_div(arg1: T, arg2: T) -> Result 44 | where 45 | T: num_traits::Float + Display, 46 | { 47 | if arg2 == T::zero() { 48 | msg!("Error: Overflow in {} / {}", arg1, arg2); 49 | return err!(LaunchpadError::MathOverflow); 50 | } 51 | let res = arg1 / arg2; 52 | if !res.is_finite() { 53 | msg!("Error: Overflow in {} / {}", arg1, arg2); 54 | err!(LaunchpadError::MathOverflow) 55 | } else { 56 | Ok(res) 57 | } 58 | } 59 | 60 | pub fn checked_ceil_div(arg1: T, arg2: T) -> Result 61 | where 62 | T: num_traits::PrimInt + Display, 63 | { 64 | if arg1 > T::zero() { 65 | if arg1 == arg2 && arg2 != T::zero() { 66 | return Ok(T::one()); 67 | } 68 | if let Some(res) = (arg1 - T::one()).checked_div(&arg2) { 69 | Ok(res + T::one()) 70 | } else { 71 | msg!("Error: Overflow in {} / {}", arg1, arg2); 72 | err!(LaunchpadError::MathOverflow) 73 | } 74 | } else if let Some(res) = arg1.checked_div(&arg2) { 75 | Ok(res) 76 | } else { 77 | msg!("Error: Overflow in {} / {}", arg1, arg2); 78 | err!(LaunchpadError::MathOverflow) 79 | } 80 | } 81 | 82 | pub fn checked_decimal_div( 83 | coefficient1: u64, 84 | exponent1: i32, 85 | coefficient2: u64, 86 | exponent2: i32, 87 | target_exponent: i32, 88 | ) -> Result { 89 | // compute scale factor for the dividend 90 | let mut scale_factor = 0; 91 | let mut target_power = checked_sub(checked_sub(exponent1, exponent2)?, target_exponent)?; 92 | if exponent1 > 0 { 93 | scale_factor = checked_add(scale_factor, exponent1)?; 94 | } 95 | if exponent2 < 0 { 96 | scale_factor = checked_sub(scale_factor, exponent2)?; 97 | target_power = checked_add(target_power, exponent2)?; 98 | } 99 | if target_exponent < 0 { 100 | scale_factor = checked_sub(scale_factor, target_exponent)?; 101 | target_power = checked_add(target_power, target_exponent)?; 102 | } 103 | let scaled_coeff1 = if scale_factor > 0 { 104 | checked_mul( 105 | coefficient1 as u128, 106 | checked_pow(10u128, scale_factor as usize)?, 107 | )? 108 | } else { 109 | coefficient1 as u128 110 | }; 111 | 112 | if target_power >= 0 { 113 | checked_as_u64(checked_mul( 114 | checked_div(scaled_coeff1, coefficient2 as u128)?, 115 | checked_pow(10u128, target_power as usize)?, 116 | )?) 117 | } else { 118 | checked_as_u64(checked_div( 119 | checked_div(scaled_coeff1, coefficient2 as u128)?, 120 | checked_pow(10u128, (-target_power) as usize)?, 121 | )?) 122 | } 123 | } 124 | 125 | pub fn checked_decimal_ceil_div( 126 | coefficient1: u64, 127 | exponent1: i32, 128 | coefficient2: u64, 129 | exponent2: i32, 130 | target_exponent: i32, 131 | ) -> Result { 132 | // compute scale factor for the dividend 133 | let mut scale_factor = 0; 134 | let mut target_power = checked_sub(checked_sub(exponent1, exponent2)?, target_exponent)?; 135 | if exponent1 > 0 { 136 | scale_factor = checked_add(scale_factor, exponent1)?; 137 | } 138 | if exponent2 < 0 { 139 | scale_factor = checked_sub(scale_factor, exponent2)?; 140 | target_power = checked_add(target_power, exponent2)?; 141 | } 142 | if target_exponent < 0 { 143 | scale_factor = checked_sub(scale_factor, target_exponent)?; 144 | target_power = checked_add(target_power, target_exponent)?; 145 | } 146 | let scaled_coeff1 = if scale_factor > 0 { 147 | checked_mul( 148 | coefficient1 as u128, 149 | checked_pow(10u128, scale_factor as usize)?, 150 | )? 151 | } else { 152 | coefficient1 as u128 153 | }; 154 | 155 | if target_power >= 0 { 156 | checked_as_u64(checked_mul( 157 | checked_ceil_div(scaled_coeff1, coefficient2 as u128)?, 158 | checked_pow(10u128, target_power as usize)?, 159 | )?) 160 | } else { 161 | checked_as_u64(checked_div( 162 | checked_ceil_div(scaled_coeff1, coefficient2 as u128)?, 163 | checked_pow(10u128, (-target_power) as usize)?, 164 | )?) 165 | } 166 | } 167 | 168 | pub fn checked_token_div( 169 | amount1: u64, 170 | decimals1: u8, 171 | amount2: u64, 172 | decimals2: u8, 173 | ) -> Result<(u64, u8)> { 174 | let target_decimals = std::cmp::max(decimals1, decimals2); 175 | Ok(( 176 | checked_decimal_div( 177 | amount1, 178 | -(decimals1 as i32), 179 | amount2, 180 | -(decimals2 as i32), 181 | -(target_decimals as i32), 182 | )?, 183 | target_decimals, 184 | )) 185 | } 186 | 187 | pub fn checked_mul(arg1: T, arg2: T) -> Result 188 | where 189 | T: num_traits::PrimInt + Display, 190 | { 191 | if let Some(res) = arg1.checked_mul(&arg2) { 192 | Ok(res) 193 | } else { 194 | msg!("Error: Overflow in {} * {}", arg1, arg2); 195 | err!(LaunchpadError::MathOverflow) 196 | } 197 | } 198 | 199 | pub fn checked_float_mul(arg1: T, arg2: T) -> Result 200 | where 201 | T: num_traits::Float + Display, 202 | { 203 | let res = arg1 * arg2; 204 | if !res.is_finite() { 205 | msg!("Error: Overflow in {} * {}", arg1, arg2); 206 | err!(LaunchpadError::MathOverflow) 207 | } else { 208 | Ok(res) 209 | } 210 | } 211 | 212 | pub fn checked_decimal_mul( 213 | coefficient1: u64, 214 | exponent1: i32, 215 | coefficient2: u64, 216 | exponent2: i32, 217 | target_exponent: i32, 218 | ) -> Result { 219 | let target_power = checked_sub(checked_add(exponent1, exponent2)?, target_exponent)?; 220 | if target_power >= 0 { 221 | checked_as_u64(checked_mul( 222 | checked_mul(coefficient1 as u128, coefficient2 as u128)?, 223 | checked_pow(10u128, target_power as usize)?, 224 | )?) 225 | } else { 226 | checked_as_u64(checked_div( 227 | checked_mul(coefficient1 as u128, coefficient2 as u128)?, 228 | checked_pow(10u128, (-target_power) as usize)?, 229 | )?) 230 | } 231 | } 232 | 233 | pub fn checked_decimal_ceil_mul( 234 | coefficient1: u64, 235 | exponent1: i32, 236 | coefficient2: u64, 237 | exponent2: i32, 238 | target_exponent: i32, 239 | ) -> Result { 240 | let target_power = checked_sub(checked_add(exponent1, exponent2)?, target_exponent)?; 241 | if target_power >= 0 { 242 | checked_as_u64(checked_mul( 243 | checked_mul(coefficient1 as u128, coefficient2 as u128)?, 244 | checked_pow(10u128, target_power as usize)?, 245 | )?) 246 | } else { 247 | checked_as_u64(checked_ceil_div( 248 | checked_mul(coefficient1 as u128, coefficient2 as u128)?, 249 | checked_pow(10u128, (-target_power) as usize)?, 250 | )?) 251 | } 252 | } 253 | 254 | pub fn checked_token_mul( 255 | amount1: u64, 256 | decimals1: u8, 257 | amount2: u64, 258 | decimals2: u8, 259 | ) -> Result<(u64, u8)> { 260 | let target_decimals = std::cmp::max(decimals1, decimals2); 261 | Ok(( 262 | checked_decimal_mul( 263 | amount1, 264 | -(decimals1 as i32), 265 | amount2, 266 | -(decimals2 as i32), 267 | -(target_decimals as i32), 268 | )?, 269 | target_decimals, 270 | )) 271 | } 272 | 273 | pub fn checked_pow(arg: T, exp: usize) -> Result 274 | where 275 | T: num_traits::PrimInt + Display, 276 | { 277 | if let Some(res) = num_traits::checked_pow(arg, exp) { 278 | Ok(res) 279 | } else { 280 | msg!("Error: Overflow in {} ^ {}", arg, exp); 281 | err!(LaunchpadError::MathOverflow) 282 | } 283 | } 284 | 285 | pub fn checked_powf(arg: f64, exp: f64) -> Result { 286 | let res = f64::powf(arg, exp); 287 | if res.is_finite() { 288 | Ok(res) 289 | } else { 290 | msg!("Error: Overflow in {} ^ {}", arg, exp); 291 | err!(LaunchpadError::MathOverflow) 292 | } 293 | } 294 | 295 | pub fn checked_powi(arg: f64, exp: i32) -> Result { 296 | let res = if exp > 0 { 297 | f64::powi(arg, exp) 298 | } else { 299 | // wrokaround due to f64::powi() not working properly on-chain with negative exponent 300 | checked_float_div(1.0, f64::powi(arg, -exp))? 301 | }; 302 | if res.is_finite() { 303 | Ok(res) 304 | } else { 305 | msg!("Error: Overflow in {} ^ {}", arg, exp); 306 | err!(LaunchpadError::MathOverflow) 307 | } 308 | } 309 | 310 | pub fn checked_as_u64(arg: T) -> Result 311 | where 312 | T: Display + num_traits::ToPrimitive + Clone, 313 | { 314 | let option: Option = num_traits::NumCast::from(arg.clone()); 315 | if let Some(res) = option { 316 | Ok(res) 317 | } else { 318 | msg!("Error: Overflow in {} as u64", arg); 319 | err!(LaunchpadError::MathOverflow) 320 | } 321 | } 322 | 323 | pub fn checked_as_u128(arg: T) -> Result 324 | where 325 | T: Display + num_traits::ToPrimitive + Clone, 326 | { 327 | let option: Option = num_traits::NumCast::from(arg.clone()); 328 | if let Some(res) = option { 329 | Ok(res) 330 | } else { 331 | msg!("Error: Overflow in {} as u128", arg); 332 | err!(LaunchpadError::MathOverflow) 333 | } 334 | } 335 | 336 | pub fn to_ui_amount(amount: u64, decimals: u8) -> Result { 337 | checked_float_div(amount as f64, checked_powi(10.0, decimals as i32)?) 338 | } 339 | 340 | pub fn to_token_amount(ui_amount: f64, decimals: u8) -> Result { 341 | checked_as_u64(checked_float_mul( 342 | ui_amount, 343 | checked_powi(10.0, decimals as i32)?, 344 | )?) 345 | } 346 | -------------------------------------------------------------------------------- /programs/launchpad/src/state.rs: -------------------------------------------------------------------------------- 1 | // Program state handling. 2 | 3 | pub mod auction; 4 | pub mod bid; 5 | pub mod custody; 6 | pub mod launchpad; 7 | pub mod multisig; 8 | pub mod oracle; 9 | pub mod seller_balance; 10 | 11 | use { 12 | crate::{error::LaunchpadError, math, state::bid::Bid}, 13 | anchor_lang::{prelude::*, Discriminator}, 14 | anchor_spl::token::{Mint, TokenAccount}, 15 | }; 16 | 17 | pub fn is_empty_account(account_info: &AccountInfo) -> Result { 18 | Ok(account_info.try_data_is_empty()? || account_info.try_lamports()? == 0) 19 | } 20 | 21 | pub fn initialize_account<'info>( 22 | payer: AccountInfo<'info>, 23 | target_account: AccountInfo<'info>, 24 | system_program: AccountInfo<'info>, 25 | owner: &Pubkey, 26 | seeds: &[&[&[u8]]], 27 | len: usize, 28 | ) -> Result<()> { 29 | let current_lamports = target_account.try_lamports()?; 30 | if current_lamports == 0 { 31 | // if account doesn't have any lamports initialize it with conventional create_account 32 | let lamports = Rent::get()?.minimum_balance(len); 33 | let cpi_accounts = anchor_lang::system_program::CreateAccount { 34 | from: payer, 35 | to: target_account, 36 | }; 37 | let cpi_context = anchor_lang::context::CpiContext::new(system_program, cpi_accounts); 38 | anchor_lang::system_program::create_account( 39 | cpi_context.with_signer(seeds), 40 | lamports, 41 | math::checked_as_u64(len)?, 42 | owner, 43 | )?; 44 | } else { 45 | // fund the account for rent exemption 46 | let required_lamports = Rent::get()? 47 | .minimum_balance(len) 48 | .saturating_sub(current_lamports); 49 | if required_lamports > 0 { 50 | let cpi_accounts = anchor_lang::system_program::Transfer { 51 | from: payer, 52 | to: target_account.clone(), 53 | }; 54 | let cpi_context = 55 | anchor_lang::context::CpiContext::new(system_program.clone(), cpi_accounts); 56 | anchor_lang::system_program::transfer(cpi_context, required_lamports)?; 57 | } 58 | // allocate space 59 | let cpi_accounts = anchor_lang::system_program::Allocate { 60 | account_to_allocate: target_account.clone(), 61 | }; 62 | let cpi_context = 63 | anchor_lang::context::CpiContext::new(system_program.clone(), cpi_accounts); 64 | anchor_lang::system_program::allocate( 65 | cpi_context.with_signer(seeds), 66 | math::checked_as_u64(len)?, 67 | )?; 68 | // assign to the program 69 | let cpi_accounts = anchor_lang::system_program::Assign { 70 | account_to_assign: target_account, 71 | }; 72 | let cpi_context = anchor_lang::context::CpiContext::new(system_program, cpi_accounts); 73 | anchor_lang::system_program::assign(cpi_context.with_signer(seeds), owner)?; 74 | } 75 | Ok(()) 76 | } 77 | 78 | #[allow(clippy::too_many_arguments)] 79 | pub fn initialize_token_account<'info>( 80 | payer: AccountInfo<'info>, 81 | token_account: AccountInfo<'info>, 82 | mint: AccountInfo<'info>, 83 | system_program: AccountInfo<'info>, 84 | token_program: AccountInfo<'info>, 85 | rent: AccountInfo<'info>, 86 | authority: AccountInfo<'info>, 87 | seeds: &[&[&[u8]]], 88 | ) -> Result<()> { 89 | initialize_account( 90 | payer, 91 | token_account.clone(), 92 | system_program.clone(), 93 | &anchor_spl::token::ID, 94 | seeds, 95 | TokenAccount::LEN, 96 | )?; 97 | 98 | let cpi_accounts = anchor_spl::token::InitializeAccount { 99 | account: token_account, 100 | mint, 101 | authority, 102 | rent, 103 | }; 104 | let cpi_context = anchor_lang::context::CpiContext::new(token_program, cpi_accounts); 105 | anchor_spl::token::initialize_account(cpi_context.with_signer(seeds)) 106 | } 107 | 108 | pub fn close_token_account<'info>( 109 | receiver: AccountInfo<'info>, 110 | token_account: AccountInfo<'info>, 111 | token_program: AccountInfo<'info>, 112 | authority: AccountInfo<'info>, 113 | seeds: &[&[&[u8]]], 114 | ) -> Result<()> { 115 | let cpi_accounts = anchor_spl::token::CloseAccount { 116 | account: token_account, 117 | destination: receiver, 118 | authority, 119 | }; 120 | let cpi_context = anchor_lang::context::CpiContext::new(token_program, cpi_accounts); 121 | anchor_spl::token::close_account(cpi_context.with_signer(seeds)) 122 | } 123 | 124 | pub fn load_accounts<'a, T: AccountSerialize + AccountDeserialize + Owner + Clone>( 125 | accounts: &[AccountInfo<'a>], 126 | expected_owner: &Pubkey, 127 | ) -> Result>> { 128 | let mut res: Vec> = Vec::with_capacity(accounts.len()); 129 | 130 | for account in accounts { 131 | if account.owner != expected_owner { 132 | return Err(ProgramError::IllegalOwner.into()); 133 | } 134 | res.push(Account::::try_from(account)?); 135 | } 136 | 137 | if res.is_empty() { 138 | return Err(ProgramError::NotEnoughAccountKeys.into()); 139 | } 140 | 141 | Ok(res) 142 | } 143 | 144 | pub fn save_accounts( 145 | accounts: &[Account], 146 | ) -> Result<()> { 147 | for account in accounts { 148 | account.exit(&crate::ID)?; 149 | } 150 | Ok(()) 151 | } 152 | 153 | pub fn create_bid_accounts<'a>( 154 | accounts: &[AccountInfo<'a>], 155 | owners: &[Pubkey], 156 | payer: AccountInfo<'a>, 157 | auction: &Pubkey, 158 | system_program: AccountInfo<'a>, 159 | ) -> Result>> { 160 | let mut res: Vec> = Vec::with_capacity(accounts.len()); 161 | 162 | for (bid_account, owner) in accounts.iter().zip(owners) { 163 | // validate bid address 164 | let (expected_bid_key, bump) = 165 | Pubkey::find_program_address(&[b"bid", owner.as_ref(), auction.as_ref()], &crate::ID); 166 | require_keys_eq!( 167 | bid_account.key(), 168 | expected_bid_key, 169 | LaunchpadError::InvalidBidAddress 170 | ); 171 | // initialize the account or check the owner 172 | let mut initialized = false; 173 | if bid_account.data_is_empty() { 174 | initialize_account( 175 | payer.clone(), 176 | bid_account.clone(), 177 | system_program.clone(), 178 | &crate::ID, 179 | &[&[b"bid", owner.key().as_ref(), auction.as_ref(), &[bump]]], 180 | Bid::LEN, 181 | )?; 182 | let mut bid_data = bid_account.try_borrow_mut_data()?; 183 | bid_data[..8].copy_from_slice(Bid::discriminator().as_slice()); 184 | initialized = true; 185 | } else if bid_account.owner != &crate::ID { 186 | return Err(ProgramError::IllegalOwner.into()); 187 | } 188 | 189 | let mut bid = Account::::try_from(bid_account)?; 190 | if initialized { 191 | bid.owner = *owner; 192 | bid.auction = *auction; 193 | bid.seller_initialized = true; 194 | bid.bump = bump; 195 | } 196 | res.push(bid); 197 | } 198 | 199 | Ok(res) 200 | } 201 | 202 | #[allow(clippy::too_many_arguments)] 203 | pub fn create_token_accounts<'a>( 204 | accounts: &[AccountInfo<'a>], 205 | mints: &[AccountInfo<'a>], 206 | authority: AccountInfo<'a>, 207 | payer: AccountInfo<'a>, 208 | auction: &Pubkey, 209 | system_program: AccountInfo<'a>, 210 | token_program: AccountInfo<'a>, 211 | rent: AccountInfo<'a>, 212 | ) -> Result>> { 213 | let mut res: Vec> = Vec::with_capacity(accounts.len()); 214 | let mut decimals = 0; 215 | 216 | for (token_account, mint) in accounts.iter().zip(mints) { 217 | // validate token account address 218 | let (expected_token_account_key, bump) = Pubkey::find_program_address( 219 | &[b"dispense", mint.key().as_ref(), auction.as_ref()], 220 | &crate::ID, 221 | ); 222 | require_keys_eq!( 223 | token_account.key(), 224 | expected_token_account_key, 225 | LaunchpadError::InvalidDispenserAddress 226 | ); 227 | let mint_data = Account::::try_from(mint)?; 228 | if res.is_empty() { 229 | decimals = mint_data.decimals; 230 | } else if decimals != mint_data.decimals { 231 | return err!(LaunchpadError::InvalidDispenserDecimals); 232 | } 233 | // initialize the account or check the owner 234 | if token_account.data_is_empty() { 235 | initialize_token_account( 236 | payer.clone(), 237 | token_account.clone(), 238 | mint.clone(), 239 | system_program.clone(), 240 | token_program.clone(), 241 | rent.clone(), 242 | authority.clone(), 243 | &[&[b"dispense", mint.key().as_ref(), auction.as_ref(), &[bump]]], 244 | )?; 245 | } else if token_account.owner != &anchor_spl::token::ID { 246 | return Err(ProgramError::IllegalOwner.into()); 247 | } 248 | res.push(Account::::try_from(token_account)?); 249 | } 250 | 251 | Ok(res) 252 | } 253 | 254 | pub fn transfer_sol_from_owned<'a>( 255 | program_owned_source_account: AccountInfo<'a>, 256 | destination_account: AccountInfo<'a>, 257 | amount: u64, 258 | ) -> Result<()> { 259 | if amount == 0 { 260 | return Ok(()); 261 | } 262 | 263 | **destination_account.try_borrow_mut_lamports()? = destination_account 264 | .try_lamports()? 265 | .checked_add(amount) 266 | .ok_or(ProgramError::InsufficientFunds)?; 267 | let source_balance = program_owned_source_account.try_lamports()?; 268 | if source_balance < amount { 269 | msg!( 270 | "Error: Not enough funds to withdraw {} lamports from {}", 271 | amount, 272 | program_owned_source_account.key 273 | ); 274 | return Err(ProgramError::InsufficientFunds.into()); 275 | } 276 | **program_owned_source_account.try_borrow_mut_lamports()? = source_balance 277 | .checked_sub(amount) 278 | .ok_or(ProgramError::InsufficientFunds)?; 279 | 280 | Ok(()) 281 | } 282 | 283 | pub fn transfer_sol<'a>( 284 | source_account: AccountInfo<'a>, 285 | destination_account: AccountInfo<'a>, 286 | system_program: AccountInfo<'a>, 287 | amount: u64, 288 | ) -> Result<()> { 289 | if source_account.try_lamports()? < amount { 290 | msg!( 291 | "Error: Not enough funds to withdraw {} lamports from {}", 292 | amount, 293 | source_account.key 294 | ); 295 | return Err(ProgramError::InsufficientFunds.into()); 296 | } 297 | 298 | let cpi_accounts = anchor_lang::system_program::Transfer { 299 | from: source_account, 300 | to: destination_account, 301 | }; 302 | let cpi_context = anchor_lang::context::CpiContext::new(system_program, cpi_accounts); 303 | anchor_lang::system_program::transfer(cpi_context, amount) 304 | } 305 | -------------------------------------------------------------------------------- /programs/launchpad/src/state/auction.rs: -------------------------------------------------------------------------------- 1 | use {crate::math, anchor_lang::prelude::*}; 2 | 3 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 4 | pub struct BidderStats { 5 | pub fills_volume: u64, 6 | pub weighted_fills_sum: u128, 7 | pub min_fill_price: u64, 8 | pub max_fill_price: u64, 9 | pub num_trades: u64, 10 | } 11 | 12 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 13 | pub struct AuctionStats { 14 | pub first_trade_time: i64, 15 | pub last_trade_time: i64, 16 | pub last_amount: u64, 17 | pub last_price: u64, 18 | pub wl_bidders: BidderStats, 19 | pub reg_bidders: BidderStats, 20 | } 21 | 22 | #[derive(Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 23 | pub struct CommonParams { 24 | pub name: String, 25 | pub description: String, 26 | pub about_seller: String, 27 | pub seller_link: String, 28 | pub start_time: i64, 29 | pub end_time: i64, 30 | pub presale_start_time: i64, 31 | pub presale_end_time: i64, 32 | pub fill_limit_reg_address: u64, 33 | pub fill_limit_wl_address: u64, 34 | pub order_limit_reg_address: u64, 35 | pub order_limit_wl_address: u64, 36 | } 37 | 38 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 39 | pub struct PaymentParams { 40 | pub accept_sol: bool, 41 | pub accept_usdc: bool, 42 | pub accept_other_tokens: bool, 43 | } 44 | 45 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)] 46 | pub enum PricingModel { 47 | Fixed, 48 | DynamicDutchAuction, 49 | } 50 | 51 | impl Default for PricingModel { 52 | fn default() -> Self { 53 | Self::Fixed 54 | } 55 | } 56 | 57 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)] 58 | pub enum RepriceFunction { 59 | Linear, 60 | Exponential, 61 | } 62 | 63 | impl Default for RepriceFunction { 64 | fn default() -> Self { 65 | Self::Linear 66 | } 67 | } 68 | 69 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)] 70 | pub enum AmountFunction { 71 | Fixed, 72 | } 73 | 74 | impl Default for AmountFunction { 75 | fn default() -> Self { 76 | Self::Fixed 77 | } 78 | } 79 | 80 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 81 | pub struct PricingParams { 82 | pub custody: Pubkey, 83 | pub pricing_model: PricingModel, 84 | pub start_price: u64, 85 | pub max_price: u64, 86 | pub min_price: u64, 87 | pub reprice_delay: i64, 88 | pub reprice_coef: f64, 89 | pub reprice_function: RepriceFunction, 90 | pub amount_function: AmountFunction, 91 | pub amount_per_level: u64, 92 | pub tick_size: u64, 93 | pub unit_size: u64, 94 | } 95 | 96 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 97 | pub struct AuctionToken { 98 | // Token ratios determine likelihood of getting a particular token if 99 | // multiple offered. If set to zero, then the available amount will be 100 | // replace it on the first trade. E.g., if an auction offers 3 tokens, 101 | // supplied amount of the third token is 3000, and ratios are set to 102 | // [1230, 2000, 0], then upon the first trade third ratio will be set 103 | // to 3000, and the buyer will randomly get one of the tokens with 104 | // 1.23:2:3 probability (i.e. third token is about 2.5 times more 105 | // likely than first). 106 | pub ratio: u64, 107 | pub account: Pubkey, 108 | } 109 | 110 | #[account] 111 | #[derive(Default, Debug)] 112 | pub struct Auction { 113 | pub owner: Pubkey, 114 | 115 | pub enabled: bool, 116 | pub updatable: bool, 117 | pub fixed_amount: bool, 118 | 119 | pub common: CommonParams, 120 | pub payment: PaymentParams, 121 | pub pricing: PricingParams, 122 | pub stats: AuctionStats, 123 | pub tokens: [AuctionToken; 10], // Auction::MAX_TOKENS 124 | pub num_tokens: u8, 125 | 126 | // time of creation, also used as current wall clock time for testing 127 | pub creation_time: i64, 128 | pub update_time: i64, 129 | pub bump: u8, 130 | } 131 | 132 | impl CommonParams { 133 | pub fn validate(&self, curtime: i64) -> bool { 134 | self.fill_limit_reg_address >= self.order_limit_reg_address 135 | && self.fill_limit_wl_address >= self.order_limit_wl_address 136 | && ((self.end_time == 0 && self.start_time == 0) 137 | || (self.end_time > self.start_time && self.end_time > curtime)) 138 | && ((self.presale_end_time == 0 && self.presale_start_time == 0) 139 | || (self.presale_end_time > self.presale_start_time 140 | && self.presale_end_time > curtime 141 | && ((self.end_time == 0 && self.start_time == 0) 142 | || self.presale_end_time <= self.start_time))) 143 | } 144 | } 145 | 146 | impl PaymentParams { 147 | pub fn validate(&self) -> bool { 148 | self.accept_sol || self.accept_usdc || self.accept_other_tokens 149 | } 150 | } 151 | 152 | impl PricingParams { 153 | pub fn validate(&self) -> bool { 154 | ((self.pricing_model == PricingModel::Fixed 155 | && self.min_price == self.start_price 156 | && self.max_price == self.start_price) 157 | || (self.pricing_model != PricingModel::Fixed 158 | && self.max_price >= self.start_price 159 | && self.max_price >= self.min_price 160 | && self.start_price >= self.min_price)) 161 | && self.reprice_delay >= 0 162 | && (self.pricing_model == PricingModel::Fixed 163 | || (self.amount_per_level > 0 && self.tick_size > 0)) 164 | && self.unit_size > 0 165 | } 166 | } 167 | 168 | impl Auction { 169 | pub const LEN: usize = 8 + std::mem::size_of::(); 170 | pub const MAX_TOKENS: usize = 10; 171 | 172 | pub fn validate(&self) -> Result { 173 | Ok(self.common.name.len() >= 6 174 | && self.common.validate(self.get_time()?) 175 | && self.payment.validate() 176 | && self.pricing.validate()) 177 | } 178 | 179 | /// checks if auction has started 180 | pub fn is_started(&self, curtime: i64, whitelisted: bool) -> bool { 181 | let auction_start_time = self.get_start_time(whitelisted); 182 | auction_start_time > 0 && curtime >= auction_start_time 183 | } 184 | 185 | /// Checks if the auction is ended 186 | pub fn is_ended(&self, curtime: i64, whitelisted: bool) -> bool { 187 | curtime >= self.get_end_time(whitelisted) 188 | } 189 | 190 | #[cfg(feature = "test")] 191 | pub fn get_time(&self) -> Result { 192 | Ok(self.creation_time) 193 | } 194 | 195 | #[cfg(not(feature = "test"))] 196 | pub fn get_time(&self) -> Result { 197 | let time = solana_program::sysvar::clock::Clock::get()?.unix_timestamp; 198 | if time > 0 { 199 | Ok(time) 200 | } else { 201 | Err(ProgramError::InvalidAccountData.into()) 202 | } 203 | } 204 | 205 | pub fn get_start_time(&self, whitelisted: bool) -> i64 { 206 | if whitelisted { 207 | if self.common.presale_start_time > 0 { 208 | self.common.presale_start_time 209 | } else { 210 | self.common.start_time 211 | } 212 | } else { 213 | self.common.start_time 214 | } 215 | } 216 | 217 | pub fn get_end_time(&self, whitelisted: bool) -> i64 { 218 | if whitelisted { 219 | std::cmp::max(self.common.presale_end_time, self.common.end_time) 220 | } else { 221 | self.common.end_time 222 | } 223 | } 224 | 225 | pub fn get_auction_amount(&self, price: u64, curtime: i64) -> Result { 226 | match self.pricing.pricing_model { 227 | PricingModel::Fixed => self.get_auction_amount_fixed(), 228 | PricingModel::DynamicDutchAuction => self.get_auction_amount_dda(price, curtime), 229 | } 230 | } 231 | 232 | pub fn get_auction_price(&self, amount: u64, curtime: i64) -> Result { 233 | match self.pricing.pricing_model { 234 | PricingModel::Fixed => self.get_auction_price_fixed(), 235 | PricingModel::DynamicDutchAuction => self.get_auction_price_dda(amount, curtime), 236 | } 237 | } 238 | 239 | fn get_auction_amount_fixed(&self) -> Result { 240 | Ok(u64::MAX) 241 | } 242 | 243 | fn get_auction_price_fixed(&self) -> Result { 244 | Ok(self.pricing.start_price) 245 | } 246 | 247 | fn get_auction_amount_dda(&self, price: u64, curtime: i64) -> Result { 248 | // compute current best offer price 249 | let best_offer_price = self.get_best_offer_price(curtime)?; 250 | 251 | // return early if user's price is not aggressive enough 252 | if price < best_offer_price { 253 | return Ok(0); 254 | } 255 | 256 | // compute number of price levels 257 | let price_levels = math::checked_add( 258 | math::checked_div( 259 | math::checked_sub(price, best_offer_price)?, 260 | self.pricing.tick_size, 261 | )?, 262 | 1, 263 | )?; 264 | 265 | // compute available amount 266 | self.get_offer_size(price_levels) 267 | } 268 | 269 | fn get_auction_price_dda(&self, amount: u64, curtime: i64) -> Result { 270 | if amount == 0 { 271 | return Ok(0); 272 | } 273 | 274 | // compute current best offer price 275 | let best_offer_price = self.get_best_offer_price(curtime)?; 276 | 277 | // get number of price levels required to take 278 | let price_levels = math::checked_sub( 279 | math::checked_ceil_div(amount, self.pricing.amount_per_level)?, 280 | 1, 281 | )?; 282 | 283 | // compute the auction price 284 | let price = math::checked_add( 285 | best_offer_price, 286 | math::checked_mul(price_levels, self.pricing.tick_size)?, 287 | )?; 288 | 289 | Ok(std::cmp::min(price, self.pricing.max_price)) 290 | } 291 | 292 | fn get_best_offer_price(&self, curtime: i64) -> Result { 293 | let (last_price, mut last_trade_time) = if self.stats.last_trade_time > 0 { 294 | (self.stats.last_price, self.stats.last_trade_time) 295 | } else { 296 | let start_time = if self.common.start_time > 0 && curtime >= self.common.start_time { 297 | self.common.start_time 298 | } else { 299 | self.get_start_time(true) 300 | }; 301 | (self.pricing.start_price, start_time) 302 | }; 303 | last_trade_time = math::checked_add(last_trade_time, self.pricing.reprice_delay)?; 304 | let end_time = self.get_end_time(true); 305 | if curtime <= last_trade_time || curtime >= end_time { 306 | return Ok(last_price); 307 | } 308 | let step = math::checked_float_div( 309 | math::checked_sub(curtime, last_trade_time)? as f64, 310 | math::checked_sub(end_time, last_trade_time)? as f64, 311 | )?; 312 | 313 | let mut best_offer_price = match self.pricing.reprice_function { 314 | RepriceFunction::Exponential => math::checked_as_u64(math::checked_div( 315 | math::checked_mul( 316 | last_price as u128, 317 | math::checked_as_u128(math::checked_float_mul( 318 | f64::exp( 319 | -self.pricing.reprice_coef * math::checked_float_mul(step, 100f64)?, 320 | ), 321 | 10000.0, 322 | )?)?, 323 | )?, 324 | 10000u128, 325 | )?)?, 326 | RepriceFunction::Linear => math::checked_as_u64(math::checked_div( 327 | math::checked_mul( 328 | last_price as u128, 329 | math::checked_as_u128(math::checked_float_mul(1.0 - step, 10000.0)?)?, 330 | )?, 331 | 10000u128, 332 | )?)?, 333 | }; 334 | 335 | // round to tick size 336 | if best_offer_price % self.pricing.tick_size != 0 { 337 | best_offer_price = math::checked_mul( 338 | math::checked_ceil_div(best_offer_price, self.pricing.tick_size)?, 339 | self.pricing.tick_size, 340 | )?; 341 | } 342 | 343 | // check for min/max 344 | best_offer_price = std::cmp::min(best_offer_price, self.pricing.max_price); 345 | best_offer_price = std::cmp::max(best_offer_price, self.pricing.min_price); 346 | 347 | Ok(best_offer_price) 348 | } 349 | 350 | pub fn get_offer_size(&self, price_levels: u64) -> Result { 351 | match self.pricing.amount_function { 352 | AmountFunction::Fixed => math::checked_mul(price_levels, self.pricing.amount_per_level), 353 | } 354 | } 355 | } 356 | 357 | #[cfg(test)] 358 | mod test { 359 | use super::*; 360 | 361 | fn get_fixture() -> Auction { 362 | let mut auction = Auction { 363 | creation_time: 100, 364 | ..Default::default() 365 | }; 366 | 367 | auction.common.name = "test_auction".to_string(); 368 | auction.common.start_time = 350; 369 | auction.common.end_time = 500; 370 | auction.common.presale_start_time = 200; 371 | auction.common.presale_end_time = 300; 372 | 373 | auction.pricing.pricing_model = PricingModel::DynamicDutchAuction; 374 | auction.pricing.start_price = 1000; 375 | auction.pricing.max_price = 2000; 376 | auction.pricing.min_price = 50; 377 | auction.pricing.reprice_delay = 10; 378 | auction.pricing.reprice_coef = 0.05; 379 | auction.pricing.reprice_function = RepriceFunction::Exponential; 380 | auction.pricing.amount_function = AmountFunction::Fixed; 381 | auction.pricing.amount_per_level = 20; 382 | auction.pricing.tick_size = 10; 383 | auction.pricing.unit_size = 100; 384 | 385 | auction.payment.accept_sol = true; 386 | 387 | assert!(auction.validate().unwrap()); 388 | 389 | auction 390 | } 391 | 392 | #[test] 393 | fn get_best_offer_price_exp() { 394 | let mut auction = get_fixture(); 395 | 396 | auction.pricing.reprice_function = RepriceFunction::Exponential; 397 | assert_eq!(1000, auction.get_best_offer_price(100).unwrap()); 398 | assert_eq!(1000, auction.get_best_offer_price(200).unwrap()); 399 | assert_eq!(510, auction.get_best_offer_price(250).unwrap()); 400 | assert_eq!(1000, auction.get_best_offer_price(350).unwrap()); 401 | assert_eq!(240, auction.get_best_offer_price(400).unwrap()); 402 | assert_eq!(50, auction.get_best_offer_price(499).unwrap()); 403 | } 404 | 405 | #[test] 406 | fn get_best_offer_price_linear() { 407 | let mut auction = get_fixture(); 408 | 409 | auction.pricing.reprice_function = RepriceFunction::Linear; 410 | assert_eq!(1000, auction.get_best_offer_price(100).unwrap()); 411 | assert_eq!(1000, auction.get_best_offer_price(200).unwrap()); 412 | assert_eq!(870, auction.get_best_offer_price(250).unwrap()); 413 | assert_eq!(1000, auction.get_best_offer_price(350).unwrap()); 414 | assert_eq!(720, auction.get_best_offer_price(400).unwrap()); 415 | assert_eq!(50, auction.get_best_offer_price(499).unwrap()); 416 | } 417 | 418 | #[test] 419 | fn get_auction_price_dda() { 420 | let mut auction = get_fixture(); 421 | 422 | auction.pricing.reprice_function = RepriceFunction::Exponential; 423 | assert_eq!(1000, auction.get_auction_price_dda(1, 100).unwrap()); 424 | assert_eq!(1000, auction.get_auction_price_dda(1, 200).unwrap()); 425 | assert_eq!(510, auction.get_auction_price_dda(1, 250).unwrap()); 426 | assert_eq!(1000, auction.get_auction_price_dda(1, 350).unwrap()); 427 | assert_eq!(240, auction.get_auction_price_dda(1, 400).unwrap()); 428 | assert_eq!(50, auction.get_auction_price_dda(1, 499).unwrap()); 429 | 430 | assert_eq!(1000, auction.get_auction_price_dda(20, 100).unwrap()); 431 | assert_eq!(1000, auction.get_auction_price_dda(20, 200).unwrap()); 432 | assert_eq!(510, auction.get_auction_price_dda(20, 250).unwrap()); 433 | assert_eq!(1000, auction.get_auction_price_dda(20, 350).unwrap()); 434 | assert_eq!(240, auction.get_auction_price_dda(20, 400).unwrap()); 435 | assert_eq!(50, auction.get_auction_price_dda(20, 499).unwrap()); 436 | 437 | assert_eq!(1010, auction.get_auction_price_dda(21, 100).unwrap()); 438 | assert_eq!(1010, auction.get_auction_price_dda(21, 200).unwrap()); 439 | assert_eq!(520, auction.get_auction_price_dda(21, 250).unwrap()); 440 | assert_eq!(1010, auction.get_auction_price_dda(21, 350).unwrap()); 441 | assert_eq!(250, auction.get_auction_price_dda(21, 400).unwrap()); 442 | assert_eq!(60, auction.get_auction_price_dda(21, 499).unwrap()); 443 | 444 | assert_eq!(1090, auction.get_auction_price_dda(200, 100).unwrap()); 445 | assert_eq!(1090, auction.get_auction_price_dda(200, 200).unwrap()); 446 | assert_eq!(600, auction.get_auction_price_dda(200, 250).unwrap()); 447 | assert_eq!(1090, auction.get_auction_price_dda(200, 350).unwrap()); 448 | assert_eq!(330, auction.get_auction_price_dda(200, 400).unwrap()); 449 | assert_eq!(140, auction.get_auction_price_dda(200, 499).unwrap()); 450 | 451 | assert_eq!(2000, auction.get_auction_price_dda(u64::MAX, 100).unwrap()); 452 | assert_eq!(2000, auction.get_auction_price_dda(u64::MAX, 200).unwrap()); 453 | assert_eq!(2000, auction.get_auction_price_dda(u64::MAX, 250).unwrap()); 454 | assert_eq!(2000, auction.get_auction_price_dda(u64::MAX, 350).unwrap()); 455 | assert_eq!(2000, auction.get_auction_price_dda(u64::MAX, 400).unwrap()); 456 | assert_eq!(2000, auction.get_auction_price_dda(u64::MAX, 499).unwrap()); 457 | } 458 | 459 | #[test] 460 | fn get_auction_amount_dda() { 461 | let mut auction = get_fixture(); 462 | 463 | auction.pricing.reprice_function = RepriceFunction::Exponential; 464 | assert_eq!(0, auction.get_auction_amount_dda(0, 100).unwrap()); 465 | assert_eq!(0, auction.get_auction_amount_dda(0, 200).unwrap()); 466 | assert_eq!(0, auction.get_auction_amount_dda(0, 250).unwrap()); 467 | assert_eq!(0, auction.get_auction_amount_dda(0, 350).unwrap()); 468 | assert_eq!(0, auction.get_auction_amount_dda(0, 400).unwrap()); 469 | assert_eq!(0, auction.get_auction_amount_dda(0, 499).unwrap()); 470 | 471 | assert_eq!(0, auction.get_auction_amount_dda(999, 100).unwrap()); 472 | assert_eq!(0, auction.get_auction_amount_dda(999, 200).unwrap()); 473 | assert_eq!(0, auction.get_auction_amount_dda(509, 250).unwrap()); 474 | assert_eq!(0, auction.get_auction_amount_dda(999, 350).unwrap()); 475 | assert_eq!(0, auction.get_auction_amount_dda(239, 400).unwrap()); 476 | assert_eq!(0, auction.get_auction_amount_dda(49, 499).unwrap()); 477 | 478 | assert_eq!(20, auction.get_auction_amount_dda(1000, 100).unwrap()); 479 | assert_eq!(20, auction.get_auction_amount_dda(1000, 200).unwrap()); 480 | assert_eq!(20, auction.get_auction_amount_dda(510, 250).unwrap()); 481 | assert_eq!(20, auction.get_auction_amount_dda(1000, 350).unwrap()); 482 | assert_eq!(20, auction.get_auction_amount_dda(240, 400).unwrap()); 483 | assert_eq!(20, auction.get_auction_amount_dda(50, 499).unwrap()); 484 | 485 | assert_eq!(40, auction.get_auction_amount_dda(1010, 100).unwrap()); 486 | assert_eq!(40, auction.get_auction_amount_dda(1010, 200).unwrap()); 487 | assert_eq!(40, auction.get_auction_amount_dda(520, 250).unwrap()); 488 | assert_eq!(40, auction.get_auction_amount_dda(1010, 350).unwrap()); 489 | assert_eq!(40, auction.get_auction_amount_dda(250, 400).unwrap()); 490 | assert_eq!(40, auction.get_auction_amount_dda(60, 499).unwrap()); 491 | 492 | assert_eq!(2020, auction.get_auction_amount_dda(2000, 100).unwrap()); 493 | assert_eq!(2020, auction.get_auction_amount_dda(2000, 200).unwrap()); 494 | assert_eq!(3000, auction.get_auction_amount_dda(2000, 250).unwrap()); 495 | assert_eq!(2020, auction.get_auction_amount_dda(2000, 350).unwrap()); 496 | assert_eq!(3540, auction.get_auction_amount_dda(2000, 400).unwrap()); 497 | assert_eq!(3920, auction.get_auction_amount_dda(2000, 499).unwrap()); 498 | 499 | assert_eq!( 500 | u64::MAX - 15, 501 | auction 502 | .get_auction_amount_dda(u64::MAX / 2 + 990, 100) 503 | .unwrap() 504 | ); 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /programs/launchpad/src/state/bid.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)] 4 | pub enum BidType { 5 | Ioc, 6 | Fok, 7 | } 8 | 9 | impl Default for BidType { 10 | fn default() -> Self { 11 | Self::Ioc 12 | } 13 | } 14 | 15 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)] 16 | pub enum BadBidType { 17 | None, 18 | TooEarly, 19 | FillLimit, 20 | } 21 | 22 | impl Default for BadBidType { 23 | fn default() -> Self { 24 | Self::None 25 | } 26 | } 27 | 28 | #[account] 29 | #[derive(Default, Debug)] 30 | pub struct Bid { 31 | pub owner: Pubkey, 32 | pub auction: Pubkey, 33 | pub whitelisted: bool, 34 | pub seller_initialized: bool, 35 | pub bid_time: i64, 36 | pub bid_price: u64, 37 | pub bid_amount: u64, 38 | pub bid_type: BidType, 39 | pub filled: u64, 40 | pub fill_time: i64, 41 | pub fill_price: u64, 42 | pub fill_amount: u64, 43 | pub bump: u8, 44 | } 45 | 46 | impl Bid { 47 | pub const LEN: usize = 8 + std::mem::size_of::(); 48 | } 49 | -------------------------------------------------------------------------------- /programs/launchpad/src/state/custody.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::oracle::OracleType, anchor_lang::prelude::*}; 2 | 3 | #[account] 4 | #[derive(Default, Debug)] 5 | pub struct Custody { 6 | pub token_account: Pubkey, 7 | pub collected_fees: u64, 8 | pub mint: Pubkey, 9 | pub decimals: u8, 10 | pub max_oracle_price_error: f64, 11 | pub max_oracle_price_age_sec: u32, 12 | pub oracle_type: OracleType, 13 | pub oracle_account: Pubkey, 14 | pub bump: u8, 15 | } 16 | 17 | impl Custody { 18 | pub const LEN: usize = 8 + std::mem::size_of::(); 19 | 20 | pub fn validate(&self) -> bool { 21 | matches!(self.oracle_type, OracleType::None) 22 | || (self.oracle_account != Pubkey::default() && self.max_oracle_price_error >= 0.0) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /programs/launchpad/src/state/launchpad.rs: -------------------------------------------------------------------------------- 1 | use {crate::math, anchor_lang::prelude::*, anchor_spl::token::Transfer}; 2 | 3 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 4 | pub struct Fee { 5 | numerator: u64, 6 | denominator: u64, 7 | } 8 | 9 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 10 | pub struct Fees { 11 | pub new_auction: u64, 12 | pub auction_update: u64, 13 | pub invalid_bid: Fee, 14 | pub trade: Fee, 15 | } 16 | 17 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 18 | pub struct CollectedFees { 19 | pub new_auction_sol: u64, 20 | pub auction_update_sol: u64, 21 | pub invalid_bid_usdc: u64, 22 | pub trade_usdc: u64, 23 | } 24 | 25 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Default, Debug)] 26 | pub struct Permissions { 27 | pub allow_new_auctions: bool, 28 | pub allow_auction_updates: bool, 29 | pub allow_auction_refills: bool, 30 | pub allow_auction_pullouts: bool, 31 | pub allow_new_bids: bool, 32 | pub allow_withdrawals: bool, 33 | } 34 | 35 | #[account] 36 | #[derive(Default, Debug)] 37 | pub struct Launchpad { 38 | pub permissions: Permissions, 39 | pub fees: Fees, 40 | pub collected_fees: CollectedFees, 41 | pub transfer_authority_bump: u8, 42 | pub launchpad_bump: u8, 43 | } 44 | 45 | impl Fee { 46 | pub fn is_zero(&self) -> bool { 47 | self.numerator == 0 48 | } 49 | 50 | pub fn get_fee_amount(&self, amount: u64) -> Result { 51 | if self.is_zero() { 52 | return Ok(0); 53 | } 54 | math::checked_as_u64(math::checked_ceil_div( 55 | math::checked_mul(amount as u128, self.numerator as u128)?, 56 | self.denominator as u128, 57 | )?) 58 | } 59 | } 60 | 61 | impl anchor_lang::Id for Launchpad { 62 | fn id() -> Pubkey { 63 | crate::ID 64 | } 65 | } 66 | 67 | impl Launchpad { 68 | pub const LEN: usize = 8 + std::mem::size_of::(); 69 | 70 | pub fn validate(&self) -> bool { 71 | self.fees.invalid_bid.numerator < self.fees.invalid_bid.denominator 72 | && self.fees.trade.numerator < self.fees.trade.denominator 73 | } 74 | 75 | pub fn transfer_tokens<'info>( 76 | &self, 77 | from: AccountInfo<'info>, 78 | to: AccountInfo<'info>, 79 | authority: AccountInfo<'info>, 80 | token_program: AccountInfo<'info>, 81 | amount: u64, 82 | ) -> Result<()> { 83 | let authority_seeds: &[&[&[u8]]] = 84 | &[&[b"transfer_authority", &[self.transfer_authority_bump]]]; 85 | 86 | let context = CpiContext::new( 87 | token_program, 88 | Transfer { 89 | from, 90 | to, 91 | authority, 92 | }, 93 | ) 94 | .with_signer(authority_seeds); 95 | 96 | anchor_spl::token::transfer(context, amount) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /programs/launchpad/src/state/multisig.rs: -------------------------------------------------------------------------------- 1 | //! Multisig state and routines 2 | 3 | use { 4 | crate::{error::LaunchpadError, math}, 5 | ahash::AHasher, 6 | anchor_lang::prelude::*, 7 | std::hash::Hasher, 8 | }; 9 | 10 | #[repr(packed)] 11 | #[account(zero_copy)] 12 | #[derive(Default)] 13 | pub struct Multisig { 14 | pub num_signers: u8, 15 | pub num_signed: u8, 16 | pub min_signatures: u8, 17 | pub instruction_accounts_len: u8, 18 | pub instruction_data_len: u16, 19 | pub instruction_hash: u64, 20 | pub signers: [Pubkey; 6], // Multisig::MAX_SIGNERS 21 | pub signed: [bool; 6], // Multisig::MAX_SIGNERS 22 | pub bump: u8, 23 | } 24 | 25 | pub enum AdminInstruction { 26 | InitCustody, 27 | SetPermissions, 28 | SetFees, 29 | SetAdminSigners, 30 | SetOracleConfig, 31 | WithdrawFees, 32 | SetTestOraclePrice, 33 | SetTestTime, 34 | DeleteAuction, 35 | } 36 | 37 | impl Multisig { 38 | pub const MAX_SIGNERS: usize = 6; 39 | pub const LEN: usize = 8 + std::mem::size_of::(); 40 | 41 | /// Returns instruction accounts and data hash. 42 | /// Hash is not cryptographic and is meant to perform a fast check that admins are signing 43 | /// the same instruction. 44 | pub fn get_instruction_hash( 45 | instruction_accounts: &[AccountInfo], 46 | instruction_data: &[u8], 47 | ) -> u64 { 48 | let mut hasher = AHasher::new_with_keys(697533735114380, 537268678243635); 49 | for account in instruction_accounts { 50 | hasher.write(account.key.as_ref()); 51 | } 52 | if !instruction_data.is_empty() { 53 | hasher.write(instruction_data); 54 | } 55 | hasher.finish() 56 | } 57 | 58 | /// Returns all accounts for the given context 59 | pub fn get_account_infos<'info, T: ToAccountInfos<'info>>( 60 | ctx: &Context<'_, '_, '_, 'info, T>, 61 | ) -> Vec> { 62 | let mut infos = ctx.accounts.to_account_infos(); 63 | infos.extend_from_slice(ctx.remaining_accounts); 64 | infos 65 | } 66 | 67 | /// Returns serialized instruction data 68 | pub fn get_instruction_data( 69 | instruction_type: AdminInstruction, 70 | params: &T, 71 | ) -> Result> { 72 | let mut res = vec![]; 73 | AnchorSerialize::serialize(¶ms, &mut res)?; 74 | res.push(instruction_type as u8); 75 | Ok(res) 76 | } 77 | 78 | /// Initializes multisig PDA with a new set of signers 79 | pub fn set_signers(&mut self, admin_signers: &[AccountInfo], min_signatures: u8) -> Result<()> { 80 | if admin_signers.is_empty() || min_signatures == 0 { 81 | msg!("Error: At least one signer is required"); 82 | return Err(ProgramError::MissingRequiredSignature.into()); 83 | } 84 | if (min_signatures as usize) > admin_signers.len() { 85 | msg!( 86 | "Error: Number of min signatures ({}) exceeded number of signers ({})", 87 | min_signatures, 88 | admin_signers.len(), 89 | ); 90 | return Err(ProgramError::InvalidArgument.into()); 91 | } 92 | if admin_signers.len() > Multisig::MAX_SIGNERS { 93 | msg!( 94 | "Error: Number of signers ({}) exceeded max ({})", 95 | admin_signers.len(), 96 | Multisig::MAX_SIGNERS 97 | ); 98 | return Err(ProgramError::InvalidArgument.into()); 99 | } 100 | 101 | let mut signers: [Pubkey; Multisig::MAX_SIGNERS] = Default::default(); 102 | let mut signed: [bool; Multisig::MAX_SIGNERS] = Default::default(); 103 | 104 | for idx in 0..admin_signers.len() { 105 | if signers.contains(admin_signers[idx].key) { 106 | msg!("Error: Duplicate signer {}", admin_signers[idx].key); 107 | return Err(ProgramError::InvalidArgument.into()); 108 | } 109 | signers[idx] = *admin_signers[idx].key; 110 | signed[idx] = false; 111 | } 112 | 113 | *self = Multisig { 114 | num_signers: admin_signers.len() as u8, 115 | num_signed: 0, 116 | min_signatures, 117 | instruction_accounts_len: 0, 118 | instruction_data_len: 0, 119 | instruction_hash: 0, 120 | signers, 121 | signed, 122 | bump: self.bump, 123 | }; 124 | 125 | Ok(()) 126 | } 127 | 128 | /// Signs multisig and returns Ok(0) if there are enough signatures to continue or Ok(signatures_left) otherwise. 129 | /// If Err() is returned then signature was not recognized and transaction must be aborted. 130 | pub fn sign_multisig( 131 | &mut self, 132 | signer_account: &AccountInfo, 133 | instruction_accounts: &[AccountInfo], 134 | instruction_data: &[u8], 135 | ) -> Result { 136 | // return early if not a signer 137 | if !signer_account.is_signer { 138 | return Err(ProgramError::MissingRequiredSignature.into()); 139 | } 140 | 141 | // find index of current signer or return error if not found 142 | let signer_idx = if let Ok(idx) = self.get_signer_index(signer_account.key) { 143 | idx 144 | } else { 145 | return err!(LaunchpadError::MultisigAccountNotAuthorized); 146 | }; 147 | 148 | // if single signer return Ok to continue 149 | if self.num_signers <= 1 { 150 | return Ok(0); 151 | } 152 | 153 | let instruction_hash = 154 | Multisig::get_instruction_hash(instruction_accounts, instruction_data); 155 | if instruction_hash != self.instruction_hash 156 | || instruction_accounts.len() != self.instruction_accounts_len as usize 157 | || instruction_data.len() != self.instruction_data_len as usize 158 | { 159 | // if this is a new instruction reset the data 160 | self.num_signed = 1; 161 | self.instruction_accounts_len = instruction_accounts.len() as u8; 162 | self.instruction_data_len = instruction_data.len() as u16; 163 | self.instruction_hash = instruction_hash; 164 | self.signed.fill(false); 165 | self.signed[signer_idx] = true; 166 | //multisig.pack(*multisig_account.try_borrow_mut_data()?)?; 167 | 168 | math::checked_sub(self.min_signatures, 1) 169 | } else if self.signed[signer_idx] { 170 | err!(LaunchpadError::MultisigAlreadySigned) 171 | } else if self.num_signed < self.min_signatures { 172 | // count the signature in 173 | self.num_signed += 1; 174 | self.signed[signer_idx] = true; 175 | 176 | if self.num_signed == self.min_signatures { 177 | Ok(0) 178 | } else { 179 | math::checked_sub(self.min_signatures, self.num_signed) 180 | } 181 | } else { 182 | err!(LaunchpadError::MultisigAlreadyExecuted) 183 | } 184 | } 185 | 186 | /// Removes admin signature from the multisig 187 | pub fn unsign_multisig(&mut self, signer_account: &AccountInfo) -> Result<()> { 188 | // return early if not a signer 189 | if !signer_account.is_signer { 190 | return Err(ProgramError::MissingRequiredSignature.into()); 191 | } 192 | 193 | // if single signer return 194 | if self.num_signers <= 1 || self.num_signed == 0 { 195 | return Ok(()); 196 | } 197 | 198 | // find index of current signer or return error if not found 199 | let signer_idx = if let Ok(idx) = self.get_signer_index(signer_account.key) { 200 | idx 201 | } else { 202 | return err!(LaunchpadError::MultisigAccountNotAuthorized); 203 | }; 204 | 205 | // if not signed by this account return 206 | if !self.signed[signer_idx] { 207 | return Ok(()); 208 | } 209 | 210 | // remove signature 211 | self.num_signed -= 1; 212 | self.signed[signer_idx] = false; 213 | 214 | Ok(()) 215 | } 216 | 217 | /// Returns the array index of the provided signer 218 | pub fn get_signer_index(&self, signer: &Pubkey) -> Result { 219 | for i in 0..self.num_signers as usize { 220 | if &self.signers[i] == signer { 221 | return Ok(i); 222 | } 223 | } 224 | err!(LaunchpadError::MultisigAccountNotAuthorized) 225 | } 226 | 227 | /// Checks if provided account is one of multisig signers 228 | pub fn is_signer(&self, key: &Pubkey) -> Result { 229 | Ok(self.get_signer_index(key).is_ok()) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /programs/launchpad/src/state/oracle.rs: -------------------------------------------------------------------------------- 1 | //! Oracle price service handling 2 | 3 | use { 4 | crate::{error::LaunchpadError, math, state}, 5 | anchor_lang::prelude::*, 6 | }; 7 | 8 | const ORACLE_EXPONENT_SCALE: i32 = -9; 9 | const ORACLE_PRICE_SCALE: u64 = 1_000_000_000; 10 | const ORACLE_MAX_PRICE: u64 = (1 << 28) - 1; 11 | 12 | #[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize, Debug)] 13 | pub enum OracleType { 14 | None, 15 | Test, 16 | Pyth, 17 | } 18 | 19 | impl Default for OracleType { 20 | fn default() -> Self { 21 | Self::None 22 | } 23 | } 24 | 25 | #[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize, Debug)] 26 | pub struct OraclePrice { 27 | pub price: u64, 28 | pub exponent: i32, 29 | } 30 | 31 | #[account] 32 | #[derive(Default, Debug)] 33 | pub struct TestOracle { 34 | pub price: u64, 35 | pub expo: i32, 36 | pub conf: u64, 37 | pub publish_time: i64, 38 | } 39 | 40 | impl TestOracle { 41 | pub const LEN: usize = 8 + std::mem::size_of::(); 42 | } 43 | 44 | #[allow(dead_code)] 45 | impl OraclePrice { 46 | pub fn new(price: u64, exponent: i32) -> Self { 47 | Self { price, exponent } 48 | } 49 | 50 | pub fn new_from_token(amount_and_decimals: (u64, u8)) -> Self { 51 | Self { 52 | price: amount_and_decimals.0, 53 | exponent: -(amount_and_decimals.1 as i32), 54 | } 55 | } 56 | 57 | pub fn new_from_oracle( 58 | oracle_type: OracleType, 59 | oracle_account: &AccountInfo, 60 | max_price_error: f64, 61 | max_price_age_sec: u32, 62 | current_time: i64, 63 | ) -> Result { 64 | match oracle_type { 65 | OracleType::Test => Self::get_test_price( 66 | oracle_account, 67 | max_price_error, 68 | max_price_age_sec, 69 | current_time, 70 | ), 71 | OracleType::Pyth => Self::get_pyth_price( 72 | oracle_account, 73 | max_price_error, 74 | max_price_age_sec, 75 | current_time, 76 | ), 77 | _ => err!(LaunchpadError::UnsupportedOracle), 78 | } 79 | } 80 | 81 | // Converts token amount to USD using oracle price 82 | pub fn get_asset_value_usd(&self, token_amount: u64, token_decimals: u8) -> Result { 83 | if token_amount == 0 { 84 | return Ok(0.0); 85 | } 86 | let res = token_amount as f64 87 | * self.price as f64 88 | * math::checked_powi( 89 | 10.0, 90 | math::checked_sub(self.exponent, token_decimals as i32)?, 91 | )?; 92 | if res.is_finite() { 93 | Ok(res) 94 | } else { 95 | err!(LaunchpadError::MathOverflow) 96 | } 97 | } 98 | 99 | /// Returns price with mantissa normalized to be less than ORACLE_MAX_PRICE 100 | pub fn normalize(&self) -> Result { 101 | let mut p = self.price; 102 | let mut e = self.exponent; 103 | 104 | while p > ORACLE_MAX_PRICE { 105 | p = math::checked_div(p, 10)?; 106 | e = math::checked_add(e, 1)?; 107 | } 108 | 109 | Ok(OraclePrice { 110 | price: p, 111 | exponent: e, 112 | }) 113 | } 114 | 115 | pub fn checked_div(&self, other: &OraclePrice) -> Result { 116 | let base = self.normalize()?; 117 | let other = other.normalize()?; 118 | 119 | Ok(OraclePrice { 120 | price: math::checked_div( 121 | math::checked_mul(base.price, ORACLE_PRICE_SCALE)?, 122 | other.price, 123 | )?, 124 | exponent: math::checked_sub( 125 | math::checked_add(base.exponent, ORACLE_EXPONENT_SCALE)?, 126 | other.exponent, 127 | )?, 128 | }) 129 | } 130 | 131 | pub fn checked_mul(&self, other: &OraclePrice) -> Result { 132 | Ok(OraclePrice { 133 | price: math::checked_mul(self.price, other.price)?, 134 | exponent: math::checked_add(self.exponent, other.exponent)?, 135 | }) 136 | } 137 | 138 | pub fn scale_to_exponent(&self, target_exponent: i32) -> Result { 139 | if target_exponent == self.exponent { 140 | return Ok(*self); 141 | } 142 | let delta = math::checked_sub(target_exponent, self.exponent)?; 143 | if delta > 0 { 144 | Ok(OraclePrice { 145 | price: math::checked_div(self.price, math::checked_pow(10, delta as usize)?)?, 146 | exponent: target_exponent, 147 | }) 148 | } else { 149 | Ok(OraclePrice { 150 | price: math::checked_mul(self.price, math::checked_pow(10, (-delta) as usize)?)?, 151 | exponent: target_exponent, 152 | }) 153 | } 154 | } 155 | 156 | pub fn checked_as_f64(&self) -> Result { 157 | math::checked_float_mul(self.price as f64, math::checked_powi(10.0, self.exponent)?) 158 | } 159 | 160 | // private helpers 161 | fn get_test_price( 162 | test_price_info: &AccountInfo, 163 | max_price_error: f64, 164 | max_price_age_sec: u32, 165 | current_time: i64, 166 | ) -> Result { 167 | require!( 168 | !state::is_empty_account(test_price_info)?, 169 | LaunchpadError::InvalidOracleAccount 170 | ); 171 | 172 | let oracle_acc = Account::::try_from(test_price_info)?; 173 | 174 | let last_update_age_sec = math::checked_sub(current_time, oracle_acc.publish_time)?; 175 | if last_update_age_sec > max_price_age_sec as i64 { 176 | msg!("Error: Test oracle price is stale"); 177 | return err!(LaunchpadError::StaleOraclePrice); 178 | } 179 | 180 | if oracle_acc.price == 0 181 | || math::checked_float_div(oracle_acc.conf as f64, oracle_acc.price as f64)? 182 | > max_price_error 183 | { 184 | msg!("Error: Test oracle price is out of bounds"); 185 | return err!(LaunchpadError::InvalidOraclePrice); 186 | } 187 | 188 | Ok(OraclePrice { 189 | // price is i64 and > 0 per check above 190 | price: oracle_acc.price, 191 | exponent: oracle_acc.expo, 192 | }) 193 | } 194 | 195 | fn get_pyth_price( 196 | pyth_price_info: &AccountInfo, 197 | max_price_error: f64, 198 | max_price_age_sec: u32, 199 | current_time: i64, 200 | ) -> Result { 201 | require!( 202 | !state::is_empty_account(pyth_price_info)?, 203 | LaunchpadError::InvalidOracleAccount 204 | ); 205 | let price_feed = pyth_sdk_solana::load_price_feed_from_account_info(pyth_price_info) 206 | .map_err(|_| LaunchpadError::InvalidOracleAccount)?; 207 | let pyth_price = price_feed 208 | .get_current_price() 209 | .ok_or(LaunchpadError::InvalidOracleState)?; 210 | 211 | let last_update_age_sec = math::checked_sub(current_time, price_feed.publish_time)?; 212 | if last_update_age_sec > max_price_age_sec as i64 { 213 | msg!("Error: Pyth oracle price is stale"); 214 | return err!(LaunchpadError::StaleOraclePrice); 215 | } 216 | 217 | if pyth_price.price <= 0 218 | || math::checked_float_div(pyth_price.conf as f64, pyth_price.price as f64)? 219 | > max_price_error 220 | { 221 | msg!("Error: Pyth oracle price is out of bounds"); 222 | return err!(LaunchpadError::InvalidOraclePrice); 223 | } 224 | 225 | Ok(OraclePrice { 226 | // price is i64 and > 0 per check above 227 | price: pyth_price.price as u64, 228 | exponent: pyth_price.expo, 229 | }) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /programs/launchpad/src/state/seller_balance.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[account] 4 | #[derive(Default, Debug)] 5 | pub struct SellerBalance { 6 | pub owner: Pubkey, 7 | pub custody: Pubkey, 8 | pub balance: u64, 9 | pub bump: u8, 10 | } 11 | 12 | impl SellerBalance { 13 | pub const LEN: usize = 8 + std::mem::size_of::(); 14 | } 15 | -------------------------------------------------------------------------------- /tests/basic.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | import { LaunchpadTester } from "./launchpad_tester"; 3 | import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js"; 4 | import * as spl from "@solana/spl-token"; 5 | import { expect, assert } from "chai"; 6 | import { BN } from "bn.js"; 7 | 8 | describe("launchpad", () => { 9 | let lpd = new LaunchpadTester(); 10 | lpd.printErrors = true; 11 | let launchpadExpected; 12 | let multisigExpected; 13 | let auctionExpected; 14 | let auctionParams; 15 | 16 | it("init", async () => { 17 | await lpd.initFixture(); 18 | await lpd.init(); 19 | 20 | let err = await lpd.ensureFails(lpd.init()); 21 | assert(err.logs[3].includes("already in use")); 22 | 23 | launchpadExpected = { 24 | permissions: { 25 | allowNewAuctions: true, 26 | allowAuctionUpdates: true, 27 | allowAuctionRefills: true, 28 | allowAuctionPullouts: true, 29 | allowNewBids: true, 30 | allowWithdrawals: true, 31 | }, 32 | fees: { 33 | newAuction: new BN(100), 34 | auctionUpdate: new BN(100), 35 | invalidBid: { numerator: "1", denominator: "100" }, 36 | trade: { numerator: "1", denominator: "100" }, 37 | }, 38 | collectedFees: { 39 | newAuctionSol: "0", 40 | auctionUpdateSol: "0", 41 | invalidBidUsdc: "0", 42 | tradeUsdc: "0", 43 | }, 44 | transferAuthorityBump: lpd.authority.bump, 45 | launchpadBump: lpd.multisig.bump, 46 | }; 47 | 48 | multisigExpected = { 49 | numSigners: 2, 50 | numSigned: 0, 51 | minSignatures: 2, 52 | instructionAccountsLen: 0, 53 | instructionDataLen: 0, 54 | instructionHash: new anchor.BN(0), 55 | signers: [ 56 | lpd.admins[0].publicKey, 57 | lpd.admins[1].publicKey, 58 | PublicKey.default, 59 | PublicKey.default, 60 | PublicKey.default, 61 | PublicKey.default, 62 | ], 63 | signed: [false, false, false, false, false, false], 64 | bump: lpd.multisig.bump, 65 | }; 66 | 67 | let multisig = await lpd.program.account.multisig.fetch( 68 | lpd.multisig.publicKey 69 | ); 70 | expect(JSON.stringify(multisig)).to.equal(JSON.stringify(multisigExpected)); 71 | 72 | let launchpad = await lpd.program.account.launchpad.fetch( 73 | lpd.launchpad.publicKey 74 | ); 75 | expect(JSON.stringify(launchpad)).to.equal( 76 | JSON.stringify(launchpadExpected) 77 | ); 78 | }); 79 | 80 | it("setAdminSigners", async () => { 81 | await lpd.setAdminSigners(1); 82 | 83 | let multisig = await lpd.program.account.multisig.fetch( 84 | lpd.multisig.publicKey 85 | ); 86 | multisigExpected.minSignatures = 1; 87 | expect(JSON.stringify(multisig)).to.equal(JSON.stringify(multisigExpected)); 88 | }); 89 | 90 | it("setFees", async () => { 91 | launchpadExpected.fees = { 92 | newAuction: new BN(10000), 93 | auctionUpdate: new BN(100000), 94 | invalidBid: { numerator: new BN(1), denominator: new BN(1000) }, 95 | trade: { numerator: new BN(1), denominator: new BN(1000) }, 96 | }; 97 | await lpd.setFees(launchpadExpected.fees); 98 | 99 | let launchpad = await lpd.program.account.launchpad.fetch( 100 | lpd.launchpad.publicKey 101 | ); 102 | expect(JSON.stringify(launchpad)).to.equal( 103 | JSON.stringify(launchpadExpected) 104 | ); 105 | }); 106 | 107 | it("setPermissions", async () => { 108 | launchpadExpected.permissions = { 109 | allowNewAuctions: false, 110 | allowAuctionUpdates: false, 111 | allowAuctionRefills: false, 112 | allowAuctionPullouts: false, 113 | allowNewBids: false, 114 | allowWithdrawals: false, 115 | }; 116 | await lpd.setPermissions(launchpadExpected.permissions); 117 | 118 | let launchpad = await lpd.program.account.launchpad.fetch( 119 | lpd.launchpad.publicKey 120 | ); 121 | expect(JSON.stringify(launchpad)).to.equal( 122 | JSON.stringify(launchpadExpected) 123 | ); 124 | }); 125 | 126 | it("initCustodies", async () => { 127 | let config = { 128 | maxOraclePriceError: 1, 129 | maxOraclePriceAgeSec: 60, 130 | oracleType: { test: {} }, 131 | oracleAccount: lpd.pricingCustody.oracleAccount, 132 | }; 133 | await lpd.initCustody(config, lpd.pricingCustody); 134 | 135 | config.oracleAccount = lpd.paymentCustody.oracleAccount; 136 | await lpd.initCustody(config, lpd.paymentCustody); 137 | 138 | let custody = await lpd.program.account.custody.fetch( 139 | lpd.pricingCustody.custody 140 | ); 141 | let custodyExpected = { 142 | tokenAccount: lpd.pricingCustody.tokenAccount, 143 | collectedFees: new BN(0), 144 | mint: lpd.pricingCustody.mint.publicKey, 145 | decimals: lpd.pricingCustody.decimals, 146 | maxOraclePriceError: config.maxOraclePriceError, 147 | maxOraclePriceAgeSec: config.maxOraclePriceAgeSec, 148 | oracleType: config.oracleType, 149 | oracleAccount: lpd.pricingCustody.oracleAccount, 150 | bump: custody.bump, 151 | }; 152 | expect(JSON.stringify(custody)).to.equal(JSON.stringify(custodyExpected)); 153 | }); 154 | 155 | it("setOracleConfig", async () => { 156 | let config = { 157 | maxOraclePriceError: 123, 158 | maxOraclePriceAgeSec: 900, 159 | oracleType: { test: {} }, 160 | oracleAccount: lpd.paymentCustody.oracleAccount, 161 | }; 162 | let custodyExpected = await lpd.program.account.custody.fetch( 163 | lpd.paymentCustody.custody 164 | ); 165 | custodyExpected.maxOraclePriceError = config.maxOraclePriceError; 166 | custodyExpected.maxOraclePriceAgeSec = config.maxOraclePriceAgeSec; 167 | custodyExpected.oracleType = config.oracleType; 168 | custodyExpected.oracleAccount = config.oracleAccount; 169 | 170 | await lpd.setOracleConfig(config, lpd.paymentCustody); 171 | 172 | let custody = await lpd.program.account.custody.fetch( 173 | lpd.paymentCustody.custody 174 | ); 175 | expect(JSON.stringify(custody)).to.equal(JSON.stringify(custodyExpected)); 176 | }); 177 | 178 | it("initAuction", async () => { 179 | auctionParams = { 180 | enabled: true, 181 | updatable: true, 182 | fixedAmount: false, 183 | common: { 184 | name: "test auction", 185 | description: "test only", 186 | aboutSeller: "Tester", 187 | sellerLink: "solana.com", 188 | startTime: new BN(222), 189 | endTime: new BN(2222), 190 | presaleStartTime: new BN(111), 191 | presaleEndTime: new BN(222), 192 | fillLimitRegAddress: new BN(10), 193 | fillLimitWlAddress: new BN(20), 194 | orderLimitRegAddress: new BN(5), 195 | orderLimitWlAddress: new BN(10), 196 | }, 197 | payment: { 198 | acceptSol: true, 199 | acceptUsdc: true, 200 | acceptOtherTokens: true, 201 | }, 202 | pricing: { 203 | custody: lpd.pricingCustody.custody, 204 | pricingModel: { dynamicDutchAuction: {} }, 205 | startPrice: new BN(100), 206 | maxPrice: new BN(200), 207 | minPrice: new BN(90), 208 | repriceDelay: new BN(5), 209 | repriceCoef: 0.05, 210 | repriceFunction: { exponential: {} }, 211 | amountFunction: { fixed: {} }, 212 | amountPerLevel: new BN(200), 213 | tickSize: new BN(2), 214 | unitSize: lpd.toTokenAmount(1, lpd.dispensingCustodies[0].decimals), 215 | }, 216 | tokenRatios: [new BN(1), new BN(2)], 217 | }; 218 | 219 | let err = await lpd.ensureFails(lpd.initAuction(auctionParams)); 220 | assert(err.error.errorCode.code === "NewAuctionsNotAllowed"); 221 | 222 | launchpadExpected.permissions = { 223 | allowNewAuctions: true, 224 | allowAuctionUpdates: true, 225 | allowAuctionRefills: true, 226 | allowAuctionPullouts: true, 227 | allowNewBids: true, 228 | allowWithdrawals: true, 229 | }; 230 | await lpd.setPermissions(launchpadExpected.permissions); 231 | 232 | await lpd.initAuction(auctionParams); 233 | 234 | let auction = await lpd.program.account.auction.fetch( 235 | lpd.auction.publicKey 236 | ); 237 | auctionExpected = { 238 | owner: lpd.seller.wallet.publicKey, 239 | enabled: true, 240 | updatable: true, 241 | fixedAmount: false, 242 | common: auctionParams.common, 243 | payment: auctionParams.payment, 244 | pricing: auctionParams.pricing, 245 | stats: { 246 | firstTradeTime: "0", 247 | lastTradeTime: "0", 248 | lastAmount: "0", 249 | lastPrice: "0", 250 | wlBidders: { 251 | fillsVolume: "0", 252 | weightedFillsSum: "0", 253 | minFillPrice: "18446744073709551615", 254 | maxFillPrice: "0", 255 | numTrades: "0", 256 | }, 257 | regBidders: { 258 | fillsVolume: "0", 259 | weightedFillsSum: "0", 260 | minFillPrice: "18446744073709551615", 261 | maxFillPrice: "0", 262 | numTrades: "0", 263 | }, 264 | }, 265 | tokens: [ 266 | { ratio: "1", account: lpd.dispensingCustodies[0].tokenAccount }, 267 | { ratio: "2", account: lpd.dispensingCustodies[1].tokenAccount }, 268 | { ratio: "0", account: "11111111111111111111111111111111" }, 269 | { ratio: "0", account: "11111111111111111111111111111111" }, 270 | { ratio: "0", account: "11111111111111111111111111111111" }, 271 | { ratio: "0", account: "11111111111111111111111111111111" }, 272 | { ratio: "0", account: "11111111111111111111111111111111" }, 273 | { ratio: "0", account: "11111111111111111111111111111111" }, 274 | { ratio: "0", account: "11111111111111111111111111111111" }, 275 | { ratio: "0", account: "11111111111111111111111111111111" }, 276 | ], 277 | numTokens: 2, 278 | creationTime: "0", 279 | updateTime: "0", 280 | bump: auction.bump, 281 | }; 282 | expect(JSON.stringify(auction)).to.equal(JSON.stringify(auctionExpected)); 283 | }); 284 | 285 | it("updateAuction", async () => { 286 | auctionParams.common.description = "updated"; 287 | let params = { 288 | common: auctionParams.common, 289 | payment: auctionParams.payment, 290 | pricing: auctionParams.pricing, 291 | tokenRatios: auctionParams.tokenRatios, 292 | }; 293 | await lpd.updateAuction(params); 294 | 295 | let auction = await lpd.program.account.auction.fetch( 296 | lpd.auction.publicKey 297 | ); 298 | auctionExpected.common.description = "updated"; 299 | expect(JSON.stringify(auction)).to.equal(JSON.stringify(auctionExpected)); 300 | }); 301 | 302 | it("disableAuction", async () => { 303 | await lpd.disableAuction(); 304 | let auction = await lpd.program.account.auction.fetch( 305 | lpd.auction.publicKey 306 | ); 307 | auctionExpected.enabled = false; 308 | expect(JSON.stringify(auction)).to.equal(JSON.stringify(auctionExpected)); 309 | }); 310 | 311 | it("enableAuction", async () => { 312 | await lpd.enableAuction(); 313 | let auction = await lpd.program.account.auction.fetch( 314 | lpd.auction.publicKey 315 | ); 316 | auctionExpected.enabled = true; 317 | expect(JSON.stringify(auction)).to.equal(JSON.stringify(auctionExpected)); 318 | }); 319 | 320 | it("addTokens", async () => { 321 | for (let i = 0; i < lpd.seller.dispensingAccounts.length; ++i) { 322 | let initialSourceBalance = await lpd.getBalance( 323 | lpd.seller.dispensingAccounts[i] 324 | ); 325 | let initialDestinationBalance = await lpd.getBalance( 326 | lpd.dispensingCustodies[i].tokenAccount 327 | ); 328 | await lpd.addTokens(200, i); 329 | let sourceBalance = await lpd.getBalance( 330 | lpd.seller.dispensingAccounts[i] 331 | ); 332 | let destinationBalance = await lpd.getBalance( 333 | lpd.dispensingCustodies[i].tokenAccount 334 | ); 335 | expect(initialSourceBalance - sourceBalance).to.equal( 336 | 200 * 10 ** lpd.dispensingCustodies[i].decimals 337 | ); 338 | expect(destinationBalance - initialDestinationBalance).to.equal( 339 | 200 * 10 ** lpd.dispensingCustodies[i].decimals 340 | ); 341 | } 342 | }); 343 | 344 | it("removeTokens", async () => { 345 | let initialSourceBalance = await lpd.getBalance( 346 | lpd.seller.dispensingAccounts[0] 347 | ); 348 | let initialDestinationBalance = await lpd.getBalance( 349 | lpd.dispensingCustodies[0].tokenAccount 350 | ); 351 | await lpd.removeTokens(50, 0); 352 | let sourceBalance = await lpd.getBalance(lpd.seller.dispensingAccounts[0]); 353 | let destinationBalance = await lpd.getBalance( 354 | lpd.dispensingCustodies[0].tokenAccount 355 | ); 356 | expect(sourceBalance - initialSourceBalance).to.equal( 357 | 50 * 10 ** lpd.dispensingCustodies[0].decimals 358 | ); 359 | expect(initialDestinationBalance - destinationBalance).to.equal( 360 | 50 * 10 ** lpd.dispensingCustodies[0].decimals 361 | ); 362 | }); 363 | 364 | it("setTestOraclePrice", async () => { 365 | await lpd.setTestOraclePrice(123, lpd.paymentCustody); 366 | await lpd.setTestOraclePrice(200, lpd.pricingCustody); 367 | 368 | let oracle = await lpd.program.account.testOracle.fetch( 369 | lpd.paymentCustody.oracleAccount 370 | ); 371 | let oracleExpected = { 372 | price: new BN(123000), 373 | expo: -3, 374 | conf: new BN(0), 375 | publishTime: oracle.publishTime, 376 | }; 377 | expect(JSON.stringify(oracle)).to.equal(JSON.stringify(oracleExpected)); 378 | }); 379 | 380 | it("setTestTime", async () => { 381 | await lpd.setTestTime(111); 382 | 383 | let auction = await lpd.program.account.auction.fetch( 384 | lpd.auction.publicKey 385 | ); 386 | expect(JSON.stringify(auction.creationTime)).to.equal( 387 | JSON.stringify(new BN(111)) 388 | ); 389 | }); 390 | 391 | it("whitelistAdd", async () => { 392 | await lpd.whitelistAdd([ 393 | lpd.users[0].wallet.publicKey, 394 | lpd.users[1].wallet.publicKey, 395 | ]); 396 | 397 | let bid = await lpd.program.account.bid.fetch( 398 | await lpd.getBidAddress(lpd.users[1].wallet.publicKey) 399 | ); 400 | let bidExpected = { 401 | owner: lpd.users[1].wallet.publicKey, 402 | auction: lpd.auction.publicKey, 403 | whitelisted: true, 404 | sellerInitialized: true, 405 | bidTime: new BN(0), 406 | bidPrice: new BN(0), 407 | bidAmount: new BN(0), 408 | bidType: { ioc: {} }, 409 | filled: new BN(0), 410 | fillTime: new BN(0), 411 | fillPrice: new BN(0), 412 | fillAmount: new BN(0), 413 | bump: bid.bump, 414 | }; 415 | expect(JSON.stringify(bid)).to.equal(JSON.stringify(bidExpected)); 416 | }); 417 | 418 | it("whitelistRemove", async () => { 419 | await lpd.whitelistRemove([lpd.users[1].wallet.publicKey]); 420 | 421 | let bid = await lpd.program.account.bid.fetch( 422 | await lpd.getBidAddress(lpd.users[1].wallet.publicKey) 423 | ); 424 | let bidExpected = { 425 | owner: lpd.users[1].wallet.publicKey, 426 | auction: lpd.auction.publicKey, 427 | whitelisted: false, 428 | sellerInitialized: true, 429 | bidTime: new BN(0), 430 | bidPrice: new BN(0), 431 | bidAmount: new BN(0), 432 | bidType: { ioc: {} }, 433 | filled: new BN(0), 434 | fillTime: new BN(0), 435 | fillPrice: new BN(0), 436 | fillAmount: new BN(0), 437 | bump: bid.bump, 438 | }; 439 | expect(JSON.stringify(bid)).to.equal(JSON.stringify(bidExpected)); 440 | }); 441 | 442 | it("getAuctionAmount", async () => { 443 | let amount = await lpd.getAuctionAmount(100); 444 | console.log("AMOUNT:", amount); 445 | //expect(amount).to.equal(100); 446 | }); 447 | 448 | it("getAuctionPrice", async () => { 449 | let price = await lpd.getAuctionPrice(100); 450 | console.log("PRICE:", price); 451 | //expect(price).to.equal(100); 452 | }); 453 | 454 | it("placeBid", async () => { 455 | let user = lpd.users[0]; 456 | 457 | // record initial balances 458 | let initialBalancePayment = await lpd.getBalance(user.paymentAccount); 459 | let initialBalancesReceiving = []; 460 | for (const meta of user.receivingAccountMetas) { 461 | initialBalancesReceiving.push(await lpd.getBalance(meta.pubkey)); 462 | } 463 | let initialReceivingSum = initialBalancesReceiving.reduce( 464 | (a, b) => a + b, 465 | 0 466 | ); 467 | 468 | // place the bid 469 | let bidAmount = 1; 470 | let bidPrice = 100; 471 | let bidType = { ioc: {} }; 472 | let availAmount = (await lpd.getAuctionAmount(bidPrice)).toNumber(); 473 | await lpd.placeBid(bidPrice, bidAmount, bidType, user); 474 | 475 | // check updated balances 476 | let balancePayment = await lpd.getBalance(user.paymentAccount); 477 | /*expect(balancePayment).to.equal( 478 | initialBalancePayment - 479 | lpd.toTokenAmount(bidPrice, lpd.paymentCustody.decimals).toNumber() 480 | );*/ 481 | //initialBalancePayment - pricePerToken * fillAmount; 482 | 483 | let balancesReceiving = []; 484 | for (const meta of user.receivingAccountMetas) { 485 | balancesReceiving.push(await lpd.getBalance(meta.pubkey)); 486 | } 487 | let expectedFillAmount = 488 | Math.min(bidAmount, availAmount) * 489 | auctionParams.pricing.unitSize.toNumber(); 490 | let receivingSum = balancesReceiving.reduce((a, b) => a + b, 0); 491 | expect(receivingSum).to.equal(initialReceivingSum + expectedFillAmount); 492 | 493 | // check bid account 494 | let bid = await lpd.program.account.bid.fetch( 495 | await lpd.getBidAddress(user.wallet.publicKey) 496 | ); 497 | let auction = await lpd.program.account.auction.fetch( 498 | lpd.auction.publicKey 499 | ); 500 | //expect(auction).to.equal(auctionExpected); 501 | let bidExpected = { 502 | owner: user.wallet.publicKey, 503 | auction: lpd.auction.publicKey, 504 | whitelisted: true, 505 | sellerInitialized: true, 506 | bidTime: auction.creationTime, 507 | bidPrice: lpd.toTokenAmount(bidPrice, lpd.pricingCustody.decimals), 508 | bidAmount: new BN(bidAmount), 509 | bidType: bidType, 510 | filled: new BN(bidAmount), 511 | fillTime: auction.creationTime, 512 | fillPrice: new BN(100), 513 | fillAmount: new BN(bidAmount), 514 | bump: bid.bump, 515 | }; 516 | expect(JSON.stringify(bid)).to.equal(JSON.stringify(bidExpected)); 517 | 518 | // check seller's balance account 519 | let sellerBalance = await lpd.program.account.sellerBalance.fetch( 520 | lpd.seller.balanceAccount 521 | ); 522 | console.log(JSON.stringify(sellerBalance)); 523 | }); 524 | 525 | it("cancelBid", async () => { 526 | await lpd.setTestTime(22222); 527 | 528 | let user = lpd.users[0]; 529 | let initializer = lpd.seller; 530 | let initialBalanceSol = await lpd.getSolBalance( 531 | initializer.wallet.publicKey 532 | ); 533 | 534 | await lpd.cancelBid(user, initializer); 535 | 536 | let balanceSol = await lpd.getSolBalance(initializer.wallet.publicKey); 537 | expect(initialBalanceSol).to.lessThan(balanceSol); 538 | 539 | await lpd.ensureFails( 540 | lpd.program.account.bid.fetch( 541 | await lpd.getBidAddress(initializer.wallet.publicKey) 542 | ), 543 | "Fetch Bid should've been failed" 544 | ); 545 | }); 546 | 547 | it("withdrawFees", async () => { 548 | let initialBalanceTokens = await lpd.getBalance(lpd.feesAccount); 549 | let withdrawAmountTokens = ( 550 | await lpd.program.account.custody.fetch(lpd.paymentCustody.custody) 551 | ).collectedFees.toNumber(); 552 | expect(withdrawAmountTokens).to.greaterThan(0); 553 | 554 | let initialBalanceSol = await lpd.getSolBalance(lpd.admins[0].publicKey); 555 | let withdrawAmountSol = await lpd.getExtraSolBalance( 556 | lpd.authority.publicKey 557 | ); 558 | expect(withdrawAmountSol).to.greaterThan(0); 559 | 560 | await lpd.withdrawFees( 561 | withdrawAmountTokens, 562 | withdrawAmountSol, 563 | lpd.paymentCustody, 564 | lpd.feesAccount, 565 | lpd.admins[0].publicKey 566 | ); 567 | 568 | let balanceTokens = await lpd.getBalance(lpd.feesAccount); 569 | expect(balanceTokens).to.equal(initialBalanceTokens + withdrawAmountTokens); 570 | 571 | let balanceSol = await lpd.getSolBalance(lpd.admins[0].publicKey); 572 | expect(balanceSol).to.equal(initialBalanceSol + withdrawAmountSol); 573 | }); 574 | 575 | it("withdrawFunds", async () => { 576 | let initialBalance = await lpd.getBalance(lpd.seller.paymentAccount); 577 | let withdrawAmount = ( 578 | await lpd.program.account.sellerBalance.fetch(lpd.seller.balanceAccount) 579 | ).balance.toNumber(); 580 | expect(withdrawAmount).to.greaterThan(0); 581 | 582 | await lpd.withdrawFunds( 583 | withdrawAmount, 584 | lpd.paymentCustody, 585 | lpd.seller.paymentAccount 586 | ); 587 | 588 | let balance = await lpd.getBalance(lpd.seller.paymentAccount); 589 | expect(balance).to.equal(initialBalance + withdrawAmount); 590 | }); 591 | 592 | it("deleteAuction", async () => { 593 | for (let i = 0; i < lpd.dispensingCustodies.length; ++i) { 594 | await lpd.removeTokens( 595 | lpd.toUiAmount( 596 | await lpd.getBalance(lpd.dispensingCustodies[i].tokenAccount), 597 | lpd.dispensingCustodies[i].decimals 598 | ), 599 | i 600 | ); 601 | } 602 | await lpd.deleteAuction(); 603 | await lpd.ensureFails( 604 | lpd.program.account.auction.fetch(lpd.auction.publicKey), 605 | "Fetch Auction should've been failed" 606 | ); 607 | await lpd.ensureFails( 608 | lpd.getTokenAccount(lpd.dispensingCustodies[0].tokenAccount), 609 | "Get dispensing token account should've been failed" 610 | ); 611 | }); 612 | }); 613 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------