-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMultisig.sol
More file actions
267 lines (227 loc) · 9.39 KB
/
Copy pathMultisig.sol
File metadata and controls
267 lines (227 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Custom Errors
error NotSigner(address account);
error AlreadySigner(address account);
error NotEnoughSigners();
error InvalidThreshold();
error InputNotRequired();
error AddressNotRequired();
error TxNonExistent();
error TxNotPending();
error AlreadyConfirmed();
error NotConfirmed();
error ThresholdNotMet();
error ExecutionFailed();
error InvalidAddress();
error TokenNotRequired();
error EmptyTransaction();
error UnknownTokenAddress();
error InvalidTransactionType();
error InsufficientFunds();
error NotProposer();
error RemovalWouldBrickWallet();
/// @title Multisig Wallet
/// @notice Multi-signature wallet supporting ETH, ERC20 transfers and signer management
/// @dev Uses SafeERC20 and ReentrancyGuard for secure execution
contract Multisig is ReentrancyGuard {
using SafeERC20 for IERC20;
/// @notice Emitted when ETH is deposited into contract
/// @param sender Address sending ETH
/// @param amount Amount of ETH deposited
event Deposit(address indexed sender, uint256 amount);
/// @notice Emitted when a transaction is submitted
/// @param txId Transaction ID
/// @param caller Address that submitted the transaction
/// @param receiver Target address
/// @param amount Transaction value
event Submitted(uint256 indexed txId, address indexed caller, address receiver, uint256 amount);
event Confirmed(uint256 indexed txId, address indexed signer);
/// @notice Emitted when a transaction is executed
/// @param txId Transaction ID
event Executed(uint256 indexed txId);
/// @notice Emitted when a transaction is cancelled
/// @param txId Transaction ID
event Cancelled(uint256 indexed txId);
/// @notice Transaction types supported by multisig
enum TxType { ETH, ERC20, ADD_SIGNER, REMOVE_SIGNER, THRESHOLD }
/// @notice Transaction states
enum States { NONE, PENDING, EXECUTED, CANCELLED }
/// @notice Transaction structure
/// @param to Target address
/// @param token ERC20 token address
/// @param value ETH or token amount
/// @param txType Transaction type
/// @param state Transaction state
/// @param proposer Caller address
struct Transaction {
address to;
address token;
uint256 value;
TxType txType;
States state;
address proposer;
}
/// @notice List of signer addresses
address[] public signers;
/// @notice Mapping to check signer status
mapping(address => bool) public isSigner;
/// @notice Tracks confirmations for transactions
mapping(uint256 => mapping(address => bool)) public hasConfirmed;
/// @notice List of submitted transactions
Transaction[] public transactions;
/// @notice Required confirmations for execution
uint256 public threshold;
/// @notice Restricts function access to signers only
modifier onlySigner() {
if (!isSigner[msg.sender]) revert NotSigner(msg.sender);
_;
}
/// @notice Initializes multisig wallet
/// @param _signers Initial signer addresses
/// @param _threshold Required confirmation threshold
constructor(address[] memory _signers, uint256 _threshold) {
uint256 len = _signers.length;
require(len >= 2, NotEnoughSigners());
require(_threshold >= 2 && _threshold <= len, InvalidThreshold());
for (uint256 i = 0; i < len; i++) {
address signer = _signers[i];
require(signer != address(0), InvalidAddress());
require(!isSigner[signer], AlreadySigner(signer));
isSigner[signer] = true;
signers.push(signer);
}
threshold = _threshold;
}
/// @notice Receive ETH deposits
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
/// @notice Submit a new transaction
/// @param _to Target address
/// @param _token Token address (if ERC20)
/// @param _value Amount to transfer
/// @param _typeindex The index of the transaction type (0-4)
function submit(address _to, address _token, uint256 _value, uint8 _typeindex) public onlySigner {
require(_typeindex <= uint8(TxType.THRESHOLD), InvalidTransactionType());
TxType _type = TxType(_typeindex);
if (_type == TxType.ETH) {
require(_to != address(0), InvalidAddress());
require(_token == address(0), TokenNotRequired());
require(_value > 0, EmptyTransaction());
}
else if (_type == TxType.ERC20) {
require(_to != address(0), InvalidAddress());
require(_token != address(0), UnknownTokenAddress());
require(_value > 0, EmptyTransaction());
}
else if (_type == TxType.ADD_SIGNER) {
require(_to != address(0), InvalidAddress());
require(!isSigner[_to], AlreadySigner(_to));
require(_token == address(0) && _value == 0, InputNotRequired());
}
else if (_type == TxType.REMOVE_SIGNER) {
require(isSigner[_to], NotSigner(_to));
require(_token == address(0) && _value == 0, InputNotRequired());
}
else if (_type == TxType.THRESHOLD) {
require(_value > 0, InvalidThreshold());
require(_to == address(0) && _token == address(0), AddressNotRequired());
}
uint256 txId = transactions.length;
transactions.push(Transaction({
to: _to,
token: _token,
value: _value,
txType: _type,
state: States.PENDING,
proposer: msg.sender
}));
_tryAutoConfirm(txId);
emit Submitted(txId, msg.sender, _to, _value);
}
/// @notice Confirm a transaction
/// @param _txId Transaction ID
function confirm(uint256 _txId) public onlySigner {
require(_txId < transactions.length, TxNonExistent());
require(transactions[_txId].state == States.PENDING, TxNotPending());
require(!hasConfirmed[_txId][msg.sender], AlreadyConfirmed());
hasConfirmed[_txId][msg.sender] = true;
emit Confirmed(_txId, msg.sender);
}
/// @notice Revoke a confirmation
/// @param _txId Transaction ID
function revoke(uint256 _txId) public onlySigner {
require(_txId < transactions.length, TxNonExistent());
require(transactions[_txId].state == States.PENDING, TxNotPending());
require(hasConfirmed[_txId][msg.sender], NotConfirmed());
hasConfirmed[_txId][msg.sender] = false;
}
/// @notice Cancel a transaction
/// @param _txId Transaction ID
function cancel(uint256 _txId) public onlySigner {
require(_txId < transactions.length, TxNonExistent());
Transaction storage t = transactions[_txId];
require(t.state == States.PENDING, TxNotPending());
require(msg.sender == t.proposer, NotProposer());
t.state = States.CANCELLED;
emit Cancelled(_txId);
}
/// @notice Execute transaction after threshold confirmations
/// @param _txId Transaction ID
function execute(uint256 _txId) public nonReentrant {
require(_txId < transactions.length, TxNonExistent());
Transaction storage t = transactions[_txId];
require(t.state == States.PENDING, TxNotPending());
uint256 activeConfirmations = 0;
for (uint256 i = 0; i < signers.length; i++) {
if (hasConfirmed[_txId][signers[i]]) {
activeConfirmations++;
}
}
require(activeConfirmations >= threshold, ThresholdNotMet());
t.state = States.EXECUTED;
if (t.txType == TxType.ETH) {
require(address(this).balance >= t.value, InsufficientFunds());
(bool success, ) = t.to.call{value: t.value}("");
require(success, ExecutionFailed());
}
else if (t.txType == TxType.ERC20) {
IERC20(t.token).safeTransfer(t.to, t.value);
}
else if (t.txType == TxType.ADD_SIGNER) {
isSigner[t.to] = true;
signers.push(t.to);
}
else if (t.txType == TxType.REMOVE_SIGNER) {
_removeSigner(t.to);
}
else if (t.txType == TxType.THRESHOLD) {
require(t.value >= 2 && t.value <= signers.length, InvalidThreshold());
threshold = t.value;
}
emit Executed(_txId);
}
/// @notice Automatically confirms submitted transaction
/// @param _txId Transaction ID
function _tryAutoConfirm(uint256 _txId) internal {
confirm(_txId);
}
/// @notice Removes a signer
/// @param _signer Signer address to remove
function _removeSigner(address _signer) internal {
uint256 len = signers.length;
require(len - 1 >= threshold, RemovalWouldBrickWallet());
isSigner[_signer] = false;
for (uint256 i = 0; i < len; i++) {
if (signers[i] == _signer) {
signers[i] = signers[len - 1];
signers.pop();
break;
}
}
}
}