2. 添加与移除签名者

任务说明

本案例要求实现多签钱包中签名者列表的动态管理功能,支持添加新签名者与移除现有签名者,并确保只有具备足够权限的账户才能执行这些操作。我们将基于一个最小化的多签合约框架,实现addSigner与removeSigner两个核心函数,并加入基于现有签名者投票确认的权限控制机制。

合约实现

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

contract DynamicMultiSig {
    // ---- 状态变量 ----
    address[] public signers;                    // 当前签名者列表
    mapping(address => bool) public isSigner;    // 地址是否为签名者
    uint256 public requiredConfirmations;        // 所需确认数

    // 待处理的签名者变更提案
    struct ChangeProposal {
        address target;          // 待添加/移除的地址
        bool isAdding;           // true=添加, false=移除
        uint256 confirmations;   // 已确认数
        mapping(address => bool) confirmedBy;   // 哪些签名者已确认
        bool executed;           // 是否已执行
    }

    ChangeProposal[] public proposals;
    mapping(uint256 => mapping(address => bool)) public proposalConfirmedBy;

    // ---- 事件 ----
    event SignerAdded(address indexed newSigner, uint256 timestamp);
    event SignerRemoved(address indexed removedSigner, uint256 timestamp);
    event ProposalCreated(uint256 indexed proposalId, address target, bool isAdding);
    event ProposalConfirmed(uint256 indexed proposalId, address confirmer);
    event ProposalExecuted(uint256 indexed proposalId);

    // ---- 修饰器 ----
    modifier onlySigner() {
        require(isSigner[msg.sender], "Not a signer");
        _;
    }

    modifier proposalExists(uint256 proposalId) {
        require(proposalId < proposals.length, "Proposal does not exist");
        _;
    }

    // ---- 构造函数 ----
    constructor(address[] memory _initialSigners, uint256 _requiredConfirmations) {
        require(_initialSigners.length > 0, "At least one signer required");
        require(_requiredConfirmations > 0 && _requiredConfirmations <= _initialSigners.length,
            "Invalid required confirmations");

        for (uint256 i = 0; i < _initialSigners.length; i++) {
            address signer = _initialSigners[i];
            require(!isSigner[signer], "Duplicate signer");
            require(signer != address(0), "Zero address not allowed");
            signers.push(signer);
            isSigner[signer] = true;
        }
        requiredConfirmations = _requiredConfirmations;
    }

    // ---- 核心功能 ----

    // 创建添加/移除签名者的提案
    function createChangeProposal(address target, bool isAdding) external onlySigner {
        require(target != address(0), "Zero address not allowed");
        if (isAdding) {
            require(!isSigner[target], "Already a signer");
        } else {
            require(isSigner[target], "Not a signer");
            require(signers.length > requiredConfirmations,
                "Cannot remove: would break quorum");
        }

        uint256 proposalId = proposals.length;
        ChangeProposal storage newProposal = proposals.push();
        newProposal.target = target;
        newProposal.isAdding = isAdding;
        newProposal.confirmations = 0;
        newProposal.executed = false;

        // 创建者自动确认
        _confirmProposal(proposalId, msg.sender);

        emit ProposalCreated(proposalId, target, isAdding);
    }

    // 签名者确认提案
    function confirmProposal(uint256 proposalId)
        external
        onlySigner
        proposalExists(proposalId)
    {
        _confirmProposal(proposalId, msg.sender);
    }

    function _confirmProposal(uint256 proposalId, address confirmer) internal {
        ChangeProposal storage proposal = proposals[proposalId];
        require(!proposal.executed, "Already executed");
        require(!proposal.confirmedBy[confirmer], "Already confirmed");

        proposal.confirmedBy[confirmer] = true;
        proposal.confirmations++;
        proposalConfirmedBy[proposalId][confirmer] = true;

        emit ProposalConfirmed(proposalId, confirmer);

        // 达到阈值自动执行
        if (proposal.confirmations >= requiredConfirmations) {
            _executeProposal(proposalId);
        }
    }

    // 执行提案(内部函数)
    function _executeProposal(uint256 proposalId) internal {
        ChangeProposal storage proposal = proposals[proposalId];
        require(!proposal.executed, "Already executed");
        require(proposal.confirmations >= requiredConfirmations,
            "Not enough confirmations");

        proposal.executed = true;

        if (proposal.isAdding) {
            _addSigner(proposal.target);
        } else {
            _removeSigner(proposal.target);
        }

        emit ProposalExecuted(proposalId);
    }

    // 实际添加签名者
    function _addSigner(address newSigner) internal {
        signers.push(newSigner);
        isSigner[newSigner] = true;
        emit SignerAdded(newSigner, block.timestamp);
    }

    // 实际移除签名者
    function _removeSigner(address oldSigner) internal {
        // 从数组中移除
        for (uint256 i = 0; i < signers.length; i++) {
            if (signers[i] == oldSigner) {
                signers[i] = signers[signers.length - 1];
                signers.pop();
                break;
            }
        }
        isSigner[oldSigner] = false;
        emit SignerRemoved(oldSigner, block.timestamp);
    }

    // ---- 查询函数 ----
    function getSigners() external view returns (address[] memory) {
        return signers;
    }

    function getProposalCount() external view returns (uint256) {
        return proposals.length;
    }

    function getProposalDetails(uint256 proposalId)
        external
        view
        proposalExists(proposalId)
        returns (address target, bool isAdding, uint256 confirmations, bool executed)
    {
        ChangeProposal storage proposal = proposals[proposalId];
        return (
            proposal.target,
            proposal.isAdding,
            proposal.confirmations,
            proposal.executed
        );
    }
}

权限控制机制说明

本合约采用提案-确认-执行模式实现权限控制:任何签名者均可发起添加或移除签名者的提案,但提案必须获得至少requiredConfirmations个签名者的确认才会自动执行。这确保了单一签名者无法擅自修改签名者列表,所有变更均需达到多签阈值。合约构造函数中设定的requiredConfirmations在部署后固定不变,但签名者总数变化时需保证移除操作不会导致签名者数量低于所需确认数(合约中已加入相应检查)。

部署与验证

部署步骤:

  1. 准备三个测试地址(如Hardhat或Remix中的账户),例如:0x1111...1111、0x2222...2222、0x3333...3333。
  2. 构造函数传入初始签名者数组[addr1, addr2, addr3],设置requiredConfirmations = 2。
  3. 部署合约。

验证流程:

  1. 使用addr1调用createChangeProposal(0x4444...4444, true)创建添加新签名者的提案,观察事件ProposalCreated与ProposalConfirmed(创建者自动确认)。
  2. 使用addr2调用confirmProposal(0)确认该提案。由于确认数达到2(阈值),合约自动执行添加操作,观察事件SignerAdded。
  3. 调用getSigners(),确认返回数组中包含新地址0x4444...4444。
  4. 使用addr1调用createChangeProposal(0x1111...1111, false)创建移除addr1的提案,再用addr3确认,观察SignerRemoved事件。
  5. 调用isSigner(0x1111...1111),确认返回false。

判定标准:若上述事件按预期顺序触发,且查询函数返回正确结果,则说明动态管理签名者列表及权限控制功能实现正确。若尝试用非签名者地址调用createChangeProposal或confirmProposal,交易应被回滚并提示"Not a signer"。

动态签名者管理 — 提案确认执行流程 签名者 A 发起/确认提案 签名者 B 确认提案 签名者 C 确认提案 多签合约 signers[] [A, B, C] requiredConfirmations = 2 提案 #0 添加 0x4444...4444 确认: 2/2 ✅ 提案 #1 移除 0x1111...1111 确认: 1/2 ⏳ 执行结果 SignerAdded 事件已触发 signers[] 已更新 ① createChangeProposal ② 自动确认 ③ confirmProposal(0) ④ confirmProposal(1) 自动执行条件 确认数 ≥ requiredConfirmations 提案 #0: 2 ≥ 2 → 执行 ✅ 提案 #1: 1 < 2 → 等待 ⏳ 权限控制 onlySigner 修饰器 非签名者调用 → 回滚 重复确认 → 回滚 图例: 函数调用 自动确认 待执行提案 已执行/进行中