Hic Et Nunc Smart Contracts (Part 2)

The HicEtNunc developer shut down the hicetnunc.xyz site. You can use one of the alternative marketplaces such as https://hicetnunc.art/.

In part 1 of this series, I discussed how Hic Et Nunc (HEN) smart contracts are used for minting NFTs.

Due to an exploit, HEN had to deploy V2 versions of the smart contracts. Now, most of the HEN features are implemented using the V2 smart contracts.

I will also discuss the V1 smart contracts exploit that drove the changes for the V2 design.

Swapping

The GUI for swapping is implemented in using the React JavaScript library.

The GUI code to request funds from a Tezos wallet and swap the artwork is in in the method.

There are three main steps for swapping:

  • Request funds from a Tezos wallet to cover the blockchain fees. This request is similar to the process for minting.
  • Update the operator of the OBJKT. An operator is a Tezos address that performs token transfer operations on behalf of the owner of the OBJKT.
  • Swap by transferring the OBJKTs to the HEN escrow wallet.

Before digging into these steps, let’s look at the class, which implements all the entry points of the V2 smart contract.

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

def __init__(self, objkt, metadata, manager, fee):
self.init(
objkt = objkt,
metadata = metadata,
manager = manager,
swaps = sp.big_map(
tkey=sp.TNat,
tvalue=sp.TRecord(issuer=sp.TAddress,
objkt_amount=sp.TNat, objkt_id=sp.TNat,
xtz_per_objkt=sp.TMutez,
royalties=sp.TNat, creator=sp.TAddress)
),
counter = 500000,
fee = fee
)

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

  • : A contract storage field for the address of the NFT token contract.
  • : The smart contract metadata. This field points to a JSON metadata file hosted on IPFS.
  • : A contract storage field that tracks the address of the manager of the smart contract. Only the manager can invoke specific methods, and all platform fees go to its address.
  • s: A contract storage field that tracks the swaps as a big map. The big maps store large amounts of data in a map.
  • : A contract storage field is used as the ID of the next swap record for the swaps map.
  • : The platform fee.

Update operator

The entry point of the V2 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 class that implements the entry point manages an operators map in the contract storage using the combination of , , and as the key. The operator address is the V2 marketplace contract. This map verifies that a transfer operation is permitted and is part of the FA2 standard for implementing the transfer permission policy.

Swap OBJKTs

Here is the Python code for the method:

@sp.entry_point
def swap(self, params):
sp.verify((params.objkt_amount > 0) &
((params.royalties >= 0) & (params.royalties <= 250)))
self.fa2_transfer(self.data.objkt, sp.sender, sp.self_address,
params.objkt_id, params.objkt_amount)
self.data.swaps[self.data.counter] =
sp.record(
issuer=sp.sender,
objkt_amount=params.objkt_amount,
objkt_id=params.objkt_id,
xtz_per_objkt=params.xtz_per_objkt,
royalties=params.royalties,
creator=params.creator
)
self.data.counter += 1

The decorator marks the entry point.

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

  • — the royalties set by the artist at minting.
  • — the price in tez multiplied by 1000000.
  • — the number of editions.
  • — the OBJKT ID.
  • — the artist Tezos wallet address.

Note: is the address that called the current entry point.

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

  • must be greater than 0.
  • must be greater or equal than 0 and less or equal than 250.

The code calls , which uses to reference the transfer entry point of the NFT token smart contract. The command invokes the transfer entry point with the parameter values. The FA2 code for the 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 map.
  • Updates the map to transfer the editions from the artist’s wallet to the V2 marketplace contract, which acts as an escrow account.

The map is updated with the OBJKT data to track each swap. The counter value updates for the next swap record.

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 artist. These OBJKTs will no longer be visible in the artist’s wallet.

The V2 marketplace contract will hold onto these OBJKTs until transferred to a buyer or the artist 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.

Collecting

The GUI for collecting is implemented in using the React JavaScript library.

The GUI code requests funds from a Tezos wallet and swaps the artwork in in the method.

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 minting.
  • Update the operator of the OBJKT. An operator is a Tezos address that performs token transfer operations on behalf of the owner of the OBJKT.
  • Swap by transferring the OBJKTs to the HEN escrow wallet.

Here is the Python code for the method:

@sp.entry_point
def collect(self, params):
sp.verify(
# verifies if tez amount is equal to price per objkt
(sp.amount == sp.utils.nat_to_mutez(sp.fst(
sp.ediv(self.data.swaps[params.swap_id].xtz_per_objkt,
sp.mutez(1)).open_some()))) &
(self.data.swaps[params.swap_id].objkt_amount != 0))

sp.if (self.data.swaps[params.swap_id].xtz_per_objkt !=
sp.tez(0)):

self.amount = sp.fst(sp.ediv(
self.data.swaps[params.swap_id].xtz_per_objkt,
sp.mutez(1)).open_some())

# calculate fees and royalties
self.fee = self.amount *
(self.data.swaps[params.swap_id].royalties +
self.data.fee) / 1000
self.royalties = self.data.swaps[params.swap_id].royalties *
self.fee /
(self.data.swaps[params.swap_id].royalties +
self.data.fee)

# send royalties to NFT creator
sp.send(self.data.swaps[params.swap_id].creator,
sp.utils.nat_to_mutez(self.royalties))

# send management fees
sp.send(self.data.manager,
sp.utils.nat_to_mutez(abs(self.fee - self.royalties)))

# send value to issuer
sp.send(self.data.swaps[params.swap_id].issuer,
sp.amount - sp.utils.nat_to_mutez(self.fee))

self.data.swaps[params.swap_id].objkt_amount =
sp.as_nat(self.data.swaps[params.swap_id].objkt_amount - 1)

self.fa2_transfer(self.data.objkt, sp.self_address, sp.sender,
self.data.swaps[params.swap_id].objkt_id, 1)

The decorator marks the entry point.

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

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

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

  • The price for collecting the OBJKT is the same as the record in the map.
  • There must be OBJKTs available to collect in the map.

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

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

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

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

The code calls , which uses to reference the transfer entry point of the NFT token smart contract. The command uses the transfer entry point with the parameter values. The FA2 code for the 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 map.
  • Updates the ledger map to transfer 1 edition from the V2 marketplace contract wallet to the collector’s account.

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

V1 Exploit

Before moving on, let’s examine the V1 contract exploit, its mitigation, and the fix with the V2 version of the smart contract.

The exploit relied on a bug in the V1 entry point logic that used the math function to determine the absolute value. The code used to calculate the difference between the number of editions swapped and collected editions. However, if the number of collected editions is larger than the number of editions swapped, a calculation error would occur since the function would return a positive value and allow the transaction to complete. For example, consider a swap with 5 editions ( = 5). Make a call to collect and pass = 10. The function would cause the swap to be updated to 5 while the collector is transferred 10 editions, effectively stealing 10 editions.

Some possible solutions could be to remove the unnecessary function or to add verification at the beginning of the collect entry point that >= the swap . But these kinds of solutions would require the deployment of a new contract, which would take time.

Instead, mitigation disabled collects by making the entry point fail. The V1 smart contract manager changed from the hicetnunc2000lab account to a new smart contract account (KT1D4L7JewyDeA21wDzfWJgRmw948bLaKymb) which makes the managers behavior programmable. The new manager account is unique in that its code is hardcoded, always to fail. The default entry point for the account would typically accept any incoming fund transfers. The code overrides this behavior and uses the Michelson instruction to explicitly abort the current program with the error message: .

The result was that the logic in the entry point to send management fees to the contract manager account would fail. The entire transaction would fail since Michelson ensures that all transactions succeed or failure is guaranteed. If any transaction fails, then the whole sequence fails, and all the effects up to the failure are reverted.

The mitigation blocked the exploit and gave the HEN developers time to design a new V2 smart contract.

The V2 smart contract allows 1 collected edition at a time. The entry point has hard-coded logic to reduce the swap by 1 for each call. This change in design eliminated the exploit.

Conclusion

This concludes the second part of exploring the HEN smart contracts. In the third part, I will go over the rest of the NFT features:

  • Canceling
  • Burning
  • Reselling

I will also cover a recent feature to allow users to edit their profiles on HEN.

If you stumbled on this story, start reading the 3 part series at part 1. You can follow my 3D art on HEN.