使用ethers.js v6测试Solidity拍卖合约的1 wei舍入差异问题
解决ethers.js v6测试中ETH余额验证的1 wei舍入误差问题
问题场景
使用ethers.js v6进行Solidity拍卖合约测试时,验证交易后的账户ETH余额变化出现1 wei的舍入误差,导致测试不通过。
合约核心buy函数逻辑
function buy(uint index) external payable activeAuction(index) { Auction storage auction = auctions[index]; require(block.timestamp < auction.endAt, "Auction has ended"); uint price = getPriceFor(index); require(msg.value >= price, "Not enough money"); auction.stopped = true; auction.finalPrice = price; //refund uint refund = msg.value - price; if(refund > 0) { payable(msg.sender).transfer(refund); } auction.seller.transfer(price - ((price * FEE)/100)); emit AuctionEnded(index, price, msg.sender); }
原测试代码(存在精度问题)
it("Should allow to buy and refund", async () => { const FEE = 10; const oneEtherInWei = ethers.parseEther("1"); // 1 ETH = 1e18 wei const twoEtherInWei = ethers.parseEther("2"); // 2 ETH = 2e18 wei await expect(smartContract.connect(seller).createAuction( oneEtherInWei, FEE, "test item", 60 )); const tx = await smartContract.connect(buyer).buy(0, { value: twoEtherInWei }); await tx.wait(); const feeAmount = (parseFloat(oneEtherInWei.toString()) * FEE) / 100; console.log("feeAmount :", feeAmount); const moneyForSeller = parseFloat(oneEtherInWei.toString()) - feeAmount; console.log("moneyForSeller :", moneyForSeller); const moneyLeftOnContract = parseFloat(oneEtherInWei.toString()) - moneyForSeller; console.log("moneyLeftOnContract :", moneyLeftOnContract); await expect(tx).to.changeEtherBalance(smartContract.target, moneyLeftOnContract.toString()) // 验证合约余额变化 await expect(tx).to.changeEtherBalance(buyer.address, (parseFloat(twoEtherInWei) * -1).toString()) // 验证买家余额变化 await expect(tx).to.changeEtherBalance(seller.address, moneyForSeller.toString()) // 验证卖家余额变化 const price = await smartContract.auctions(0).finalPrice; expect(tx).to.emit(smartContract, "AuctionEnded") .withArgs(0, price, buyer); });
问题原因
核心问题是使用parseFloat处理wei级别的大整数导致精度丢失:
- wei是1018级别的超大整数,而JavaScript的`parseFloat`基于双精度浮点数,仅能精确表示253(约9×10^15)以内的整数。
- 当把1e18这样的大整数转成浮点数时,会被近似存储,后续计算(比如乘以10%手续费)会产生微小的精度偏差,最终导致1 wei的误差。
解决方案
全程使用ethers.js返回的BigInt类型进行整数运算,完全避免浮点数的精度问题,同时和Solidity中的uint整数除法行为保持一致(截断取整)。
修改后的测试代码
it("Should allow to buy and refund", async () => { const FEE = 10; const FEE_BIGINT = BigInt(FEE); const HUNDRED = BigInt(100); const oneEtherInWei = ethers.parseEther("1"); // 返回BigInt类型 const twoEtherInWei = ethers.parseEther("2"); await smartContract.connect(seller).createAuction( oneEtherInWei, FEE, "test item", 60 ); const tx = await smartContract.connect(buyer).buy(0, { value: twoEtherInWei }); await tx.wait(); // 用BigInt做整数运算,和Solidity逻辑完全一致 const feeAmount = oneEtherInWei * FEE_BIGINT / HUNDRED; const moneyForSeller = oneEtherInWei - feeAmount; // 合约留存的就是手续费,无需额外计算 const moneyLeftOnContract = feeAmount; // 直接传入BigInt值,无需转字符串(ethers.js会自动处理) await expect(tx).to.changeEtherBalance(smartContract.target, moneyLeftOnContract) // 买家实际支出:付2ETH,退款1ETH,所以余额变化是-1ETH await expect(tx).to.changeEtherBalance(buyer.address, oneEtherInWei * BigInt(-1)) await expect(tx).to.changeEtherBalance(seller.address, moneyForSeller) const price = await smartContract.auctions(0).finalPrice; await expect(tx).to.emit(smartContract, "AuctionEnded") .withArgs(0, price, buyer.address); });
关键修改点
- 用
BigInt替代parseFloat进行所有金额计算,确保整数精度 - 买家余额变化直接计算为
-oneEtherInWei(因为付2ETH退1ETH,实际支出1ETH),避免浮点数转换 - 合约留存金额直接等于手续费,简化计算逻辑
- 验证时直接传入BigInt值,无需转字符串(ethers.js的
changeEtherBalance支持BigInt参数)
内容的提问来源于stack exchange,提问作者Vlad Tsykin
相关产品推荐
相关产品推荐

