Module admin_multisig
Expand source code
import smartpy as sp
"""
A Multisig Contract used to administrate other contracts.
THIS CONTRACT IS FOR ILLUSTRATIVE PURPOSE.
IT HAS NOT BEEN AUDITED.
"""
@sp.module
def MS_TYPES():
# Internal administration action type specification
InternalAdminAction: type = sp.variant(
changeSigners = sp.variant(
removed = sp.set[sp.address],
added = sp.list[
sp.record(
address = sp.address,
publicKey = sp.key
)
]
),
changeQuorum = sp.nat,
changeMetadata = sp.pair[sp.string, sp.option[sp.bytes]],
)
# External administration action type specification
ExternalAdminAction: type = sp.record(
target = sp.address,
actions = sp.bytes
)
# Proposal action type specification
ProposalAction: type = sp.variant(
internal = sp.list[InternalAdminAction],
external = sp.list[ExternalAdminAction]
)
# Proposal type specification
Proposal: type = sp.record(
startedAt = sp.timestamp,
initiator = sp.address,
endorsements = sp.set[sp.address],
actions = ProposalAction
)
AggregatedProposalParams: type = sp.record(
signatures = sp.list[
sp.record(
signerAddress = sp.address,
signature = sp.signature
)
],
proposalId = sp.nat,
actions = ProposalAction
)
AggregatedEndorsementParams: type = sp.list[
sp.record(
signatures = sp.list[
sp.record(
signerAddress = sp.address,
signature = sp.signature
)
],
proposalId = sp.nat
)
]
@sp.module
def ERR():
Badsig = "MULTISIG_Badsig"
ProposalUnknown = "MULTISIG_ProposalUnknown"
NotInitiator = "MULTISIG_NotInitiator"
SignerUnknown = "MULTISIG_SignerUnknown"
InvalidTarget = "MULTISIG_InvalidTarget"
MoreQuorumThanSigners = "MULTISIG_MoreQuorumThanSigners"
InvalidProposalId = "MULTISIG_InvalidProposalId"
METADATA = {
"name" : "Generic Multisig Administrator",
"version" : "1",
"description" : "Generic Multisig Administrator",
"source" : {
"tools": [ "SmartPy" ]
},
"interfaces" : [ "TZIP-016" ],
}
@sp.module
def main():
@sp.effects(with_storage="read-only")
def failIfNotSigner(address):
sp.cast(self.data.signers, sp.map[sp.address, sp.record(publicKey=sp.key, lastProposalId=sp.option[sp.nat])])
assert self.data.signers.contains(address), ERR.SignerUnknown
class MultisigAdmin(sp.Contract):
def __init__(
self,
quorum,
signers,
metadata
):
# Metadata helper
# self.init_metadata("metadata", METADATA)
self.data.quorum = quorum
self.data.lastProposalId = 0
self.data.signers = signers
self.data.proposals = sp.big_map()
self.data.activeProposals = sp.set()
self.data.metadata = metadata
sp.cast(self.data,
sp.record(
quorum = sp.nat,
lastProposalId = sp.nat,
signers = sp.map[
sp.address,
sp.record(
publicKey = sp.key,
lastProposalId = sp.option[sp.nat]
)
],
proposals = sp.big_map[sp.nat, MS_TYPES.Proposal],
activeProposals = sp.set[sp.nat],
metadata = sp.big_map[sp.string, sp.bytes],
)
)
@sp.entrypoint
def proposal(self, actions):
"""
Each user can have at most one proposal active at a time.
Submitting a new proposal overrides the previous one.
"""
# Proposals can only be submitted by registered signers
failIfNotSigner(sp.sender)
# If the proposal initiator has an active proposal,
# then replace that proposal with the new one
signerLastProposalId = self.data.signers[sp.sender].lastProposalId
with sp.match(signerLastProposalId):
with sp.case.Some as id:
self.data.activeProposals.remove(id)
# Increment proposal counter
self.data.lastProposalId += 1
proposalId = self.data.lastProposalId
# Store new proposal
self.data.activeProposals.add(proposalId)
self.data.proposals[proposalId] = sp.record(
startedAt = sp.now,
initiator = sp.sender,
endorsements = sp.set(sp.sender),
actions = actions
)
# Update signer's last proposal
self.data.signers[sp.sender].lastProposalId = sp.Some(proposalId)
# Approve the proposal if quorum only requires 1 vote
if self.data.quorum < 2:
self.onApproved(
sp.record(
proposalId = proposalId,
actions = actions,
)
)
@sp.entrypoint
def endorsement(self, endorsements):
"""
Entrypoint used to submit endorsements to single/multiple proposals.
"""
# Endorsements can only be submitted by registered signers
failIfNotSigner(sp.sender)
# Iterate over every endorsement
for pId in endorsements:
self.registerEndorsement(
sp.record(
proposalId = pId,
signerAddress = sp.sender
)
)
# Approve the proposal if quorum was reached
proposal = self.data.proposals[pId]
if sp.len(proposal.endorsements) >= self.data.quorum:
self.onApproved(
sp.record(
proposalId = pId,
actions = proposal.actions,
)
)
@sp.entrypoint
def aggregated_proposal(self, params):
"""
Users can send aggregated proposal, which are signed offchain and validated onchain.
"""
sp.cast(params, MS_TYPES.AggregatedProposalParams)
failIfNotSigner(sp.sender)
self.data.lastProposalId += 1
assert self.data.lastProposalId == params.proposalId, ERR.InvalidProposalId
proposal = sp.record(
startedAt = sp.now,
initiator = sp.sender,
endorsements = sp.set(sp.sender),
actions = params.actions
)
# If the proposal initiator has an active proposal,
# then replace that proposal with the new one
proposerLastProposalId = self.data.signers[sp.sender].lastProposalId
with sp.match(proposerLastProposalId):
with sp.case.Some as id:
self.data.activeProposals.remove(id)
self.data.signers[sp.sender].lastProposalId = sp.Some(params.proposalId)
self.data.activeProposals.add(params.proposalId)
self.data.proposals[params.proposalId] = proposal
preSignature = sp.pack(
sp.record(
actions = params.actions,
# (contractAddress + proposalId) protect against replay attacks
proposalId = params.proposalId,
contractAddress = sp.self_address(),
)
)
# Validate and apply endorsements
for signature in params.signatures:
failIfNotSigner(signature.signerAddress)
publicKey = self.data.signers[signature.signerAddress].publicKey
assert sp.check_signature(publicKey, signature.signature, preSignature), ERR.Badsig
proposal.endorsements.add(signature.signerAddress)
# Check quorum
if sp.len(proposal.endorsements) >= self.data.quorum:
self.onApproved(
sp.record(
proposalId = params.proposalId,
actions = proposal.actions,
)
)
@sp.entrypoint
def aggregated_endorsement(self, endorsements):
"""
Users can send aggregated votes, which are signed offchain and validated onchain.
"""
sp.cast(endorsements, MS_TYPES.AggregatedEndorsementParams)
for endorsement in endorsements:
for signature in endorsement.signatures:
failIfNotSigner(signature.signerAddress)
preSignature = sp.pack(
sp.record(
# (contractAddress + proposalId) protect against replay attacks
contractAddress = sp.self_address(),
proposalId = endorsement.proposalId
)
)
publicKey = self.data.signers[signature.signerAddress].publicKey
assert sp.check_signature(publicKey, signature.signature, preSignature), ERR.Badsig
self.registerEndorsement(
sp.record(
proposalId = endorsement.proposalId,
signerAddress = signature.signerAddress
)
)
proposal = self.data.proposals[endorsement.proposalId]
if sp.len(proposal.endorsements) >= self.data.quorum:
self.onApproved(
sp.record(
proposalId = endorsement.proposalId,
actions = proposal.actions,
)
)
@sp.entrypoint
def cancel_proposal(self, proposalId):
failIfNotSigner(sp.sender)
# Signers can only cancel their own proposals
assert self.data.proposals[proposalId].initiator == sp.sender, ERR.NotInitiator
self.data.activeProposals.remove(proposalId)
@sp.private(with_storage="read-write")
def registerEndorsement(self, params):
assert self.data.activeProposals.contains(params.proposalId), ERR.ProposalUnknown
# Add endorsement to proposal
self.data.proposals[params.proposalId].endorsements.add(params.signerAddress)
@sp.private(with_storage="read-write", with_operations=True)
def onApproved(self, params):
with sp.match(params.actions):
# Internal actions are applied to the multisig contract
with sp.case.internal as internalActions:
for action in internalActions:
with sp.match(action):
with sp.case.changeQuorum as quorum:
self.data.quorum = quorum
with sp.case.changeMetadata as metadata:
(k, v) = metadata
if v.is_some():
self.data.metadata[k] = v.unwrap_some()
else:
del self.data.metadata[k]
with sp.case.changeSigners as changeSigners:
with sp.match(changeSigners):
with sp.case.removed as removeSet:
for address in removeSet.elements():
if self.data.signers.contains(address):
# Remove signer
del self.data.signers[address]
# We don't remove signer[address].lastProposalId
# because we remove all activeProposals after it.
with sp.case.added as addList:
for signer in addList:
self.data.signers[signer.address] = sp.record(
publicKey = signer.publicKey,
lastProposalId = None
)
# Ensure that the contract never requires more quorum than the total of signers.
assert self.data.quorum <= sp.len(self.data.signers), ERR.MoreQuorumThanSigners
# Removes all active proposals after an administrative change.
self.data.activeProposals = sp.set()
# External actions are applied to other contracts
with sp.case.external as externalActions:
for action in externalActions:
target = sp.contract(sp.bytes, action.target).unwrap_some(error=ERR.InvalidTarget)
sp.transfer(action.actions, sp.tez(0), target)
self.data.activeProposals.remove(params.proposalId)
@sp.module
def helpers():
AdministrationType: type = sp.variant(
changeAdmin = sp.address,
changeActive = sp.bool
)
class Administrated(sp.Contract):
"""
This contract is a sample
It shows how a contract can be administrated
through the multisig administration contract
"""
def __init__(self, admin, active):
self.data.admin = admin
self.data.active = active
@sp.entrypoint
def administrate(self, actionsBytes):
assert sp.sender == self.data.admin, "NOT ADMIN"
# actionsBytes is packed and must be unpacked
actions = sp.unpack(actionsBytes, sp.list[AdministrationType]).unwrap_some(error="Actions are invalid")
for action in actions:
with sp.match(action):
with sp.case.changeActive as active:
self.data.active = active
with sp.case.changeAdmin as admin:
self.data.admin = admin
@sp.entrypoint
def verifyActive(self):
assert self.data.active, "NOT ACTIVE"
if "templates" not in __name__:
#########
# Helpers
class InternalHelper():
def variant(content):
return sp.variant("internal", content)
def changeQuorum(quorum):
return sp.variant("changeQuorum", quorum)
def removeSigners(l):
return sp.variant("changeSigners",
sp.variant("removed", sp.set(l))
)
def addSigners(l):
added_list = []
for added_info in l:
addr, publicKey = added_info
added_list.append(
sp.record(
address = addr,
publicKey = publicKey)
)
return sp.variant("changeSigners",
sp.variant("added", sp.list(added_list))
)
class ExternalHelper():
def variant(content):
return sp.variant("external", content)
def changeActive(active):
return sp.variant("changeActive", active)
def changeAdmin(address):
return sp.variant("changeAdmin", address)
def sign(account, contract):
message = sp.pack(
sp.record(
contractAddress = contract.address,
proposalId = contract.data.lastProposalId
)
)
signature = sp.make_signature(account.secret_key, message, message_format = 'Raw')
vote = sp.record(
signerAddress = account.address,
signature = signature
)
return vote
def packActions(actions):
actions = sp.set_type_expr(actions, sp.TList(helpers.AdministrationType))
return sp.pack(actions)
def add_test(internal_tests, is_default = True):
name = "Internal Administration tests" if internal_tests else "External Administration tests"
@sp.add_test(name = name, is_default = is_default)
def test():
sc = sp.test_scenario([MS_TYPES, ERR, main, helpers])
sc.h1(name)
admin = sp.test_account("admin")
signer1 = sp.test_account("signer1")
signer2 = sp.test_account("signer2")
signer3 = sp.test_account("signer3")
signer4 = sp.test_account("signer4")
if internal_tests:
sc.h3("Originate Multisig Admin")
multisigAdmin = main.MultisigAdmin(
quorum = 1,
signers = sp.map(
{
signer1.address : sp.record(
publicKey = signer1.public_key,
lastProposalId = sp.none
),
signer2.address : sp.record(
publicKey = signer2.public_key,
lastProposalId = sp.none
)
}
),
metadata = sp.utils.metadata_of_url("ipfs://")
)
sc += multisigAdmin
##########################
# Auto-accepted proposal #
##########################
sc.h2("Auto-accepted proposal when quorum is 1")
sc.h3("signer1 propose to change quorum to 2")
sc.verify(multisigAdmin.data.quorum == 1)
changeQuorum = InternalHelper.changeQuorum(2)
multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1)
sc.verify(multisigAdmin.data.quorum == 2)
####################
# Add a 3rd signer #
####################
sc.h2("Adding a 3rd signer")
sc.h3("signer2 new proposal to include signer3")
sc.verify(sp.len(multisigAdmin.data.signers) == 2)
sc.verify(~multisigAdmin.data.signers.contains(signer3.address))
changeSigners = InternalHelper.addSigners([(signer3.address, signer3.public_key)])
multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(sender = signer2)
sc.h3("signer1 votes the proposal")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer1)
sc.verify(multisigAdmin.data.signers.contains(signer3.address))
sc.verify(sp.len(multisigAdmin.data.signers) == 3)
############################################
# New proposal (change Quorum from 2 to 3) #
############################################
sc.h2("New proposal (change Quorum from 2 to 3)")
sc.h3("signer1 new proposal to change quorum to 3")
changeQuorum = InternalHelper.changeQuorum(3)
multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1)
# Proposal has not been validated yet
sc.verify(multisigAdmin.data.quorum == 2)
sc.h3("signer2 votes the proposal (2/2)")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2)
sc.verify(multisigAdmin.data.quorum == 3)
###########################################
# Newly included signer starts a proposal #
###########################################
sc.h2("Newly included signer starts a proposal")
sc.h3("New proposal by signer 3 to decrease quorum to 2")
changeQuorum = InternalHelper.changeQuorum(2)
multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer3)
sc.h3("signer1 votes the proposal")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer1)
sc.verify(multisigAdmin.data.quorum == 3)
sc.h3("signer2 votes the proposal")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2)
sc.verify(multisigAdmin.data.quorum == 2)
##########
# Cancel #
##########
sc.h2("Proposal cancellation")
sc.h3("New proposal by signer 1")
changeTimeout = InternalHelper.changeQuorum(3)
multisigAdmin.proposal(InternalHelper.variant([changeTimeout])).run(sender = signer1)
sc.verify(sp.len(multisigAdmin.data.activeProposals) == 1)
sc.h3("Signer 2 tries to cancel the proposal (must fail, only the initiator can cancel)")
multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(sender = signer2, valid = False)
sc.h3("Signer 1 cancels the proposal")
multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(sender = signer1)
sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
sc.h3("Signer 2 tries to vote the canceled proposal")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2, valid = False)
sc.verify(multisigAdmin.data.quorum != 3)
######################
# 2 actions proposal #
######################
sc.h2("2 actions proposal")
sc.h3("Signer 1 new proposal: change quorum to 2 and add signer 4")
sc.verify(~multisigAdmin.data.signers.contains(signer4.address))
changeQuorum = InternalHelper.changeQuorum(3)
changeSigners = InternalHelper.addSigners([(signer4.address, signer4.public_key)])
multisigAdmin.proposal(InternalHelper.variant([changeQuorum, changeSigners])).run(sender = signer1)
sc.h3("Signer 2 votes the proposal")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2)
sc.verify(multisigAdmin.data.quorum == 3)
sc.verify(multisigAdmin.data.signers.contains(signer4.address))
#########################################
# 2 Internal proposals at the same time #
#########################################
sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 4")
changeQuorum = InternalHelper.changeQuorum(2)
multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1)
changeSigners = InternalHelper.removeSigners([signer4.address])
multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(sender = signer2)
sc.verify(sp.len(multisigAdmin.data.activeProposals) == 2)
sc.h3("Signer 3 votes on quorum proposal")
multisigAdmin.endorsement([sp.as_nat(multisigAdmin.data.lastProposalId - 1)]).run(sender = signer3)
sc.h3("Signer 4 votes on signers proposal")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer4)
sc.h3("Confirm that nothing has changed")
sc.verify(multisigAdmin.data.quorum == 3)
sc.verify(multisigAdmin.data.signers.contains(signer4.address))
sc.h3("Signer 4 votes on quorum proposal")
multisigAdmin.endorsement([sp.as_nat(multisigAdmin.data.lastProposalId - 1)]).run(sender = signer4)
sc.h3("Confirm that quorum was updated and signers proposal was canceled")
sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
sc.verify(multisigAdmin.data.quorum == 2)
sc.verify(multisigAdmin.data.signers.contains(signer4.address))
#########################
# Multisig endorsements #
#########################
sc.h2("Multi vote in one call")
sc.h3("Signer 1 new proposal")
changeQuorum = InternalHelper.changeQuorum(3)
multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1)
sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
signer2_endorsement = sign(signer2, contract = multisigAdmin)
signer3_endorsement = sign(signer3, contract = multisigAdmin)
proposalEndorsements = sp.record(
proposalId = multisigAdmin.data.lastProposalId,
signatures = [signer2_endorsement, signer3_endorsement]
)
multisigAdmin.aggregated_endorsement([proposalEndorsements]).run(sender = signer1)
sc.verify(multisigAdmin.data.quorum == 3)
#####################
# Multisig proposal #
#####################
sc.h2("Multi vote in one call")
sc.h3("Signer 1 new proposal")
changeQuorum = InternalHelper.changeQuorum(3)
multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1)
sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
signer2_vote = sign(signer2, contract = multisigAdmin)
signer3_vote = sign(signer3, contract = multisigAdmin)
proposalVotes = sp.record(
proposalId = multisigAdmin.data.lastProposalId,
signatures = [signer2_vote, signer3_vote]
)
multisigAdmin.aggregated_endorsement([proposalVotes]).run(sender = signer1)
sc.verify(multisigAdmin.data.quorum == 3)
##########################################
else:
sc.h3("Originate Multisig Admin")
multisigAdmin = main.MultisigAdmin(
quorum = 3,
signers = sp.map(
{
signer1.address : sp.record(
publicKey = signer1.public_key,
lastProposalId = sp.none
),
signer2.address : sp.record(
publicKey = signer2.public_key,
lastProposalId = sp.none
),
signer3.address : sp.record(
publicKey = signer3.public_key,
lastProposalId = sp.none
)
}
),
metadata = sp.utils.metadata_of_url("ipfs://")
)
sc += multisigAdmin
sc.h3("Originate administrated contract")
administrated = helpers.Administrated(admin.address, False)
sc += administrated
administrated_entrypoint = sp.contract(sp.TBytes, administrated.address, entrypoint="administrate").open_some()
sc.h2("Set multisig as admin of administrated contract")
sc.verify(administrated.data.active == False)
sc.verify(administrated.data.admin == admin.address)
actions = packActions([ExternalHelper.changeAdmin(multisigAdmin.address)])
administrated.administrate(actions).run(sender = admin)
sc.verify(administrated.data.active == False)
sc.verify(administrated.data.admin == multisigAdmin.address)
sc.h2("Activate the administrated contract")
sc.h3("Signer 1 new proposal: changeActive")
actions = packActions([ExternalHelper.changeActive(True)])
multisigAdmin.proposal(ExternalHelper.variant(
[sp.record(
target = sp.to_address(administrated_entrypoint),
actions = actions
)]
)).run(sender = signer1)
sc.verify(administrated.data.active == False)
sc.h3("Signer 2 votes")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2)
sc.verify(administrated.data.active == False)
sc.h3("Signer 3 votes")
multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer3)
sc.verify(administrated.data.active == True)
sc.h2("Use Multisig vote to deactivate the administrated contract")
sc.h3("Signer 1 new proposal: changeActive")
actions = packActions([ExternalHelper.changeActive(False)])
multisigAdmin.proposal(ExternalHelper.variant(
[sp.record(
target = sp.to_address(administrated_entrypoint),
actions = actions
)]
)).run(sender = signer1)
sc.verify(administrated.data.active == True)
sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
signer2_vote = sign(signer2, contract = multisigAdmin)
signer3_vote = sign(signer3, contract = multisigAdmin)
proposalVotes = sp.record(
proposalId = multisigAdmin.data.lastProposalId,
signatures = [signer2_vote, signer3_vote]
)
multisigAdmin.aggregated_endorsement([proposalVotes]).run(sender = signer1)
sc.verify(administrated.data.active == False)
add_test(internal_tests = True)
add_test(internal_tests = False)
Functions
def sign(account, contract)
-
Expand source code
def sign(account, contract): message = sp.pack( sp.record( contractAddress = contract.address, proposalId = contract.data.lastProposalId ) ) signature = sp.make_signature(account.secret_key, message, message_format = 'Raw') vote = sp.record( signerAddress = account.address, signature = signature ) return vote
def packActions(actions)
-
Expand source code
def packActions(actions): actions = sp.set_type_expr(actions, sp.TList(helpers.AdministrationType)) return sp.pack(actions)
def add_test(internal_tests, is_default=True)
-
Expand source code
def add_test(internal_tests, is_default = True): name = "Internal Administration tests" if internal_tests else "External Administration tests" @sp.add_test(name = name, is_default = is_default) def test(): sc = sp.test_scenario([MS_TYPES, ERR, main, helpers]) sc.h1(name) admin = sp.test_account("admin") signer1 = sp.test_account("signer1") signer2 = sp.test_account("signer2") signer3 = sp.test_account("signer3") signer4 = sp.test_account("signer4") if internal_tests: sc.h3("Originate Multisig Admin") multisigAdmin = main.MultisigAdmin( quorum = 1, signers = sp.map( { signer1.address : sp.record( publicKey = signer1.public_key, lastProposalId = sp.none ), signer2.address : sp.record( publicKey = signer2.public_key, lastProposalId = sp.none ) } ), metadata = sp.utils.metadata_of_url("ipfs://") ) sc += multisigAdmin ########################## # Auto-accepted proposal # ########################## sc.h2("Auto-accepted proposal when quorum is 1") sc.h3("signer1 propose to change quorum to 2") sc.verify(multisigAdmin.data.quorum == 1) changeQuorum = InternalHelper.changeQuorum(2) multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1) sc.verify(multisigAdmin.data.quorum == 2) #################### # Add a 3rd signer # #################### sc.h2("Adding a 3rd signer") sc.h3("signer2 new proposal to include signer3") sc.verify(sp.len(multisigAdmin.data.signers) == 2) sc.verify(~multisigAdmin.data.signers.contains(signer3.address)) changeSigners = InternalHelper.addSigners([(signer3.address, signer3.public_key)]) multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(sender = signer2) sc.h3("signer1 votes the proposal") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer1) sc.verify(multisigAdmin.data.signers.contains(signer3.address)) sc.verify(sp.len(multisigAdmin.data.signers) == 3) ############################################ # New proposal (change Quorum from 2 to 3) # ############################################ sc.h2("New proposal (change Quorum from 2 to 3)") sc.h3("signer1 new proposal to change quorum to 3") changeQuorum = InternalHelper.changeQuorum(3) multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1) # Proposal has not been validated yet sc.verify(multisigAdmin.data.quorum == 2) sc.h3("signer2 votes the proposal (2/2)") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2) sc.verify(multisigAdmin.data.quorum == 3) ########################################### # Newly included signer starts a proposal # ########################################### sc.h2("Newly included signer starts a proposal") sc.h3("New proposal by signer 3 to decrease quorum to 2") changeQuorum = InternalHelper.changeQuorum(2) multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer3) sc.h3("signer1 votes the proposal") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer1) sc.verify(multisigAdmin.data.quorum == 3) sc.h3("signer2 votes the proposal") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2) sc.verify(multisigAdmin.data.quorum == 2) ########## # Cancel # ########## sc.h2("Proposal cancellation") sc.h3("New proposal by signer 1") changeTimeout = InternalHelper.changeQuorum(3) multisigAdmin.proposal(InternalHelper.variant([changeTimeout])).run(sender = signer1) sc.verify(sp.len(multisigAdmin.data.activeProposals) == 1) sc.h3("Signer 2 tries to cancel the proposal (must fail, only the initiator can cancel)") multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(sender = signer2, valid = False) sc.h3("Signer 1 cancels the proposal") multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(sender = signer1) sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0) sc.h3("Signer 2 tries to vote the canceled proposal") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2, valid = False) sc.verify(multisigAdmin.data.quorum != 3) ###################### # 2 actions proposal # ###################### sc.h2("2 actions proposal") sc.h3("Signer 1 new proposal: change quorum to 2 and add signer 4") sc.verify(~multisigAdmin.data.signers.contains(signer4.address)) changeQuorum = InternalHelper.changeQuorum(3) changeSigners = InternalHelper.addSigners([(signer4.address, signer4.public_key)]) multisigAdmin.proposal(InternalHelper.variant([changeQuorum, changeSigners])).run(sender = signer1) sc.h3("Signer 2 votes the proposal") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2) sc.verify(multisigAdmin.data.quorum == 3) sc.verify(multisigAdmin.data.signers.contains(signer4.address)) ######################################### # 2 Internal proposals at the same time # ######################################### sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 4") changeQuorum = InternalHelper.changeQuorum(2) multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1) changeSigners = InternalHelper.removeSigners([signer4.address]) multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(sender = signer2) sc.verify(sp.len(multisigAdmin.data.activeProposals) == 2) sc.h3("Signer 3 votes on quorum proposal") multisigAdmin.endorsement([sp.as_nat(multisigAdmin.data.lastProposalId - 1)]).run(sender = signer3) sc.h3("Signer 4 votes on signers proposal") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer4) sc.h3("Confirm that nothing has changed") sc.verify(multisigAdmin.data.quorum == 3) sc.verify(multisigAdmin.data.signers.contains(signer4.address)) sc.h3("Signer 4 votes on quorum proposal") multisigAdmin.endorsement([sp.as_nat(multisigAdmin.data.lastProposalId - 1)]).run(sender = signer4) sc.h3("Confirm that quorum was updated and signers proposal was canceled") sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0) sc.verify(multisigAdmin.data.quorum == 2) sc.verify(multisigAdmin.data.signers.contains(signer4.address)) ######################### # Multisig endorsements # ######################### sc.h2("Multi vote in one call") sc.h3("Signer 1 new proposal") changeQuorum = InternalHelper.changeQuorum(3) multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1) sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") signer2_endorsement = sign(signer2, contract = multisigAdmin) signer3_endorsement = sign(signer3, contract = multisigAdmin) proposalEndorsements = sp.record( proposalId = multisigAdmin.data.lastProposalId, signatures = [signer2_endorsement, signer3_endorsement] ) multisigAdmin.aggregated_endorsement([proposalEndorsements]).run(sender = signer1) sc.verify(multisigAdmin.data.quorum == 3) ##################### # Multisig proposal # ##################### sc.h2("Multi vote in one call") sc.h3("Signer 1 new proposal") changeQuorum = InternalHelper.changeQuorum(3) multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(sender = signer1) sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") signer2_vote = sign(signer2, contract = multisigAdmin) signer3_vote = sign(signer3, contract = multisigAdmin) proposalVotes = sp.record( proposalId = multisigAdmin.data.lastProposalId, signatures = [signer2_vote, signer3_vote] ) multisigAdmin.aggregated_endorsement([proposalVotes]).run(sender = signer1) sc.verify(multisigAdmin.data.quorum == 3) ########################################## else: sc.h3("Originate Multisig Admin") multisigAdmin = main.MultisigAdmin( quorum = 3, signers = sp.map( { signer1.address : sp.record( publicKey = signer1.public_key, lastProposalId = sp.none ), signer2.address : sp.record( publicKey = signer2.public_key, lastProposalId = sp.none ), signer3.address : sp.record( publicKey = signer3.public_key, lastProposalId = sp.none ) } ), metadata = sp.utils.metadata_of_url("ipfs://") ) sc += multisigAdmin sc.h3("Originate administrated contract") administrated = helpers.Administrated(admin.address, False) sc += administrated administrated_entrypoint = sp.contract(sp.TBytes, administrated.address, entrypoint="administrate").open_some() sc.h2("Set multisig as admin of administrated contract") sc.verify(administrated.data.active == False) sc.verify(administrated.data.admin == admin.address) actions = packActions([ExternalHelper.changeAdmin(multisigAdmin.address)]) administrated.administrate(actions).run(sender = admin) sc.verify(administrated.data.active == False) sc.verify(administrated.data.admin == multisigAdmin.address) sc.h2("Activate the administrated contract") sc.h3("Signer 1 new proposal: changeActive") actions = packActions([ExternalHelper.changeActive(True)]) multisigAdmin.proposal(ExternalHelper.variant( [sp.record( target = sp.to_address(administrated_entrypoint), actions = actions )] )).run(sender = signer1) sc.verify(administrated.data.active == False) sc.h3("Signer 2 votes") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer2) sc.verify(administrated.data.active == False) sc.h3("Signer 3 votes") multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(sender = signer3) sc.verify(administrated.data.active == True) sc.h2("Use Multisig vote to deactivate the administrated contract") sc.h3("Signer 1 new proposal: changeActive") actions = packActions([ExternalHelper.changeActive(False)]) multisigAdmin.proposal(ExternalHelper.variant( [sp.record( target = sp.to_address(administrated_entrypoint), actions = actions )] )).run(sender = signer1) sc.verify(administrated.data.active == True) sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1") signer2_vote = sign(signer2, contract = multisigAdmin) signer3_vote = sign(signer3, contract = multisigAdmin) proposalVotes = sp.record( proposalId = multisigAdmin.data.lastProposalId, signatures = [signer2_vote, signer3_vote] ) multisigAdmin.aggregated_endorsement([proposalVotes]).run(sender = signer1) sc.verify(administrated.data.active == False)
Classes
class InternalHelper
-
Expand source code
class InternalHelper(): def variant(content): return sp.variant("internal", content) def changeQuorum(quorum): return sp.variant("changeQuorum", quorum) def removeSigners(l): return sp.variant("changeSigners", sp.variant("removed", sp.set(l)) ) def addSigners(l): added_list = [] for added_info in l: addr, publicKey = added_info added_list.append( sp.record( address = addr, publicKey = publicKey) ) return sp.variant("changeSigners", sp.variant("added", sp.list(added_list)) )
Methods
def variant(content)
-
Expand source code
def variant(content): return sp.variant("internal", content)
def changeQuorum(quorum)
-
Expand source code
def changeQuorum(quorum): return sp.variant("changeQuorum", quorum)
def removeSigners(l)
-
Expand source code
def removeSigners(l): return sp.variant("changeSigners", sp.variant("removed", sp.set(l)) )
def addSigners(l)
-
Expand source code
def addSigners(l): added_list = [] for added_info in l: addr, publicKey = added_info added_list.append( sp.record( address = addr, publicKey = publicKey) ) return sp.variant("changeSigners", sp.variant("added", sp.list(added_list)) )
class ExternalHelper
-
Expand source code
class ExternalHelper(): def variant(content): return sp.variant("external", content) def changeActive(active): return sp.variant("changeActive", active) def changeAdmin(address): return sp.variant("changeAdmin", address)
Methods
def variant(content)
-
Expand source code
def variant(content): return sp.variant("external", content)
def changeActive(active)
-
Expand source code
def changeActive(active): return sp.variant("changeActive", active)
def changeAdmin(address)
-
Expand source code
def changeAdmin(address): return sp.variant("changeAdmin", address)