Fee-on-Transfer Token Support Guide
Overview
Fee-on-Transfer tokens are a class of ERC-20 tokens that automatically deduct a certain fee during transfers. JAMM DEX supports trading these tokens through specialized functions, ensuring accuracy and reliability in the swap process.
Characteristics of Fee-on-Transfer Tokens
How They Work
Fee-on-transfer tokens deduct a certain percentage of tokens as fees during each transfer
or transferFrom
call:
// Regular ERC-20 transfer
function transfer(address to, uint256 amount) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
// Fee-on-transfer token transfer
function transfer(address to, uint256 amount) external returns (bool) {
uint256 fee = amount * feeRate / 10000; // Calculate fee
uint256 actualAmount = amount - fee; // Actual transfer amount
_transfer(msg.sender, to, actualAmount);
_transfer(msg.sender, feeRecipient, fee); // Transfer fee
return true;
}
Common Types
Deflationary tokens: Burn a certain percentage of tokens on each transfer
Reflection tokens: Distribute transfer fees to all holders
Tax tokens: Send transfer fees to a specified address
Dynamic fee tokens: Dynamically adjust fee rates based on conditions
JAMM DEX Support Mechanism
Standard Swaps vs Fee-on-Transfer Support Swaps
Standard swap functions:
Pre-calculate exact input/output amounts
Assume transfer amount equals actual received amount
Not suitable for fee-on-transfer tokens
Fee-on-transfer support functions:
Calculate based on actual received amounts
Dynamically adjust swap logic
Specifically handle fee-on-transfer tokens
Supported Functions
JAMM DEX provides the following fee-on-transfer support functions:
// Exact input, supporting transfer fees
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
uint24[] calldata fees,
address to,
address referrer,
uint deadline
) external;
// JU for tokens, supporting transfer fees
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
uint24[] calldata fees,
address to,
address referrer,
uint deadline
) external payable;
// Tokens for JU, supporting transfer fees
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
uint24[] calldata fees,
address to,
address referrer,
uint deadline
) external;
Implementation Principles
Internal Swap Logic
function _swapSupportingFeeOnTransferTokens(
address[] memory path,
uint24[] memory fees,
address _to,
address _referrer
) internal virtual {
for (uint i; i < path.length - 1; i++) {
SwapData memory data;
(data.input, data.output) = (path[i], path[i + 1]);
(data.token0, ) = JAMMLibrary.sortTokens(data.input, data.output);
IJAMMPair pair = IJAMMPair(
JAMMLibrary.pairFor(factory, data.input, data.output, fees[i])
);
(uint reserve0, uint reserve1, ) = pair.getReserves();
(uint reserveInput, uint reserveOutput) = data.input == data.token0
? (reserve0, reserve1)
: (reserve1, reserve0);
// Key: Calculate input amount based on actual balance
data.amountInput =
IERC20(data.input).balanceOf(address(pair)) -
reserveInput;
data.amountOutput = JAMMLibrary.getAmountOut(
data.amountInput,
reserveInput,
reserveOutput,
fees[i]
);
(data.amount0Out, data.amount1Out) = data.input == data.token0
? (uint(0), data.amountOutput)
: (data.amountOutput, uint(0));
address to = i < path.length - 2
? JAMMLibrary.pairFor(
factory,
data.output,
path[i + 2],
fees[i + 1]
)
: _to;
pair.swap(
data.amount0Out,
data.amount1Out,
to,
new bytes(0),
_referrer
);
}
}
Key Differences:
Don't pre-calculate input amounts
Calculate based on actual balance changes in trading pairs
Recalculate input amount for each hop
Usage Guide
Token to Token Swaps
async function swapFeeOnTransferTokens(
tokenIn,
tokenOut,
amountIn,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// 1. Check if token is fee-on-transfer
const isFeeOnTransfer = await checkIfFeeOnTransfer(tokenIn);
if (!isFeeOnTransfer) {
console.warn("Token may not be fee-on-transfer, consider using standard swap functions");
}
// 2. Estimate output (note: this is only an estimate)
const path = [tokenIn, tokenOut];
const fees = [100];
// For fee-on-transfer tokens, estimates may be inaccurate
let estimatedOutput;
try {
const amounts = await router.getAmountsOut(amountIn, path, fees);
estimatedOutput = amounts[1];
} catch (error) {
// If estimation fails, use conservative minimum output
estimatedOutput = ethers.BigNumber.from(0);
}
// 3. Set larger slippage tolerance
const adjustedSlippage = Math.max(slippagePercent, 5); // At least 5% slippage
const slippageBps = Math.floor(adjustedSlippage * 100);
const amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
// 4. Execute swap
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amountIn,
amountOutMin,
path,
fees,
userAddress,
ethers.constants.AddressZero,
deadline
);
console.log("Fee-on-transfer token swap submitted:", tx.hash);
return tx;
}
JU to Fee-on-Transfer Token
async function swapJUForFeeOnTransferToken(
tokenOut,
juAmount,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const path = [WJU_ADDRESS, tokenOut];
const fees = [100];
// For fee-on-transfer tokens, set more conservative minimum output
const adjustedSlippage = Math.max(slippagePercent, 8); // At least 8% slippage
const slippageBps = Math.floor(adjustedSlippage * 100);
// Try to estimate output
let amountOutMin = ethers.BigNumber.from(0);
try {
const amounts = await router.getAmountsOut(juAmount, path, fees);
const estimatedOutput = amounts[1];
amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
} catch (error) {
console.warn("Cannot estimate output, using 0 as minimum output");
}
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.swapExactETHForTokensSupportingFeeOnTransferTokens(
amountOutMin,
path,
fees,
userAddress,
ethers.constants.AddressZero,
deadline,
{ value: juAmount }
);
return tx;
}
Fee-on-Transfer Token to JU
async function swapFeeOnTransferTokenForJU(
tokenIn,
amountIn,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const path = [tokenIn, WJU_ADDRESS];
const fees = [100];
// Set conservative minimum output
const adjustedSlippage = Math.max(slippagePercent, 10); // At least 10% slippage
const slippageBps = Math.floor(adjustedSlippage * 100);
let amountOutMin = ethers.BigNumber.from(0);
try {
const amounts = await router.getAmountsOut(amountIn, path, fees);
const estimatedOutput = amounts[1];
amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
} catch (error) {
console.warn("Cannot estimate output");
}
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.swapExactTokensForETHSupportingFeeOnTransferTokens(
amountIn,
amountOutMin,
path,
fees,
userAddress,
ethers.constants.AddressZero,
deadline
);
return tx;
}
Fee-on-Transfer Token Detection
Automatic Detection Methods
async function checkIfFeeOnTransfer(tokenAddress, testAmount = null) {
try {
const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
// Method 1: Check contract code for fee-related keywords
const code = await provider.getCode(tokenAddress);
const feeKeywords = ['fee', 'tax', 'burn', 'reflect', 'deflationary'];
const hasFeekeywords = feeKeywords.some(keyword =>
code.toLowerCase().includes(ethers.utils.id(keyword).slice(2, 10))
);
if (hasFeekeywords) {
return { isFeeOnTransfer: true, method: 'code_analysis', confidence: 'medium' };
}
// Method 2: Simulate transfer test (requires test tokens)
if (testAmount) {
const result = await simulateTransfer(tokenAddress, testAmount);
return result;
}
// Method 3: Check known fee-on-transfer token list
const knownFeeTokens = await getKnownFeeOnTransferTokens();
if (knownFeeTokens.includes(tokenAddress.toLowerCase())) {
return { isFeeOnTransfer: true, method: 'known_list', confidence: 'high' };
}
return { isFeeOnTransfer: false, method: 'default', confidence: 'low' };
} catch (error) {
console.error("Fee-on-transfer token detection failed:", error);
return { isFeeOnTransfer: false, method: 'error', confidence: 'unknown' };
}
}
async function simulateTransfer(tokenAddress, testAmount) {
// This needs to be done in a test environment or using static calls
// Actual implementation requires more complex logic
try {
const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
// Use callStatic to simulate transfer
const balanceBefore = await token.balanceOf(testAddress);
await token.callStatic.transfer(testAddress, testAmount);
const balanceAfter = await token.balanceOf(testAddress);
const actualReceived = balanceAfter.sub(balanceBefore);
const feeRate = testAmount.sub(actualReceived).mul(10000).div(testAmount);
return {
isFeeOnTransfer: feeRate.gt(0),
feeRate: feeRate.toNumber(), // Basis points
method: 'simulation',
confidence: 'high'
};
} catch (error) {
return { isFeeOnTransfer: false, method: 'simulation_failed', confidence: 'unknown' };
}
}
Known Fee-on-Transfer Token List
// Maintain a list of known fee-on-transfer tokens
const KNOWN_FEE_ON_TRANSFER_TOKENS = {
// Mainnet addresses example (actual addresses need to be determined based on JuChain network)
'UNSURE': { name: 'Example Fee Token', feeRate: 200 }, // 2% fee rate
// Add more known fee-on-transfer tokens
};
async function getKnownFeeOnTransferTokens() {
return Object.keys(KNOWN_FEE_ON_TRANSFER_TOKENS).map(addr => addr.toLowerCase());
}
function getTokenFeeInfo(tokenAddress) {
return KNOWN_FEE_ON_TRANSFER_TOKENS[tokenAddress.toLowerCase()] || null;
}
Best Practices
1. Slippage Management
function calculateFeeTokenSlippage(baseslippage, tokenInfo) {
let adjustedSlippage = baseslippage;
// Adjust slippage based on token type
if (tokenInfo.isFeeOnTransfer) {
// Base adjustment: add 2x the transfer fee rate as additional slippage
adjustedSlippage += (tokenInfo.feeRate || 500) * 2 / 100; // Convert to percentage
// Minimum slippage protection
adjustedSlippage = Math.max(adjustedSlippage, 5); // At least 5%
// Maximum slippage limit
adjustedSlippage = Math.min(adjustedSlippage, 20); // At most 20%
}
return adjustedSlippage;
}
2. Pre-Swap Validation
async function validateFeeTokenSwap(tokenIn, tokenOut, amountIn) {
const validations = [];
// Check input token
const tokenInInfo = await checkIfFeeOnTransfer(tokenIn);
if (tokenInInfo.isFeeOnTransfer) {
validations.push({
type: 'warning',
message: `Input token ${tokenIn} is fee-on-transfer, actual input may be less than expected`
});
}
// Check output token
const tokenOutInfo = await checkIfFeeOnTransfer(tokenOut);
if (tokenOutInfo.isFeeOnTransfer) {
validations.push({
type: 'warning',
message: `Output token ${tokenOut} is fee-on-transfer, actual output may be less than expected`
});
}
// Check liquidity
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
const pairAddress = await factory.getPair(tokenIn, tokenOut, 100);
if (pairAddress === ethers.constants.AddressZero) {
validations.push({
type: 'error',
message: 'Trading pair does not exist'
});
} else {
const pair = new ethers.Contract(pairAddress, pairABI, provider);
const reserves = await pair.getReserves();
if (reserves.reserve0.eq(0) || reserves.reserve1.eq(0)) {
validations.push({
type: 'error',
message: 'Insufficient liquidity in trading pair'
});
}
}
return validations;
}
3. Swap Result Analysis
async function analyzeFeeTokenSwapResult(transactionReceipt, expectedOutput) {
try {
// Parse swap events
const swapEvents = parseSwapEvents(transactionReceipt);
if (swapEvents.length === 0) {
return { success: false, error: 'No swap events found' };
}
const lastSwap = swapEvents[swapEvents.length - 1];
const actualOutput = lastSwap.amount0Out.gt(0) ? lastSwap.amount0Out : lastSwap.amount1Out;
// Calculate actual slippage
let actualSlippage = ethers.BigNumber.from(0);
if (expectedOutput.gt(0)) {
actualSlippage = expectedOutput.sub(actualOutput).mul(10000).div(expectedOutput);
}
return {
success: true,
expectedOutput: expectedOutput,
actualOutput: actualOutput,
actualSlippage: actualSlippage.toNumber(), // Basis points
swapCount: swapEvents.length,
gasUsed: transactionReceipt.gasUsed
};
} catch (error) {
return { success: false, error: error.message };
}
}
4. Multi-Hop Fee-on-Transfer Token Swaps
async function multiHopFeeTokenSwap(path, fees, amountIn, userAddress, signer) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// Check fee-on-transfer tokens in path
const feeTokens = [];
for (let i = 0; i < path.length; i++) {
const tokenInfo = await checkIfFeeOnTransfer(path[i]);
if (tokenInfo.isFeeOnTransfer) {
feeTokens.push({ index: i, address: path[i], ...tokenInfo });
}
}
if (feeTokens.length > 0) {
console.log("Fee-on-transfer tokens detected:", feeTokens);
}
// Calculate dynamic slippage
let totalFeeRate = feeTokens.reduce((sum, token) => sum + (token.feeRate || 500), 0);
let slippagePercent = Math.max(5, totalFeeRate / 50); // Calculate slippage based on total fee rate
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
// Use fee-on-transfer supporting function
const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amountIn,
0, // Set to 0 because difficult to estimate accurately
path,
fees,
userAddress,
ethers.constants.AddressZero,
deadline
);
return tx;
}
Monitoring and Debugging
Swap Monitoring
async function monitorFeeTokenSwaps() {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
// Monitor fee-on-transfer supporting swap events
const filter = router.filters.SwapExactTokensForTokensSupportingFeeOnTransferTokens();
router.on(filter, async (sender, amountIn, amountOutMin, path, fees, to, referrer, deadline, event) => {
console.log("Fee-on-transfer token swap:", {
sender,
path: path.map(addr => addr.slice(0, 6) + '...'),
amountIn: ethers.utils.formatEther(amountIn),
amountOutMin: ethers.utils.formatEther(amountOutMin),
transactionHash: event.transactionHash
});
// Analyze swap result
const receipt = await event.getTransactionReceipt();
const analysis = await analyzeFeeTokenSwapResult(receipt, amountOutMin);
console.log("Swap analysis:", analysis);
});
}
Debugging Tools
class FeeTokenDebugger {
constructor(routerAddress, provider) {
this.router = new ethers.Contract(routerAddress, routerABI, provider);
this.provider = provider;
}
async debugSwap(transactionHash) {
const receipt = await this.provider.getTransactionReceipt(transactionHash);
const transaction = await this.provider.getTransaction(transactionHash);
// Parse transaction input
const decodedInput = this.router.interface.parseTransaction({ data: transaction.data });
// Analyze swap path
const path = decodedInput.args.path;
const fees = decodedInput.args.fees;
console.log("Swap debug info:");
console.log("- Path:", path);
console.log("- Fees:", fees);
console.log("- Gas used:", receipt.gasUsed.toString());
// Check if each token is fee-on-transfer
for (let i = 0; i < path.length; i++) {
const tokenInfo = await checkIfFeeOnTransfer(path[i]);
console.log(`- Token${i} (${path[i]}):`, tokenInfo);
}
// Analyze swap events
const swapEvents = parseSwapEvents(receipt);
console.log("- Swap event count:", swapEvents.length);
return {
transaction,
receipt,
decodedInput,
swapEvents
};
}
}
Summary
Fee-on-transfer token support is an important feature of JAMM DEX. This guide covers:
Fee-on-Transfer Token Principles: Working mechanisms and common types
JAMM DEX Support: Specialized support functions and implementation principles
Usage Methods: Swap implementations in various scenarios
Token Detection: Methods to automatically identify fee-on-transfer tokens
Best Practices: Slippage management, validation, analysis, etc.
Monitoring and Debugging: Swap monitoring and troubleshooting tools
By correctly using fee-on-transfer token support features, developers can ensure normal trading of these special tokens on JAMM DEX.