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://teia.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 hicetnunc/src/pages/objkt-display/tabs/swap.js
using the React JavaScript library.
The GUI code to request funds from a Tezos wallet and swap the artwork is in src/context/HicetnuncContext.js
in the swapv2
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 Marketplace
class, which implements all the entry points of the V2 smart 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 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:
objkt
: A contract storage field for the address of the NFT token contract.metadata
: The smart contract metadata. This field points to a JSON metadata file hosted on IPFS.manager
: 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.swap
s: A contract storage field that tracks the swaps as a big map. The big maps store large amounts of data in a map.counter
: A contract storage field is used as the ID of the next swap record for the swaps map.fee
: The platform fee.
Update operator
The update_operators
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 FA2_core
class that implements the update_operators
entry point manages an operators map in the contract storage using the combination of owner
, operator
, and objkt_id
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 swap
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 @sp.entry_point
decorator marks the entry point.
The following parameter values are passed to the swap function by the HEN GUI code:
royalties
— the royalties set by the artist at minting.xtz_per_objkt
— the price in tez multiplied by 1000000.objkt_amount
— the number of editions.objkt_id
— the OBJKT ID.creator
— the artist Tezos wallet address.
Note: sp.sender
is the address that called the current entry point.
The code uses the sp.verify
command to prevent the entry point from proceeding with the following conditions:
objkt_amount
must be greater than 0.royalties
must be greater or equal than 0 and less or equal than 250.
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
operators
map. - Updates the
ledger
map to transfer theobjkt_amount
editions from the artist’s wallet to the V2 marketplace contract, which acts as an escrow account.
The swaps
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 hicetnunc/src/pages/objkt-display/tabs/collectors.js
using the React JavaScript library.
The GUI code requests funds from a Tezos wallet and swaps the artwork in src/context/HicetnuncContext.js
in the collectv2
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 collect
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 @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 V2 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:
- 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 2.5% 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 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 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:
- Confirms that the calling contract is an operator for the OBJKT by querying the
operators
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 collect
entry point logic that used the math abs
function to determine the absolute value. The code used abs
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 abs
function would return a positive value and allow the transaction to complete. For example, consider a swap with 5 editions (objkt_amount
= 5). Make a call to collect and pass params.objkt_amount
= 10. The abs
function would cause the swap objkt_amount
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 abs
function or to add verification at the beginning of the collect entry point that params.objkt_amount
>= the swap objkt_amount
. But these kinds of solutions would require the deployment of a new contract, which would take time.
Instead, mitigation disabled collects by making the collect
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 FAILWITH
Michelson instruction to explicitly abort the current program with the error message: “WrongCondition: sp.sender == self.data.manager”
.
The result was that the logic in the collect
entry point to send management fees to the contract manager account would fail. The entire collect
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 collect
entry point has hard-coded logic to reduce the swap objkt_amount
by 1 for each collect
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.