Hic Et Nunc 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.

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

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

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.

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

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.
@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
  • 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.
  • amount — the price of the OBJKT in tez.
  • fee — the platform fee.
  • royalties — the royalties to be paid to the artist.
  • Updates the ledger map to transfer 1 edition from the marketplace contract account to the collector’s account.

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.
@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]
  • swap_id — the ID of the swap record in the swaps map of the marketplace contract.
  • 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.
  • 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.

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.

--

--

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