You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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);
    });

关键修改点

  1. 用BigInt替代parseFloat进行所有金额计算,确保整数精度
  2. 买家余额变化直接计算为-oneEtherInWei(因为付2ETH退1ETH,实际支出1ETH),避免浮点数转换
  3. 合约留存金额直接等于手续费,简化计算逻辑
  4. 验证时直接传入BigInt值,无需转字符串(ethers.js的changeEtherBalance支持BigInt参数)

内容的提问来源于stack exchange,提问作者Vlad Tsykin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 01:30:56