如何实现管理员调用withdraw函数前需2/3多签批准交易?
Absolutely, your requirement is fully achievable! Let’s break down how to add the multi-signature approval flow to your withdraw function, plus explain why your current Gnosis Safe interaction is failing.
Why Your Current Gnosis Safe Interaction Fails
Your existing withdraw function sends ETH directly to msg.sender (the account calling the function) as soon as a managerRole holder invokes it. When using Gnosis Safe here:
msg.senderbecomes the Gnosis Safe contract address, not your intended owner wallet, which breaks your target recipient logic.- There’s no built-in approval check, so the transaction tries to execute immediately without meeting your 2/3 multi-sig requirement—leading to the "this transaction will fail" warning.
Step-by-Step Implementation
We’ll split the withdraw process into three secure phases: initiate a request, collect approvals, and execute the transfer once 2/3 of the owner multisig members have signed off.
1. Add Core State Variables & Roles
First, define structures to track requests and approvals, plus a dedicated role for your owner multisig members:
import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract YourContract is AccessControl, ReentrancyGuard { bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); bytes32 public constant OWNER_MULTISIG_ROLE = keccak256("OWNER_MULTISIG_ROLE"); struct WithdrawRequest { address requester; uint256 amount; uint256 approvalCount; bool isExecuted; mapping(address => bool) hasApproved; } mapping(uint256 => WithdrawRequest) public withdrawRequests; uint256 public requestCounter; // Events for transparency on-chain event WithdrawRequestInitiated(uint256 requestId, address requester, uint256 amount); event WithdrawRequestApproved(uint256 requestId, address approver); event WithdrawExecuted(uint256 requestId, address recipient, uint256 amount); // Constructor - set initial admin/manager roles constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); // Add initial MANAGER_ROLE holders here if needed } }
2. Replace Direct Withdraw with Request Initiation
Instead of transferring ETH immediately, let managerRole holders create a pending withdraw request:
function initiateWithdraw() external onlyRole(MANAGER_ROLE) returns(uint256 requestId) { uint256 withdrawAmount = getBalance(); require(withdrawAmount > 0, "No balance available to withdraw"); requestId = requestCounter++; WithdrawRequest storage newRequest = withdrawRequests[requestId]; newRequest.requester = msg.sender; newRequest.amount = withdrawAmount; newRequest.approvalCount = 0; newRequest.isExecuted = false; emit WithdrawRequestInitiated(requestId, msg.sender, withdrawAmount); }
3. Add Approval Function for Multisig Members
Only OWNER_MULTISIG_ROLE holders can approve pending requests:
function approveWithdraw(uint256 requestId) external onlyRole(OWNER_MULTISIG_ROLE) { WithdrawRequest storage request = withdrawRequests[requestId]; require(!request.isExecuted, "Request has already been executed"); require(!request.hasApproved[msg.sender], "You've already approved this request"); request.hasApproved[msg.sender] = true; request.approvalCount++; emit WithdrawRequestApproved(requestId, msg.sender); }
4. Add Executable Withdraw (After 2/3 Approvals)
Once enough approvals are collected, anyone (or restricted roles) can trigger the ETH transfer to your owner wallet:
function executeWithdraw(uint256 requestId) external nonReentrant { WithdrawRequest storage request = withdrawRequests[requestId]; require(!request.isExecuted, "Request has already been executed"); require(address(this).balance >= request.amount, "Insufficient contract balance"); // Calculate required approvals (2/3 of multisig members, rounded up for odd counts) uint256 totalMultisigMembers = getRoleMemberCount(OWNER_MULTISIG_ROLE); uint256 requiredApprovals = (totalMultisigMembers * 2) / 3; if ((totalMultisigMembers * 2) % 3 != 0) { requiredApprovals++; } require(request.approvalCount >= requiredApprovals, "Not enough approvals to execute"); // Mark request as executed first to prevent reentrancy risks request.isExecuted = true; // Transfer ETH to your intended owner wallet (replace with your target address if needed) (bool sent, ) = payable(owner()).call{value: request.amount}(""); require(sent, "Ether transfer failed"); emit WithdrawExecuted(requestId, owner(), request.amount); }
Gnosis Safe Integration Tips
- Grant the
OWNER_MULTISIG_ROLEto your Gnosis Safe contract address (not individual owners). This lets multi-sig transactions from the Safe count as valid approvals. - Workflow for Gnosis Safe users:
- A
managerRoleholder initiates the withdraw request via a regular transaction. - Gnosis Safe members create and approve a multi-sig transaction to call
approveWithdrawfor the request ID. - Once enough approvals are collected, anyone can call
executeWithdraw(this can also be done via a Gnosis Safe transaction for extra security).
- A
Security Checks to Remember
- Keep
nonReentrantonexecuteWithdrawto block reentrancy attacks. - Verify
getBalance()correctly returns the contract’s available ETH (exclude any locked funds if applicable). - Test with different multisig sizes (e.g., 3 members need 2 approvals, 5 need 4) to confirm the approval calculation works as intended.
内容的提问来源于stack exchange,提问作者Eniola Agboola




