Hic Et Nunc community multi-sig smart contract

Development

The main developer of the multi-sig is @jagracar, and got help from community members. The code is written in the SmartPy language.

Design

The high-level features of the multi-sig design are:

  • The maintenance of a set of users.
  • Each user is allowed to create a proposal.
  • The users can vote on a proposal.
  • If the proposal receives enough votes before the expiration time, the proposal can be executed.
  • A proposal to transfer tez from the contract to a list of accounts.
  • A proposal to transfer FA2 tokens from the contract to a list of accounts (FA2 is the standard for tokens on the Tezos blockchain).
  • A proposal to change the minimum number of votes required to execute a proposal.
  • A proposal to change the expiration time for voting.
  • A proposal to add a new user.
  • A proposal to remove a user.
  • A text proposal whose content is stored on IPFS.
  • A proposal to execute a lambda function which is a way for the contract to run arbitrary Michelson code

Initializing the contract

The MultisignWalletContract constructor (__init__ method) is used to initialize new class instances. The smart contract initializes storage values and data structures used in other class methods.

def __init__(self, metadata, users, minimum_votes, expiration_time=sp.nat(5)):
self.init(
metadata=metadata,
users=users,
proposals=sp.big_map(),
votes=sp.big_map(),
minimum_votes=minimum_votes,
expiration_time=expiration_time,
counter=0)
  • metadata — The smart contract metadata is stored as a big map. This map contains an entry pointing to a JSON metadata file hosted on IPFS.
  • users — The set of users that are allowed to make, vote, and execute proposals.
  • proposals — A contract storage field that tracks the proposals as a big map.
  • votes — A contract storage field that tracks the votes as a big map.
  • minimum_votes — The minimum number of votes required to execute a proposal.
  • expiration_time — An expiration time to vote on the proposals counted in days.
  • counter — A contract storage field is used as the ID of the next proposal record for the proposals big map.

Utility methods

The code for the smart contract contains a few utility methods that are used by the entry points:

  • check_is_user — This method checks that the account invoking an entry point is in the users set. This limits access to the entry points.
  • check_proposal_is_valid — This method checks that the proposal is valid to be voted or executed. A valid proposal exists in the proposals big map and has not expired and has not been executed.
  • fa2_transfer — Transfers some editions of an FA2 token between two addresses.

Proposals

The record for each proposal that gets stored in the proposals big map all contain these fields:

  • kind — The type of proposal
  • executed — A boolean flag to track if the proposal has been executed.
  • issuer — The user account that submitted the proposal.
  • timestamp — The time when the proposal was submitted.
  • positive_votes — The number of positive votes that the proposal has received.
  • text — The proposed text is stored in an IPFS file.
  • mutez_transfers — The list of accounts for mutez transfers.
  • token_transfers — The list of accounts for token transfers.
  • minimum_votes — The proposed minimum positive votes.
  • expiration_time — The proposed expiration time is in a number of days.
  • user — The account of a new user to add to the users set.
  • lambda_function — The lambda function code to execute.
  • text_proposal — To create a text proposal.
  • transfer_mutez_proposal — To transfer amounts of mutez to multiple accounts (1 tez = 1000000 mutez). The parameter needed to create the proposal is a list of amount + proposal pairs.
  • transfer_token_proposal — To transfer tokens (e.g., OBJKTs) to various accounts. The parameters are a list of amount + account pairs, the token ID (555 for OBJKT#555), and the token contract (KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton for OBJKTs).
  • add_user_proposal — To add a new user to the multi-sig contract. The only parameter is the new user account.
  • remove_user_proposal — To remove an existing user from the multi-sig contract. The only parameter is the user account to remove.
  • minimum_votes_proposal — To change the minimum positive votes to approve a proposal. The only parameter is the number of minimum votes (greater than 0 but less than the number of users).
  • expiration_time_proposal — To change the proposal expiration time. The only parameter is a positive integer number of days higher than 0.
  • lambda_proposal — To execute a lambda function. This is the most powerful but the most complicated since the code will be in Michelson, which is hard to understand. It would be essential to verify exactly what the code will do.

Voting

Users can vote on any proposal by invoking the vote_proposal entry point of the multi-sig.

@sp.entry_point
def vote_proposal(self, vote):
# Define the input parameter data type
sp.set_type(vote, sp.TRecord(
proposal_id=sp.TNat,
approval=sp.TBool).layout(("proposal_id", "approval")))

# Check that one of the users executed the entry point
self.check_is_user()

# Check that is a valid proposal
self.check_proposal_is_valid(vote.proposal_id)

# Check if the user voted positive before and remove their
previous vote
# from the proposal positive votes counter
proposal = self.data.proposals[vote.proposal_id]

sp.if self.data.votes.get((vote.proposal_id, sp.sender),
default_value=False):
proposal.positive_votes =
sp.as_nat(proposal.positive_votes - 1)

# Add the vote to the proposal positive votes counter if it's
positive
sp.if vote.approval:
proposal.positive_votes += 1

# Add or update the users vote
self.data.votes[(vote.proposal_id, sp.sender)] = vote.approval

Executing

Users can execute any proposal by invoking the execute_proposal entry point of the multi-sig.

@sp.entry_point
def execute_proposal(self, proposal_id):
# Define the input parameter data type
sp.set_type(proposal_id, sp.TNat)

# Check that one of the users executed the entry point
self.check_is_user()

# Check that is a valid proposal
self.check_proposal_is_valid(proposal_id)

# Check that the proposal received enough positive votes
proposal = self.data.proposals[proposal_id]
sp.verify(proposal.positive_votes >= self.data.minimum_votes,
message="The proposal didn't receive enough positive
votes")

# Execute the proposal
proposal.executed = True

# Proposal logic
. . .

Views

Views are informative functions that can be invoked by other smart contracts and are a new feature recently added to Tezos smart contracts. Views are not entry points and cannot update the smart contract storage. Supporting views allows other smart contracts to get interesting information from the multi-sig and is useful for the broader ecosystem.

  • get_users — Returns the set of multi-sig users for this contract.
  • is_user — Checks if a given account is in the set of users.
  • get_minimum_votes — Returns the minimum_votes value.
  • get_expiration_time — Returns the expiration_time value.
  • get_proposal_count — Returns the number of proposals.
  • get_proposal — Returns the proposal information of a given proposal ID.
  • get_vote — Returns a given user account’s vote for a given proposal ID.
  • has_voted — Returns if a given user account has voted for a given proposal ID.

Risks & pitfalls

The multi-sig smart contract is generic and could be used for various use-cases outside an NFT marketplace. However, it is essential to understand that the design comes with potential risks for each use case the contract is being considered.

  • When removing users from voting, the user to be removed can vote for their removal. This may lead to a problem in cases where inactive users have lost access to their keys. These inactive users now have to become active to reach quorum in proposals after the user has been removed. For example, removing a regularly participating user in a 4-of-5 voting system will end up in a 4-of-4 voting system, where all remaining users are now crucial. However, the current design prevents a fraction of the users from removing a user against their will since the affected user can defend themselves with a vote.
  • Changing the global parameters “expiration time” or “minimum votes” changes all ongoing proposals’ conditions. For example, decreasing or increasing the minimum votes parameter will lower or raise the quorum required for proposals already ongoing. Removing a user does not remove the votes already posted on ongoing proposals. Adding new users allows them to vote for proposals already ongoing. However, keeping the parameter’s history would complicate the smart contract code and increase gas costs.

Next steps

The Tezos Foundation introduced the smart contract developers to an auditing firm, inference ag, which agreed to an independent audit of the community marketplace and multi-sig contracts. @jagracar handed off the code for the audit on Dec 20, 2021.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store