Skip to content

Commit

Permalink
Implementing fixed buy side fee (#8)
Browse files Browse the repository at this point in the history
* implementing fixed buy side fee

* add buyside fee + restructure tests

* bump web3js dependency
  • Loading branch information
Giannis Chatziveroglou authored Oct 26, 2022
1 parent 1166402 commit 8bd41ed
Show file tree
Hide file tree
Showing 11 changed files with 701 additions and 109 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@saberhq/anchor-contrib": "^1.11",
"@saberhq/solana-contrib": "^1.11",
"@saberhq/token-utils": "^1.11",
"@solana/web3.js": "^1",
"@solana/web3.js": "^1.66.2",
"bn.js": "^5.2.0",
"jsbi": "^3 || ^4"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ pub struct HandlePaymentWithRoyaltiesCtx<'info> {

pub fn handler<'key, 'accounts, 'remaining, 'info>(ctx: Context<'key, 'accounts, 'remaining, 'info, HandlePaymentWithRoyaltiesCtx<'info>>, payment_amount: u64) -> Result<()> {
let payment_manager = &mut ctx.accounts.payment_manager;

// maker-taker fees
let maker_fee = payment_amount
.checked_mul(payment_manager.maker_fee_basis_points.into())
Expand All @@ -56,6 +55,7 @@ pub fn handler<'key, 'accounts, 'remaining, 'info>(ctx: Context<'key, 'accounts,

// royalties
let mut fees_paid_out: u64 = 0;
let remaining_accs = &mut ctx.remaining_accounts.iter();
if !ctx.accounts.mint_metadata.data_is_empty() {
if ctx.accounts.mint_metadata.to_account_info().owner.key() != mpl_token_metadata::id() {
return Err(error!(ErrorCode::InvalidMintMetadataOwner));
Expand Down Expand Up @@ -83,67 +83,85 @@ pub fn handler<'key, 'accounts, 'remaining, 'info>(ctx: Context<'key, 'accounts,
.expect("Add error");
total_fees = total_fees.checked_add(seller_fee).expect("Add error");

if total_creators_fee > 0 {
if let Some(creators) = mint_metadata.data.creators {
let remaining_accs = &mut ctx.remaining_accounts.iter();

let creator_amounts: Vec<u64> = creators
.clone()
.into_iter()
.map(|creator| {
total_creators_fee
.checked_mul(u64::try_from(creator.share).expect("Could not cast u8 to u64"))
.unwrap()
.checked_div(100)
.expect("Div error")
})
.collect();
let mut creators_fee_remainder = total_creators_fee.checked_sub(creator_amounts.iter().sum()).expect("Sub error");
for creator in creators {
if creator.share != 0 {
let creator_token_account_info = next_account_info(remaining_accs)?;
let creator_token_account = Account::<TokenAccount>::try_from(creator_token_account_info)?;
if creator_token_account.owner != creator.address && creator_token_account.mint != ctx.accounts.payment_mint.key() {
return Err(error!(ErrorCode::InvalidTokenAccount));
}
let share = u64::try_from(creator.share).expect("Could not cast u8 to u64");
let creator_fee_remainder_amount = u64::from(creators_fee_remainder > 0);
let creator_fee_amount = total_creators_fee
.checked_mul(share)
.unwrap()
.checked_div(100)
.expect("Div error")
.checked_add(creator_fee_remainder_amount)
.expect("Add error");
creators_fee_remainder = creators_fee_remainder.checked_sub(creator_fee_remainder_amount).expect("Sub error");

if creator_fee_amount > 0 {
fees_paid_out = fees_paid_out.checked_add(creator_fee_amount).expect("Add error");
let cpi_accounts = Transfer {
from: ctx.accounts.payer_token_account.to_account_info(),
to: creator_token_account_info.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_context, creator_fee_amount)?;
}
if let Some(creators) = mint_metadata.data.creators {
let creator_amounts: Vec<u64> = creators
.clone()
.into_iter()
.map(|creator| total_creators_fee.checked_mul(u64::try_from(creator.share).expect("Could not cast u8 to u64")).unwrap())
.collect();
let creator_amounts_sum: u64 = creator_amounts.iter().sum();
let mut creators_fee_remainder = total_creators_fee.checked_sub(creator_amounts_sum.checked_div(100).expect("Div error")).expect("Sub error");
for creator in creators {
if creator.share != 0 {
let creator_token_account_info = next_account_info(remaining_accs)?;
let creator_token_account = Account::<TokenAccount>::try_from(creator_token_account_info)?;
if creator_token_account.owner != creator.address && creator_token_account.mint != ctx.accounts.payment_mint.key() {
return Err(error!(ErrorCode::InvalidTokenAccount));
}
let share = u64::try_from(creator.share).expect("Could not cast u8 to u64");
let creator_fee_remainder_amount = u64::from(creators_fee_remainder > 0);
let creator_fee_amount = total_creators_fee
.checked_mul(share)
.unwrap()
.checked_div(100)
.expect("Div error")
.checked_add(creator_fee_remainder_amount)
.expect("Add error");
creators_fee_remainder = creators_fee_remainder.checked_sub(creator_fee_remainder_amount).expect("Sub error");

if creator_fee_amount > 0 {
fees_paid_out = fees_paid_out.checked_add(creator_fee_amount).expect("Add error");
let cpi_accounts = Transfer {
from: ctx.accounts.payer_token_account.to_account_info(),
to: creator_token_account_info.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_context, creator_fee_amount)?;
}
}
}
}
}

// pay remaining fees to fee_colector
if total_fees.checked_sub(fees_paid_out).expect("Sub error") > 0 {
// calculate fees
let buy_side_fee = payment_amount
.checked_mul(DEFAULT_BUY_SIDE_FEE_SHARE)
.unwrap()
.checked_div(BASIS_POINTS_DIVISOR.into())
.expect("Div error");
let mut fee_collector_fee = total_fees.checked_add(buy_side_fee).expect("Add error").checked_sub(fees_paid_out).expect("Sub error");

// pay buy side fee
let buy_side_token_account_info = next_account_info(remaining_accs);
if buy_side_token_account_info.is_ok() {
let buy_side_token_account = Account::<TokenAccount>::try_from(buy_side_token_account_info?);
if buy_side_token_account.is_ok() {
let cpi_accounts = Transfer {
from: ctx.accounts.payer_token_account.to_account_info(),
to: buy_side_token_account?.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_context, buy_side_fee)?;

// remove buy side fee out of fee collector fee
fee_collector_fee = fee_collector_fee.checked_sub(buy_side_fee).expect("Sub error");
}
}

if fee_collector_fee > 0 {
// pay remaining fees to fee_colector
let cpi_accounts = Transfer {
from: ctx.accounts.payer_token_account.to_account_info(),
to: ctx.accounts.fee_collector_token_account.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_context, total_fees.checked_sub(fees_paid_out).expect("Add error"))?;
token::transfer(cpi_context, fee_collector_fee)?;
}

// pay target
Expand All @@ -154,7 +172,16 @@ pub fn handler<'key, 'accounts, 'remaining, 'info>(ctx: Context<'key, 'accounts,
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_context, payment_amount.checked_sub(total_fees).expect("Sub error").checked_add(taker_fee).expect("Sub error"))?;
token::transfer(
cpi_context,
payment_amount
.checked_add(taker_fee)
.expect("Add error")
.checked_sub(total_fees)
.expect("Sub error")
.checked_sub(buy_side_fee)
.expect("Sub error"),
)?;

Ok(())
}
1 change: 1 addition & 0 deletions programs/cardinal-payment-manager/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub const PAYMENT_MANAGER_SEED: &str = "payment-manager";
pub const PAYMENT_MANAGER_SIZE: usize = 8 + std::mem::size_of::<PaymentManager>() + 16;
pub const BASIS_POINTS_DIVISOR: u16 = 10000;
pub const DEFAULT_ROYALTY_FEE_SHARE: u64 = 5000;
pub const DEFAULT_BUY_SIDE_FEE_SHARE: u64 = 50;

#[account]
pub struct PaymentManager {
Expand Down
1 change: 1 addition & 0 deletions sdk/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PublicKey } from "@solana/web3.js";
import * as PAYMENT_MANAGER_TYPES from "./idl/cardinal_payment_manager";

export const BASIS_POINTS_DIVISOR = 10000;
export const DEFAULT_BUY_SIDE_FEE_SHARE = 50;

export const PAYMENT_MANAGER_ADDRESS = new PublicKey(
"pmvYY6Wgvpe3DEj3UX1FcRpMx43sMLYLJrFTVGcqpdn"
Expand Down
2 changes: 2 additions & 0 deletions sdk/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const withHandlePaymentWithRoyalties = async (
payerTokenAccountId: PublicKey,
feeCollectorTokenAccountId: PublicKey,
paymentTokenAccountId: PublicKey,
buySideTokenAccountId?: PublicKey,
excludeCretors = []
): Promise<Transaction> => {
const [paymentManagerId] = await findPaymentManagerAddress(name);
Expand All @@ -88,6 +89,7 @@ export const withHandlePaymentWithRoyalties = async (
wallet,
mintId,
paymentMintId,
buySideTokenAccountId,
excludeCretors
);

Expand Down
26 changes: 19 additions & 7 deletions sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export const withRemainingAccountsForPayment = async (
paymentMint: web3.PublicKey,
issuerId: web3.PublicKey,
paymentManagerId: web3.PublicKey,
buySideTokenAccountId?: web3.PublicKey,
options?: {
payer?: web3.PublicKey;
receiptMint?: web3.PublicKey | null;
Expand All @@ -175,6 +176,7 @@ export const withRemainingAccountsForPayment = async (
wallet,
mint,
paymentMint,
buySideTokenAccountId,
[issuerId.toString()]
);
const mintMetadataId = await Metadata.getPDA(mint);
Expand Down Expand Up @@ -289,20 +291,19 @@ export const withRemainingAccountsForHandlePaymentWithRoyalties = async (
wallet: Wallet,
mint: web3.PublicKey,
paymentMint: web3.PublicKey,
buySideTokenAccountId?: web3.PublicKey,
excludeCreators?: string[]
): Promise<web3.AccountMeta[]> => {
const creatorsRemainingAccounts: web3.AccountMeta[] = [];
const remainingAccounts: web3.AccountMeta[] = [];
const mintMetadataId = await Metadata.getPDA(mint);
const accountInfo = await connection.getAccountInfo(mintMetadataId);
let metaplexMintData: MetadataData | undefined;
try {
metaplexMintData = MetadataData.deserialize(
accountInfo?.data as Buffer
) as MetadataData;
} catch (e) {
return [];
}
if (metaplexMintData.data.creators) {
} catch (e) {}
if (metaplexMintData && metaplexMintData.data.creators) {
for (const creator of metaplexMintData.data.creators) {
if (creator.share !== 0) {
const creatorAddress = new web3.PublicKey(creator.address);
Expand All @@ -318,7 +319,7 @@ export const withRemainingAccountsForHandlePaymentWithRoyalties = async (
wallet.publicKey,
true
);
creatorsRemainingAccounts.push({
remainingAccounts.push({
pubkey: creatorMintTokenAccount,
isSigner: false,
isWritable: true,
Expand All @@ -327,5 +328,16 @@ export const withRemainingAccountsForHandlePaymentWithRoyalties = async (
}
}

return creatorsRemainingAccounts;
return [
...remainingAccounts,
...(buySideTokenAccountId
? [
{
pubkey: buySideTokenAccountId,
isSigner: false,
isWritable: true,
},
]
: []),
];
};
51 changes: 33 additions & 18 deletions tests/handlePaymetWithRoyaltiesNoMetadata.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
findAta,
withFindOrInitAssociatedTokenAccount,
} from "@cardinal/common";
import { withFindOrInitAssociatedTokenAccount } from "@cardinal/common";
import { Metadata } from "@metaplex-foundation/mpl-token-metadata";
import { BN, web3 } from "@project-serum/anchor";
import { expectTXTable } from "@saberhq/chai-solana";
Expand All @@ -10,6 +7,7 @@ import type { Token } from "@solana/spl-token";
import * as splToken from "@solana/spl-token";
import { Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";
import { expect } from "chai";
import { DEFAULT_BUY_SIDE_FEE_SHARE } from "../sdk";
import { getPaymentManager } from "../sdk/accounts";
import { findPaymentManagerAddress } from "../sdk/pda";
import { withHandlePaymentWithRoyalties, withInit } from "../sdk/transaction";
Expand All @@ -18,6 +16,7 @@ import { createMint, withRemainingAccountsForPayment } from "../sdk/utils";
import { getProvider } from "./workspace";

describe("Handle payment with royalties with no metadata", () => {
const includeSellerFeeBasisPoints = false;
const MAKER_FEE = new BN(500);
const TAKER_FEE = new BN(300);
const ROYALTEE_FEE_SHARE = new BN(5000);
Expand All @@ -26,7 +25,7 @@ describe("Handle payment with royalties with no metadata", () => {
const RECIPIENT_START_PAYMENT_AMOUNT = new BN(10000000000);
const paymentManagerName = Math.random().toString(36).slice(2, 7);
const feeCollector = Keypair.generate();
const issuer = Keypair.generate();
const paymentReceiver = Keypair.generate();

const tokenCreator = Keypair.generate();
let paymentMint: Token;
Expand Down Expand Up @@ -70,7 +69,7 @@ describe("Handle payment with royalties with no metadata", () => {
feeCollector.publicKey,
MAKER_FEE.toNumber(),
TAKER_FEE.toNumber(),
false,
includeSellerFeeBasisPoints,
ROYALTEE_FEE_SHARE
);

Expand Down Expand Up @@ -119,7 +118,7 @@ describe("Handle payment with royalties with no metadata", () => {
provider.wallet,
rentalMint.publicKey,
paymentMint.publicKey,
issuer.publicKey,
paymentReceiver.publicKey,
paymentManagerId
);

Expand All @@ -141,6 +140,15 @@ describe("Handle payment with royalties with no metadata", () => {
null
);

let beforePayerTokenAccountAmount = new BN(0);
try {
beforePayerTokenAccountAmount = (
await paymentMintInfo.getAccountInfo(payerTokenAccountId)
).amount;
} catch (e) {
// pass
}

await withHandlePaymentWithRoyalties(
transaction,
provider.connection,
Expand All @@ -153,6 +161,7 @@ describe("Handle payment with royalties with no metadata", () => {
payerTokenAccountId,
feeCollectorTokenAccountId,
paymentTokenAccountId,
undefined,
[]
);

Expand All @@ -171,21 +180,27 @@ describe("Handle payment with royalties with no metadata", () => {

const makerFee = paymentAmount.mul(MAKER_FEE).div(BASIS_POINTS_DIVISOR);
const takerFee = paymentAmount.mul(TAKER_FEE).div(BASIS_POINTS_DIVISOR);
const totalFees = makerFee.add(takerFee);
let totalFees = makerFee.add(takerFee);
let feesPaidOut = new BN(0);

const sellerFee = new BN(0);
totalFees = totalFees.add(sellerFee);

const buySideFee = paymentAmount
.mul(new BN(DEFAULT_BUY_SIDE_FEE_SHARE))
.div(BASIS_POINTS_DIVISOR);
const feeCollectorAtaInfo = await paymentMintInfo.getAccountInfo(
feeCollectorTokenAccountId
);
expect(Number(feeCollectorAtaInfo.amount)).to.eq(totalFees.toNumber());

const issuerAtaId = await findAta(
paymentMint.publicKey,
issuer.publicKey,
true
);
const issuerAtaInfo = await paymentMintInfo.getAccountInfo(issuerAtaId);
expect(Number(issuerAtaInfo.amount)).to.eq(
paymentAmount.sub(makerFee).toNumber()
expect(Number(feeCollectorAtaInfo.amount)).to.eq(
totalFees.add(buySideFee).sub(feesPaidOut).toNumber()
);

const afterPayerTokenAccountAmount = (
await paymentMintInfo.getAccountInfo(payerTokenAccountId)
).amount;
expect(
beforePayerTokenAccountAmount.sub(afterPayerTokenAccountAmount).toNumber()
).to.eq(paymentAmount.add(takerFee).toNumber());
});
});
Loading

0 comments on commit 8bd41ed

Please sign in to comment.