Uniswap V3添加流动性创建新头寸频繁交易失败排查
Uniswap V3新建头寸高失败率问题排查
问题现象
- 已完成Uniswap V3交易、新建头寸、平仓三类逻辑开发,其中交易、超价格范围头寸平仓功能运行稳定,代码可在yarn环境正常执行
- 异常表现:新建头寸操作交易失败率极高,通常需要连续重试30-40次才能成功,暂未定位根因
现有实现代码
未开仓头寸定义逻辑
export class NewPosition extends UniPosition { static fromPosition(position: Position): NewPosition { return new NewPosition({ pool: position.pool, liquidity: position.liquidity, tickLower: position.tickLower, tickUpper: position.tickUpper, }); } static withRange(pool: Pool, rangePercentage: number, amount0: string): NewPosition { const newPriceRange = calculatePriceRange(pool.token0Price, rangePercentage); const tickSpacing = pool.tickSpacing; const rawLowerTick = priceToClosestTick(newPriceRange.lower); const rawUpperTick = priceToClosestTick(newPriceRange.upper); // 现有对齐逻辑 const tickLower = rawLowerTick - (rawLowerTick % tickSpacing); const tickUpper = rawUpperTick + (tickSpacing - (rawUpperTick % tickSpacing)); return NewPosition.fromPosition( NewPosition.fromAmount0({ pool, tickLower, tickUpper, amount0, useFullPrecision: true, }), ); } }
头寸铸造逻辑
async mint(signer: Signer): Promise<BigNumber> { let useNative = Ether.onChain(await (await signer.getChainId())) as any; useNative = null; const params = NonfungiblePositionManager.addCallParameters(this, { slippageTolerance: new Percent(5, 100), deadline: ethers.constants.MaxUint256.toString(), recipient: await signer.getAddress(), createPool: false, useNative: useNative, }); const tx = await signer.sendTransaction({ to: POSITIONS_ADDRESS, from: await signer.getAddress(), data: params.calldata, value: params.value, maxFeePerGas: await getmaxFeePerGas(), maxPriorityFeePerGas: getmaxPriorityFee(), gasLimit: 600000, }); const receipt = await tx.wait(); }
根因排查与修复方案
按出现概率从高到低排序:
- Tick对齐逻辑存在负数计算bug
现有自行实现的tick取模对齐逻辑,在价格对应tick为负数时会计算出不符合Uniswap V3要求的tick值:Uniswap V3要求头寸的上下tick必须是tickSpacing的整数倍,且tickLower < tickUpper,JS语言中取模运算对负数返回负余数,会导致算出的tick不满足整除要求、甚至出现tickLower >= tickUpper的情况,直接触发合约revert。重试多次成功本质是某次数值刚好落在正数tick区间,计算结果碰巧符合要求。
修复方式:直接使用Uniswap SDK自带的nearestUsableTick方法完成tick对齐,不要自行实现取模逻辑。 - 池子状态读取陈旧触发滑点保护
现有逻辑如果复用缓存的Pool实例计算头寸参数,RPC节点返回的区块数据可能落后于链上最新状态,算出来的所需token数量和链上最新价格下的实际需求量偏差超过5%滑点容忍度时,会被合约滑点保护机制直接revert。另外代码中把deadline设为MaxUint256,会导致交易在mempool中排队时没有过期限制,排队期间价格波动就会失败。
修复方式:每次发起mint交易前,强制拉取最新区块对应的池子slot0、流动性、tick数据重新生成Pool实例再计算头寸参数;deadline改为当前区块时间戳+120秒即可,不要用无限期过期值。 - Gas参数硬编码导致交易失败
代码中硬编码gasLimit为600000,不同链、不同头寸规模、不同价格区间下mint操作的实际gas消耗存在波动,当实际消耗超过60万时会触发out of gas错误;如果自定义的getmaxFeePerGas、getmaxPriorityFee方法返回的gas价格低于链上当前baseFee要求,交易会被节点直接丢弃,表现为失败。
修复方式:发起交易前调用provider.estimateGas预估实际gas消耗,上浮20%作为gasLimit传入,不要硬编码固定值;EIP1559费用参数优先通过RPC的eth_feeHistory接口取最近几个区块的费用数据计算,不要用固定写死的返回值。 - Token授权校验缺失
现有代码未包含头寸对应两个ERC20代币对NonfungiblePositionManager合约的授权检查逻辑,如果当前地址授权额度低于本次mint需要划转的代币数量,会触发合约的余额/授权校验revert。
修复方式:每次发起mint前先检查两个代币的allowance额度,额度不足时先发起approve交易,等授权确认上链后再发送mint交易。 - 原生币传值逻辑错误
代码中先获取了链上原生币标识又强制把useNative设为null,如果当前操作的交易对包含原生币(如WETH/USDC交易对直接存ETH的场景),会导致msg.value传值和合约要求的原生币数量不匹配,触发校验失败。
修复方式:根据交易对实际包含的资产类型设置useNative参数,发送交易时确认value值和addCallParameters返回的value完全一致,不要多传或者少传。
内容的提问来源于stack exchange,提问作者Dajeir
相关产品推荐
相关产品推荐

