4. 提交交易提案

任务说明

本案例实现多签钱包的交易提案提交功能。当任意一个已注册的签名人调用submitTransaction时,合约将记录目标地址、转账金额、调用数据以及提案的初始执行状态(未执行),并返回唯一的提案ID。该提案后续可被其他签名人确认,并在达到阈值后执行。

合约实现

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract MultiSigWallet {
    // ---- 数据结构 ----
    struct Transaction {
        address destination;   // 目标地址
        uint256 value;         // 转账金额(wei)
        bytes data;            // 调用数据
        bool executed;         // 执行状态
        uint256 confirmations; // 已确认数
    }

    // ---- 状态变量 ----
    address[] public owners;                    // 签名人列表
    mapping(address => bool) public isOwner;    // 地址是否为签名人
    uint256 public required;                    // 所需确认阈值
    Transaction[] public transactions;          // 交易提案列表

    // ---- 事件 ----
    event SubmitTransaction(
        address indexed owner,
        uint256 indexed txIndex,
        address indexed destination,
        uint256 value,
        bytes data
    );

    // ---- 修饰器 ----
    modifier onlyOwner() {
        require(isOwner[msg.sender], "not owner");
        _;
    }

    // ---- 构造函数 ----
    constructor(address[] memory _owners, uint256 _required) {
        require(_owners.length > 0, "owners required");
        require(_required > 0 && _required <= _owners.length, "invalid required");
        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "invalid owner");
            require(!isOwner[owner], "owner not unique");
            isOwner[owner] = true;
            owners.push(owner);
        }
        required = _required;
    }

    // ---- 核心功能:提交交易提案 ----
    function submitTransaction(
        address _destination,
        uint256 _value,
        bytes calldata _data
    ) external onlyOwner returns (uint256 txIndex) {
        require(_destination != address(0), "invalid destination");
        txIndex = transactions.length;
        transactions.push(Transaction({
            destination: _destination,
            value: _value,
            data: _data,
            executed: false,
            confirmations: 0
        }));
        emit SubmitTransaction(msg.sender, txIndex, _destination, _value, _data);
    }

    // ---- 查询辅助 ----
    function getTransactionCount() external view returns (uint256) {
        return transactions.length;
    }

    function getTransaction(uint256 _txIndex)
        external
        view
        returns (address destination, uint256 value, bytes memory data, bool executed, uint256 confirmations)
    {
        require(_txIndex < transactions.length, "tx index out of bounds");
        Transaction storage txItem = transactions[_txIndex];
        return (txItem.destination, txItem.value, txItem.data, txItem.executed, txItem.confirmations);
    }
}

关键设计说明

合约使用动态数组transactions存储所有提案,每个提案包含目标地址、金额、调用数据、执行状态和当前确认数。提交时自动将executed置为false,confirmations置为0,表示尚未被其他签名人确认。提案ID即数组下标,从0开始递增,保证唯一性。构造函数中完成签名人列表和阈值的初始化,确保只有合法签名人才能提交提案。

SVG:交易提案数据结构与提交流程

交易提案提交 — 数据结构与调用流程 签名人 msg.sender 调用 submitTransaction (dest, value, data) MultiSigWallet onlyOwner 校验 destination != 0 校验 push 到 transactions[] emit SubmitTransaction Transaction 结构 destination: 目标地址 value: 金额 (wei) data: 调用数据 executed: false confirmations: 0 返回值 txIndex = transactions.length - 1(提案ID,从0开始) 提案ID唯一且递增,后续确认与执行均通过该ID引用 初始状态 executed = false(未执行) | confirmations = 0(尚未被其他签名人确认)

部署与验证

部署参数:构造函数需传入签名人地址数组和所需确认阈值。例如使用三个测试地址作为签名人,阈值设为2。

// 部署示例(使用 Hardhat 或 Remix)
address[] memory owners = new address[](3);
owners[0] = address(0x1111...);
owners[1] = address(0x2222...);
owners[2] = address(0x3333...);
MultiSigWallet wallet = new MultiSigWallet(owners, 2);

功能验证步骤:

  1. 使用任一签名人地址调用submitTransaction,传入目标地址(如0x4444...)、金额(如1 ether)和空数据0x;
  2. 调用返回提案ID(首次提交返回0);
  3. 调用getTransactionCount(),确认返回1;
  4. 调用getTransaction(0),确认返回的destination、value与提交参数一致,executed为false,confirmations为0;
  5. 使用非签名人地址调用submitTransaction,应抛出异常"not owner";
  6. 传入零地址作为目标地址,应抛出异常"invalid destination"。

判定标准:以上步骤中返回值、状态字段和异常行为均符合预期,即证明交易提案提交功能正确实现。