templates.price_feed_multisign_admin
1import smartpy as sp 2 3""" 4 Multisign Admin is a multisign administration contract. 5 6 Multiple signers can propose and vote for a list of batch of actions 7 on the contract itself or the target. 8 9 The contract is configurable at origination time to adapt to target needed. 10 11 The contract supports gathering off-chain signed votes and pushing them in one call. 12 13 Voting system: 14 When the quorum is reached on a proposal, 15 If the number of yay > nay: the proposal is acceped 16 Else: the proposal is rejected 17 18 Once a proposal is accepted the batchs of actions are executed, 19 the other proposals are automatically canceled. 20 21 Vocabulary: 22 Action: something to do on the MultisignAdmin or Target 23 Batch of Actions: multiple actions to do on the same internal call 24 List of Batchs: multiple internal calls containing batch of actions 25 26 A complexe proposal can include for example 2 batchs of actions: 27 1) Self admin: change quorum + change signers 28 2) Target admin: change parameter + change active 29 30 Antipattern: 31 Instead of doing 2 batchs of 2 actions 32 some could be tempted to use 4 batchs of 1 action 33 In this case the params will be bigger and there will be 2 internal calls. 34 Advice: Use less batchs possible and most actions per batch 35""" 36 37 38@sp.module 39def m2(): 40 # TODO: activate when metadata is available 41 # AGGREGATOR_METADATA = { 42 # "name" : "Multi-sign Administrator", 43 # "version" : "1", 44 # "description" : "Multi-sign administrator for the price feed aggregator", 45 # "source" : { 46 # "tools": [ "SmartPy" ] 47 # }, 48 # "interfaces" : [ "TZIP-016" ], 49 # } 50 51 MultisignAdmin_Badsig = "MultisignAdmin_Badsig" 52 MultisignAdmin_ProposalClosed = "MultisignAdmin_ProposalClosed" 53 MultisignAdmin_ProposalUnknown = "MultisignAdmin_ProposalUnknown" 54 MultisignAdmin_AlreadyVoted = "MultisignAdmin_AlreadyVoted" 55 MultisignAdmin_VoterUnknown = "MultisignAdmin_VoterUnknown" 56 MultisignAdmin_VoterAlreadyknown = "MultisignAdmin_VoterAlreadyknown" 57 MultisignAdmin_ProposalTimedout = "MultisignAdmin_ProposalTimedout" 58 MultisignAdmin_TargetUnkown = "MultisignAdmin_TargetUnkown" 59 MultisignAdmin_MoreQuorumThanVoters = "MultisignAdmin_MoreQuorumThanVoters" 60 MultisignAdmin_VotersLessThan1 = "MultisignAdmin_VotersLessThan1" 61 MultisignAdmin_TargetNotSet = "MultisignAdmin_TargetNotSet" 62 MultisignAdmin_WrongCallbackInterface = "MultisignAdmin_WrongCallbackInterface" 63 64 t_selfAdminAction: type = sp.variant( 65 changeVoters=sp.record( 66 removed=sp.set[sp.address], 67 added=sp.list[sp.record(addr=sp.address, publicKey=sp.key)], 68 ), 69 changeTarget=sp.address, 70 changeQuorum=sp.nat, 71 changeTimeout=sp.int, 72 ) 73 74 t_targetAdminAction: type = sp.variant( 75 changeOracles=sp.record( 76 removed=sp.list[sp.address], 77 added=sp.list[ 78 sp.pair[ 79 sp.address, 80 sp.record( 81 startingRound=sp.nat, 82 endingRound=sp.option[sp.nat], 83 adminAddress=sp.address, 84 ), 85 ] 86 ], 87 ), 88 changeActive=sp.bool, 89 changeAdmin=sp.address, 90 updateFutureRounds=sp.record( 91 minSubmissions=sp.nat, 92 maxSubmissions=sp.nat, 93 restartDelay=sp.nat, 94 timeout=sp.nat, 95 oraclePayment=sp.nat, 96 ), 97 ) 98 t_ProposalBatchs: type = sp.variant( 99 selfAdmin=sp.list[t_selfAdminAction], targetAdmin=sp.list[t_targetAdminAction] 100 ) 101 t_Proposal: type = sp.record( 102 startedAt=sp.timestamp, 103 id=sp.nat, 104 yay=sp.set[sp.nat], 105 nay=sp.set[sp.nat], 106 batchs=sp.list[t_ProposalBatchs], 107 canceled=sp.bool, 108 ) 109 110 t_votes: type = sp.list[ 111 sp.record(voterId=sp.nat, signature=sp.signature, yay=sp.bool) 112 ] 113 114 t_proposalsVotes: type = sp.list[ 115 sp.record(initiatorId=sp.nat, proposalId=sp.nat, votes=t_votes) 116 ] 117 118 t_activeProposal: type = sp.record( 119 startedAt=sp.timestamp, 120 initiatorId=sp.nat, 121 initiatorProposalId=sp.nat, 122 yay=sp.set[sp.nat], 123 nay=sp.set[sp.nat], 124 batchs=sp.list[t_ProposalBatchs], 125 ) 126 127 class MultisignAdmin(sp.Contract): 128 """ 129 Each user can have at most one proposal 130 """ 131 132 def __init__( 133 self, 134 quorum, 135 timeout, 136 addrVoterId, 137 keyVoterId, 138 voters, 139 lastVoterId, 140 metadata, 141 ): 142 # TODO: activate when metadata is available 143 # self.init_metadata("metadata", { 144 # **AGGREGATOR_METADATA, 145 # "views": [self.listActiveProposals] 146 # }) 147 148 self.data.target = None 149 self.data.quorum = quorum 150 self.data.timeout = timeout 151 self.data.lastVoterId = lastVoterId 152 self.data.lastVoteTimestamp = sp.timestamp(0) 153 self.data.nbVoters = sp.len(voters) 154 self.data.voters = sp.big_map() 155 self.data.addrVoterId = addrVoterId 156 self.data.keyVoterId = keyVoterId 157 self.data.proposals = sp.big_map() 158 self.data.metadata = metadata 159 160 for voter in voters.items(): 161 self.data.voters[voter.key] = voter.value 162 163 sp.cast( 164 self.data, 165 sp.record( 166 target=sp.option[sp.address], 167 quorum=sp.nat, 168 timeout=sp.int, 169 lastVoterId=sp.nat, 170 lastVoteTimestamp=sp.timestamp, 171 nbVoters=sp.nat, 172 voters=sp.big_map[ 173 sp.nat, 174 sp.record( 175 publicKey=sp.key, addr=sp.address, lastProposalId=sp.nat 176 ), 177 ], 178 addrVoterId=sp.big_map[sp.address, sp.nat], 179 keyVoterId=sp.big_map[sp.key, sp.nat], 180 proposals=sp.big_map[sp.nat, t_Proposal], 181 metadata=sp.big_map[sp.string, sp.bytes], 182 ), 183 ) 184 185 # ---------------- 186 # Public entrypoints 187 188 @sp.entrypoint 189 def newProposal(self, batchs): 190 voterId = self.data.addrVoterId.get( 191 sp.sender, error=MultisignAdmin_VoterUnknown 192 ) 193 self.data.voters[voterId].lastProposalId += 1 194 self.data.proposals[voterId] = sp.record( 195 startedAt=sp.now, 196 id=self.data.voters[voterId].lastProposalId, 197 yay={voterId}, 198 nay=set(), 199 batchs=batchs, 200 canceled=False, 201 ) 202 if self.data.quorum < 2: 203 self.onVoted(self.data.proposals[voterId]) 204 205 @sp.entrypoint 206 def vote(self, votes): 207 voterId = self.data.addrVoterId.get( 208 sp.sender, error=MultisignAdmin_VoterUnknown 209 ) 210 for vote in votes: 211 self.registerVote( 212 sp.record( 213 initiatorId=vote.initiatorId, 214 proposalId=vote.proposalId, 215 voterId=voterId, 216 yay=vote.yay, 217 ) 218 ) 219 proposal = self.data.proposals[vote.initiatorId] 220 if sp.len(proposal.nay) + sp.len(proposal.yay) >= self.data.quorum: 221 if sp.len(proposal.nay) >= sp.len(proposal.yay): 222 self.data.proposals[vote.initiatorId].canceled = True 223 else: 224 self.onVoted(proposal) 225 226 @sp.entrypoint 227 def multiVote(self, proposalsVotes): 228 """ 229 Signed: Pair(ADDRESS multisignAdmin, Pair(initiatorId, proposalId)) 230 """ 231 sp.cast(proposalsVotes, t_proposalsVotes) 232 for proposalVotes in proposalsVotes: 233 initiatorId = proposalVotes.initiatorId 234 proposalId = proposalVotes.proposalId 235 for vote in proposalVotes.votes: 236 signed = sp.pack((sp.self_address(), (initiatorId, proposalId))) 237 publicKey = self.data.voters.get( 238 vote.voterId, error=MultisignAdmin_VoterUnknown 239 ).publicKey 240 assert sp.check_signature( 241 publicKey, vote.signature, signed 242 ), MultisignAdmin_Badsig 243 self.registerVote( 244 sp.record( 245 initiatorId=initiatorId, 246 proposalId=proposalId, 247 voterId=vote.voterId, 248 yay=vote.yay, 249 ) 250 ) 251 proposal = self.data.proposals[initiatorId] 252 if sp.len(proposal.nay) + sp.len(proposal.yay) >= self.data.quorum: 253 if sp.len(proposal.nay) >= sp.len(proposal.yay): 254 self.data.proposals[initiatorId].canceled = True 255 else: 256 self.onVoted(proposal) 257 258 @sp.entrypoint 259 def cancelProposal(self, proposalId): 260 voterId = self.data.addrVoterId.get( 261 sp.sender, error=MultisignAdmin_VoterUnknown 262 ) 263 proposal = self.data.proposals.get( 264 voterId, error=MultisignAdmin_ProposalUnknown 265 ) 266 assert proposal.id == proposalId, MultisignAdmin_ProposalUnknown 267 self.data.proposals[voterId].canceled = True 268 269 # ---------------- 270 # Public views 271 @sp.entrypoint 272 def getParams(self, callback): 273 params = sp.record( 274 target=self.data.target, 275 quorum=self.data.quorum, 276 timeout=self.data.timeout, 277 ) 278 sp.transfer(params, sp.tez(0), callback) 279 280 @sp.entrypoint 281 def getLastProposal(self, params): 282 proposal = self.data.proposals[sp.fst(params)] 283 callback = sp.contract(t_Proposal, sp.snd(params)) 284 sp.transfer( 285 proposal, 286 sp.tez(0), 287 callback.unwrap_some(error=MultisignAdmin_WrongCallbackInterface), 288 ) 289 290 # ---------------- 291 # Offchain views 292 293 @sp.offchain_view 294 def listActiveProposals(self): 295 """List active proposals""" 296 proposals = self.data.proposals 297 response = sp.cast([], sp.list[t_activeProposal]) 298 299 for voterId in range(0, self.data.nbVoters, 1): 300 if proposals.contains(voterId): 301 proposal = proposals[voterId] 302 if ( 303 # not canceled 304 not proposal.canceled 305 # not timedout 306 and sp.now 307 < sp.add_seconds(proposal.startedAt, self.data.timeout * 60) 308 # not already voted 309 and (sp.len(proposal.nay) + sp.len(proposal.yay)) 310 < self.data.quorum 311 # not closed by other vote 312 and proposal.startedAt > self.data.lastVoteTimestamp 313 ): 314 response.push( 315 sp.record( 316 startedAt=proposal.startedAt, 317 initiatorId=voterId, 318 initiatorProposalId=proposal.id, 319 yay=proposal.yay, 320 nay=proposal.nay, 321 batchs=proposal.batchs, 322 ) 323 ) 324 325 return response 326 327 # ---------------- 328 # Private functions 329 330 @sp.private(with_storage="read-write") 331 def registerVote(self, params): 332 proposal = self.data.proposals.get( 333 params.initiatorId, error=MultisignAdmin_ProposalUnknown 334 ) 335 assert proposal.id == params.proposalId, MultisignAdmin_ProposalUnknown 336 assert ( 337 proposal.startedAt > self.data.lastVoteTimestamp 338 and not proposal.canceled 339 ), MultisignAdmin_ProposalClosed 340 assert sp.now < sp.add_seconds( 341 proposal.startedAt, self.data.timeout * 60 342 ), MultisignAdmin_ProposalTimedout 343 assert not proposal.yay.contains( 344 params.voterId 345 ) and not proposal.nay.contains(params.voterId), MultisignAdmin_AlreadyVoted 346 if params.yay: 347 proposal.yay.add(params.voterId) 348 else: 349 proposal.nay.add(params.voterId) 350 self.data.proposals[params.initiatorId] = proposal 351 352 @sp.private(with_storage="read-write", with_operations=True) 353 def onVoted(self, proposal): 354 self.data.lastVoteTimestamp = sp.now 355 for batch in proposal.batchs: 356 with sp.match(batch): 357 with sp.case.selfAdmin as selfAdminActions: 358 for action in selfAdminActions: 359 with sp.match(action): 360 with sp.case.changeQuorum as quorum: 361 assert ( 362 quorum <= self.data.nbVoters 363 ), MultisignAdmin_MoreQuorumThanVoters 364 self.data.quorum = quorum 365 with sp.case.changeTarget as target: 366 self.data.target = sp.Some(target) 367 with sp.case.changeTimeout as timeout: 368 self.data.timeout = timeout 369 with sp.case.changeVoters as changeVoters: 370 for voterAddress in changeVoters.removed.elements(): 371 voterId = self.data.addrVoterId.get( 372 voterAddress, 373 error=MultisignAdmin_VoterUnknown, 374 ) 375 del self.data.addrVoterId[voterAddress] 376 del self.data.keyVoterId[ 377 self.data.voters[voterId].publicKey 378 ] 379 del self.data.proposals[voterId] 380 del self.data.voters[voterId] 381 self.data.nbVoters = sp.as_nat( 382 self.data.nbVoters 383 - sp.len(changeVoters.removed) 384 ) 385 for voter in changeVoters.added: 386 assert not self.data.addrVoterId.contains( 387 voter.addr 388 ) and not self.data.keyVoterId.contains( 389 voter.publicKey 390 ), MultisignAdmin_VoterAlreadyknown 391 self.data.lastVoterId += 1 392 voterId = self.data.lastVoterId 393 self.data.voters[voterId] = sp.record( 394 publicKey=voter.publicKey, 395 addr=voter.addr, 396 lastProposalId=0, 397 ) 398 self.data.addrVoterId[voter.addr] = voterId 399 self.data.keyVoterId[voter.publicKey] = voterId 400 self.data.nbVoters += sp.len(changeVoters.added) 401 assert ( 402 self.data.nbVoters > 0 403 ), MultisignAdmin_VotersLessThan1 404 assert ( 405 self.data.quorum <= self.data.nbVoters 406 ), MultisignAdmin_MoreQuorumThanVoters 407 with sp.case.targetAdmin as targetActions: 408 target = self.data.target.unwrap_some( 409 error=MultisignAdmin_TargetNotSet 410 ) 411 target_contract = sp.contract( 412 sp.list[t_targetAdminAction], target 413 ).unwrap_some(error=MultisignAdmin_TargetUnkown) 414 sp.transfer(targetActions, sp.tez(0), target_contract) 415 416 ################ 417 # Test contract 418 419 class Administrated(sp.Contract): 420 """ 421 This contract is a sample 422 It shouws how a contract can be administrated 423 through the multisign administration contract 424 """ 425 426 def __init__(self, admin, active): 427 self.data.admin = admin 428 self.data.active = active 429 self.data.value = None 430 431 sp.cast( 432 self.data, 433 sp.record( 434 admin=sp.address, 435 active=sp.bool, 436 value=sp.option[sp.int], 437 ), 438 ) 439 440 @sp.entrypoint 441 def administrate(self, actions): 442 assert sp.sender == self.data.admin, "NOT ADMIN" 443 sp.cast(actions, sp.list[t_targetAdminAction]) 444 for action in actions: 445 with sp.match(action): 446 with sp.case.changeActive as active: 447 self.data.active = active 448 with sp.case.changeAdmin as admin: 449 self.data.admin = admin 450 451 @sp.entrypoint 452 def setValue(self, value): 453 assert self.data.active, "NOT ACTIVE" 454 self.data.value = value 455 456 457######### 458# Helpers 459 460 461class SelfHelper: 462 def variant(content): 463 return sp.variant.selfAdmin(content) 464 465 def changeQuorum(quorum): 466 return SelfHelper.variant([sp.variant.changeQuorum(quorum)]) 467 468 def changeAdmin(admin): 469 return SelfHelper.variant([sp.variant.changeAdmin(admin)]) 470 471 def changeTarget(target): 472 return SelfHelper.variant([sp.variant.changeTarget(target)]) 473 474 def changeTimeout(timeout): 475 return SelfHelper.variant([sp.variant.changeTimeout(timeout)]) 476 477 def changeVoters(removed=[], added=[]): 478 added_list = [] 479 for added_info in added: 480 addr, publicKey = added_info 481 added_list.append(sp.record(addr=addr, publicKey=publicKey)) 482 return SelfHelper.variant( 483 [ 484 sp.variant( 485 "changeVoters", 486 sp.record(removed=sp.set(removed), added=sp.list(added_list)), 487 ) 488 ] 489 ) 490 491 492def sign(account, contract, voterId, initiatorId, proposalId, yay): 493 message = sp.pack(sp.pair(contract.address, sp.pair(initiatorId, proposalId))) 494 signature = sp.make_signature(account.secret_key, message, message_format="Raw") 495 vote = sp.record(voterId=voterId, signature=signature, yay=yay) 496 return vote 497 498 499def vote(contract, signer, yay): 500 """ 501 Only use for test as you can't know if the proposalId correspond to 502 the one you want to vote 503 """ 504 voterId = contract.data.addrVoterId[signer.address] 505 proposal = contract.data.proposals[voterId] 506 return sp.record(proposalId=proposal.id, initiatorId=voterId, yay=yay) 507 508 509################ 510# Tests 511 512 513def add_test(name): 514 @sp.add_test() 515 def test(): 516 sc = sp.test_scenario(name, m2) 517 sc.h1(name) 518 519 admin = sp.test_account("admin") 520 signer1 = sp.test_account("signer1") 521 signer2 = sp.test_account("signer2") 522 signer3 = sp.test_account("signer3") 523 signer4 = sp.test_account("signer4") 524 525 sc.h2("Init contracts") 526 527 sc.h3("Administrated") 528 administrated = m2.Administrated(admin.address, False) 529 sc += administrated 530 administrated_entrypoint = sp.to_address( 531 sp.contract( 532 m2.t_targetAdminAction, administrated.address, entrypoint="administrate" 533 ).unwrap_some() 534 ) 535 536 sc.h3("multisignAdmin") 537 multisignAdmin = m2.MultisignAdmin( 538 quorum=1, 539 timeout=5, 540 addrVoterId=sp.big_map({signer1.address: 0, signer2.address: 1}), 541 keyVoterId=sp.big_map({signer1.public_key: 0, signer2.public_key: 1}), 542 voters={ 543 0: sp.record( 544 addr=signer1.address, publicKey=signer1.public_key, lastProposalId=0 545 ), 546 1: sp.record( 547 addr=signer2.address, publicKey=signer2.public_key, lastProposalId=0 548 ), 549 }, 550 lastVoterId=1, 551 metadata=sp.scenario_utils.metadata_of_url( 552 "ipfs://QmYnU2jwq4XU4jwX1Rmu1EGfmyhMhpDCpPpHF5j9jF9y1w" 553 ), 554 ) 555 sc += multisignAdmin 556 now = sp.timestamp(1) 557 558 if name == "Self Administration tests": 559 ########################## 560 # Auto-accepted proposal # 561 ########################## 562 sc.h2("Auto-accepted proposal when quorum is 1") 563 sc.h3("signer1 propose to change quorum to 2") 564 sc.verify(multisignAdmin.data.quorum == 1) 565 changeQuorum = SelfHelper.changeQuorum(2) 566 multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now) 567 now = now.add_seconds(1) 568 sc.verify(multisignAdmin.data.quorum == 2) 569 570 ################## 571 # Invalid quorum # 572 ################## 573 sc.h2("Invalid quorum proposal") 574 sc.h3("signer1 new proposal to change quorum to 3") 575 sc.verify(multisignAdmin.data.quorum != 3) 576 changeQuorum = SelfHelper.changeQuorum(3) 577 multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now) 578 # Proposal has not been validated yet 579 sc.verify(multisignAdmin.data.quorum == 2) 580 sc.h3("signer2 votes the proposal") 581 sc.p("proposal is rejected because nbSigners < proposed quorum") 582 multisignAdmin.vote( 583 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False 584 ) 585 sc.verify(multisignAdmin.data.quorum != 3) 586 587 ############## 588 # Add signer # 589 ############## 590 sc.h2("Adding new voters") 591 sc.h3("signer2 new proposal to include signer3") 592 sc.verify(~multisignAdmin.data.voters.contains(2)) 593 changeVoters = SelfHelper.changeVoters( 594 [], added=[(signer3.address, signer3.public_key)] 595 ) 596 multisignAdmin.newProposal([changeVoters], _sender=signer2, _now=now) 597 sc.h3("signer1 votes the proposal") 598 multisignAdmin.vote( 599 [vote(multisignAdmin, signer2, yay=True)], _sender=signer1 600 ) 601 sc.verify(multisignAdmin.data.voters.contains(2)) 602 now = now.add_seconds(1) 603 604 ######################### 605 # Newly included signer # 606 ######################### 607 sc.h2("Newly included signer starts a proposal") 608 sc.h3("New proposal by signer 3 to increase quorum to 3") 609 sc.verify(multisignAdmin.data.quorum != 3) 610 changeQuorum = SelfHelper.changeQuorum(3) 611 multisignAdmin.newProposal([changeQuorum], _sender=signer3, _now=now) 612 sc.h3("signer1 votes the proposal") 613 multisignAdmin.vote( 614 [vote(multisignAdmin, signer3, yay=True)], _sender=signer1 615 ) 616 sc.verify(multisignAdmin.data.quorum == 3) 617 now = now.add_seconds(1) 618 619 ########## 620 # Cancel # 621 ########## 622 sc.h2("Proposal cancellation") 623 sc.h3("New proposal") 624 sc.verify(multisignAdmin.data.timeout != 10) 625 changeTimeout = SelfHelper.changeTimeout(10) 626 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 627 sc.h3("Signer 2 tries to cancel the proposal") 628 voterId = multisignAdmin.data.addrVoterId[signer1.address] 629 proposalId = multisignAdmin.data.proposals[voterId].id 630 multisignAdmin.cancelProposal(proposalId, _sender=signer2, _valid=False) 631 sc.h3("Signer 1 cancels the proposal") 632 multisignAdmin.cancelProposal(proposalId, _sender=signer1) 633 sc.h3("Signer 2 tries to vote the canceled proposal") 634 multisignAdmin.vote( 635 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False 636 ) 637 sc.verify(multisignAdmin.data.timeout != 10) 638 639 ############ 640 # Rejected # 641 ############ 642 sc.h2("Proposal rejection") 643 sc.h3("New proposal") 644 sc.verify(multisignAdmin.data.timeout != 10) 645 changeTimeout = SelfHelper.changeTimeout(10) 646 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 647 sc.h3("Signer 2 votes against the proposal") 648 multisignAdmin.vote( 649 [vote(multisignAdmin, signer1, yay=False)], _sender=signer2 650 ) 651 sc.h3("Signer 3 votes against the proposal") 652 sc.verify(multisignAdmin.data.proposals[voterId].canceled == False) 653 multisignAdmin.vote( 654 [vote(multisignAdmin, signer1, yay=False)], _sender=signer3 655 ) 656 voterId = multisignAdmin.data.addrVoterId[signer1.address] 657 sc.verify(multisignAdmin.data.proposals[voterId].canceled == True) 658 sc.verify(multisignAdmin.data.timeout != 10) 659 660 ###################### 661 # Remove signer fail # 662 ###################### 663 sc.h2("Invalid Removed signer proposal") 664 sc.h3("Signer 1 new proposal: remove signer 3") 665 changeVoters = SelfHelper.changeVoters(removed=[signer3.address]) 666 multisignAdmin.newProposal([changeVoters], _sender=signer1, _now=now) 667 sc.h3("Signer 2 votes the remove proposal") 668 multisignAdmin.vote( 669 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2 670 ) 671 sc.h3("Signer 3 tries to vote the remove proposal") 672 sc.p("Fails because quorum would be > number of signers") 673 multisignAdmin.vote( 674 [vote(multisignAdmin, signer1, yay=True)], _sender=signer3, _valid=False 675 ) 676 sc.verify(multisignAdmin.data.voters.contains(2)) 677 678 ###################### 679 # 2 actions proposal # 680 ###################### 681 sc.h2("2 actions proposal") 682 sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 3") 683 sc.verify(multisignAdmin.data.quorum == 3) 684 sc.verify(multisignAdmin.data.voters.contains(2)) 685 changeQuorum = SelfHelper.changeQuorum(2) 686 changeVoters = SelfHelper.changeVoters(removed=[signer3.address]) 687 multisignAdmin.newProposal( 688 [changeQuorum, changeVoters], _sender=signer1, _now=now 689 ) 690 sc.h3("Signer 2 votes the proposal") 691 multisignAdmin.vote( 692 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2 693 ) 694 sc.h3("Signer 3 votes the proposal") 695 multisignAdmin.vote( 696 [vote(multisignAdmin, signer1, yay=True)], _sender=signer3 697 ) 698 now = now.add_seconds(1) 699 sc.verify(multisignAdmin.data.quorum == 2) 700 sc.verify(~multisignAdmin.data.voters.contains(2)) 701 702 ########################### 703 # Votes for past proposal # 704 ########################### 705 sc.h2("Vote for past proposal") 706 sc.h3("Signer 1 new proposal: change timeout to 2") 707 changeTimeout = SelfHelper.changeTimeout(2) 708 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 709 sc.h3("Signer 2 new proposal: add new signer") 710 sc.verify(multisignAdmin.data.timeout != 3) 711 changeVoters = SelfHelper.changeVoters( 712 [], added=[(signer4.address, signer4.public_key)] 713 ) 714 multisignAdmin.newProposal([changeVoters], _sender=signer2) 715 now = now.add_seconds(1) 716 sc.h3("Signer 2 tries to vote signer1's proposal after timedout") 717 multisignAdmin.vote( 718 [vote(multisignAdmin, signer1, yay=True)], 719 _sender=signer2, 720 _now=now.add_minutes(100), 721 _valid=False, 722 ) 723 sc.h3("Signer 1 votes for signer2's proposal") 724 multisignAdmin.vote( 725 [vote(multisignAdmin, signer2, yay=True)], _sender=signer1, _now=now 726 ) 727 sc.h3( 728 "Signer 2 tries to vote signer1's proposal while a more recent one was accepted" 729 ) 730 multisignAdmin.vote( 731 [vote(multisignAdmin, signer1, yay=True)], 732 _sender=signer2, 733 _now=now.add_seconds(1), 734 _valid=False, 735 ) 736 now = now.add_seconds(1) 737 738 ############## 739 # Multivotes # 740 ############## 741 sc.h2("Multi vote in one call") 742 sc.h3("Signer 1 new proposal") 743 changeTimeout = SelfHelper.changeTimeout(2) 744 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 745 now = now.add_seconds(1) 746 sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") 747 signer2_vote = sign( 748 signer2, 749 voterId=1, 750 contract=multisignAdmin, 751 initiatorId=0, 752 proposalId=proposalId, 753 yay=True, 754 ) 755 signer4_vote = sign( 756 signer4, 757 voterId=3, 758 contract=multisignAdmin, 759 initiatorId=0, 760 proposalId=proposalId, 761 yay=True, 762 ) 763 proposalVotes = sp.record( 764 initiatorId=0, proposalId=proposalId, votes=[signer2_vote, signer4_vote] 765 ) 766 multisignAdmin.multiVote([proposalVotes], _sender=signer1, _now=now) 767 sc.verify(multisignAdmin.data.timeout == 2) 768 now = now.add_seconds(1) 769 770 ########################################## 771 772 else: 773 ######################### 774 # Target Administration # 775 ######################### 776 now = sp.timestamp(2) 777 sc.h2("Set multisignAdmin as administrated's admin") 778 administrated.administrate( 779 [sp.variant.changeAdmin(multisignAdmin.address)], 780 _sender=admin, 781 _now=now, 782 ) 783 784 sc.h2("Use multisignadmin to set a target") 785 sc.h3("Signer 1 new proposal: changeTarget") 786 now = now.add_seconds(10) 787 targetAddress = sp.contract( 788 sp.list[m2.t_targetAdminAction], 789 administrated.address, 790 entrypoint="administrate", 791 ).unwrap_some() 792 multisignAdmin.newProposal( 793 [SelfHelper.changeTarget(sp.to_address(targetAddress))], 794 _sender=signer1, 795 _now=now, 796 ) 797 798 sc.h2("Use multisignadmin to administrate target") 799 sc.h3("Signer 1 new proposal: changeActive = True on target") 800 sc.verify(~administrated.data.active) 801 now = now.add_seconds(12) 802 changeTargetActive = sp.variant( 803 "targetAdmin", [sp.variant.changeActive(True)] 804 ) 805 multisignAdmin.newProposal([changeTargetActive], _sender=signer1, _now=now) 806 sc.verify(administrated.data.active) 807 808 809if "main" in __name__: 810 add_test("Self Administration tests") 811 add_test("Target Administration tests")
class
SelfHelper:
462class SelfHelper: 463 def variant(content): 464 return sp.variant.selfAdmin(content) 465 466 def changeQuorum(quorum): 467 return SelfHelper.variant([sp.variant.changeQuorum(quorum)]) 468 469 def changeAdmin(admin): 470 return SelfHelper.variant([sp.variant.changeAdmin(admin)]) 471 472 def changeTarget(target): 473 return SelfHelper.variant([sp.variant.changeTarget(target)]) 474 475 def changeTimeout(timeout): 476 return SelfHelper.variant([sp.variant.changeTimeout(timeout)]) 477 478 def changeVoters(removed=[], added=[]): 479 added_list = [] 480 for added_info in added: 481 addr, publicKey = added_info 482 added_list.append(sp.record(addr=addr, publicKey=publicKey)) 483 return SelfHelper.variant( 484 [ 485 sp.variant( 486 "changeVoters", 487 sp.record(removed=sp.set(removed), added=sp.list(added_list)), 488 ) 489 ] 490 )
def
changeVoters(removed=[], added=[]):
478 def changeVoters(removed=[], added=[]): 479 added_list = [] 480 for added_info in added: 481 addr, publicKey = added_info 482 added_list.append(sp.record(addr=addr, publicKey=publicKey)) 483 return SelfHelper.variant( 484 [ 485 sp.variant( 486 "changeVoters", 487 sp.record(removed=sp.set(removed), added=sp.list(added_list)), 488 ) 489 ] 490 )
def
sign(account, contract, voterId, initiatorId, proposalId, yay):
493def sign(account, contract, voterId, initiatorId, proposalId, yay): 494 message = sp.pack(sp.pair(contract.address, sp.pair(initiatorId, proposalId))) 495 signature = sp.make_signature(account.secret_key, message, message_format="Raw") 496 vote = sp.record(voterId=voterId, signature=signature, yay=yay) 497 return vote
def
vote(contract, signer, yay):
500def vote(contract, signer, yay): 501 """ 502 Only use for test as you can't know if the proposalId correspond to 503 the one you want to vote 504 """ 505 voterId = contract.data.addrVoterId[signer.address] 506 proposal = contract.data.proposals[voterId] 507 return sp.record(proposalId=proposal.id, initiatorId=voterId, yay=yay)
Only use for test as you can't know if the proposalId correspond to the one you want to vote
def
add_test(name):
514def add_test(name): 515 @sp.add_test() 516 def test(): 517 sc = sp.test_scenario(name, m2) 518 sc.h1(name) 519 520 admin = sp.test_account("admin") 521 signer1 = sp.test_account("signer1") 522 signer2 = sp.test_account("signer2") 523 signer3 = sp.test_account("signer3") 524 signer4 = sp.test_account("signer4") 525 526 sc.h2("Init contracts") 527 528 sc.h3("Administrated") 529 administrated = m2.Administrated(admin.address, False) 530 sc += administrated 531 administrated_entrypoint = sp.to_address( 532 sp.contract( 533 m2.t_targetAdminAction, administrated.address, entrypoint="administrate" 534 ).unwrap_some() 535 ) 536 537 sc.h3("multisignAdmin") 538 multisignAdmin = m2.MultisignAdmin( 539 quorum=1, 540 timeout=5, 541 addrVoterId=sp.big_map({signer1.address: 0, signer2.address: 1}), 542 keyVoterId=sp.big_map({signer1.public_key: 0, signer2.public_key: 1}), 543 voters={ 544 0: sp.record( 545 addr=signer1.address, publicKey=signer1.public_key, lastProposalId=0 546 ), 547 1: sp.record( 548 addr=signer2.address, publicKey=signer2.public_key, lastProposalId=0 549 ), 550 }, 551 lastVoterId=1, 552 metadata=sp.scenario_utils.metadata_of_url( 553 "ipfs://QmYnU2jwq4XU4jwX1Rmu1EGfmyhMhpDCpPpHF5j9jF9y1w" 554 ), 555 ) 556 sc += multisignAdmin 557 now = sp.timestamp(1) 558 559 if name == "Self Administration tests": 560 ########################## 561 # Auto-accepted proposal # 562 ########################## 563 sc.h2("Auto-accepted proposal when quorum is 1") 564 sc.h3("signer1 propose to change quorum to 2") 565 sc.verify(multisignAdmin.data.quorum == 1) 566 changeQuorum = SelfHelper.changeQuorum(2) 567 multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now) 568 now = now.add_seconds(1) 569 sc.verify(multisignAdmin.data.quorum == 2) 570 571 ################## 572 # Invalid quorum # 573 ################## 574 sc.h2("Invalid quorum proposal") 575 sc.h3("signer1 new proposal to change quorum to 3") 576 sc.verify(multisignAdmin.data.quorum != 3) 577 changeQuorum = SelfHelper.changeQuorum(3) 578 multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now) 579 # Proposal has not been validated yet 580 sc.verify(multisignAdmin.data.quorum == 2) 581 sc.h3("signer2 votes the proposal") 582 sc.p("proposal is rejected because nbSigners < proposed quorum") 583 multisignAdmin.vote( 584 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False 585 ) 586 sc.verify(multisignAdmin.data.quorum != 3) 587 588 ############## 589 # Add signer # 590 ############## 591 sc.h2("Adding new voters") 592 sc.h3("signer2 new proposal to include signer3") 593 sc.verify(~multisignAdmin.data.voters.contains(2)) 594 changeVoters = SelfHelper.changeVoters( 595 [], added=[(signer3.address, signer3.public_key)] 596 ) 597 multisignAdmin.newProposal([changeVoters], _sender=signer2, _now=now) 598 sc.h3("signer1 votes the proposal") 599 multisignAdmin.vote( 600 [vote(multisignAdmin, signer2, yay=True)], _sender=signer1 601 ) 602 sc.verify(multisignAdmin.data.voters.contains(2)) 603 now = now.add_seconds(1) 604 605 ######################### 606 # Newly included signer # 607 ######################### 608 sc.h2("Newly included signer starts a proposal") 609 sc.h3("New proposal by signer 3 to increase quorum to 3") 610 sc.verify(multisignAdmin.data.quorum != 3) 611 changeQuorum = SelfHelper.changeQuorum(3) 612 multisignAdmin.newProposal([changeQuorum], _sender=signer3, _now=now) 613 sc.h3("signer1 votes the proposal") 614 multisignAdmin.vote( 615 [vote(multisignAdmin, signer3, yay=True)], _sender=signer1 616 ) 617 sc.verify(multisignAdmin.data.quorum == 3) 618 now = now.add_seconds(1) 619 620 ########## 621 # Cancel # 622 ########## 623 sc.h2("Proposal cancellation") 624 sc.h3("New proposal") 625 sc.verify(multisignAdmin.data.timeout != 10) 626 changeTimeout = SelfHelper.changeTimeout(10) 627 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 628 sc.h3("Signer 2 tries to cancel the proposal") 629 voterId = multisignAdmin.data.addrVoterId[signer1.address] 630 proposalId = multisignAdmin.data.proposals[voterId].id 631 multisignAdmin.cancelProposal(proposalId, _sender=signer2, _valid=False) 632 sc.h3("Signer 1 cancels the proposal") 633 multisignAdmin.cancelProposal(proposalId, _sender=signer1) 634 sc.h3("Signer 2 tries to vote the canceled proposal") 635 multisignAdmin.vote( 636 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False 637 ) 638 sc.verify(multisignAdmin.data.timeout != 10) 639 640 ############ 641 # Rejected # 642 ############ 643 sc.h2("Proposal rejection") 644 sc.h3("New proposal") 645 sc.verify(multisignAdmin.data.timeout != 10) 646 changeTimeout = SelfHelper.changeTimeout(10) 647 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 648 sc.h3("Signer 2 votes against the proposal") 649 multisignAdmin.vote( 650 [vote(multisignAdmin, signer1, yay=False)], _sender=signer2 651 ) 652 sc.h3("Signer 3 votes against the proposal") 653 sc.verify(multisignAdmin.data.proposals[voterId].canceled == False) 654 multisignAdmin.vote( 655 [vote(multisignAdmin, signer1, yay=False)], _sender=signer3 656 ) 657 voterId = multisignAdmin.data.addrVoterId[signer1.address] 658 sc.verify(multisignAdmin.data.proposals[voterId].canceled == True) 659 sc.verify(multisignAdmin.data.timeout != 10) 660 661 ###################### 662 # Remove signer fail # 663 ###################### 664 sc.h2("Invalid Removed signer proposal") 665 sc.h3("Signer 1 new proposal: remove signer 3") 666 changeVoters = SelfHelper.changeVoters(removed=[signer3.address]) 667 multisignAdmin.newProposal([changeVoters], _sender=signer1, _now=now) 668 sc.h3("Signer 2 votes the remove proposal") 669 multisignAdmin.vote( 670 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2 671 ) 672 sc.h3("Signer 3 tries to vote the remove proposal") 673 sc.p("Fails because quorum would be > number of signers") 674 multisignAdmin.vote( 675 [vote(multisignAdmin, signer1, yay=True)], _sender=signer3, _valid=False 676 ) 677 sc.verify(multisignAdmin.data.voters.contains(2)) 678 679 ###################### 680 # 2 actions proposal # 681 ###################### 682 sc.h2("2 actions proposal") 683 sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 3") 684 sc.verify(multisignAdmin.data.quorum == 3) 685 sc.verify(multisignAdmin.data.voters.contains(2)) 686 changeQuorum = SelfHelper.changeQuorum(2) 687 changeVoters = SelfHelper.changeVoters(removed=[signer3.address]) 688 multisignAdmin.newProposal( 689 [changeQuorum, changeVoters], _sender=signer1, _now=now 690 ) 691 sc.h3("Signer 2 votes the proposal") 692 multisignAdmin.vote( 693 [vote(multisignAdmin, signer1, yay=True)], _sender=signer2 694 ) 695 sc.h3("Signer 3 votes the proposal") 696 multisignAdmin.vote( 697 [vote(multisignAdmin, signer1, yay=True)], _sender=signer3 698 ) 699 now = now.add_seconds(1) 700 sc.verify(multisignAdmin.data.quorum == 2) 701 sc.verify(~multisignAdmin.data.voters.contains(2)) 702 703 ########################### 704 # Votes for past proposal # 705 ########################### 706 sc.h2("Vote for past proposal") 707 sc.h3("Signer 1 new proposal: change timeout to 2") 708 changeTimeout = SelfHelper.changeTimeout(2) 709 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 710 sc.h3("Signer 2 new proposal: add new signer") 711 sc.verify(multisignAdmin.data.timeout != 3) 712 changeVoters = SelfHelper.changeVoters( 713 [], added=[(signer4.address, signer4.public_key)] 714 ) 715 multisignAdmin.newProposal([changeVoters], _sender=signer2) 716 now = now.add_seconds(1) 717 sc.h3("Signer 2 tries to vote signer1's proposal after timedout") 718 multisignAdmin.vote( 719 [vote(multisignAdmin, signer1, yay=True)], 720 _sender=signer2, 721 _now=now.add_minutes(100), 722 _valid=False, 723 ) 724 sc.h3("Signer 1 votes for signer2's proposal") 725 multisignAdmin.vote( 726 [vote(multisignAdmin, signer2, yay=True)], _sender=signer1, _now=now 727 ) 728 sc.h3( 729 "Signer 2 tries to vote signer1's proposal while a more recent one was accepted" 730 ) 731 multisignAdmin.vote( 732 [vote(multisignAdmin, signer1, yay=True)], 733 _sender=signer2, 734 _now=now.add_seconds(1), 735 _valid=False, 736 ) 737 now = now.add_seconds(1) 738 739 ############## 740 # Multivotes # 741 ############## 742 sc.h2("Multi vote in one call") 743 sc.h3("Signer 1 new proposal") 744 changeTimeout = SelfHelper.changeTimeout(2) 745 multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now) 746 now = now.add_seconds(1) 747 sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") 748 signer2_vote = sign( 749 signer2, 750 voterId=1, 751 contract=multisignAdmin, 752 initiatorId=0, 753 proposalId=proposalId, 754 yay=True, 755 ) 756 signer4_vote = sign( 757 signer4, 758 voterId=3, 759 contract=multisignAdmin, 760 initiatorId=0, 761 proposalId=proposalId, 762 yay=True, 763 ) 764 proposalVotes = sp.record( 765 initiatorId=0, proposalId=proposalId, votes=[signer2_vote, signer4_vote] 766 ) 767 multisignAdmin.multiVote([proposalVotes], _sender=signer1, _now=now) 768 sc.verify(multisignAdmin.data.timeout == 2) 769 now = now.add_seconds(1) 770 771 ########################################## 772 773 else: 774 ######################### 775 # Target Administration # 776 ######################### 777 now = sp.timestamp(2) 778 sc.h2("Set multisignAdmin as administrated's admin") 779 administrated.administrate( 780 [sp.variant.changeAdmin(multisignAdmin.address)], 781 _sender=admin, 782 _now=now, 783 ) 784 785 sc.h2("Use multisignadmin to set a target") 786 sc.h3("Signer 1 new proposal: changeTarget") 787 now = now.add_seconds(10) 788 targetAddress = sp.contract( 789 sp.list[m2.t_targetAdminAction], 790 administrated.address, 791 entrypoint="administrate", 792 ).unwrap_some() 793 multisignAdmin.newProposal( 794 [SelfHelper.changeTarget(sp.to_address(targetAddress))], 795 _sender=signer1, 796 _now=now, 797 ) 798 799 sc.h2("Use multisignadmin to administrate target") 800 sc.h3("Signer 1 new proposal: changeActive = True on target") 801 sc.verify(~administrated.data.active) 802 now = now.add_seconds(12) 803 changeTargetActive = sp.variant( 804 "targetAdmin", [sp.variant.changeActive(True)] 805 ) 806 multisignAdmin.newProposal([changeTargetActive], _sender=signer1, _now=now) 807 sc.verify(administrated.data.active)