diff --git a/tokens/token-2022/transfer-fee/anchor/tests/transfer-fee.ts b/tokens/token-2022/transfer-fee/anchor/tests/transfer-fee.ts index 86c906d8f..09a92edb9 100644 --- a/tokens/token-2022/transfer-fee/anchor/tests/transfer-fee.ts +++ b/tokens/token-2022/transfer-fee/anchor/tests/transfer-fee.ts @@ -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', () => { @@ -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; @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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); }); });