Hic Et Nunc community multi-sig smart contract

Leon Nicholls
8 min readJan 21, 2022

Along with a new community marketplace smart contract for Teia (formerly HEN community), a multi-signature contract (or multi-sig for short) was being developed. A multi-sig is a smart contract that allows several users to manage and divide responsibility for funds.

The multi-sig would allow the HEN community to manage the platform fees collected by the community marketplace contract.

The community was also working on formalizing a decentralized autonomous organization (DAO). Still, it would take some time, so the multi-sig features were expanded to act as a proposed basic DAO contract in the meantime.

This article takes a technical deep dive into the design and capabilities of the HEN community multi-sig.

Development

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

The multi-sig was tested and reviewed in the HEN Community Discord. Community members were added as users for the contract. There were several rounds of testing on both the testnet and then eventually the mainnet networks of the Tezos blockchain. Since the contract didn’t have a web GUI yet, the SmartPy Contract Explorer and Better Call Dev invoke the contract entry points.

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.

There are different kinds of proposals supported:

  • 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)

Here is a list that explains each of the storage fields declared in the constructor:

  • 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.

For each kind of proposal, there is an entry point in the multi-sig to create that kind of proposal.

  • 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

The code confirms that the account invoking the entry point is in the users set and that the proposal ID passed in the parameter is in the proposals big map.

If a user made a positive vote for a proposal before, the number of positive votes is first reduced by one before adding the new vote approval if it is positive. This means if a user voted positive and then negative, the value of the proposal_votes for the proposal record would be correct. The votes big map is then updated with the user’s vote.

The blockchain transaction logs can be used to verify when the user voted.

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
. . .

The code confirms that the account invoking the entry point is in the users set and that the proposal ID passed in the parameter is in the proposals big map. The code then confirms that the proposal has received equal or more votes than the minimum_votes value.

The executed flag for the proposal record is then updated to true. The rest of the entry point code (not shown above) is logic that is executed for each kind of proposal. For example, the transfer_token proposal type will use the fa2_transfer entry point to transfer the proposals tokens to the specified accounts.

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.

Here are some of the risks that we are aware of:

  • 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.

The SmartPy Contract Explorer and Better Call Dev tools are very technical and generic. For the HEN community, it would be better to create a custom GUI designed around the needs of the multi-sig. @jagracar used the Taquito library to create a web GUI that will invoke the various multi-sig entry points (try the test version).

Until the new DAO is in place, the multi-sig contract can be used as the community marketplace manager so that the community can manage the platform fees.

Various instances of the multi-sig can also manage groups within the community. Each group could be funded from the main multi-sig that manages the community marketplace contract.

Using the multi-sig will be an interesting learning curve that could provide feedback for the design of the DAO smart contract.

Once the code has passed the audit anybody will be able to deploy their own multi-sig for their own use since the code is open-sourced.

If you are interested in HEN NFTs, read my introductory article. If you want to know more about the technical aspects, read my HEN smart contracts and indexer articles. You can follow my 3D art on HEN.

--

--