templates.admin_multisig
1import smartpy as sp 2 3""" 4 A Multisig Contract used to administrate other contracts. 5 THIS CONTRACT IS FOR ILLUSTRATIVE PURPOSE. 6 IT HAS NOT BEEN AUDITED. 7""" 8 9 10@sp.module 11def MS_TYPES(): 12 # Internal administration action type specification 13 InternalAdminAction: type = sp.variant( 14 changeSigners=sp.variant( 15 removed=sp.set[sp.address], 16 added=sp.list[sp.record(address=sp.address, publicKey=sp.key)], 17 ), 18 changeQuorum=sp.nat, 19 changeMetadata=sp.pair[sp.string, sp.option[sp.bytes]], 20 ) 21 22 # External administration action type specification 23 ExternalAdminAction: type = sp.record(target=sp.address, actions=sp.bytes) 24 25 # Proposal action type specification 26 ProposalAction: type = sp.variant( 27 internal=sp.list[InternalAdminAction], external=sp.list[ExternalAdminAction] 28 ) 29 30 # Proposal type specification 31 Proposal: type = sp.record( 32 startedAt=sp.timestamp, 33 initiator=sp.address, 34 endorsements=sp.set[sp.address], 35 actions=ProposalAction, 36 ) 37 38 AggregatedProposalParams: type = sp.record( 39 signatures=sp.list[sp.record(signerAddress=sp.address, signature=sp.signature)], 40 proposalId=sp.nat, 41 actions=ProposalAction, 42 ) 43 44 AggregatedEndorsementParams: type = sp.list[ 45 sp.record( 46 signatures=sp.list[ 47 sp.record(signerAddress=sp.address, signature=sp.signature) 48 ], 49 proposalId=sp.nat, 50 ) 51 ] 52 53 54@sp.module 55def ERR(): 56 Badsig = "MULTISIG_Badsig" 57 ProposalUnknown = "MULTISIG_ProposalUnknown" 58 NotInitiator = "MULTISIG_NotInitiator" 59 SignerUnknown = "MULTISIG_SignerUnknown" 60 InvalidTarget = "MULTISIG_InvalidTarget" 61 MoreQuorumThanSigners = "MULTISIG_MoreQuorumThanSigners" 62 InvalidProposalId = "MULTISIG_InvalidProposalId" 63 64 65METADATA = { 66 "name": "Generic Multisig Administrator", 67 "version": "1", 68 "description": "Generic Multisig Administrator", 69 "source": {"tools": ["SmartPy"]}, 70 "interfaces": ["TZIP-016"], 71} 72 73 74@sp.module 75def main(): 76 @sp.effects(with_storage="read-only") 77 def failIfNotSigner(address): 78 sp.cast( 79 self.data.signers, 80 sp.map[ 81 sp.address, 82 sp.record(publicKey=sp.key, lastProposalId=sp.option[sp.nat]), 83 ], 84 ) 85 assert self.data.signers.contains(address), ERR.SignerUnknown 86 87 class MultisigAdmin(sp.Contract): 88 def __init__(self, quorum, signers, metadata): 89 # Metadata helper 90 # self.init_metadata("metadata", METADATA) 91 92 self.data.quorum = quorum 93 self.data.lastProposalId = 0 94 self.data.signers = signers 95 self.data.proposals = sp.big_map() 96 self.data.activeProposals = set() 97 self.data.metadata = metadata 98 99 sp.cast( 100 self.data, 101 sp.record( 102 quorum=sp.nat, 103 lastProposalId=sp.nat, 104 signers=sp.map[ 105 sp.address, 106 sp.record(publicKey=sp.key, lastProposalId=sp.option[sp.nat]), 107 ], 108 proposals=sp.big_map[sp.nat, MS_TYPES.Proposal], 109 activeProposals=sp.set[sp.nat], 110 metadata=sp.big_map[sp.string, sp.bytes], 111 ), 112 ) 113 114 @sp.entrypoint 115 def proposal(self, actions): 116 """ 117 Each user can have at most one proposal active at a time. 118 Submitting a new proposal overrides the previous one. 119 """ 120 # Proposals can only be submitted by registered signers 121 failIfNotSigner(sp.sender) 122 123 # If the proposal initiator has an active proposal, 124 # then replace that proposal with the new one 125 signerLastProposalId = self.data.signers[sp.sender].lastProposalId 126 with sp.match(signerLastProposalId): 127 with sp.case.Some as id: 128 self.data.activeProposals.remove(id) 129 130 # Increment proposal counter 131 self.data.lastProposalId += 1 132 proposalId = self.data.lastProposalId 133 # Store new proposal 134 self.data.activeProposals.add(proposalId) 135 self.data.proposals[proposalId] = sp.record( 136 startedAt=sp.now, 137 initiator=sp.sender, 138 endorsements={sp.sender}, 139 actions=actions, 140 ) 141 # Update signer's last proposal 142 self.data.signers[sp.sender].lastProposalId = sp.Some(proposalId) 143 144 # Approve the proposal if quorum only requires 1 vote 145 if self.data.quorum < 2: 146 self.onApproved( 147 sp.record( 148 proposalId=proposalId, 149 actions=actions, 150 ) 151 ) 152 153 @sp.entrypoint 154 def endorsement(self, endorsements): 155 """ 156 Entrypoint used to submit endorsements to single/multiple proposals. 157 """ 158 # Endorsements can only be submitted by registered signers 159 failIfNotSigner(sp.sender) 160 161 # Iterate over every endorsement 162 for pId in endorsements: 163 self.registerEndorsement( 164 sp.record(proposalId=pId, signerAddress=sp.sender) 165 ) 166 167 # Approve the proposal if quorum was reached 168 proposal = self.data.proposals[pId] 169 if sp.len(proposal.endorsements) >= self.data.quorum: 170 self.onApproved( 171 sp.record( 172 proposalId=pId, 173 actions=proposal.actions, 174 ) 175 ) 176 177 @sp.entrypoint 178 def aggregated_proposal(self, params): 179 """ 180 Users can send aggregated proposal, which are signed offchain and validated onchain. 181 """ 182 sp.cast(params, MS_TYPES.AggregatedProposalParams) 183 failIfNotSigner(sp.sender) 184 185 self.data.lastProposalId += 1 186 assert self.data.lastProposalId == params.proposalId, ERR.InvalidProposalId 187 188 proposal = sp.record( 189 startedAt=sp.now, 190 initiator=sp.sender, 191 endorsements={sp.sender}, 192 actions=params.actions, 193 ) 194 195 # If the proposal initiator has an active proposal, 196 # then replace that proposal with the new one 197 proposerLastProposalId = self.data.signers[sp.sender].lastProposalId 198 with sp.match(proposerLastProposalId): 199 with sp.case.Some as id: 200 self.data.activeProposals.remove(id) 201 self.data.signers[sp.sender].lastProposalId = sp.Some(params.proposalId) 202 203 self.data.activeProposals.add(params.proposalId) 204 self.data.proposals[params.proposalId] = proposal 205 206 preSignature = sp.pack( 207 sp.record( 208 actions=params.actions, 209 # (contractAddress + proposalId) protect against replay attacks 210 proposalId=params.proposalId, 211 contractAddress=sp.self_address(), 212 ) 213 ) 214 215 # Validate and apply endorsements 216 for signature in params.signatures: 217 failIfNotSigner(signature.signerAddress) 218 219 publicKey = self.data.signers[signature.signerAddress].publicKey 220 assert sp.check_signature( 221 publicKey, signature.signature, preSignature 222 ), ERR.Badsig 223 224 proposal.endorsements.add(signature.signerAddress) 225 226 # Check quorum 227 if sp.len(proposal.endorsements) >= self.data.quorum: 228 self.onApproved( 229 sp.record( 230 proposalId=params.proposalId, 231 actions=proposal.actions, 232 ) 233 ) 234 235 @sp.entrypoint 236 def aggregated_endorsement(self, endorsements): 237 """ 238 Users can send aggregated votes, which are signed offchain and validated onchain. 239 """ 240 sp.cast(endorsements, MS_TYPES.AggregatedEndorsementParams) 241 242 for endorsement in endorsements: 243 for signature in endorsement.signatures: 244 failIfNotSigner(signature.signerAddress) 245 preSignature = sp.pack( 246 sp.record( 247 # (contractAddress + proposalId) protect against replay attacks 248 contractAddress=sp.self_address(), 249 proposalId=endorsement.proposalId, 250 ) 251 ) 252 publicKey = self.data.signers[signature.signerAddress].publicKey 253 assert sp.check_signature( 254 publicKey, signature.signature, preSignature 255 ), ERR.Badsig 256 self.registerEndorsement( 257 sp.record( 258 proposalId=endorsement.proposalId, 259 signerAddress=signature.signerAddress, 260 ) 261 ) 262 proposal = self.data.proposals[endorsement.proposalId] 263 if sp.len(proposal.endorsements) >= self.data.quorum: 264 self.onApproved( 265 sp.record( 266 proposalId=endorsement.proposalId, 267 actions=proposal.actions, 268 ) 269 ) 270 271 @sp.entrypoint 272 def cancel_proposal(self, proposalId): 273 failIfNotSigner(sp.sender) 274 275 # Signers can only cancel their own proposals 276 assert ( 277 self.data.proposals[proposalId].initiator == sp.sender 278 ), ERR.NotInitiator 279 self.data.activeProposals.remove(proposalId) 280 281 @sp.private(with_storage="read-write") 282 def registerEndorsement(self, params): 283 assert self.data.activeProposals.contains( 284 params.proposalId 285 ), ERR.ProposalUnknown 286 # Add endorsement to proposal 287 self.data.proposals[params.proposalId].endorsements.add( 288 params.signerAddress 289 ) 290 291 @sp.private(with_storage="read-write", with_operations=True) 292 def onApproved(self, params): 293 with sp.match(params.actions): 294 # Internal actions are applied to the multisig contract 295 with sp.case.internal as internalActions: 296 for action in internalActions: 297 with sp.match(action): 298 with sp.case.changeQuorum as quorum: 299 self.data.quorum = quorum 300 with sp.case.changeMetadata as metadata: 301 (k, v) = metadata 302 if v.is_some(): 303 self.data.metadata[k] = v.unwrap_some() 304 else: 305 del self.data.metadata[k] 306 307 with sp.case.changeSigners as changeSigners: 308 with sp.match(changeSigners): 309 with sp.case.removed as removeSet: 310 for address in removeSet.elements(): 311 if self.data.signers.contains(address): 312 # Remove signer 313 del self.data.signers[address] 314 # We don't remove signer[address].lastProposalId 315 # because we remove all activeProposals after it. 316 with sp.case.added as addList: 317 for signer in addList: 318 self.data.signers[ 319 signer.address 320 ] = sp.record( 321 publicKey=signer.publicKey, 322 lastProposalId=None, 323 ) 324 # Ensure that the contract never requires more quorum than the total of signers. 325 assert self.data.quorum <= sp.len( 326 self.data.signers 327 ), ERR.MoreQuorumThanSigners 328 # Removes all active proposals after an administrative change. 329 self.data.activeProposals = set() 330 # External actions are applied to other contracts 331 with sp.case.external as externalActions: 332 for action in externalActions: 333 target = sp.contract(sp.bytes, action.target).unwrap_some( 334 error=ERR.InvalidTarget 335 ) 336 sp.transfer(action.actions, sp.tez(0), target) 337 338 self.data.activeProposals.remove(params.proposalId) 339 340 341@sp.module 342def helpers(): 343 AdministrationType: type = sp.variant(changeAdmin=sp.address, changeActive=sp.bool) 344 345 class Administrated(sp.Contract): 346 """ 347 This contract is a sample 348 It shows how a contract can be administrated 349 through the multisig administration contract 350 """ 351 352 def __init__(self, admin, active): 353 self.data.admin = admin 354 self.data.active = active 355 356 @sp.entrypoint 357 def administrate(self, actionsBytes): 358 assert sp.sender == self.data.admin, "NOT ADMIN" 359 360 # actionsBytes is packed and must be unpacked 361 actions = sp.unpack(actionsBytes, sp.list[AdministrationType]).unwrap_some( 362 error="Actions are invalid" 363 ) 364 365 for action in actions: 366 with sp.match(action): 367 with sp.case.changeActive as active: 368 self.data.active = active 369 with sp.case.changeAdmin as admin: 370 self.data.admin = admin 371 372 @sp.entrypoint 373 def verifyActive(self): 374 assert self.data.active, "NOT ACTIVE" 375 376 377if "main" in __name__: 378 ######### 379 # Helpers 380 381 class InternalHelper: 382 def variant(content): 383 return sp.variant.internal(content) 384 385 def changeQuorum(quorum): 386 return sp.variant.changeQuorum(quorum) 387 388 def removeSigners(l): 389 return sp.variant.changeSigners(sp.variant.removed(sp.set(l))) 390 391 def addSigners(l): 392 added_list = [] 393 for added_info in l: 394 addr, publicKey = added_info 395 added_list.append(sp.record(address=addr, publicKey=publicKey)) 396 return sp.variant.changeSigners(sp.variant.added(sp.list(added_list))) 397 398 class ExternalHelper: 399 def variant(content): 400 return sp.variant.external(content) 401 402 def changeActive(active): 403 return sp.variant.changeActive(active) 404 405 def changeAdmin(address): 406 return sp.variant.changeAdmin(address) 407 408 def sign(account, contract): 409 message = sp.pack( 410 sp.record( 411 contractAddress=contract.address, 412 proposalId=contract.data.lastProposalId, 413 ) 414 ) 415 signature = sp.make_signature(account.secret_key, message, message_format="Raw") 416 vote = sp.record(signerAddress=account.address, signature=signature) 417 return vote 418 419 def packActions(actions): 420 actions = sp.set_type_expr(actions, sp.list[helpers.AdministrationType]) 421 return sp.pack(actions) 422 423 def add_test(internal_tests): 424 name = ( 425 "Internal Administration tests" 426 if internal_tests 427 else "External Administration tests" 428 ) 429 430 @sp.add_test() 431 def test(): 432 sc = sp.test_scenario(name, [MS_TYPES, ERR, main, helpers]) 433 sc.h1(name) 434 435 admin = sp.test_account("admin") 436 signer1 = sp.test_account("signer1") 437 signer2 = sp.test_account("signer2") 438 signer3 = sp.test_account("signer3") 439 signer4 = sp.test_account("signer4") 440 441 if internal_tests: 442 sc.h3("Originate Multisig Admin") 443 multisigAdmin = main.MultisigAdmin( 444 quorum=1, 445 signers=sp.map( 446 { 447 signer1.address: sp.record( 448 publicKey=signer1.public_key, lastProposalId=None 449 ), 450 signer2.address: sp.record( 451 publicKey=signer2.public_key, lastProposalId=None 452 ), 453 } 454 ), 455 metadata=sp.scenario_utils.metadata_of_url("ipfs://"), 456 ) 457 sc += multisigAdmin 458 459 ########################## 460 # Auto-accepted proposal # 461 ########################## 462 sc.h2("Auto-accepted proposal when quorum is 1") 463 sc.h3("signer1 propose to change quorum to 2") 464 sc.verify(multisigAdmin.data.quorum == 1) 465 changeQuorum = InternalHelper.changeQuorum(2) 466 multisigAdmin.proposal( 467 InternalHelper.variant([changeQuorum]), _sender=signer1 468 ) 469 sc.verify(multisigAdmin.data.quorum == 2) 470 471 #################### 472 # Add a 3rd signer # 473 #################### 474 sc.h2("Adding a 3rd signer") 475 sc.h3("signer2 new proposal to include signer3") 476 sc.verify(sp.len(multisigAdmin.data.signers) == 2) 477 sc.verify(~multisigAdmin.data.signers.contains(signer3.address)) 478 changeSigners = InternalHelper.addSigners( 479 [(signer3.address, signer3.public_key)] 480 ) 481 multisigAdmin.proposal( 482 InternalHelper.variant([changeSigners]), _sender=signer2 483 ) 484 sc.h3("signer1 votes the proposal") 485 multisigAdmin.endorsement( 486 [multisigAdmin.data.lastProposalId], _sender=signer1 487 ) 488 sc.verify(multisigAdmin.data.signers.contains(signer3.address)) 489 sc.verify(sp.len(multisigAdmin.data.signers) == 3) 490 491 ############################################ 492 # New proposal (change Quorum from 2 to 3) # 493 ############################################ 494 sc.h2("New proposal (change Quorum from 2 to 3)") 495 sc.h3("signer1 new proposal to change quorum to 3") 496 changeQuorum = InternalHelper.changeQuorum(3) 497 multisigAdmin.proposal( 498 InternalHelper.variant([changeQuorum]), _sender=signer1 499 ) 500 # Proposal has not been validated yet 501 sc.verify(multisigAdmin.data.quorum == 2) 502 sc.h3("signer2 votes the proposal (2/2)") 503 multisigAdmin.endorsement( 504 [multisigAdmin.data.lastProposalId], _sender=signer2 505 ) 506 sc.verify(multisigAdmin.data.quorum == 3) 507 508 ########################################### 509 # Newly included signer starts a proposal # 510 ########################################### 511 sc.h2("Newly included signer starts a proposal") 512 sc.h3("New proposal by signer 3 to decrease quorum to 2") 513 changeQuorum = InternalHelper.changeQuorum(2) 514 multisigAdmin.proposal( 515 InternalHelper.variant([changeQuorum]), _sender=signer3 516 ) 517 sc.h3("signer1 votes the proposal") 518 multisigAdmin.endorsement( 519 [multisigAdmin.data.lastProposalId], _sender=signer1 520 ) 521 sc.verify(multisigAdmin.data.quorum == 3) 522 sc.h3("signer2 votes the proposal") 523 multisigAdmin.endorsement( 524 [multisigAdmin.data.lastProposalId], _sender=signer2 525 ) 526 sc.verify(multisigAdmin.data.quorum == 2) 527 528 ########## 529 # Cancel # 530 ########## 531 sc.h2("Proposal cancellation") 532 sc.h3("New proposal by signer 1") 533 changeTimeout = InternalHelper.changeQuorum(3) 534 multisigAdmin.proposal( 535 InternalHelper.variant([changeTimeout]), _sender=signer1 536 ) 537 sc.verify(sp.len(multisigAdmin.data.activeProposals) == 1) 538 sc.h3( 539 "Signer 2 tries to cancel the proposal (must fail, only the initiator can cancel)" 540 ) 541 multisigAdmin.cancel_proposal( 542 multisigAdmin.data.lastProposalId, _sender=signer2, _valid=False 543 ) 544 sc.h3("Signer 1 cancels the proposal") 545 multisigAdmin.cancel_proposal( 546 multisigAdmin.data.lastProposalId, _sender=signer1 547 ) 548 sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0) 549 sc.h3("Signer 2 tries to vote the canceled proposal") 550 multisigAdmin.endorsement( 551 [multisigAdmin.data.lastProposalId], _sender=signer2, _valid=False 552 ) 553 sc.verify(multisigAdmin.data.quorum != 3) 554 555 ###################### 556 # 2 actions proposal # 557 ###################### 558 sc.h2("2 actions proposal") 559 sc.h3("Signer 1 new proposal: change quorum to 2 and add signer 4") 560 sc.verify(~multisigAdmin.data.signers.contains(signer4.address)) 561 changeQuorum = InternalHelper.changeQuorum(3) 562 changeSigners = InternalHelper.addSigners( 563 [(signer4.address, signer4.public_key)] 564 ) 565 multisigAdmin.proposal( 566 InternalHelper.variant([changeQuorum, changeSigners]), 567 _sender=signer1, 568 ) 569 sc.h3("Signer 2 votes the proposal") 570 multisigAdmin.endorsement( 571 [multisigAdmin.data.lastProposalId], _sender=signer2 572 ) 573 sc.verify(multisigAdmin.data.quorum == 3) 574 sc.verify(multisigAdmin.data.signers.contains(signer4.address)) 575 576 ######################################### 577 # 2 Internal proposals at the same time # 578 ######################################### 579 sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 4") 580 changeQuorum = InternalHelper.changeQuorum(2) 581 multisigAdmin.proposal( 582 InternalHelper.variant([changeQuorum]), _sender=signer1 583 ) 584 changeSigners = InternalHelper.removeSigners([signer4.address]) 585 multisigAdmin.proposal( 586 InternalHelper.variant([changeSigners]), _sender=signer2 587 ) 588 sc.verify(sp.len(multisigAdmin.data.activeProposals) == 2) 589 sc.h3("Signer 3 votes on quorum proposal") 590 multisigAdmin.endorsement( 591 [sp.as_nat(multisigAdmin.data.lastProposalId - 1)], _sender=signer3 592 ) 593 sc.h3("Signer 4 votes on signers proposal") 594 multisigAdmin.endorsement( 595 [multisigAdmin.data.lastProposalId], _sender=signer4 596 ) 597 sc.h3("Confirm that nothing has changed") 598 sc.verify(multisigAdmin.data.quorum == 3) 599 sc.verify(multisigAdmin.data.signers.contains(signer4.address)) 600 sc.h3("Signer 4 votes on quorum proposal") 601 multisigAdmin.endorsement( 602 [sp.as_nat(multisigAdmin.data.lastProposalId - 1)], _sender=signer4 603 ) 604 sc.h3( 605 "Confirm that quorum was updated and signers proposal was canceled" 606 ) 607 sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0) 608 sc.verify(multisigAdmin.data.quorum == 2) 609 sc.verify(multisigAdmin.data.signers.contains(signer4.address)) 610 611 ######################### 612 # Multisig endorsements # 613 ######################### 614 sc.h2("Multi vote in one call") 615 sc.h3("Signer 1 new proposal") 616 changeQuorum = InternalHelper.changeQuorum(3) 617 multisigAdmin.proposal( 618 InternalHelper.variant([changeQuorum]), _sender=signer1 619 ) 620 sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") 621 signer2_endorsement = sign(signer2, contract=multisigAdmin) 622 signer3_endorsement = sign(signer3, contract=multisigAdmin) 623 proposalEndorsements = sp.record( 624 proposalId=multisigAdmin.data.lastProposalId, 625 signatures=[signer2_endorsement, signer3_endorsement], 626 ) 627 multisigAdmin.aggregated_endorsement( 628 [proposalEndorsements], _sender=signer1 629 ) 630 sc.verify(multisigAdmin.data.quorum == 3) 631 632 ##################### 633 # Multisig proposal # 634 ##################### 635 sc.h2("Multi vote in one call") 636 sc.h3("Signer 1 new proposal") 637 changeQuorum = InternalHelper.changeQuorum(3) 638 multisigAdmin.proposal( 639 InternalHelper.variant([changeQuorum]), _sender=signer1 640 ) 641 sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") 642 signer2_vote = sign(signer2, contract=multisigAdmin) 643 signer3_vote = sign(signer3, contract=multisigAdmin) 644 proposalVotes = sp.record( 645 proposalId=multisigAdmin.data.lastProposalId, 646 signatures=[signer2_vote, signer3_vote], 647 ) 648 multisigAdmin.aggregated_endorsement([proposalVotes], _sender=signer1) 649 sc.verify(multisigAdmin.data.quorum == 3) 650 651 ########################################## 652 653 else: 654 sc.h3("Originate Multisig Admin") 655 multisigAdmin = main.MultisigAdmin( 656 quorum=3, 657 signers=sp.map( 658 { 659 signer1.address: sp.record( 660 publicKey=signer1.public_key, lastProposalId=None 661 ), 662 signer2.address: sp.record( 663 publicKey=signer2.public_key, lastProposalId=None 664 ), 665 signer3.address: sp.record( 666 publicKey=signer3.public_key, lastProposalId=None 667 ), 668 } 669 ), 670 metadata=sp.scenario_utils.metadata_of_url("ipfs://"), 671 ) 672 sc += multisigAdmin 673 674 sc.h3("Originate administrated contract") 675 administrated = helpers.Administrated(admin.address, False) 676 sc += administrated 677 administrated_entrypoint = sp.contract( 678 sp.bytes, administrated.address, entrypoint="administrate" 679 ).unwrap_some() 680 681 sc.h2("Set multisig as admin of administrated contract") 682 sc.verify(administrated.data.active == False) 683 sc.verify(administrated.data.admin == admin.address) 684 actions = packActions( 685 [ExternalHelper.changeAdmin(multisigAdmin.address)] 686 ) 687 administrated.administrate(actions, _sender=admin) 688 sc.verify(administrated.data.active == False) 689 sc.verify(administrated.data.admin == multisigAdmin.address) 690 691 sc.h2("Activate the administrated contract") 692 sc.h3("Signer 1 new proposal: changeActive") 693 actions = packActions([ExternalHelper.changeActive(True)]) 694 multisigAdmin.proposal( 695 ExternalHelper.variant( 696 [ 697 sp.record( 698 target=sp.to_address(administrated_entrypoint), 699 actions=actions, 700 ) 701 ] 702 ), 703 _sender=signer1, 704 ) 705 sc.verify(administrated.data.active == False) 706 sc.h3("Signer 2 votes") 707 multisigAdmin.endorsement( 708 [multisigAdmin.data.lastProposalId], _sender=signer2 709 ) 710 sc.verify(administrated.data.active == False) 711 sc.h3("Signer 3 votes") 712 multisigAdmin.endorsement( 713 [multisigAdmin.data.lastProposalId], _sender=signer3 714 ) 715 sc.verify(administrated.data.active == True) 716 717 sc.h2("Use Multisig vote to deactivate the administrated contract") 718 sc.h3("Signer 1 new proposal: changeActive") 719 actions = packActions([ExternalHelper.changeActive(False)]) 720 multisigAdmin.proposal( 721 ExternalHelper.variant( 722 [ 723 sp.record( 724 target=sp.to_address(administrated_entrypoint), 725 actions=actions, 726 ) 727 ] 728 ), 729 _sender=signer1, 730 ) 731 sc.verify(administrated.data.active == True) 732 sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") 733 signer2_vote = sign(signer2, contract=multisigAdmin) 734 signer3_vote = sign(signer3, contract=multisigAdmin) 735 proposalVotes = sp.record( 736 proposalId=multisigAdmin.data.lastProposalId, 737 signatures=[signer2_vote, signer3_vote], 738 ) 739 multisigAdmin.aggregated_endorsement([proposalVotes], _sender=signer1) 740 sc.verify(administrated.data.active == False) 741 742 add_test(internal_tests=True) 743 add_test(internal_tests=False)
METADATA =
{'name': 'Generic Multisig Administrator', 'version': '1', 'description': 'Generic Multisig Administrator', 'source': {'tools': ['SmartPy']}, 'interfaces': ['TZIP-016']}