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

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

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
  • 解析:部署多签和代币合约,记录地址。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!