Solidity多重签名合约:打造超安全的区块链投票机制

Solidity里怎么搞一个多重签名(Multi-Signature,简称多签)合约。这玩意儿在区块链世界里可是个硬核工具,特别适合需要多人共同决策的场景,比如团队控制资金、公司治理、或者去中心化组织(DAO)的投票。多签合约的核心是:没得到足够的人同意,任何操作都别想执行,安全得像个铁桶!

多签合约核心概念

先来搞明白多签合约的几个关键点:

  • 多重签名(Multi-Signature):需要多个账户(称为“所有者”或“签名者”)对操作(如转账、调用合约)进行确认才能执行。
  • 提案(Transaction):每项操作(如转ETH、调用函数)作为一个提案,记录目标地址、数据、金额等。
  • 确认(Confirmation):所有者对提案投票,达到指定数量的确认后执行。
  • 安全机制
    • 防止重入:避免外部合约重复调用。
    • 权限控制:只有所有者能提交或确认提案。
    • 状态管理:确保提案不被重复执行。
  • OpenZeppelin:提供安全的工具库,如ReentrancyGuardOwnable
  • Solidity 0.8.x:自带溢出/下溢检查,减少安全隐患。
  • Hardhat:开发和测试工具,支持编译、部署、测试。

咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,写一个多签合约,包含提案提交、确认、执行、撤销等功能。

环境准备

用Hardhat搭建开发环境,写和测试合约。

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

初始化Hardhat:

npx hardhat init

选择TypeScript项目,安装依赖:

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

目录结构:

multisig-demo/ ├── contracts/ │ ├── MultiSigWallet.sol │ ├── AdvancedMultiSig.sol ├── scripts/ │ ├── deploy.ts ├── test/ │ ├── MultiSigWallet.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: "0.8.20", networks: { hardhat: { chainId: 1337, }, }, }; export default config;

跑本地节点:

npx hardhat node

基础多签合约

先写一个简单的多签合约,支持ETH转账。

合约代码

contracts/MultiSigWallet.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract MultiSigWallet is ReentrancyGuard { address[] public owners; uint256 public required; uint256 public transactionCount; mapping(uint256 => Transaction) public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 confirmationCount; } event SubmitTransaction(uint256 indexed txId, address indexed to, uint256 value, bytes data); event ConfirmTransaction(uint256 indexed txId, address indexed owner); event ExecuteTransaction(uint256 indexed txId); event RevokeConfirmation(uint256 indexed txId, address indexed owner); modifier onlyOwner() { bool isOwner = false; for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == msg.sender) { isOwner = true; break; } } require(isOwner, "Not owner"); _; } constructor(address[] memory _owners, uint256 _required) { require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required confirmations"); owners = _owners; required = _required; } receive() external payable {} function submitTransaction(address to, uint256 value, bytes memory data) public onlyOwner { uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: to, value: value, data: data, executed: false, confirmationCount: 0 }); emit SubmitTransaction(txId, to, value, data); } function confirmTransaction(uint256 txId) public onlyOwner nonReentrant { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(!confirmations[txId][msg.sender], "Already confirmed"); confirmations[txId][msg.sender] = true; transaction.confirmationCount++; emit ConfirmTransaction(txId, msg.sender); if (transaction.confirmationCount >= required) { executeTransaction(txId); } } function executeTransaction(uint256 txId) internal nonReentrant { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(transaction.confirmationCount >= required, "Insufficient confirmations"); transaction.executed = true; (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction failed"); emit ExecuteTransaction(txId); } function revokeConfirmation(uint256 txId) public onlyOwner { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(confirmations[txId][msg.sender], "Not confirmed"); confirmations[txId][msg.sender] = false; transaction.confirmationCount--; emit RevokeConfirmation(txId, msg.sender); } function getTransaction(uint256 txId) public view returns (address to, uint256 value, bytes memory data, bool executed, uint256 confirmationCount) { Transaction memory transaction = transactions[txId]; return (transaction.to, transaction.value, transaction.data, transaction.executed, transaction.confirmationCount); } }

解析

  • 核心结构
    • owners:所有者地址数组。
    • required:所需确认数。
    • transactions:存储提案(目标地址、金额、数据等)。
    • confirmations:记录每个所有者的确认状态。
  • 功能
    • submitTransaction:提交提案(转账或调用)。
    • confirmTransaction:确认提案,自动执行。
    • executeTransaction:执行提案,调用目标地址。
    • revokeConfirmation:撤销确认。
    • getTransaction:查询提案详情。
  • 安全特性
    • onlyOwner:限制操作者为所有者。
    • nonReentrant:防止重入攻击。
    • 检查提案状态和确认数。

测试

test/MultiSigWallet.test.ts

import { ethers } from "hardhat"; import { expect } from "chai"; import { MultiSigWallet } from "../typechain-types"; describe("MultiSigWallet", function () { let wallet: MultiSigWallet; let owner1: any, owner2: any, owner3: any, nonOwner: any; beforeEach(async function () { [owner1, owner2, owner3, nonOwner] = await ethers.getSigners(); const WalletFactory = await ethers.getContractFactory("MultiSigWallet"); wallet = await WalletFactory.deploy([owner1.address, owner2.address, owner3.address], 2); await wallet.deployed(); await owner1.sendTransaction({ to: wallet.address, value: ethers.parseEther("10") }); }); it("should initialize correctly", async function () { expect(await wallet.owners(0)).to.equal(owner1.address); expect(await wallet.owners(1)).to.equal(owner2.address); expect(await wallet.owners(2)).to.equal(owner3.address); expect(await wallet.required()).to.equal(2); }); it("should allow submitting transaction", async function () { await wallet.submitTransaction(nonOwner.address, ethers.parseEther("1"), "0x"); const [to, value, , ,] = await wallet.getTransaction(0); expect(to).to.equal(nonOwner.address); expect(value).to.equal(ethers.parseEther("1")); }); it("should restrict submit to owners", async function () { await expect( wallet.connect(nonOwner).submitTransaction(nonOwner.address, ethers.parseEther("1"), "0x") ).to.be.revertedWith("Not owner"); }); it("should allow confirming and executing transaction", async function () { await wallet.submitTransaction(nonOwner.address, ethers.parseEther("1"), "0x"); await wallet.connect(owner2).confirmTransaction(0); const balanceBefore = await ethers.provider.getBalance(nonOwner.address); await wallet.connect(owner3).confirmTransaction(0); const balanceAfter = await ethers.provider.getBalance(nonOwner.address); expect(balanceAfter.sub(balanceBefore)).to.equal(ethers.parseEther("1")); }); it("should not execute without enough confirmations", async function () { await wallet.submitTransaction(nonOwner.address, ethers.parseEther("1"), "0x"); await wallet.connect(owner2).confirmTransaction(0); const [, , , executed,] = await wallet.getTransaction(0); expect(executed).to.be.false; }); it("should allow revoking confirmation", async function () { await wallet.submitTransaction(nonOwner.address, ethers.parseEther("1"), "0x"); await wallet.connect(owner2).confirmTransaction(0); await wallet.connect(owner2).revokeConfirmation(0); await wallet.connect(owner3).confirmTransaction(0); const [, , , executed,] = await wallet.getTransaction(0); expect(executed).to.be.false; }); });

跑测试:

npx hardhat test
  • 解析
    • 部署:3个所有者,需2人确认。
    • 提交:owner1提交转账1 ETH。
    • 确认:owner2owner3确认后自动执行。
    • 撤销:owner2撤销确认,阻止执行。
  • 安全nonReentrantonlyOwner确保安全。

调用外部合约

多签合约可以调用其他合约,比如转账ERC-20代币。

辅助ERC-20合约

contracts/Token.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract Token is ERC20 { constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) { _mint(msg.sender, initialSupply); } }

测试调用

test/MultiSigWallet.test.ts(更新):

import { ethers } from "hardhat"; import { expect } from "chai"; import { MultiSigWallet, Token } from "../typechain-types"; describe("MultiSigWallet", function () { let wallet: MultiSigWallet; let token: Token; let owner1: any, owner2: any, owner3: any, nonOwner: any; beforeEach(async function () { [owner1, owner2, owner3, nonOwner] = await ethers.getSigners(); const WalletFactory = await ethers.getContractFactory("MultiSigWallet"); wallet = await WalletFactory.deploy([owner1.address, owner2.address, owner3.address], 2); await wallet.deployed(); const TokenFactory = await ethers.getContractFactory("Token"); token = await TokenFactory.deploy("TestToken", "TTK", ethers.parseEther("1000")); await token.deployed(); await owner1.sendTransaction({ to: wallet.address, value: ethers.parseEther("10") }); await token.transfer(wallet.address, ethers.parseEther("500")); }); it("should call external contract", async function () { const data = token.interface.encodeFunctionData("transfer", [nonOwner.address, ethers.parseEther("100")]); await wallet.submitTransaction(token.address, 0, data); await wallet.connect(owner2).confirmTransaction(0); await wallet.connect(owner3).confirmTransaction(0); expect(await token.balanceOf(nonOwner.address)).to.equal(ethers.parseEther("100")); }); });
  • 解析
    • data:编码transfer函数调用。
    • 提案:调用token.transfer转100 TTK。
    • 确认:2人确认后执行。
  • 安全executeTransaction检查调用结果,失败则回滚。

高级多签:动态管理所有者

实现添加/删除所有者和修改确认数的提案。

contracts/AdvancedMultiSig.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract AdvancedMultiSig is ReentrancyGuard { address[] public owners; uint256 public required; uint256 public transactionCount; mapping(uint256 => Transaction) public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; enum TransactionType { Transfer, AddOwner, RemoveOwner, ChangeRequirement } struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 confirmationCount; TransactionType txType; address newOwner; uint256 newRequired; } event SubmitTransaction( uint256 indexed txId, address indexed to, uint256 value, bytes data, TransactionType txType, address newOwner, uint256 newRequired ); event ConfirmTransaction(uint256 indexed txId, address indexed owner); event ExecuteTransaction(uint256 indexed txId); event RevokeConfirmation(uint256 indexed txId, address indexed owner); event AddOwner(address indexed owner); event RemoveOwner(address indexed owner); event ChangeRequirement(uint256 required); modifier onlyOwner() { bool isOwner = false; for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == msg.sender) { isOwner = true; break; } } require(isOwner, "Not owner"); _; } constructor(address[] memory _owners, uint256 _required) { require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required confirmations"); owners = _owners; required = _required; } receive() external payable {} function submitTransfer(address to, uint256 value, bytes memory data) public onlyOwner { uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: to, value: value, data: data, executed: false, confirmationCount: 0, txType: TransactionType.Transfer, newOwner: address(0), newRequired: 0 }); emit SubmitTransaction(txId, to, value, data, TransactionType.Transfer, address(0), 0); } function submitAddOwner(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); for (uint256 i = 0; i < owners.length; i++) { require(owners[i] != newOwner, "Owner exists"); } uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: address(0), value: 0, data: "0x", executed: false, confirmationCount: 0, txType: TransactionType.AddOwner, newOwner: newOwner, newRequired: 0 }); emit SubmitTransaction(txId, address(0), 0, "0x", TransactionType.AddOwner, newOwner, 0); } function submitRemoveOwner(address owner) public onlyOwner { bool isOwner = false; for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == owner) { isOwner = true; break; } } require(isOwner, "Not an owner"); uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: address(0), value: 0, data: "0x", executed: false, confirmationCount: 0, txType: TransactionType.RemoveOwner, newOwner: owner, newRequired: 0 }); emit SubmitTransaction(txId, address(0), 0, "0x", TransactionType.RemoveOwner, owner, 0); } function submitChangeRequirement(uint256 newRequired) public onlyOwner { require(newRequired > 0 && newRequired <= owners.length, "Invalid required"); uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: address(0), value: 0, data: "0x", executed: false, confirmationCount: 0, txType: TransactionType.ChangeRequirement, newOwner: address(0), newRequired: newRequired }); emit SubmitTransaction(txId, address(0), 0, "0x", TransactionType.ChangeRequirement, address(0), newRequired); } function confirmTransaction(uint256 txId) public onlyOwner nonReentrant { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(!confirmations[txId][msg.sender], "Already confirmed"); confirmations[txId][msg.sender] = true; transaction.confirmationCount++; emit ConfirmTransaction(txId, msg.sender); if (transaction.confirmationCount >= required) { executeTransaction(txId); } } function executeTransaction(uint256 txId) internal nonReentrant { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(transaction.confirmationCount >= required, "Insufficient confirmations"); transaction.executed = true; if (transaction.txType == TransactionType.Transfer) { (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction failed"); } else if (transaction.txType == TransactionType.AddOwner) { owners.push(transaction.newOwner); emit AddOwner(transaction.newOwner); } else if (transaction.txType == TransactionType.RemoveOwner) { for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == transaction.newOwner) { owners[i] = owners[owners.length - 1]; owners.pop(); break; } } require(required <= owners.length, "Too few owners"); emit RemoveOwner(transaction.newOwner); } else if (transaction.txType == TransactionType.ChangeRequirement) { require(transaction.newRequired <= owners.length, "Invalid required"); required = transaction.newRequired; emit ChangeRequirement(transaction.newRequired); } emit ExecuteTransaction(txId); } function revokeConfirmation(uint256 txId) public onlyOwner { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(confirmations[txId][msg.sender], "Not confirmed"); confirmations[txId][msg.sender] = false; transaction.confirmationCount--; emit RevokeConfirmation(txId, msg.sender); } function getTransaction(uint256 txId) public view returns ( address to, uint256 value, bytes memory data, bool executed, uint256 confirmationCount, TransactionType txType, address newOwner, uint256 newRequired ) { Transaction memory transaction = transactions[txId]; return ( transaction.to, transaction.value, transaction.data, transaction.executed, transaction.confirmationCount, transaction.txType, transaction.newOwner, transaction.newRequired ); } }

解析

  • 扩展功能
    • TransactionType:定义提案类型(转账、添加所有者、删除所有者、修改确认数)。
    • submitAddOwner:提交添加所有者提案。
    • submitRemoveOwner:提交删除所有者提案。
    • submitChangeRequirement:提交修改确认数提案。
  • 执行逻辑
    • 根据txType执行不同操作。
    • 添加/删除所有者:更新owners数组。
    • 修改确认数:更新required
  • 安全特性
    • 检查新所有者不重复。
    • 确保required不超过所有者数。
    • nonReentrant防止重入。

测试

test/AdvancedMultiSig.test.ts

import { ethers } from "hardhat"; import { expect } from "chai"; import { AdvancedMultiSig, Token } from "../typechain-types"; describe("AdvancedMultiSig", function () { let wallet: AdvancedMultiSig; let token: Token; let owner1: any, owner2: any, owner3: any, newOwner: any, nonOwner: any; beforeEach(async function () { [owner1, owner2, owner3, newOwner, nonOwner] = await ethers.getSigners(); const WalletFactory = await ethers.getContractFactory("AdvancedMultiSig"); wallet = await WalletFactory.deploy([owner1.address, owner2.address, owner3.address], 2); await wallet.deployed(); const TokenFactory = await ethers.getContractFactory("Token"); token = await TokenFactory.deploy("TestToken", "TTK", ethers.parseEther("1000")); await token.deployed(); await owner1.sendTransaction({ to: wallet.address, value: ethers.parseEther("10") }); await token.transfer(wallet.address, ethers.parseEther("500")); }); it("should initialize correctly", async function () { expect(await wallet.owners(0)).to.equal(owner1.address); expect(await wallet.required()).to.equal(2); }); it("should handle transfer transaction", async function () { await wallet.submitTransfer(nonOwner.address, ethers.parseEther("1"), "0x"); await wallet.connect(owner2).confirmTransaction(0); await wallet.connect(owner3).confirmTransaction(0); expect(await ethers.provider.getBalance(nonOwner.address)).to.be.above(ethers.parseEther("100")); }); it("should add new owner", async function () { await wallet.submitAddOwner(newOwner.address); await wallet.connect(owner2).confirmTransaction(0); await wallet.connect(owner3).confirmTransaction(0); expect(await wallet.owners(3)).to.equal(newOwner.address); }); it("should remove owner", async function () { await wallet.submitRemoveOwner(owner3.address); await wallet.connect(owner2).confirmTransaction(0); await wallet.connect(owner3).confirmTransaction(0); expect(await wallet.owners(0)).to.equal(owner1.address); expect(await wallet.owners(1)).to.equal(owner2.address); await expect(wallet.owners(2)).to.be.reverted; }); it("should change required confirmations", async function () { await wallet.submitChangeRequirement(3); await wallet.connect(owner2).confirmTransaction(0); await wallet.connect(owner3).confirmTransaction(0); expect(await wallet.required()).to.equal(3); }); it("should call external contract", async function () { const data = token.interface.encodeFunctionData("transfer", [nonOwner.address, ethers.parseEther("100")]); await wallet.submitTransfer(token.address, 0, data); await wallet.connect(owner2).confirmTransaction(0); await wallet.connect(owner3).confirmTransaction(0); expect(await token.balanceOf(nonOwner.address)).to.equal(ethers.parseEther("100")); }); it("should restrict add owner to valid address", async function () { await expect(wallet.submitAddOwner(owner1.address)).to.be.revertedWith("Owner exists"); await expect(wallet.submitAddOwner(address(0))).to.be.revertedWith("Invalid address"); }); it("should restrict remove owner to existing owner", async function () { await expect(wallet.submitRemoveOwner(nonOwner.address)).to.be.revertedWith("Not an owner"); }); it("should restrict new required to valid value", async function () { await expect(wallet.submitChangeRequirement(4)).to.be.revertedWith("Invalid required"); }); });
  • 解析
    • 添加所有者:newOwner加入owners
    • 删除所有者:移除owner3,数组调整。
    • 修改确认数:required从2变为3。
    • 外部调用:转100 TTK。
  • 安全:检查所有者存在性和required合法性。

提案超时机制

添加超时功能,过期提案自动失效。

contracts/AdvancedMultiSig.sol(更新):

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract AdvancedMultiSig is ReentrancyGuard { address[] public owners; uint256 public required; uint256 public transactionCount; uint256 public timeoutDuration = 1 days; mapping(uint256 => Transaction) public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; enum TransactionType { Transfer, AddOwner, RemoveOwner, ChangeRequirement } struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 confirmationCount; TransactionType txType; address newOwner; uint256 newRequired; uint256 submittedAt; } event SubmitTransaction( uint256 indexed txId, address indexed to, uint256 value, bytes data, TransactionType txType, address newOwner, uint256 newRequired ); event ConfirmTransaction(uint256 indexed txId, address indexed owner); event ExecuteTransaction(uint256 indexed txId); event RevokeConfirmation(uint256 indexed txId, address indexed owner); event AddOwner(address indexed owner); event RemoveOwner(address indexed owner); event ChangeRequirement(uint256 required); event SetTimeoutDuration(uint256 duration); modifier onlyOwner() { bool isOwner = false; for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == msg.sender) { isOwner = true; break; } } require(isOwner, "Not owner"); _; } constructor(address[] memory _owners, uint256 _required) { require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required confirmations"); owners = _owners; required = _required; } receive() external payable {} function setTimeoutDuration(uint256 duration) public onlyOwner { timeoutDuration = duration; emit SetTimeoutDuration(duration); } function submitTransfer(address to, uint256 value, bytes memory data) public onlyOwner { uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: to, value: value, data: data, executed: false, confirmationCount: 0, txType: TransactionType.Transfer, newOwner: address(0), newRequired: 0, submittedAt: block.timestamp }); emit SubmitTransaction(txId, to, value, data, TransactionType.Transfer, address(0), 0); } function submitAddOwner(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); for (uint256 i = 0; i < owners.length; i++) { require(owners[i] != newOwner, "Owner exists"); } uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: address(0), value: 0, data: "0x", executed: false, confirmationCount: 0, txType: TransactionType.AddOwner, newOwner: newOwner, newRequired: 0, submittedAt: block.timestamp }); emit SubmitTransaction(txId, address(0), 0, "0x", TransactionType.AddOwner, newOwner, 0); } function submitRemoveOwner(address owner) public onlyOwner { bool isOwner = false; for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == owner) { isOwner = true; break; } } require(isOwner, "Not an owner"); uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: address(0), value: 0, data: "0x", executed: false, confirmationCount: 0, txType: TransactionType.RemoveOwner, newOwner: owner, newRequired: 0, submittedAt: block.timestamp }); emit SubmitTransaction(txId, address(0), 0, "0x", TransactionType.RemoveOwner, owner, 0); } function submitChangeRequirement(uint256 newRequired) public onlyOwner { require(newRequired > 0 && newRequired <= owners.length, "Invalid required"); uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: address(0), value: 0, data: "0x", executed: false, confirmationCount: 0, txType: TransactionType.ChangeRequirement, newOwner: address(0), newRequired: newRequired, submittedAt: block.timestamp }); emit SubmitTransaction(txId, address(0), 0, "0x", TransactionType.ChangeRequirement, address(0), newRequired); } function confirmTransaction(uint256 txId) public onlyOwner nonReentrant { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(!confirmations[txId][msg.sender], "Already confirmed"); require(block.timestamp <= transaction.submittedAt + timeoutDuration, "Transaction timed out"); confirmations[txId][msg.sender] = true; transaction.confirmationCount++; emit ConfirmTransaction(txId, msg.sender); if (transaction.confirmationCount >= required) { executeTransaction(txId); } } function executeTransaction(uint256 txId) internal nonReentrant { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(transaction.confirmationCount >= required, "Insufficient confirmations"); require(block.timestamp <= transaction.submittedAt + timeoutDuration, "Transaction timed out"); transaction.executed = true; if (transaction.txType == TransactionType.Transfer) { (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction failed"); } else if (transaction.txType == TransactionType.AddOwner) { owners.push(transaction.newOwner); emit AddOwner(transaction.newOwner); } else if (transaction.txType == TransactionType.RemoveOwner) { for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == transaction.newOwner) { owners[i] = owners[owners.length - 1]; owners.pop(); break; } } require(required <= owners.length, "Too few owners"); emit RemoveOwner(transaction.newOwner); } else if (transaction.txType == TransactionType.ChangeRequirement) { require(transaction.newRequired <= owners.length, "Invalid required"); required = transaction.newRequired; emit ChangeRequirement(transaction.newRequired); } emit ExecuteTransaction(txId); } function revokeConfirmation(uint256 txId) public onlyOwner { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(confirmations[txId][msg.sender], "Not confirmed"); confirmations[txId][msg.sender] = false; transaction.confirmationCount--; emit RevokeConfirmation(txId, msg.sender); } function getTransaction(uint256 txId) public view returns ( address to, uint256 value, bytes memory data, bool executed, uint256 confirmationCount, TransactionType txType, address newOwner, uint256 newRequired, uint256 submittedAt ) { Transaction memory transaction = transactions[txId]; return ( transaction.to, transaction.value, transaction.data, transaction.executed, transaction.confirmationCount, transaction.txType, transaction.newOwner, transaction.newRequired, transaction.submittedAt ); } }

解析

  • 超时机制
    • timeoutDuration:默认1天,可通过setTimeoutDuration调整。
    • submittedAt:记录提案提交时间。
    • 检查:confirmTransactionexecuteTransaction验证提案未超时。
  • 安全特性
    • 超时检查防止无限期挂起。
    • onlyOwner限制超时设置。
    • 状态管理确保提案一致性。

测试

test/AdvancedMultiSig.test.ts(更新):

import { ethers } from "hardhat"; import { expect } from "chai"; import { AdvancedMultiSig, Token } from "../typechain-types"; describe("AdvancedMultiSig", function () { let wallet: AdvancedMultiSig; let token: Token; let owner1: any, owner2: any, owner3: any, newOwner: any, nonOwner: any; beforeEach(async function () { [owner1, owner2, owner3, newOwner, nonOwner] = await ethers.getSigners(); const WalletFactory = await ethers.getContractFactory("AdvancedMultiSig"); wallet = await WalletFactory.deploy([owner1.address, owner2.address, owner3.address], 2); await wallet.deployed(); const TokenFactory = await ethers.getContractFactory("Token"); token = await TokenFactory.deploy("TestToken", "TTK", ethers.parseEther("1000")); await token.deployed(); await owner1.sendTransaction({ to: wallet.address, value: ethers.parseEther("10") }); await token.transfer(wallet.address, ethers.parseEther("500")); }); it("should handle timeout", async function () { await wallet.submitTransfer(nonOwner.address, ethers.parseEther("1"), "0x"); await ethers.provider.send("evm_increaseTime", [86400 + 1]); // 1 day + 1 second await expect(wallet.connect(owner2).confirmTransaction(0)).to.be.revertedWith("Transaction timed out"); }); it("should allow setting timeout duration", async function () { await wallet.setTimeoutDuration(3600); // 1 hour expect(await wallet.timeoutDuration()).to.equal(3600); await wallet.submitTransfer(nonOwner.address, ethers.parseEther("1"), "0x"); await ethers.provider.send("evm_increaseTime", [3601]); await expect(wallet.connect(owner2).confirmTransaction(0)).to.be.revertedWith("Transaction timed out"); }); it("should restrict timeout setting to owners", async function () { await expect(wallet.connect(nonOwner).setTimeoutDuration(3600)).to.be.revertedWith("Not owner"); }); });
  • 解析
    • 超时:提案超过1天后失效。
    • 设置超时:调整为1小时,验证失效。
    • 权限:非所有者无法设置。

部署脚本

scripts/deploy.ts

import { ethers } from "hardhat"; async function main() { const [owner1, owner2, owner3] = await ethers.getSigners(); const WalletFactory = await ethers.getContractFactory("AdvancedMultiSig"); const wallet = await WalletFactory.deploy([owner1.address, owner2.address, owner3.address], 2); await wallet.deployed(); console.log(`AdvancedMultiSig deployed to: ${wallet.address}`); const TokenFactory = await ethers.getContractFactory("Token"); const token = await TokenFactory.deploy("TestToken", "TTK", ethers.parseEther("1000")); await token.deployed(); console.log(`Token deployed to: ${token.address}`); } main().catch((error) => { console.error(error); process.exitCode = 1; });

跑部署:

npx hardhat run scripts/deploy.ts --network hardhat
  • 解析:部署多签和代币合约,记录地址。
点赞 1
收藏 1
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论