转账费代币支持
转账费代币(Fee-on-Transfer Tokens)是一类在转账时自动扣除一定费用的ERC-20代币。JAMM DEX通过专门的函数支持这类代币的交易,确保交换过程的准确性和可靠性。
转账费代币的特点
工作原理
转账费代币在每次transfer
或transferFrom
调用时会扣除一定比例的代币作为费用:
// 普通ERC-20转账
function transfer(address to, uint256 amount) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
// 转账费代币转账
function transfer(address to, uint256 amount) external returns (bool) {
uint256 fee = amount * feeRate / 10000; // 计算费用
uint256 actualAmount = amount - fee; // 实际转账数量
_transfer(msg.sender, to, actualAmount);
_transfer(msg.sender, feeRecipient, fee); // 转账费用
return true;
}
常见类型
通缩代币: 每次转账销毁一定比例的代币
反射代币: 将转账费用分配给所有持有者
税收代币: 将转账费用发送到指定地址
动态费率代币: 根据条件动态调整费率
JAMM DEX的支持机制
标准交换 vs 转账费支持交换
标准交换函数:
预先计算精确的输入/输出数量
假设转账数量等于实际到账数量
不适用于转账费代币
转账费支持函数:
基于实际到账数量进行计算
动态调整交换逻辑
专门处理转账费代币
支持的函数
JAMM DEX提供以下转账费支持函数:
// 精确输入,支持转账费
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
uint24[] calldata fees,
address to,
address referrer,
uint deadline
) external;
// JU换代币,支持转账费
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
uint24[] calldata fees,
address to,
address referrer,
uint deadline
) external payable;
// 代币换JU,支持转账费
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
uint24[] calldata fees,
address to,
address referrer,
uint deadline
) external;
实现原理
内部交换逻辑
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);
// 关键:基于实际余额计算输入数量
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
);
}
}
关键差异:
不预先计算输入数量
基于交易对的实际余额变化计算
每一跳都重新计算输入数量
使用指南
代币到代币交换
async function swapFeeOnTransferTokens(
tokenIn,
tokenOut,
amountIn,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// 1. 检查代币是否为转账费代币
const isFeeOnTransfer = await checkIfFeeOnTransfer(tokenIn);
if (!isFeeOnTransfer) {
console.warn("代币可能不是转账费代币,考虑使用标准交换函数");
}
// 2. 预估输出(注意:这只是估算)
const path = [tokenIn, tokenOut];
const fees = [100];
// 对于转账费代币,预估可能不准确
let estimatedOutput;
try {
const amounts = await router.getAmountsOut(amountIn, path, fees);
estimatedOutput = amounts[1];
} catch (error) {
// 如果预估失败,使用保守的最小输出
estimatedOutput = ethers.BigNumber.from(0);
}
// 3. 设置较大的滑点容忍度
const adjustedSlippage = Math.max(slippagePercent, 5); // 至少5%滑点
const slippageBps = Math.floor(adjustedSlippage * 100);
const amountOutMin = estimatedOutput.mul(10000 - slippageBps).div(10000);
// 4. 执行交换
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("转账费代币交换已提交:", tx.hash);
return tx;
}
JU到转账费代币
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];
// 对于转账费代币,设置更保守的最小输出
const adjustedSlippage = Math.max(slippagePercent, 8); // 至少8%滑点
const slippageBps = Math.floor(adjustedSlippage * 100);
// 尝试预估输出
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("无法预估输出,使用0作为最小输出");
}
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;
}
转账费代币到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];
// 设置保守的最小输出
const adjustedSlippage = Math.max(slippagePercent, 10); // 至少10%滑点
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("无法预估输出");
}
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;
}
转账费代币检测
自动检测方法
async function checkIfFeeOnTransfer(tokenAddress, testAmount = null) {
try {
const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
// 方法1: 检查合约代码中是否包含费用相关关键词
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' };
}
// 方法2: 模拟转账测试(需要测试代币)
if (testAmount) {
const result = await simulateTransfer(tokenAddress, testAmount);
return result;
}
// 方法3: 检查已知的转账费代币列表
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("检测转账费代币失败:", error);
return { isFeeOnTransfer: false, method: 'error', confidence: 'unknown' };
}
}
async function simulateTransfer(tokenAddress, testAmount) {
// 这需要在测试环境中进行,或者使用静态调用
// 实际实现需要更复杂的逻辑
try {
const token = new ethers.Contract(tokenAddress, erc20ABI, provider);
// 使用callStatic模拟转账
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(), // 基点
method: 'simulation',
confidence: 'high'
};
} catch (error) {
return { isFeeOnTransfer: false, method: 'simulation_failed', confidence: 'unknown' };
}
}
已知转账费代币列表
// 维护一个已知的转账费代币列表
const KNOWN_FEE_ON_TRANSFER_TOKENS = {
// 主网地址示例(实际地址需要根据JuChain网络确定)
'UNSURE': { name: 'Example Fee Token', feeRate: 200 }, // 2%费率
// 添加更多已知的转账费代币
};
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;
}
最佳实践
1. 滑点管理
function calculateFeeTokenSlippage(baseslippage, tokenInfo) {
let adjustedSlippage = baseslippage;
// 根据代币类型调整滑点
if (tokenInfo.isFeeOnTransfer) {
// 基础调整:增加转账费率的2倍作为额外滑点
adjustedSlippage += (tokenInfo.feeRate || 500) * 2 / 100; // 转换为百分比
// 最小滑点保护
adjustedSlippage = Math.max(adjustedSlippage, 5); // 至少5%
// 最大滑点限制
adjustedSlippage = Math.min(adjustedSlippage, 20); // 最多20%
}
return adjustedSlippage;
}
2. 交换前验证
async function validateFeeTokenSwap(tokenIn, tokenOut, amountIn) {
const validations = [];
// 检查输入代币
const tokenInInfo = await checkIfFeeOnTransfer(tokenIn);
if (tokenInInfo.isFeeOnTransfer) {
validations.push({
type: 'warning',
message: `输入代币 ${tokenIn} 是转账费代币,实际输入可能少于预期`
});
}
// 检查输出代币
const tokenOutInfo = await checkIfFeeOnTransfer(tokenOut);
if (tokenOutInfo.isFeeOnTransfer) {
validations.push({
type: 'warning',
message: `输出代币 ${tokenOut} 是转账费代币,实际输出可能少于预期`
});
}
// 检查流动性
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: '交易对不存在'
});
} 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: '交易对流动性不足'
});
}
}
return validations;
}
3. 交换结果分析
async function analyzeFeeTokenSwapResult(transactionReceipt, expectedOutput) {
try {
// 解析交换事件
const swapEvents = parseSwapEvents(transactionReceipt);
if (swapEvents.length === 0) {
return { success: false, error: '未找到交换事件' };
}
const lastSwap = swapEvents[swapEvents.length - 1];
const actualOutput = lastSwap.amount0Out.gt(0) ? lastSwap.amount0Out : lastSwap.amount1Out;
// 计算实际滑点
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(), // 基点
swapCount: swapEvents.length,
gasUsed: transactionReceipt.gasUsed
};
} catch (error) {
return { success: false, error: error.message };
}
}
4. 多跳转账费代币交换
async function multiHopFeeTokenSwap(path, fees, amountIn, userAddress, signer) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// 检查路径中的转账费代币
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("检测到转账费代币:", feeTokens);
}
// 计算动态滑点
let totalFeeRate = feeTokens.reduce((sum, token) => sum + (token.feeRate || 500), 0);
let slippagePercent = Math.max(5, totalFeeRate / 50); // 基于总费率计算滑点
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
// 使用支持转账费的函数
const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amountIn,
0, // 设置为0,因为难以准确预估
path,
fees,
userAddress,
ethers.constants.AddressZero,
deadline
);
return tx;
}
监控和调试
交换监控
async function monitorFeeTokenSwaps() {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
// 监听支持转账费的交换事件
const filter = router.filters.SwapExactTokensForTokensSupportingFeeOnTransferTokens();
router.on(filter, async (sender, amountIn, amountOutMin, path, fees, to, referrer, deadline, event) => {
console.log("转账费代币交换:", {
sender,
path: path.map(addr => addr.slice(0, 6) + '...'),
amountIn: ethers.utils.formatEther(amountIn),
amountOutMin: ethers.utils.formatEther(amountOutMin),
transactionHash: event.transactionHash
});
// 分析交换结果
const receipt = await event.getTransactionReceipt();
const analysis = await analyzeFeeTokenSwapResult(receipt, amountOutMin);
console.log("交换分析:", analysis);
});
}
调试工具
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);
// 解析交易输入
const decodedInput = this.router.interface.parseTransaction({ data: transaction.data });
// 分析交换路径
const path = decodedInput.args.path;
const fees = decodedInput.args.fees;
console.log("交换调试信息:");
console.log("- 路径:", path);
console.log("- 费率:", fees);
console.log("- Gas使用:", receipt.gasUsed.toString());
// 检查每个代币是否为转账费代币
for (let i = 0; i < path.length; i++) {
const tokenInfo = await checkIfFeeOnTransfer(path[i]);
console.log(`- 代币${i} (${path[i]}):`, tokenInfo);
}
// 分析交换事件
const swapEvents = parseSwapEvents(receipt);
console.log("- 交换事件数量:", swapEvents.length);
return {
transaction,
receipt,
decodedInput,
swapEvents
};
}
}
总结
转账费代币支持是JAMM DEX的重要功能,本指南涵盖了:
转账费代币原理: 工作机制和常见类型
JAMM DEX支持: 专门的支持函数和实现原理
使用方法: 各种场景下的交换实现
代币检测: 自动识别转账费代币的方法
最佳实践: 滑点管理、验证、分析等
监控调试: 交换监控和问题调试工具
通过正确使用转账费代币支持功能,开发者可以确保这类特殊代币在JAMM DEX上的正常交易。
Last updated