-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unexpected behavior of createInitializeTransferFeeConfigInstruction #6113
Comments
This comment was marked as spam.
This comment was marked as spam.
Is this accurately reproducing your issue? it('initialize with null authority', async () => {
const mintAuthority = Keypair.generate();
const mintKeypair = Keypair.generate();
mint = mintKeypair.publicKey;
const mintLen = getMintLen(MINT_EXTENSIONS);
const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen);
const mintTransaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TEST_PROGRAM_ID,
}),
createInitializeTransferFeeConfigInstruction(
mint,
null, // Authority set to null on initialization
withdrawWithheldAuthority.publicKey,
FEE_BASIS_POINTS,
MAX_FEE,
TEST_PROGRAM_ID
),
createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID)
);
await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined);
// Fees should match
const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
const transferFeeConfig = getTransferFeeConfig(mintInfo);
expect(transferFeeConfig).to.not.be.null;
if (transferFeeConfig !== null) {
expect(transferFeeConfig.olderTransferFee.transferFeeBasisPoints).to.eql(0); // Should be `FEE_BASIS_POINTS`
expect(transferFeeConfig.olderTransferFee.maximumFee).to.eql(0n); // Should be `MAX_FEE`
expect(transferFeeConfig.newerTransferFee.transferFeeBasisPoints).to.eql(0); // Should be `FEE_BASIS_POINTS`
expect(transferFeeConfig.newerTransferFee.maximumFee).to.eql(0n); // Should be `MAX_FEE`
}
}); If so, it appears you're correct and I can re-produce the issue. This test passes with all those If I write the same test in Rust: #[tokio::test]
async fn success_initialize_fee_with_null_authority() {
let TransferFeeConfigWithKeypairs {
withdraw_withheld_authority,
transfer_fee_config,
..
} = test_transfer_fee_config_with_keypairs();
let mut context = TestContext::new().await;
let transfer_fee_basis_points = u16::from(
transfer_fee_config
.newer_transfer_fee
.transfer_fee_basis_points,
);
let maximum_fee = u64::from(transfer_fee_config.newer_transfer_fee.maximum_fee);
context
.init_token_with_freezing_mint(vec![ExtensionInitializationParams::TransferFeeConfig {
transfer_fee_config_authority: None,
withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(),
transfer_fee_basis_points,
maximum_fee,
}])
.await
.unwrap();
let TokenContext {
token,
..
} = context.token_context.take().unwrap();
// The fees should be the values we set
let state = token.get_mint_info().await.unwrap();
let extension = state.get_extension::<TransferFeeConfig>().unwrap();
assert_eq!(
extension.transfer_fee_config_authority,
Option::<Pubkey>::None.try_into().unwrap()
);
assert_eq!(
extension.older_transfer_fee.transfer_fee_basis_points,
transfer_fee_basis_points.into()
);
assert_eq!(
extension.older_transfer_fee.maximum_fee,
maximum_fee.into()
);
assert_eq!(
extension.newer_transfer_fee.transfer_fee_basis_points,
transfer_fee_basis_points.into()
);
assert_eq!(
extension.newer_transfer_fee.maximum_fee,
maximum_fee.into()
);
} It passes, and the fees are set correctly. ProblemFrom my investigations, it appears what's happening is the following: On the Rust side of things, the instruction builder function takes an When that's packed into a buffer, solana-program-library/token/program-2022/src/instruction.rs Lines 1042 to 1050 in 14db758
On the JavaScript side of things, we're following the pattern of a This is causing the program's deserializer to see a leading You'll notice that since there's 32 zeroes after the leading Option In summary, we need to modify the JavaScript instruction serializer to serialize for Thanks for reporting this! |
Here's some code I used to investigate the issue, for anyone who might be interested: // Rust
let ix = spl_token_2022::extension::transfer_fee::instruction::initialize_transfer_fee_config(
&spl_token_2022::id(),
token.get_address(),
None,
Some(&withdraw_withheld_authority.pubkey()),
transfer_fee_basis_points,
maximum_fee,
).unwrap();
println!("Data length: {}", ix.data.len());
println!("{:#?}", ix.data);
let ix = spl_token_2022::extension::transfer_fee::instruction::initialize_transfer_fee_config(
&spl_token_2022::id(),
token.get_address(),
Some(&withdraw_withheld_authority.pubkey()),
Some(&withdraw_withheld_authority.pubkey()),
transfer_fee_basis_points,
maximum_fee,
).unwrap();
println!("Data length: {}", ix.data.len());
println!("{:#?}", ix.data); // JavaScript
const instructionWithAuthority = createInitializeTransferFeeConfigInstruction(
new PublicKey(1),
new PublicKey(3),
new PublicKey(4),
100,
10_000n
);
const dataWithAuthority = instructionWithAuthority.data;
// Instruction without authority set
const instructionWithoutAuthority =
createInitializeTransferFeeConfigInstruction(
new PublicKey(1),
null,
new PublicKey(4),
100,
10_000n
);
const dataWithoutAuthority = instructionWithoutAuthority.data;
console.log("Data length:", dataWithAuthority.length);
console.log(dataWithAuthority.toJSON());
console.log("Data length:", dataWithoutAuthority.length);
console.log(dataWithoutAuthority.toJSON()); In Rust, the two lengths are |
When using createInitializeTransferFeeConfigInstruction with transferFeeConfigAuthority set to null but with a fee and/or maxFee defined:
Expected behavior: Either the TX fails, because you're trying to set a fee and there's no relevant authority -- OR -- the fee is set, but cannot be further changed.
Actual behavior: The TX containing this instruction succeeds but fee and maxFee are set to 0.
Tested on DevNet. Specifying any public key (e.g. with unknown private key) makes it correctly set the fee and maxFee.
According to the documentation here, transferFeeConfigAuthority should accept either a PublicKey or null:
https://solana-labs.github.io/solana-program-library/token/js/functions/createInitializeTransferFeeConfigInstruction.html
I've also found that setting maxFee to undefined sets it to zero. Not a bug -- the docs say it must be a BigInt. Maybe more intuitive if it returns an error if undefined?
The text was updated successfully, but these errors were encountered: