Solidity里绝对硬核的活儿——批量操作!想象一下,你的合约需要给上千个用户发奖励、更新状态,或者批量转账,单干一个一个来?那Gas费得飞天!批量操作就是一键批量处理,省时省力省钱,但写不好容易Gas超限或逻辑翻车。批量操作的底子批量操作在Solidity里,主要靠数组和循环实现,但EVM的
Solidity里绝对硬核的活儿——批量操作!想象一下,你的合约需要给上千个用户发奖励、更新状态,或者批量转账,单干一个一个来?那Gas费得飞天!批量操作就是一键批量处理,省时省力省钱,但写不好容易Gas超限或逻辑翻车。
批量操作在Solidity里,主要靠数组和循环实现,但EVM的Gas限制和内存管理是关键。数组是dynamic array或static array,动态数组用push/pop,但操作成本高——每个pushSSTORE一次,Gas~20k。循环用for或while,但内嵌存储操作(如SSTORE)会爆炸Gas,EVM有21M Gas上限,超了就revert。
核心技巧:
unchecked跳溢出检查(Solidity 0.8.x);批量SSTORE用assembly。nonReentrant。onlyOwner或角色控制。unchecked优化。SafeMath(0.8.x前)和ReentrancyGuard。咱们用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飞。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。
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。unchecked省~5 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"));
});
});
BasicBatch和MemoryBatch,内存版省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)
}
}
}
}
mload),循环更新存储(sstore)。keccak256(user)计算存储槽(模拟余额映射)。sstoretest/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
});
});
批量操作易被重入或权限滥用,加保护。
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防重入。_transfer:ERC20内部转账。nonReentrant防止重入。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");
});
});
批量操作加多签,需多人同意。
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);
}
}
signers和required定义多签。submitBatchTransfer:提交批量转账提案。confirmBatchTransfer:确认提案,够票执行。executeBatchTransfer:循环_transfer。revokeConfirmation:撤销确认。nonReentrant保护执行。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");
});
});
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 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!