Hic Et Nunc community marketplace smart contract

On Nov 11, the Hic Et Nunc (HEN) leading developer, Rafael Lima, shut down the hicetnunc.xyz site. Since the HEN code is open-sourced, the HEN community launched an alternate marketplace at https://teia.art/.

The new marketplace is still using the existing HEN smart contracts, so the platform fees are still going to Rafael. Rafael attempted to reduce the fee from 2.5% to 0%, but it wasn’t possible due to a technical issue with the smart contract, so he reduced it to 1%.

Since the HEN community wanted to keep the marketplace alive, it needed funding for running costs like IPFS pinning and maintaining the platform. A new community marketplace smart contract was created to send all platform fees to an account controlled by the community. Alongside the marketplace contract, a multi-sig contract was also being developed, which would ultimately be the manager of the marketplace contract and allow the community to control the new contracts. Until the community formalizes a decentralized autonomous organization (DAO) which will take some time, the multi-sig contract will also effectively act as a basic DAO contract.

In Hic Et Nunc Smart Contracts (Part 2), I discussed the v2 HEN marketplace smart contract. The community marketplace smart contract would be mostly the same as v2 but would change the account receiving the platform fees. The existing minting smart contract, discussed in Hic Et Nunc Smart Contracts (Part 1), will still be used for the minting, and the new marketplace will still be swapping OBJKT NFT’s.

In this article, I take a technical deep dive into the community marketplace smart contract.

Development

The main developer of the community marketplace smart contract is @jagracar and got help from community members. The community contract code is written in the SmartPy language, the same as v2.

In addition to changing the account receiving the fees, it was also decided to fix some minor design issues and make the contract more future-proof. To decrease complexity and avoid regressions, all significant changes to the contracts would have to wait for a future update.

The new smart contracts were tested and reviewed in the #marketplace-contract channel of the HEN Community Discord. There were several rounds of testing on both the testnet and then eventually the mainnet networks of the Tezos blockchain.

Other benefits of the new contract are that the code is more readable, includes more comments, and has extensive tests.

GUI

The current HEN marketplace GUI will work mostly like with the v2 contract, so this article will not cover the GUI in any detail. If you want to understand how the GUI and the smart contracts interact, read the 3-part series on the HEN smart contracts, starting with Hic Et Nunc Smart Contracts (Part 1).

A community member has changes ready to support the community contract. The plan is to keep v2 logic to collect v2 swaps and add logic to use community for all new swaps. A tool is needed if artists want bulk swapping from v2 to the community contract.

Initializing the contract

The Marketplace 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, manager, metadata, allowed_fa2s, fee):
self.init(
manager=manager,
metadata=metadata,
allowed_fa2s=allowed_fa2s,
swaps=sp.big_map(),
fee=fee,
fee_recipient=manager,
counter=0,
proposed_manager=sp.none,
swaps_paused=False,
collects_paused=False)

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

  • manager — A contract storage field that tracks the address of the manager of the smart contract. Only the manager can invoke specific methods.
  • metadata — The smart contract metadata is stored as a big map. This map contains an entry that points to a JSON metadata file hosted on IPFS.
  • swaps — A contract storage field that tracks the swaps as a big map.
  • fee — The platform fee is per 1000.
  • fee_recipient — All the platform fees go to this address. In v2, the platform fees went to the manager account.
  • counter — A contract storage field is used as the ID of the next swap record for the swaps map.
  • proposed_manager — The account is to be the next manager of the contract. Instead of directly updating the manager storage field, a two-step acceptance process is followed.
  • swaps_paused — A boolean storage field to pause all swap transactions.
  • collects_pause — A boolean storage field to pause all collect transactions.

There are several design improvements in the community marketplace contract over the HEN v2 marketplace contract:

  • The roles of the manager and the fee recipient are now separated.
  • The contract can support more than one FA2 token contract, which gets it ready for the community’s possible new NFT token launch (FA2 is the standard for tokens on the Tezos blockchain).
  • The contract’s metadata can be updated after it is deployed.
  • Updating the manager, which plays a critical role in the management and security of the contract, is protected by a two-phase commit process.
  • The contract now has two ways to pause the contract transactions in case a bug or vulnerability is found in the contract. The v1 contract did not have a pause ability and required the contract to be effectively shut down to block an exploit.

In v2 and the community contract, the metadata storage field contains a link to a JSON file stored on IPFS. The metadata includes the marketplace name, URL, and code repository information. The community contract metadata values are to be determined by the HEN community.

Utility methods

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

  • check_is_manager — This method checks that the contract manager is the address calling the entry point. This secures access to the following entry points: update_fee, update_fee_recipient, transfer_manager, update_metadata, add_fa2, remove_fa2, set_pause_swaps, set_pause_collects.
  • check_no_tez_transfer — This method checks that no tez was transferred in the operation. Any amount of tez sent to the entry points not intended to receive tez, by mistake or intentionally, will be permanently lost and are therefore rejected.
  • fa2_transfer — Transfers a number of editions of an FA2 token between two addresses.

Swapping

There are four main steps for swapping:

  • Request funds from a Tezos wallet to cover the blockchain fees. This request is similar to the process for the minting and is handled by the GUI.
  • Update the operator of the OBJKT to the marketplace contract. An operator is a Tezos account that performs token transfer operations on behalf of the owner of the OBJKT.
  • Swap by transferring the OBJKTs to the marketplace account, which acts as an escrow account.
  • Remove the marketplace contract as an operator once the swap is completed.

Update operator

The update_operators entry point of the HEN NFT token smart contract changes the operator for the OBJKT. This entry point allows owners of tokens to permit other addresses to handle their tokens. HEN uses this entry point to handle the sale on behalf of the owner.

The FA2_core class that implements the update_operators entry point manages an operator’s map in the contract storage using the combination of owner, operator, and objkt_id as the key. The operator address can be the v2 or community marketplace contracts. This map verifies that a transfer operation is permitted and is part of the FA2 standard for implementing the transfer permission policy.

Swap tokens

Here is the Python code for the swap method:

@sp.entry_point
def swap(self, params):
# Define the input parameter data type
sp.set_type(params, sp.TRecord(
fa2=sp.TAddress,
objkt_id=sp.TNat,
objkt_amount=sp.TNat,
xtz_per_objkt=sp.TMutez,
royalties=sp.TNat,
creator=sp.TAddress).layout(
("fa2", ("objkt_id", ("objkt_amount", ("xtz_per_objkt", ("royalties", "creator")))))))

# Check that swaps are not paused
sp.verify(~self.data.swaps_paused, message="Swaps are paused")

# Check that no tez have been transferred
self.check_no_tez_transfer()

# Check that the token is one of the allowed tokens to trade
sp.verify(self.data.allowed_fa2s.get(params.fa2, default_value=False),
message="This token type cannot be traded")

# Check that at least one edition will be swapped
sp.verify(params.objkt_amount > 0,
message="At least one edition needs to be swapped")

# Check that the royalties are within the expected limits
sp.verify(params.royalties <= 250,
message="The royalties cannot be higher than 25%")

# Transfer all the editions to the marketplace account
self.fa2_transfer(
fa2=params.fa2,
from_=sp.sender,
to_=sp.self_address,
token_id=params.objkt_id,
token_amount=params.objkt_amount)

# Update the swaps bigmap with the new swap information
self.data.swaps[self.data.counter] = sp.record(
issuer=sp.sender,
fa2=params.fa2,
objkt_id=params.objkt_id,
objkt_amount=params.objkt_amount,
xtz_per_objkt=params.xtz_per_objkt,
royalties=params.royalties,
creator=params.creator)

# Increase the swaps counter
self.data.counter += 1

The @sp.entry_point decorator marks the entry point.

The following parameter values are passed to the swap entry point by the HEN GUI code:

  • fa2 — the FA2 contract for the swap.
  • royalties — the royalties set by the artist at minting.
  • xtz_per_objkt — the price in mutez (1000000 mutez = 1 tez).
  • objkt_amount — the number of editions.
  • objkt_id — the OBJKT ID.
  • creator — the artist Tezos account.

Note: sp.sender is the address calling the current entry point.

The input parameter types are declared to ensure the correct typed parameter values are passed into the entry point. In particular, sp.TNat data type is used to ensure that parameters objkt_id, objkt_amount, and royalties are non-negative integer values.

The code uses the sp.verify command to prevent the entry point from proceeding with the following conditions:

  • swaps_paused is not true.
  • The FA2 parameter value is in the allowed_fa2s map.
  • objkt_amount must be greater than 0.
  • royalties must be less or equal to 250 (25%).

The code calls fa2_transfer, which uses sp.contract to reference the transfer entry point of the NFT token smart contract. The sp.transfer command invokes the transfer entry point with the parameter values. The FA2 code for the transfer entry point uses the big maps in the storage of the NFT token smart contract:

  • Confirms that the calling contract is an operator for the OBJKT by querying the operator’s big map.
  • Updates the ledger big map to transfer the objkt_amount of editions from the artist’s wallet to the marketplace contract.

The swaps map is updated with the token data to track each swap. The counter value updates for the next swap record. The information that is stored for each swap is almost the same as v2:

  • issuer — The user account that did the swap.
  • fa2 — The FA2 token address. This field is new to the community contract since it supports multiple FA2 tokens.
  • objkt_id — The token ID.
  • objkt_amount — The number of editions swapped.
  • xtz_per_objkt — The price of each edition in mutez (1000000 mutez = 1 tez)
  • royalties — The royalties paid to the artist (1000 is 100%)
  • creator — The account for the royalty payment.

Note that the field names use the existing token symbol name of OBJKT. When other FA2 token contracts are supported, these fields track the new tokens even though the token symbol might not be OBJKT. Keeping the same field names makes the move to the community contract easier for the indexer and other third-party tools.

Once this entry point’s transaction completes, the HEN GUI displays the number of OBJKT editions available for purchase at a price specified by the swap creator. These OBJKTs will no longer be visible in the swap creator’s wallet.

The marketplace contract will hold onto these OBJKTs until transferred to a buyer or the swap creator cancels the swap.

Note: The HEN GUI uses the actual royalty specified by the artist during minting. For a secondary sale of an OBJKT, the collector could invoke the swap entry point outside the HEN GUI and override the royalty specified by the original OBJKT artist. However, the HEN GUI doesn’t support such swaps since they are not displayed on the site and therefore cannot be collected. The same holds for the creator parameter.

Collecting

There are three main steps for collecting:

  • Request funds from a Tezos wallet to cover the blockchain fees. This request is similar to the process for the minting and is handled by the GUI.
  • The tokens are transferred from the marketplace account to the collector’s account.

Here is the Python code for the collect method:

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

# Check that collects are not paused
sp.verify(~self.data.collects_paused, message="Collects are
paused")

# Check that the swap id is present in the swaps big map
sp.verify(self.data.swaps.contains(swap_id),
message="The provided swap_id doesn't exist")

# Check that the collector is not the creator of the swap
swap = self.data.swaps[swap_id]
sp.verify(sp.sender != swap.issuer,
message="The collector cannot be the swap issuer")

# Check that the provided tez amount is exactly the edition price
sp.verify(sp.amount == swap.xtz_per_objkt,
message="The sent tez amount does not coincide with the
edition price")

# Check that there is at least one edition available to collect
sp.verify(swap.objkt_amount > 0,
message="All editions have already been collected")

# Handle tez tranfers if the edition price is not zero
sp.if swap.xtz_per_objkt != sp.tez(0):
# Send the royalties to the NFT creator
royalties_amount = sp.local(
"royalties_amount", sp.split_tokens(swap.xtz_per_objkt,
swap.royalties, 1000))

sp.if royalties_amount.value > sp.mutez(0):
sp.send(swap.creator, royalties_amount.value)

# Send the management fees
fee_amount = sp.local(
"fee_amount", sp.split_tokens(swap.xtz_per_objkt,
self.data.fee, 1000))

sp.if fee_amount.value > sp.mutez(0):
sp.send(self.data.fee_recipient, fee_amount.value)

# Send what is left to the swap issuer
sp.send(swap.issuer, sp.amount - royalties_amount.value -
fee_amount.value)

# Transfer the token edition to the collector
self.fa2_transfer(
fa2=swap.fa2,
from_=sp.self_address,
to_=sp.sender,
token_id=swap.objkt_id,
token_amount=1)

# Update the number of editions available in the swaps big map
swap.objkt_amount = sp.as_nat(swap.objkt_amount - 1

The @sp.entry_point decorator marks the entry point.

The following parameter values are passed to the collect function by the HEN GUI code:

swap_id — the ID of the swap record in the swaps map of the community marketplace contract.

Note: sp.amount is the amount of the current transaction. For collects, it’s the OBJKT price in the marketplace.

The code uses the sp.verify command to prevent the entry point from proceeding with the following conditions:

  • collects_paused is not true.
  • Confirms that the swap_id is a valid ID in the swaps map.
  • Checks that the collector is not the creator of the swap.
  • Confirms the price for collecting the OBJKT is the same as the record in the swaps map.
  • There must be OBJKTs available to collect in the swaps map.

If the tez price of the OBJKT is more than zero (not free), the entry point calculates the following fees:

  • amount — the price of the OBJKT in tez.
  • fee — the platform fee.
  • royalties — the royalties to be paid to the artist.

The code uses sp.send to send the royalties to the artist’s (creator) wallet address and the management fees to the contract fee recipient. The code deducts the management fees from the amount paid by the collector then sends it to the account of the current OBJKT owner.

The swaps map record for the OBJKT amount is decremented by 1. This contract only allows 1 edition collection at a time.

The code calls fa2_transfer, which uses sp.contract to reference the transfer entry point of the NFT token smart contract. The sp.transfer command uses the transfer entry point with the parameter values. The FA2 code for the transfer entry point uses the maps in the storage of the NFT token smart contract:

  • Updates the ledger map to transfer 1 edition from the marketplace contract account to the collector’s account.

Note: Even though the community contract only allows collects of 1 edition, a developer could write a script to batch collect transactions to buy multiple editions.

Canceling

There are two main steps for canceling:

  • Request funds from a Tezos wallet to cover the blockchain fees. This request is similar to the process for the minting and is handled by the GUI.
  • Transfer the swapped OBJKTs in escrow by the marketplace contract back to the owner’s account.

Here is the Python code for the cancel_swap method:

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

# Check that no tez have been transferred
self.check_no_tez_transfer()

# Check that the swap id is present in the swaps big map
sp.verify(self.data.swaps.contains(swap_id),
message="The provided swap_id doesn't exist")

# Check that the swap issuer is cancelling the swap
swap = self.data.swaps[swap_id]
sp.verify(sp.sender == swap.issuer,
message="Only the swap issuer can cancel the swap")

# Check that there is at least one swapped edition
sp.verify(swap.objkt_amount > 0,
message="All editions have been collected")

# Transfer the remaining token editions back to the owner
self.fa2_transfer(
fa2=swap.fa2,
from_=sp.self_address,
to_=sp.sender,
token_id=swap.objkt_id,
token_amount=swap.objkt_amount)

# Delete the swap entry in the swaps big map
del self.data.swaps[swap_id]

The @sp.entry_point decorator marks the entry point.

The following parameter values are passed to the cancel_swap function by the HEN GUI code:

  • swap_id — the ID of the swap record in the swaps map of the marketplace contract.

Note: sp.sender is the address that calls the current entry point.

The code uses the sp.verify command to prevent the entry point from proceeding with the following conditions:

  • Confirms that the swap_id is a valid ID in the swaps map.
  • The sender and the issuer addresses have to match. Only the address that swapped the OBJKTs can cancel the swaps.
  • Confirms that the swaps map contains at least one swapped token edition.

The code calls fa2_transfer, which uses sp.contract to reference the transfer entry point of the NFT token smart contract. The sp.transfer command invokes the transfer entry point with the parameter values. The FA2 code for the transfer entry point uses the maps in the storage of the NFT token smart contract:

  • Confirms that the calling contract is an operator for the OBJKT by querying the operator’s map.
  • Updates the ledger map to transfer the swapped number of editions from the marketplace contract wallet to the sender’s account.

The swaps map removes all data associated with the swap_id.

Management

The following entry points in the marketplace smart contract are used to manage and update the smart contract:

  • update_fee — Updates the marketplace management fees to be paid to the fee_recipient account.
  • update_fee_recipient — Updates the marketplace fee_recipient account.
  • transfer_manager — Proposes a new manager account address for the contract. The existing manager account has to invoke the accept_manager entry point to change the manager account address.
  • accept_manager — The existing contract manager account invokes changing the manager account to the address proposed with the transfer_manager entry point.
  • update_metadata — Updates a key/value pair of the contract’s metadata.
  • add_fa2 — Adds an FA2 contract address to the allowed_fa2s map.
  • remove_fa2 — Removes an FA2 contract address from the allowed_fa2s map.
  • set_pause_swaps — Updates the swaps_paused value.
  • set_pause_collects— Updates the collects_paused value.

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 marketplace contract and is useful for the broader ecosystem.

  • get_manager — Returns the current marketplace manager account.
  • is_allowed_fa2 — Checks if an FA2 contract is in the allowed_fa2s map and traded in the marketplace.
  • has_swap — Confirms if a swap ID is in the contracts swaps map.
  • get_swap — Retrieves the swap record associated with a provided swap ID.
  • get_swaps_counter — Returns the current swaps counter value.
  • get_fee — Returns the current fee value.
  • get_fee_recipient — Returns the marketplace fee_recipient value.

Next steps

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

Once the contracts are audited and deployed, other marketplaces in the ecosystem such as objkt.com, henext.xyz, and hen.radio will have to get ready to support the new marketplace contract. Third-party tools might also have to be updated.

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

A community member, @xat, already has changes ready to support the community contract in hicdex, which is the indexer used by HEN (you can read more about how hicdex works). Changes in the indexer database schema will require a full re-index which typically takes several days. Once the indexer is updated, the marketplace web GUI changes can be deployed to support swapping with the community contract.

Once the new GUI is deployed, the HEN community would have control over the destiny of its own marketplace.

You can read my article on the Hic Et Nunc community multi-sig smart contract. 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.

--

--

Artist, engineer.

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