Solidity批量操作:让你的合约一键搞定海量任务,Gas费省到哭

Solidity里绝对硬核的活儿——批量操作!想象一下,你的合约需要给上千个用户发奖励、更新状态,或者批量转账,单干一个一个来?那Gas费得飞天!批量操作就是一键批量处理,省时省力省钱,但写不好容易Gas超限或逻辑翻车。批量操作的底子批量操作在Solidity里,主要靠数组和循环实现,但EVM的

Solidity里绝对硬核的活儿——批量操作!想象一下,你的合约需要给上千个用户发奖励、更新状态,或者批量转账,单干一个一个来?那Gas费得飞天!批量操作就是一键批量处理,省时省力省钱,但写不好容易Gas超限或逻辑翻车。

批量操作的底子

批量操作在Solidity里,主要靠数组和循环实现,但EVM的Gas限制和内存管理是关键。数组是dynamic arraystatic array,动态数组用push/pop,但操作成本高——每个pushSSTORE一次,Gas~20k。循环用forwhile,但内嵌存储操作(如SSTORE)会爆炸Gas,EVM有21M Gas上限,超了就revert。

核心技巧:

  • 内存 vs 存储:内存操作(如MLOAD/MSTORE)便宜(3 Gas),存储(SLOAD/SSTORE)贵(200/20k Gas)。批量操作尽量在内存里处理,再批量写存储。
  • Gas优化:缓存数组长度,避免重复计算;用unchecked跳溢出检查(Solidity 0.8.x);批量SSTORE用assembly
  • 安全坑
    • 重入:批量转账调用外部,需nonReentrant
    • 数组越界:循环检查长度,防越界。
    • 权限:批量操作加onlyOwner或角色控制。
  • 工具
    • Solidity 0.8.20:安全数学,unchecked优化。
    • OpenZeppelin:SafeMath(0.8.x前)和ReentrancyGuard
    • Hardhat:测试批量场景,测Gas。
  • 事件:批量操作触发事件记录,链上追踪。

咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,从简单批量转账到复杂批量更新,逐个拆解。

开发环境整活

工具得齐,用Hardhat建环境,方便写代码和测Gas。

命令行开干:

mkdir batch-op-demo
cd batch-op-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts
npm install ethers

初始化Hardhat,选TypeScript:

npx hardhat init

装依赖跑测试:

npm install --save-dev ts-node typescript @types/node @types/mocha

目录结构:

batch-op-demo/
├── contracts/
│   ├── BasicBatch.sol
│   ├── MemoryBatch.sol
│   ├── AssemblyBatch.sol
│   ├── SafeBatch.sol
│   ├── MultiSigBatch.sol
├── scripts/
│   ├── deploy.ts
├── test/
│   ├── BatchOp.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./"
  },
  "include": ["hardhat.config.ts", "scripts", "test"]
}

hardhat.config.ts

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    hardhat: {
      chainId: 1337,
    },
  },
};

export default config;

启动节点:

npx hardhat node

环境就位,代码走起!

基础批量转账

从最简单的批量转账开始,了解Gas消耗。

代码实操

contracts/BasicBatch.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract BasicBatchToken is ERC20 {
    constructor() ERC20("BasicBatchToken", "BBT") {
        _mint(msg.sender, 1000000 * 10**decimals());
    }

    function batchTransfer(address[] memory to, uint256[] memory amounts) public {
        require(to.length == amounts.length, "Arrays length mismatch");
        for (uint256 i = 0; i < to.length; i++) {
            _transfer(msg.sender, to[i], amounts[i]);
        }
    }
}

代码解析

  • 核心逻辑
    • batchTransfer:循环调用_transfer转账给多个地址。
    • 检查数组长度一致,防越界。
    • _transfer:ERC20内部转账,更新余额和totalSupply不变。
  • 安全点
    • 长度检查防止错误。
    • _transfer内置余额检查。
  • 潜在问题
    • 每个_transferSSTORE两次(发送者和接收者余额),数组长了Gas飞。
    • 无权限控制,任何人可批量转。
    • 循环内外部调用可能重入。
  • Gas成本
    • 每个转账30k Gas,100个3M Gas,接近上限。
  • 测试test/BatchOp.test.ts

测试

test/BatchOp.test.ts

import { ethers } from "hardhat";
import { expect } from "chai";
import { BasicBatchToken } from "../typechain-types";

describe("BasicBatch", function () {
  let token: BasicBatchToken;
  let owner: any, user1: any, user2: any;

  beforeEach(async function () {
    [owner, user1, user2] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("BasicBatchToken");
    token = await TokenFactory.deploy();
    await token.deployed();
  });

  it("batches transfers correctly", async function () {
    const users = [user1.address, user2.address];
    const amounts = [ethers.utils.parseEther("500"), ethers.utils.parseEther("300")];
    await token.batchTransfer(users, amounts);
    expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
    expect(await token.balanceOf(user2.address)).to.equal(ethers.utils.parseEther("300"));
  });

  it("reverts on array mismatch", async function () {
    const users = [user1.address];
    const amounts = [ethers.utils.parseEther("500"), ethers.utils.parseEther("300")];
    await expect(token.batchTransfer(users, amounts)).to.be.revertedWith("Arrays length mismatch");
  });

  it("reverts if insufficient balance", async function () {
    const users = [user1.address, user2.address];
    const amounts = [ethers.utils.parseEther("600000"), ethers.utils.parseEther("300000")];
    await expect(token.batchTransfer(users, amounts)).to.be.revertedWith("ERC20: transfer amount exceeds balance");
  });
});

跑测试:

npx hardhat test
  • 测试解析
    • 批量转账成功,余额更新。
    • 数组长度不匹配失败。
    • 余额不足失败。
  • 问题:Gas随数组长度线性增长,需优化。

内存批量处理

批量操作在内存里预处理,再批量写存储,省Gas。

代码实操

contracts/MemoryBatch.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MemoryBatchToken is ERC20 {
    constructor() ERC20("MemoryBatchToken", "MBT") {
        _mint(msg.sender, 1000000 * 10**decimals());
    }

    function batchTransfer(address[] memory to, uint256[] memory amounts) public {
        require(to.length == amounts.length, "Arrays length mismatch");
        uint256 length = to.length;
        for (uint256 i = 0; i < length; ) {
            uint256 amount = amounts[i];
            address recipient = to[i];
            _transfer(msg.sender, recipient, amount);
            unchecked { i++; }
        }
    }
}

代码解析

  • 核心逻辑
    • 缓存length = to.length,避免循环内重复MLOAD。
    • unchecked { i++; }:跳过溢出检查,省Gas(已知i不会溢出)。
    • _transfer:ERC20转账。
  • 安全点
    • 长度检查防越界。
    • unchecked安全,因循环固定长度。
  • 潜在问题
    • 仍需外部调用_transfer
  • Gas节省
    • 缓存长度省~1k Gas/循环。
    • unchecked省~5 Gas/迭代。
    • 100个转账省~10k Gas。

测试

test/BatchOp.test.ts(add):

import { MemoryBatchToken } from "../typechain-types";

describe("MemoryBatch", function () {
  let token: MemoryBatchToken;
  let owner: any, user1: any, user2: any;

  beforeEach(async function () {
    [owner, user1, user2] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("MemoryBatchToken");
    token = await TokenFactory.deploy();
    await token.deployed();
  });

  it("batches transfers with memory optimization", async function () {
    const users = [user1.address, user2.address];
    const amounts = [ethers.utils.parseEther("500"), ethers.utils.parseEther("300")];
    await token.batchTransfer(users, amounts);
    expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
    expect(await token.balanceOf(user2.address)).to.equal(ethers.utils.parseEther("300"));
  });
});
  • 测试解析:批量转账成功,优化不影响功能。
  • Gas对比:跑BasicBatchMemoryBatch,内存版省Gas。

汇编批量存储

用assembly直接操作存储,极致省Gas。

代码实操

contracts/AssemblyBatch.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract AssemblyBatchToken is ERC20 {
    constructor() ERC20("AssemblyBatchToken", "ABT") {
        _mint(msg.sender, 1000000 * 10**decimals());
    }

    function batchUpdateBalances(address[] memory users, uint256[] memory amounts) public {
        require(users.length == amounts.length, "Arrays length mismatch");
        assembly {
            let length := mload(users)
            let usersPtr := add(users, 0x20)
            let amountsPtr := add(amounts, 0x20)
            for { let i := 0 } lt(i, length) { i := add(i, 1) } {
                let user := mload(add(usersPtr, mul(i, 0x20)))
                let amount := mload(add(amountsPtr, mul(i, 0x20)))
                // Simulate balance update with assembly (actual implementation needs storage slot calculation)
                sstore(keccak256(user), amount)
            }
        }
    }
}

代码解析

  • 核心逻辑
    • 用assembly直接读内存(mload),循环更新存储(sstore)。
    • keccak256(user)计算存储槽(模拟余额映射)。
    • 避免Solidity的开销,直接EVM指令。
  • 安全点
    • 长度检查防越界。
    • assembly需小心槽位计算。
  • 潜在问题
    • 汇编调试难,错误易出错。
    • 实际余额需正确槽位。
  • Gas节省
    • 每个sstore20k Gas,assembly省5k/循环。
    • 100个更新省~500k Gas。

测试

test/BatchOp.test.ts(add):

import { AssemblyBatchToken } from "../typechain-types";

describe("AssemblyBatch", function () {
  let token: AssemblyBatchToken;
  let owner: any, user1: any, user2: any;

  beforeEach(async function () {
    [owner, user1, user2] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("AssemblyBatchToken");
    token = await TokenFactory.deploy();
    await token.deployed();
  });

  it("batches updates with assembly", async function () {
    const users = [user1.address, user2.address];
    const amounts = [ethers.utils.parseEther("500"), ethers.utils.parseEther("300")];
    await token.batchUpdateBalances(users, amounts);
    // Verify via balanceOf or custom getter
    expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500")); // Assume getter added
  });
});
  • 测试解析:assembly批量更新,Gas最低。
  • 注意:需自定义getter验证存储。

安全批量操作

批量操作易被重入或权限滥用,加保护。

代码实操

contracts/SafeBatch.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SafeBatchToken is ERC20, Ownable, ReentrancyGuard {
    constructor() ERC20("SafeBatchToken", "SBT") Ownable() {
        _mint(msg.sender, 1000000 * 10**decimals());
    }

    function batchTransfer(address[] memory to, uint256[] memory amounts) public nonReentrant {
        require(to.length == amounts.length, "Arrays length mismatch");
        require(to.length <= 100, "Batch too large"); // Gas limit
        for (uint256 i = 0; i < to.length; i++) {
            _transfer(msg.sender, to[i], amounts[i]);
        }
    }
}

代码解析

  • 核心逻辑
    • batchTransfer:批量转账,加nonReentrant防重入。
    • 限制批量大小(100),防Gas超限。
    • _transfer:ERC20内部转账。
  • 安全点
    • nonReentrant防止重入。
    • 长度和大小检查防越界和Gas攻击。
  • 潜在问题
    • 批量限制需根据Gas上限调整。
  • Gas成本
    • 每个转账30k Gas,100个3M Gas(上限内)。

测试

test/BatchOp.test.ts(add):

import { SafeBatchToken } from "../typechain-types";

describe("SafeBatch", function () {
  let token: SafeBatchToken;
  let owner: any, user1: any;

  beforeEach(async function () {
    [owner, user1] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("SafeBatchToken");
    token = await TokenFactory.deploy();
    await token.deployed();
  });

  it("batches transfers safely", async function () {
    const users = new Array(5).fill(user1.address);
    const amounts = new Array(5).fill(ethers.utils.parseEther("100"));
    await token.batchTransfer(users, amounts);
    expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
  });

  it("reverts on batch too large", async function () {
    const users = new Array(101).fill(user1.address);
    const amounts = new Array(101).fill(ethers.utils.parseEther("1"));
    await expect(token.batchTransfer(users, amounts)).to.be.revertedWith("Batch too large");
  });
});
  • 测试解析
    • 小批量转账成功。
    • 大批量失败,防Gas超限。
  • 优势:安全批量,权限控制。

多签批量操作

批量操作加多签,需多人同意。

代码实操

contracts/MultiSigBatch.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MultiSigBatchToken is ERC20, Ownable, ReentrancyGuard {
    address[] public signers;
    uint256 public required;
    uint256 public transactionCount;
    mapping(uint256 => Transaction) public transactions;
    mapping(uint256 => mapping(address => bool)) public confirmations;

    struct Transaction {
        address[] to;
        uint256[] amounts;
        bool executed;
        uint256 confirmationCount;
    }

    event SubmitBatchTransfer(uint256 indexed txId);
    event ConfirmBatchTransfer(uint256 indexed txId, address indexed signer);
    event ExecuteBatchTransfer(uint256 indexed txId);
    event RevokeConfirmation(uint256 indexed txId, address indexed signer);

    modifier onlySigner() {
        bool isSigner = false;
        for (uint256 i = 0; i < signers.length; i++) {
            if (signers[i] == msg.sender) {
                isSigner = true;
                break;
            }
        }
        require(isSigner, "Not signer");
        _;
    }

    constructor(address[] memory _signers, uint256 _required) ERC20("MultiSigBatchToken", "MSBT") Ownable() {
        require(_signers.length > 0, "Signers required");
        require(_required > 0 && _required <= _signers.length, "Invalid required");
        signers = _signers;
        required = _required;
        _mint(msg.sender, 1000000 * 10**decimals());
    }

    function submitBatchTransfer(address[] memory to, uint256[] memory amounts) public onlySigner {
        require(to.length == amounts.length, "Arrays length mismatch");
        require(to.length <= 10, "Batch too large");
        uint256 txId = transactionCount++;
        transactions[txId] = Transaction({
            to: to,
            amounts: amounts,
            executed: false,
            confirmationCount: 0
        });
        emit SubmitBatchTransfer(txId);
    }

    function confirmBatchTransfer(uint256 txId) public onlySigner nonReentrant {
        Transaction storage transaction = transactions[txId];
        require(!transaction.executed, "Transaction executed");
        require(!confirmations[txId][msg.sender], "Already confirmed");
        confirmations[txId][msg.sender] = true;
        transaction.confirmationCount++;
        emit ConfirmBatchTransfer(txId, msg.sender);
        if (transaction.confirmationCount >= required) {
            executeBatchTransfer(txId);
        }
    }

    function executeBatchTransfer(uint256 txId) internal {
        Transaction storage transaction = transactions[txId];
        require(!transaction.executed, "Transaction executed");
        require(transaction.confirmationCount >= required, "Insufficient confirmations");
        transaction.executed = true;
        for (uint256 i = 0; i < transaction.to.length; i++) {
            _transfer(msg.sender, transaction.to[i], transaction.amounts[i]);
        }
        emit ExecuteBatchTransfer(txId);
    }

    function revokeConfirmation(uint256 txId) public onlySigner {
        Transaction storage transaction = transactions[txId];
        require(!transaction.executed, "Transaction executed");
        require(confirmations[txId][msg.sender], "Not confirmed");
        confirmations[txId][msg.sender] = false;
        transaction.confirmationCount--;
        emit RevokeConfirmation(txId, msg.sender);
    }
}

代码解析

  • 核心逻辑
    • signersrequired定义多签。
    • submitBatchTransfer:提交批量转账提案。
    • confirmBatchTransfer:确认提案,够票执行。
    • executeBatchTransfer:循环_transfer
    • revokeConfirmation:撤销确认。
  • 安全点
    • 多签防滥用。
    • 批量大小限制防Gas超限。
    • nonReentrant保护执行。
  • 潜在问题
    • 数组存储在交易中,Gas成本随大小增加。
  • Gas成本
    • 提交~15k Gas。
    • 确认~10k Gas。
    • 执行~50k Gas(10个转账)。

测试

test/BatchOp.test.ts(add):

import { MultiSigBatchToken } from "../typechain-types";

describe("MultiSigBatch", function () {
  let token: MultiSigBatchToken;
  let owner: any, signer1: any, signer2: any, signer3: any, user1: any;

  beforeEach(async function () {
    [owner, signer1, signer2, signer3, user1] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("MultiSigBatchToken");
    token = await TokenFactory.deploy([signer1.address, signer2.address, signer3.address], 2);
    await token.deployed();
  });

  it("executes batch transfer with multi-sig", async function () {
    const users = [user1.address];
    const amounts = [ethers.utils.parseEther("500")];
    await token.connect(signer1).submitBatchTransfer(users, amounts);
    await token.connect(signer2).confirmBatchTransfer(0);
    await expect(token.connect(signer3).confirmBatchTransfer(0))
      .to.emit(token, "ExecuteBatchTransfer")
      .withArgs(0);
    expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
  });

  it("blocks batch without enough confirmations", async function () {
    const users = [user1.address];
    const amounts = [ethers.utils.parseEther("500")];
    await token.connect(signer1).submitBatchTransfer(users, amounts);
    await token.connect(signer2).confirmBatchTransfer(0);
    expect(await token.balanceOf(user1.address)).to.equal(0);
  });

  it("reverts on batch too large", async function () {
    const users = new Array(11).fill(user1.address);
    const amounts = new Array(11).fill(ethers.utils.parseEther("1"));
    await expect(token.connect(signer1).submitBatchTransfer(users, amounts)).to.be.revertedWith("Batch too large");
  });
});
  • 测试解析
    • 2/3确认后批量转账成功。
    • 单确认不执行。
    • 批量超限失败。
  • 优势:多签+批量,安全高效。

部署脚本

scripts/deploy.ts

import { ethers } from "hardhat";

async function main() {
  const [owner, signer1, signer2, signer3] = await ethers.getSigners();

  const BasicBatchFactory = await ethers.getContractFactory("BasicBatchToken");
  const basicBatch = await BasicBatchFactory.deploy();
  await basicBatch.deployed();
  console.log(`BasicBatchToken deployed to: ${basicBatch.address}`);

  const MemoryBatchFactory = await ethers.getContractFactory("MemoryBatchToken");
  const memoryBatch = await MemoryBatchFactory.deploy();
  await memoryBatch.deployed();
  console.log(`MemoryBatchToken deployed to: ${memoryBatch.address}`);

  const AssemblyBatchFactory = await ethers.getContractFactory("AssemblyBatchToken");
  const assemblyBatch = await AssemblyBatchFactory.deploy();
  await assemblyBatch.deployed();
  console.log(`AssemblyBatchToken deployed to: ${assemblyBatch.address}`);

  const SafeBatchFactory = await ethers.getContractFactory("SafeBatchToken");
  const safeBatch = await SafeBatchFactory.deploy();
  await safeBatch.deployed();
  console.log(`SafeBatchToken deployed to: ${safeBatch.address}`);

  const MultiSigBatchFactory = await ethers.getContractFactory("MultiSigBatchToken");
  const multiSigBatch = await MultiSigBatchFactory.deploy([signer1.address, signer2.address, signer3.address], 2);
  await multiSigBatch.deployed();
  console.log(`MultiSigBatchToken deployed to: ${multiSigBatch.address}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

跑部署:

npx hardhat run scripts/deploy.ts --network hardhat
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!