Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions tokens/token-2022/transfer-fee/anchor/tests/transfer-fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import type { Program } from '@anchor-lang/core';
import * as anchor from '@anchor-lang/core';
import { ASSOCIATED_PROGRAM_ID } from '@anchor-lang/core/dist/cjs/utils/token';
import {
getAccount,
getAssociatedTokenAddressSync,
getMint,
getOrCreateAssociatedTokenAccount,
getTransferFeeAmount,
getTransferFeeConfig,
mintTo,
TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';
import { assert } from 'chai';
import type { TransferFee } from '../target/types/transfer_fee';

describe('transfer-fee', () => {
Expand Down Expand Up @@ -34,6 +39,20 @@ describe('transfer-fee', () => {
TOKEN_2022_PROGRAM_ID,
);

const fetchFeeConfig = async () => {
const mint = await getMint(connection, mintKeypair.publicKey, undefined, TOKEN_2022_PROGRAM_ID);
const config = getTransferFeeConfig(mint);
assert.isNotNull(config, 'mint has no TransferFeeConfig extension');
return config!;
};

const fetchTokenAccount = async (address: anchor.web3.PublicKey) => {
const account = await getAccount(connection, address, undefined, TOKEN_2022_PROGRAM_ID);
const withheld = getTransferFeeAmount(account);
assert.isNotNull(withheld, 'token account has no TransferFeeAmount extension');
return { amount: account.amount, withheld: withheld!.withheldAmount };
};

it('Create Mint with Transfer Fee', async () => {
const transferFeeBasisPoints = 100;
const maximumFee = 1;
Expand All @@ -44,6 +63,15 @@ describe('transfer-fee', () => {
.signers([mintKeypair])
.rpc({ skipPreflight: true });
console.log('Your transaction signature', transactionSignature);

const config = await fetchFeeConfig();
assert.isTrue(config.transferFeeConfigAuthority.equals(wallet.publicKey));
assert.isTrue(config.withdrawWithheldAuthority.equals(wallet.publicKey));
assert.strictEqual(config.withheldAmount, 0n);
assert.strictEqual(config.newerTransferFee.transferFeeBasisPoints, transferFeeBasisPoints);
assert.strictEqual(config.newerTransferFee.maximumFee, BigInt(maximumFee));
assert.strictEqual(config.olderTransferFee.transferFeeBasisPoints, transferFeeBasisPoints);
assert.strictEqual(config.olderTransferFee.maximumFee, BigInt(maximumFee));
});

it('Mint Tokens', async () => {
Expand All @@ -70,6 +98,10 @@ describe('transfer-fee', () => {
null,
TOKEN_2022_PROGRAM_ID,
);

const sender = await fetchTokenAccount(senderTokenAccountAddress);
assert.strictEqual(sender.amount, 300n);
assert.strictEqual(sender.withheld, 0n);
});

it('Transfer', async () => {
Expand All @@ -84,6 +116,14 @@ describe('transfer-fee', () => {
})
.rpc({ skipPreflight: true });
console.log('Your transaction signature', transactionSignature);

// 1% of 100 is 1, which is also the cap. The sender is debited the full
// amount; the fee comes out of the recipient's credit and is withheld there.
const sender = await fetchTokenAccount(senderTokenAccountAddress);
const recipientAccount = await fetchTokenAccount(recipientTokenAccountAddress);
assert.strictEqual(sender.amount, 200n);
assert.strictEqual(recipientAccount.amount, 99n);
assert.strictEqual(recipientAccount.withheld, 1n);
});

it('Transfer Again, fee limit by maximumFee', async () => {
Expand All @@ -98,6 +138,13 @@ describe('transfer-fee', () => {
})
.rpc({ skipPreflight: true });
console.log('Your transaction signature', transactionSignature);

// 1% of 200 would be 2; maximumFee holds it to 1, so withheld grows by 1, not 2.
const sender = await fetchTokenAccount(senderTokenAccountAddress);
const recipientAccount = await fetchTokenAccount(recipientTokenAccountAddress);
assert.strictEqual(sender.amount, 0n);
assert.strictEqual(recipientAccount.amount, 298n);
assert.strictEqual(recipientAccount.withheld, 2n);
});

it('Harvest Transfer Fees to Mint Account', async () => {
Expand All @@ -113,6 +160,12 @@ describe('transfer-fee', () => {
])
.rpc({ skipPreflight: true });
console.log('Your transaction signature', transactionSignature);

const recipientAccount = await fetchTokenAccount(recipientTokenAccountAddress);
const config = await fetchFeeConfig();
assert.strictEqual(recipientAccount.withheld, 0n);
assert.strictEqual(recipientAccount.amount, 298n);
assert.strictEqual(config.withheldAmount, 2n);
});

it('Withdraw Transfer Fees from Mint Account', async () => {
Expand All @@ -124,16 +177,34 @@ describe('transfer-fee', () => {
})
.rpc({ skipPreflight: true });
console.log('Your transaction signature', transactionSignature);

const sender = await fetchTokenAccount(senderTokenAccountAddress);
const config = await fetchFeeConfig();
assert.strictEqual(config.withheldAmount, 0n);
assert.strictEqual(sender.amount, 2n);
});

it('Update Transfer Fee', async () => {
const transferFeeBasisPoints = 0;
const maximumFee = 0;

const epochBefore = BigInt((await connection.getEpochInfo()).epoch);
const transactionSignature = await program.methods
.updateFee(transferFeeBasisPoints, new anchor.BN(maximumFee))
.accountsPartial({ mintAccount: mintKeypair.publicKey })
.rpc({ skipPreflight: true });
console.log('Your transaction signature', transactionSignature);

const epochAfter = BigInt((await connection.getEpochInfo()).epoch);

// The new fee is scheduled two epochs out from the epoch the transaction
// executed in; the old one stays in force until then. The epoch is read on
// both sides of the transaction so a rollover between them cannot flake.
const config = await fetchFeeConfig();
assert.strictEqual(config.newerTransferFee.transferFeeBasisPoints, 0);
assert.strictEqual(config.newerTransferFee.maximumFee, 0n);
assert.oneOf(config.newerTransferFee.epoch, [epochBefore + 2n, epochAfter + 2n]);
assert.strictEqual(config.olderTransferFee.transferFeeBasisPoints, 100);
assert.strictEqual(config.olderTransferFee.maximumFee, 1n);
});
});
Loading