Skip to content

Commit

Permalink
[MplCore] Deposit sell (#93)
Browse files Browse the repository at this point in the history
* init

* deposit sell contract change

* add sdk support

* format

* add tests

* remove only

* add missing metadata allowlist

* format

* address comments
  • Loading branch information
JeremyLi28 authored Apr 26, 2024
1 parent a9302c1 commit 33c5fc3
Show file tree
Hide file tree
Showing 19 changed files with 864 additions and 7 deletions.
3 changes: 3 additions & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ address = "99jtJwGDfaBKXtc7kxQneAGbERGK8F5XyJWHv7qTbj9G" # global deny list for
[[test.validator.clone]]
address = "CZ1rQoAHSqWBoAEfqGsiLhgbM59dDrCWk3rnG5FXaoRV" # libreplex royalty enforcement

[[test.validator.clone]]
address = "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d" # metaplex core program

[[test.genesis]]
address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
program = "./tests/deps/spl_token_2022.so"
Expand Down
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dependencies": {
"@magiceden-oss/open_creator_protocol": "^0.3.2",
"@metaplex-foundation/js": "^0.19.4",
"@metaplex-foundation/mpl-core": "^0.4.5",
"@metaplex-foundation/mpl-token-auth-rules": "^2.0.0",
"@metaplex-foundation/mpl-token-metadata": "^3.1.2",
"@metaplex-foundation/umi": "^0.8.2",
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions programs/mmm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ spl-associated-token-account = { version = "2.2.0", features = [
] }
spl-token-2022 = {version = "1.0.0", features = ["no-entrypoint"] }
m2_interface = { path = "../m2_interface" }
mpl-core = "0.4.4"
2 changes: 2 additions & 0 deletions programs/mmm/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,6 @@ pub enum MMMErrorCode {
InvalidTokenMetadataExtension, // 0x178e
#[msg("Invalid token member extensions")]
InvalidTokenMemberExtension, // 0x178f
#[msg("Invalid asset collection")]
InvalidAssetCollection,
}
2 changes: 2 additions & 0 deletions programs/mmm/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
pub mod admin;
pub mod ext_vanilla;
pub mod mip1;
pub mod mpl_core_asset;
pub mod ocp;
pub mod vanilla;

pub use admin::*;
pub use ext_vanilla::*;
pub use mip1::*;
pub use mpl_core_asset::*;
pub use ocp::*;
pub use vanilla::*;

Expand Down
7 changes: 7 additions & 0 deletions programs/mmm/src/instructions/mpl_core_asset/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#![allow(missing_docs)]

pub mod mpl_core_deposit_sell;
pub mod mpl_core_wrap;

pub use mpl_core_deposit_sell::*;
pub use mpl_core_wrap::*;
113 changes: 113 additions & 0 deletions programs/mmm/src/instructions/mpl_core_asset/mpl_core_deposit_sell.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use anchor_lang::prelude::*;
use mpl_core::instructions::TransferV1Builder;
use mpl_core::types::UpdateAuthority;
use solana_program::program::invoke;

use crate::{
constants::*,
errors::MMMErrorCode,
state::{Pool, SellState},
util::{check_allowlists_for_mpl_core, log_pool},
AssetInterface, IndexableAsset,
};

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MplCoreDepositSellArgs {
pub allowlist_aux: Option<String>,
}

#[derive(Accounts)]
#[instruction(args:MplCoreDepositSellArgs)]
pub struct MplCoreDepositSell<'info> {
#[account(mut)]
pub owner: Signer<'info>,
pub cosigner: Signer<'info>,
#[account(
mut,
seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()],
has_one = owner @ MMMErrorCode::InvalidOwner,
has_one = cosigner @ MMMErrorCode::InvalidCosigner,
bump
)]
pub pool: Box<Account<'info, Pool>>,
#[account(
mut,
constraint = asset.to_account_info().owner == asset_program.key,
)]
pub asset: Box<Account<'info, IndexableAsset>>,
#[account(
init_if_needed,
payer = owner,
seeds = [
SELL_STATE_PREFIX.as_bytes(),
pool.key().as_ref(),
asset.key().as_ref(),
],
space = SellState::LEN,
bump
)]
pub sell_state: Account<'info, SellState>,
/// CHECK: check collection later
collection: UncheckedAccount<'info>,

pub system_program: Program<'info, System>,
pub asset_program: Interface<'info, AssetInterface>,
}

pub fn handler(ctx: Context<MplCoreDepositSell>, args: MplCoreDepositSellArgs) -> Result<()> {
let owner = &ctx.accounts.owner;
let asset = &ctx.accounts.asset;
let pool = &mut ctx.accounts.pool;
let sell_state = &mut ctx.accounts.sell_state;
let collection = &ctx.accounts.collection;

if pool.using_shared_escrow() {
return Err(MMMErrorCode::InvalidAccountState.into());
}

let _ = check_allowlists_for_mpl_core(&pool.allowlists, asset, args.allowlist_aux)?;

let transfer_asset_builder = TransferV1Builder::new()
.asset(asset.key())
.payer(owner.key())
.collection(
if let UpdateAuthority::Collection(collection_address) = asset.update_authority {
Some(collection_address)
} else {
None
},
)
.new_owner(pool.key())
.instruction();

let mut account_infos = vec![
asset.to_account_info(),
owner.to_account_info(),
pool.to_account_info(),
];
if collection.key != &Pubkey::default() {
if UpdateAuthority::Collection(collection.key()) != asset.update_authority {
return Err(MMMErrorCode::InvalidAssetCollection.into());
}
account_infos.push(collection.to_account_info());
}

invoke(&transfer_asset_builder, account_infos.as_slice())?;

pool.sellside_asset_amount = pool
.sellside_asset_amount
.checked_add(1)
.ok_or(MMMErrorCode::NumericOverflow)?;

sell_state.pool = pool.key();
sell_state.pool_owner = owner.key();
sell_state.asset_mint = asset.key();
sell_state.cosigner_annotation = pool.cosigner_annotation;
sell_state.asset_amount = sell_state
.asset_amount
.checked_add(1)
.ok_or(MMMErrorCode::NumericOverflow)?;
log_pool("post_mpl_core_deposit_sell", pool)?;

Ok(())
}
39 changes: 39 additions & 0 deletions programs/mmm/src/instructions/mpl_core_asset/mpl_core_wrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use mpl_core::ID;
use solana_program::pubkey::Pubkey;
use std::ops::Deref;

#[derive(Clone)]
pub struct AssetInterface;

impl anchor_lang::Ids for AssetInterface {
fn ids() -> &'static [Pubkey] {
&[ID]
}
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IndexableAsset(mpl_core::IndexableAsset);

impl anchor_lang::AccountDeserialize for IndexableAsset {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
mpl_core::IndexableAsset::fetch(mpl_core::types::Key::AssetV1, buf)
.map(IndexableAsset)
.map_err(Into::into)
}
}

impl anchor_lang::AccountSerialize for IndexableAsset {}

impl anchor_lang::Owner for IndexableAsset {
fn owner() -> Pubkey {
ID
}
}

impl Deref for IndexableAsset {
type Target = mpl_core::IndexableAsset;

fn deref(&self) -> &Self::Target {
&self.0
}
}
7 changes: 7 additions & 0 deletions programs/mmm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,11 @@ pub mod mmm {
) -> Result<()> {
instructions::ext_withdraw_sell::handler(ctx, args)
}

pub fn mpl_core_deposit_sell(
ctx: Context<MplCoreDepositSell>,
args: MplCoreDepositSellArgs,
) -> Result<()> {
instructions::mpl_core_deposit_sell::handler(ctx, args)
}
}
6 changes: 4 additions & 2 deletions programs/mmm/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub const ALLOWLIST_KIND_MINT: u8 = 2;
pub const ALLOWLIST_KIND_MCC: u8 = 3;
pub const ALLOWLIST_KIND_METADATA: u8 = 4;
pub const ALLOWLIST_KIND_GROUP: u8 = 5;
pub const ALLOWLIST_KIND_MPL_CORE_COLLECTION: u8 = 6;
// ANY nft will pass the allowlist check, please make sure to use cosigner to check NFT validity
pub const ALLOWLIST_KIND_ANY: u8 = u8::MAX;

Expand All @@ -27,10 +28,11 @@ impl Allowlist {
// kind == 3: verified MCC
// kind == 4: metadata
// kind == 5: group extension
// kind == 6,7,... will be supported in the future
// kind == 6: upgrade authority
// kind == 7,8,... will be supported in the future
// kind == 255: any
pub fn valid(&self) -> bool {
if self.kind > ALLOWLIST_KIND_GROUP && self.kind != ALLOWLIST_KIND_ANY {
if self.kind > ALLOWLIST_KIND_MPL_CORE_COLLECTION && self.kind != ALLOWLIST_KIND_ANY {
return false;
}
if self.kind != 0 && self.kind != ALLOWLIST_KIND_ANY {
Expand Down
59 changes: 59 additions & 0 deletions programs/mmm/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use crate::{
},
errors::MMMErrorCode,
state::*,
IndexableAsset,
};
use anchor_lang::{prelude::*, solana_program::log::sol_log_data};
use anchor_spl::token_interface::Mint;
use m2_interface::{
withdraw_by_mmm_ix_with_program_id, WithdrawByMMMArgs, WithdrawByMmmIxArgs, WithdrawByMmmKeys,
};
use mpl_core::types::UpdateAuthority;
use mpl_token_metadata::{
accounts::{MasterEdition, Metadata},
types::TokenStandard,
Expand Down Expand Up @@ -783,6 +785,63 @@ pub fn check_allowlists_for_mint_ext(
Err(MMMErrorCode::InvalidAllowLists.into())
}

pub fn check_allowlists_for_mpl_core(
allowlists: &[Allowlist],
asset: &IndexableAsset,
allowlist_aux: Option<String>,
) -> Result<()> {
if allowlists
.iter()
.any(|&val| val.kind == ALLOWLIST_KIND_METADATA)
{
// If allowlist_aux is not passed in, do not validate URI.
if let Some(ref aux_key) = allowlist_aux {
// Handle URI padding.
if !asset.uri.trim().starts_with(aux_key) {
msg!(
"Failed metadata validation. Expected URI: |{}| but got |{}|",
*aux_key,
asset.uri
);
return Err(MMMErrorCode::UnexpectedMetadataUri.into());
}
}
}

for allowlist_val in allowlists.iter() {
match allowlist_val.kind {
ALLOWLIST_KIND_EMPTY => {
continue;
}
ALLOWLIST_KIND_ANY => {
// any is a special case, we don't need to check anything else
return Ok(());
}
ALLOWLIST_KIND_MPL_CORE_COLLECTION => {
if let UpdateAuthority::Collection(collection_address) = asset.update_authority {
if collection_address != allowlist_val.value {
return Err(MMMErrorCode::InvalidAllowLists.into());
}
return Ok(());
} else {
return Err(MMMErrorCode::InvalidAllowLists.into());
}
}
ALLOWLIST_KIND_METADATA => {
// Do not validate URI here, as we already did it above.
// These checks are separate since allowlist values are unioned together.
continue;
}
_ => {
return Err(MMMErrorCode::InvalidAllowLists.into());
}
}
}

// at the end, we didn't find a match, thus return err
Err(MMMErrorCode::InvalidAllowLists.into())
}

pub fn assert_and_get_valid_group(mint: &AccountInfo) -> Result<Option<Pubkey>> {
let borrowed_data = mint.data.borrow();
let mint_deserialized = StateWithExtensions::<Token22Mint>::unpack(&borrowed_data)?;
Expand Down
1 change: 1 addition & 0 deletions sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum AllowlistKind {
mcc = 3,
metadata = 4,
group = 5,
mpl_core_collection = 6,
any = 255,
}

Expand Down
Loading

0 comments on commit 33c5fc3

Please sign in to comment.