Repository navigation
Expand file tree
/
Copy pathmultisig.cell
More file actions
243 lines (215 loc) · 11.6 KB
/
Copy pathmultisig.cell
File metadata and controls
243 lines (215 loc) · 11.6 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
module cellscript::multisig
// Threshold-approval workflow, not a cryptographic custody multisig.
// Approver addresses and timestamps are entry-witness data. The surrounding
// Lock Script must authenticate authority, and a HeaderDep-backed policy must
// authenticate time. CellScript 0.22 has no implicit signer, but packages that
// need cryptographic custody can call the explicit BIP340 CellDep verifier ABI.
// This example deliberately stops at an approval record and does not transfer
// assets or mutate signer configuration by itself.
const MIN_SIGNERS: u8 = 2
const MAX_SIGNERS: u8 = 10
const DEFAULT_TIMEOUT: u64 = 1440
enum ProposalStatus {
Pending,
Executed,
Cancelled,
Expired,
}
enum OperationType {
Transfer,
AddSigner,
RemoveSigner,
ChangeThreshold,
Custom,
}
resource MultisigWallet has store, create, replace, read_ref {
wallet_id: Hash,
signers: Vec<Address>,
threshold: u8,
nonce: u64,
created_at: u64,
}
receipt Proposal has create, consume, replace, burn {
wallet_id: Hash,
proposal_id: u64,
proposer: Address,
operation: OperationType,
target: Address,
amount: u64,
data: Vec<u8>,
required_approvals: u8,
approvals: Vec<Approval>,
created_at: u64,
expires_at: u64,
state: ProposalStatus,
}
struct Approval {
approver: Address,
}
receipt ApprovalConfirmation has create {
proposal_id: u64,
approver: Address,
reported_at: u64,
}
receipt ExecutionRecord has create {
proposal_id: u64,
executor: Address,
executed_at: u64,
success: bool,
}
action create_wallet(wallet_id: Hash, signers: Vec<Address>, threshold: u8, reported_time: u64) -> wallet: MultisigWallet {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require signers.len() >= MIN_SIGNERS as usize, "Not enough signers"
require signers.len() <= MAX_SIGNERS as usize, "Too many signers"
require threshold >= MIN_SIGNERS && threshold <= signers.len() as u8, "Invalid threshold"
for i in 0..signers.len() {
for j in i + 1..signers.len() {
require signers[i] != signers[j], "Duplicate signer"
}
}
create wallet = MultisigWallet { wallet_id, signers, threshold, nonce: 0, created_at: reported_time }
}
action propose_transfer(wallet_before: MultisigWallet, proposer: Address, target: Address, amount: u64, reported_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require is_signer(&wallet_before, proposer), "Not a signer"
require amount > 0, "Amount must be positive"
require target != Address::zero(), "Invalid target address"
let proposal_id = wallet_before.nonce + 1
require wallet_after.wallet_id == wallet_before.wallet_id
require wallet_after.signers == wallet_before.signers
require wallet_after.threshold == wallet_before.threshold
require wallet_after.nonce == proposal_id
require wallet_after.created_at == wallet_before.created_at
create proposal = Proposal { wallet_id: wallet_before.wallet_id, proposal_id, proposer, operation: OperationType::Transfer, target, amount, data: [], required_approvals: wallet_before.threshold, approvals: [], created_at: reported_time, expires_at: reported_time + DEFAULT_TIMEOUT, state: ProposalStatus::Pending }
}
action record_approval(proposal_before: Proposal, read wallet: MultisigWallet, approver: Address, reported_time: u64) -> (proposal_after: Proposal, confirmation: ApprovalConfirmation) {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require reported_time < proposal_before.expires_at, "Reported approval time is outside the proposal window"
require is_signer(wallet, approver), "Approver is not a configured member"
require !has_approved(&proposal_before, approver), "Already approved"
require proposal_after.wallet_id == proposal_before.wallet_id
require proposal_after.proposal_id == proposal_before.proposal_id
require proposal_after.proposer == proposal_before.proposer
require proposal_after.operation == proposal_before.operation
require proposal_after.target == proposal_before.target
require proposal_after.amount == proposal_before.amount
require proposal_after.data == proposal_before.data
require proposal_after.required_approvals == proposal_before.required_approvals
require proposal_after.approvals.len() == proposal_before.approvals.len() + 1
for i in 0..proposal_before.approvals.len() {
require proposal_after.approvals[i].approver == proposal_before.approvals[i].approver, "Existing approvals changed"
}
require proposal_after.approvals[proposal_before.approvals.len()].approver == approver
require proposal_after.created_at == proposal_before.created_at
require proposal_after.expires_at == proposal_before.expires_at
require proposal_after.state == proposal_before.state
create confirmation = ApprovalConfirmation { proposal_id: proposal_before.proposal_id, approver, reported_at: reported_time }
}
action execute_proposal(read wallet: MultisigWallet, proposal: Proposal, executor: Address, reported_time: u64) -> record: ExecutionRecord {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require is_signer(wallet, executor), "Not a signer"
require reported_time < proposal.expires_at, "Reported execution time is outside the proposal window"
require proposal.approvals.len() >= proposal.required_approvals as usize, "Not enough recorded approvals"
require proposal.wallet_id == wallet.wallet_id, "Wallet mismatch"
destroy proposal
create record = ExecutionRecord { proposal_id: proposal.proposal_id, executor, executed_at: reported_time, success: true }
}
action cancel_proposal(read wallet: MultisigWallet, proposal: Proposal, canceller: Address) {
verification
require proposal.proposer == canceller, "Only proposer can cancel"
require proposal.wallet_id == wallet.wallet_id, "Wallet mismatch"
destroy proposal
}
action propose_add_signer(wallet_before: MultisigWallet, proposer: Address, new_signer: Address, reported_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require is_signer(&wallet_before, proposer), "Not a signer"
require !is_signer(&wallet_before, new_signer), "Already a signer"
require wallet_before.signers.len() < MAX_SIGNERS as usize, "Max signers reached"
let proposal_id = wallet_before.nonce + 1
require wallet_after.wallet_id == wallet_before.wallet_id
require wallet_after.signers == wallet_before.signers
require wallet_after.threshold == wallet_before.threshold
require wallet_after.nonce == proposal_id
require wallet_after.created_at == wallet_before.created_at
let mut data: Vec<u8> = []
data.extend_from_slice(&new_signer.0)
create proposal = Proposal { wallet_id: wallet_before.wallet_id, proposal_id, proposer, operation: OperationType::AddSigner, target: new_signer, amount: 0, data, required_approvals: wallet_before.threshold, approvals: [], created_at: reported_time, expires_at: reported_time + DEFAULT_TIMEOUT, state: ProposalStatus::Pending }
}
action propose_remove_signer(wallet_before: MultisigWallet, proposer: Address, signer_to_remove: Address, reported_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require is_signer(&wallet_before, proposer), "Not a signer"
require is_signer(&wallet_before, signer_to_remove), "Not a signer"
require wallet_before.signers.len() - 1 >= wallet_before.threshold as usize, "Would fall below threshold"
let proposal_id = wallet_before.nonce + 1
require wallet_after.wallet_id == wallet_before.wallet_id
require wallet_after.signers == wallet_before.signers
require wallet_after.threshold == wallet_before.threshold
require wallet_after.nonce == proposal_id
require wallet_after.created_at == wallet_before.created_at
create proposal = Proposal { wallet_id: wallet_before.wallet_id, proposal_id, proposer, operation: OperationType::RemoveSigner, target: signer_to_remove, amount: 0, data: [], required_approvals: wallet_before.threshold, approvals: [], created_at: reported_time, expires_at: reported_time + DEFAULT_TIMEOUT, state: ProposalStatus::Pending }
}
action propose_change_threshold(wallet_before: MultisigWallet, proposer: Address, new_threshold: u8, reported_time: u64) -> (wallet_after: MultisigWallet, proposal: Proposal) {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require is_signer(&wallet_before, proposer), "Not a signer"
require new_threshold >= MIN_SIGNERS && new_threshold <= wallet_before.signers.len() as u8, "Invalid threshold"
let proposal_id = wallet_before.nonce + 1
require wallet_after.wallet_id == wallet_before.wallet_id
require wallet_after.signers == wallet_before.signers
require wallet_after.threshold == wallet_before.threshold
require wallet_after.nonce == proposal_id
require wallet_after.created_at == wallet_before.created_at
let mut data: Vec<u8> = []
data.push(new_threshold)
create proposal = Proposal { wallet_id: wallet_before.wallet_id, proposal_id, proposer, operation: OperationType::ChangeThreshold, target: Address::zero(), amount: new_threshold as u64, data, required_approvals: wallet_before.threshold, approvals: [], created_at: reported_time, expires_at: reported_time + DEFAULT_TIMEOUT, state: ProposalStatus::Pending }
}
fn current_epoch() -> EpochNumber {
return ckb::header_dep(0).epoch_number
}
fn current_epoch_raw() -> u64 {
return ckb::epoch_number_to_u64(current_epoch())
}
fn is_signer(wallet: &MultisigWallet, addr: Address) -> bool {
for signer in &wallet.signers {
if *signer == addr {
return true
}
}
false
}
fn has_approved(proposal: &Proposal, addr: Address) -> bool {
for approval in &proposal.approvals {
if approval.approver == addr {
return true
}
}
false
}
lock is_signer_lock(protected wallet: MultisigWallet, witness addr: Address) -> bool {
verification
require is_signer(wallet, addr)
}
lock can_execute(protected proposal: Proposal, witness reported_time: u64) -> bool {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require proposal.approvals.len() >= proposal.required_approvals as usize && reported_time < proposal.expires_at
}
lock can_cancel(protected proposal: Proposal, witness caller: Address) -> bool {
verification
require proposal.proposer == caller
}
lock has_enough_approvals(protected proposal: Proposal) -> bool {
verification
require proposal.approvals.len() >= proposal.required_approvals as usize
}
lock not_expired(protected proposal: Proposal, witness reported_time: u64) -> bool {
verification
require reported_time == current_epoch_raw(), "Reported time does not match HeaderDep epoch"
require reported_time < proposal.expires_at
}