Solidity里怎么搞一个多重签名(Multi-Signature,简称多签)合约。这玩意儿在区块链世界里可是个硬核工具,特别适合需要多人共同决策的场景,比如团队控制资金、公司治理、或者去中心化组织(DAO)的投票。多签合约的核心是:没得到足够的人同意,任何操作都别想执行,安全得像个铁桶!多签合
Solidity里怎么搞一个多重签名(Multi-Signature,简称多签)合约。这玩意儿在区块链世界里可是个硬核工具,特别适合需要多人共同决策的场景,比如团队控制资金、公司治理、或者去中心化组织(DAO)的投票。多签合约的核心是:没得到足够的人同意,任何操作都别想执行,安全得像个铁桶!
先来搞明白多签合约的几个关键点:
ReentrancyGuard和Ownable。咱们用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 owner1提交转账1 ETH。owner2和owner3确认后自动执行。owner2撤销确认,阻止执行。nonReentrant和onlyOwner确保安全。多签合约可以调用其他合约,比如转账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。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。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:记录提案提交时间。confirmTransaction和executeTransaction验证提案未超时。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"); }); }); 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 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!