Skip to content

Commit

Permalink
Update this multisig contract (#524)
Browse files Browse the repository at this point in the history
* Update this multisig contract

* Simplify

* Port change to explanation
  • Loading branch information
timothymcmackin authored Jan 23, 2025
1 parent 120f29f commit a878954
Showing 1 changed file with 139 additions and 15 deletions.
154 changes: 139 additions & 15 deletions docs/smart-contracts/multisig.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
title: Multi-signature contracts
authors: Tim McMackin
last_update:
date: 2 February 2024
date: 23 January 2025
dependencies:
octez: 21.2
smartpy: 0.20.0
---

Multi-signature (or multisig) contracts require multiple accounts to authorize operations before running them.
Expand All @@ -18,10 +21,130 @@ The contract originator does not even need to be one of the accounts that can au
## Using proposals

One common way to create a multisig contract is to allow authorized users to submit a proposal that other authorized users can vote on.
For example [this multisig contract](https://github.com/onedebos/multisig/blob/main/multisig.py) stores tez and allows users to propose and vote on the account that should receive the tez.
It stores a big-map of proposals, each with an amount to pay, the account to pay, and information about who has voted for the proposal:
For example, this multisig contract stores tez and allows users to propose and vote on the account that should receive the tez:

```smartpy
import smartpy as sp
@sp.module
def main():
proposal_type: type = sp.big_map[
sp.int,
sp.record(
paymentAmt=sp.mutez,
receiver=sp.address,
voters=sp.set[sp.address],
votingComplete=sp.bool,
),
]
class MultiSigContract(sp.Contract):
def __init__(self, members, requiredVotes):
# Keep track of all the proposals submitted to the multisig
self.data.proposals = sp.cast(sp.big_map(), proposal_type)
self.data.activeProposalId = 0
self.data.members = sp.cast(members, sp.set[sp.address])
self.data.requiredVotes = sp.cast(requiredVotes, sp.nat)
@sp.entrypoint
def deposit(self):
assert self.data.members.contains(sp.sender), "Not a Member of MultiSig"
@sp.entrypoint
def submit_proposal(self, params):
"""
Submit a new proposal/lambda for members
of the MultiSig to vote for.
"""
assert self.data.members.contains(sp.sender), "Not a Member of MultiSig"
assert (
params.paymentAmt <= sp.balance
), "The MultiSig does not have enough funds for this proposal"
self.data.activeProposalId += (
1 # submitting a new proposal inactivates the last one
)
self.data.proposals[self.data.activeProposalId] = sp.record(
paymentAmt=params.paymentAmt,
receiver=params.receiver,
voters={sp.sender},
votingComplete=False,
)
@sp.entrypoint
def vote_on_proposal(self):
assert self.data.members.contains(sp.sender), "Not a Member of MultiSig"
# check if the user has previously voted on the proposal
assert not self.data.proposals[self.data.activeProposalId].voters.contains(
sp.sender
), "Member has voted on this proposal"
self.data.proposals[self.data.activeProposalId].voters.add(sp.sender)
if (
sp.len(self.data.proposals[self.data.activeProposalId].voters)
== self.data.requiredVotes
):
sp.send(
self.data.proposals[self.data.activeProposalId].receiver,
self.data.proposals[self.data.activeProposalId].paymentAmt,
)
self.data.proposals[self.data.activeProposalId].votingComplete = True
@sp.add_test()
def test():
scenario = sp.test_scenario("Multisig test", main)
alice = sp.test_account("alice")
bob = sp.test_account("bob")
charlie = sp.test_account("charlie")
dani = sp.test_account("dani")
earl = sp.test_account("earl")
scenario.h3("MultiSig Proposal Contract")
members = sp.set()
members.add(alice.address)
members.add(bob.address)
members.add(charlie.address)
members.add(earl.address)
contract = main.MultiSigContract(members, 3)
scenario += contract
scenario.h3("Members can add funds to the contract")
contract.deposit(_sender=alice.address, _amount=sp.tez(50))
scenario.h3(
"Members can submit a proposal for funds to be sent to an address - Proposal 1."
)
contract.submit_proposal(
sp.record(paymentAmt=sp.tez(30), receiver=dani.address),
_sender=alice.address
)
scenario.h3("Non-members cannot vote on proposals")
contract.vote_on_proposal(_valid=False, _sender=dani.address)
scenario.h3("Member 2 can vote on proposal")
contract.vote_on_proposal(_sender=bob.address)
scenario.h3("Member 3 can vote on proposal")
contract.vote_on_proposal(_sender=charlie.address)
scenario.h3("Contract balance should drop to 20tez after transfer")
scenario.verify(contract.balance == sp.tez(20))
scenario.h3("A New proposal can be created")
contract.submit_proposal(
sp.record(paymentAmt=sp.tez(20), receiver=dani.address),
_sender=alice.address
)
scenario.h3("New proposal can be voted on")
contract.vote_on_proposal(_sender=charlie.address)
```

This contract stores a big-map of proposals, each with an amount to pay, the account to pay, and information about who has voted for the proposal:

```python
```smartpy
proposal_type: type = sp.big_map[
sp.int,
sp.record(
Expand All @@ -35,32 +158,33 @@ proposal_type: type = sp.big_map[

The `submit_proposal` entrypoint allows authorized users to submit a payment amount and an account address, which adds a proposal to the storage:

```python
```smartpy
self.data.proposals[self.data.activeProposalId] = sp.record(
paymentAmt=params.paymentAmt,
receiver=params.receiver,
voters=sp.set(sp.sender),
voters={sp.sender},
votingComplete=False,
)
```

Authorized accounts call the `vote_on_proposal` entrypoint to vote for the currently active proposal:

```python
assert self.data.members.contains(sp.sender), "Not a Member of MultiSig"
# Check if the user has previously voted on the proposal
assert not self.data.proposals[self.data.activeProposalId].voters.contains(
sp.sender
), "Member has voted on this proposal"
# Add the user's vote for the proposal
self.data.proposals[self.data.activeProposalId].voters.add(sp.sender)
```smartpy
@sp.entrypoint
def vote_on_proposal(self):
assert self.data.members.contains(sp.sender), "Not a Member of MultiSig"
# check if the user has previously voted on the proposal
assert not self.data.proposals[self.data.activeProposalId].voters.contains(
sp.sender
), "Member has voted on this proposal"
self.data.proposals[self.data.activeProposalId].voters.add(sp.sender)
```

Accounts that don't want to vote for the proposal don't need to do anything.

When the necessary number of votes have been reached, the `vote_on_proposal` entrypoint automatically sends the tez to the account in the proposal:

```python
```smartpy
if (
sp.len(self.data.proposals[self.data.activeProposalId].voters)
== self.data.requiredVotes
Expand Down

0 comments on commit a878954

Please sign in to comment.