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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!