Rocco Musolino
Rocco Musolino

Reputation: 638

Compute fee for Solana transferCheckedWithFee instruction

When broadcastring a token-2022 program transaction using the transferCheckedWithFee instruction, solana documentation suggest to compute token fee using this formula:

const fee = (amountToSend * feeBasisPoints) / 10_000
const feeCharged = fee > maximumFee ? maximumFee : fee

But you can end up getting an error like:

Program log: TransferFeeInstruction: TransferCheckedWithFee","Program log: Calculated fee 18799792, received 18799791","Program log: Calculated fee does not match expected fee","Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 7544 of 200000 compute units

Likely a rounding issue. Is it fine to Math.ceil the fee amount?

Upvotes: 0

Views: 232

Answers (1)

Jon C
Jon C

Reputation: 8472

You've got it right, you need to ceil the division.

The fee calculation in the program will ceiling the division for you at https://github.com/solana-labs/solana-program-library/blob/b3f68a8c3c362ed2ce3677c565bcf9ed9f136310/token/program-2022/src/extension/transfer_fee/mod.rs#L45-L71.

Posting the code here for posterity:

    /// Calculate ceiling-division
    ///
    /// Ceiling-division
    ///     `ceil[ numerator / denominator ]`
    /// can be represented as a floor-division
    ///     `floor[ (numerator + denominator - 1) / denominator]`
    fn ceil_div(numerator: u128, denominator: u128) -> Option<u128> {
        numerator
            .checked_add(denominator)?
            .checked_sub(1)?
            .checked_div(denominator)
    }

    /// Calculate the transfer fee
    pub fn calculate_fee(&self, pre_fee_amount: u64) -> Option<u64> {
        let transfer_fee_basis_points = u16::from(self.transfer_fee_basis_points) as u128;
        if transfer_fee_basis_points == 0 || pre_fee_amount == 0 {
            Some(0)
        } else {
            let numerator = (pre_fee_amount as u128).checked_mul(transfer_fee_basis_points)?;
            let raw_fee = Self::ceil_div(numerator, ONE_IN_BASIS_POINTS)?
                .try_into() // guaranteed to be okay
                .ok()?;

            Some(cmp::min(raw_fee, u64::from(self.maximum_fee)))
        }
    }

Upvotes: 1

Related Questions