Solidity里怎么搞一个多重签名(Multi-Signature,简称多签)合约。这玩意儿在区块链世界里可是个硬核工具,特别适合需要多人共同决策的场景,比如团队控制资金、公司治理、或者去中心化组织(DAO)的投票。多签合约的核心是:没得到足够的人同意,任何操作都别想执行,安全得像个铁桶!
多签合约核心概念
先来搞明白多签合约的几个关键点:
- 多重签名(Multi-Signature):需要多个账户(称为"所有者"或"签名者")对操作(如转账、调用合约)进行确认才能执行。
- 提案(Transaction):每项操作(如转ETH、调用函数)作为一个提案,记录目标地址、数据、金额等。
- 确认(Confirmation):所有者对提案投票,达到指定数量的确认后执行。
- 安全机制 :
- 防止重入:避免外部合约重复调用。
- 权限控制:只有所有者能提交或确认提案。
- 状态管理:确保提案不被重复执行。
- OpenZeppelin :提供安全的工具库,如
ReentrancyGuard
和Ownable
。 - Solidity 0.8.x:自带溢出/下溢检查,减少安全隐患。
- Hardhat:开发和测试工具,支持编译、部署、测试。
咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,写一个多签合约,包含提案提交、确认、执行、撤销等功能。
环境准备
用Hardhat搭建开发环境,写和测试合约。
bash
mkdir multisig-demo
cd multisig-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts
npm install ethers
初始化Hardhat:
bash
npx hardhat init
选择TypeScript项目,安装依赖:
bash
npm install --save-dev ts-node typescript @types/node @types/mocha
目录结构:
lua
multisig-demo/
├── contracts/
│ ├── MultiSigWallet.sol
│ ├── AdvancedMultiSig.sol
├── scripts/
│ ├── deploy.ts
├── test/
│ ├── MultiSigWallet.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json
tsconfig.json
:
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
:
typescript
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.20",
networks: {
hardhat: {
chainId: 1337,
},
},
};
export default config;
跑本地节点:
bash
npx hardhat node
基础多签合约
先写一个简单的多签合约,支持ETH转账。
合约代码
contracts/MultiSigWallet.sol
:
solidity
// 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
:
typescript
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;
});
});
跑测试:
bash
npx hardhat test
- 解析 :
- 部署:3个所有者,需2人确认。
- 提交:
owner1
提交转账1 ETH。 - 确认:
owner2
和owner3
确认后自动执行。 - 撤销:
owner2
撤销确认,阻止执行。
- 安全 :
nonReentrant
和onlyOwner
确保安全。
调用外部合约
多签合约可以调用其他合约,比如转账ERC-20代币。
辅助ERC-20合约
contracts/Token.sol
:
solidity
// 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
(更新):
typescript
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
:
solidity
// 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
:
typescript
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
(更新):
solidity
// 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
(更新):
typescript
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
:
typescript
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;
});
跑部署:
bash
npx hardhat run scripts/deploy.ts --network hardhat
- 解析:部署多签和代币合约,记录地址。