Building a non-fungible token sales smart contract

CryptoArte.io Painting #8118

Selling non-fungible tokens and the ERC721 standard

If you’re working on a Ethereum NFT (ERC721) project, you may have noticed the standard describes mechanisms for transferring tokens through functions such as safeTransferFrom() and, while some of the functions are payable, the standard leaves out any details on how to implement the mechanism to actually sell tokens in exchange for ether or other tokens. While this is obviously intentional and well thought out, you may still have to come up with a way to sell the tokens.

Non-fungible token marketplaces

One way to do this is to leverage existing marketplaces, such as OpenSea. This is truly a great option because:

  • you won’t have to code and maintain your own sales contract and sales functionality built-in your own dApp,
  • you get built-in liquidity from their community, and
  • you still get tons of flexibility through their SDK and APIs.

In some cases, however, you may want to have more control and enable sophisticated user experiences or pricing strategies that require you to code your own smart contract. That was the case at CryptoArte: while we integrate with OpenSea and regularly sell and auction paintings through them, we also want to provide a streamlined user experience when a buyer is already looking at a painting on our dApp, and have the flexibility to easily experiment with custom pricing strategies at any time.

A simple NFT sales contract model

So we went on to building our own sales contract and its corresponding functionality in our dApp. While it’ll likely evolve over time, our first implementation of the CryptoArte.io sales smart contract code is published and verified here. This article breaks it apart so you can use it, at you own risk, as inspiration to build your own. Here we go:

import “openzeppelin-solidity/contracts/token/ERC721/ERC721.sol”;
import ‘openzeppelin-solidity/contracts/ownership/Ownable.sol’;
import ‘openzeppelin-solidity/contracts/lifecycle/Pausable.sol’;
import ‘openzeppelin-solidity/contracts/lifecycle/Destructible.sol’;
/**
 * @title CryptoArteSales
 * CryptoArteSales — a sales contract for CryptoArte non-fungible tokens 
 * corresponding to paintings from the www.cryptoarte.io collection
 */
contract CryptoArteSales is Ownable, Pausable, Destructible {

We start by leveraging a few OpenZeppelin ownership, lifecycle and token contracts:

  • Ownable: this sets the contract owner to the address deploying the contract, and gives you a way to later transfer said ownership to a different address shall you chose too. It also gives you an onlyOwner() modifier you can use to ensure functions can only be executed by the contract’s owner (we’ll use this in a little bit).
  • Destructible: this lifecycle construct gives you a way to destruct the contract and send any remaining funds to the owner (or to an address of the owner’s choice). This can be useful both in times of panic and when replacing or upgrading the entire contract. Plus, it sounds cool and very mission-impossible-esque.
  • Pausable: this lifecycle construct gives you a way to pause() and unpause() operations, which can be useful for maintenance and other purposes. Note that only functions that use the whenNotPaused() or whenPaused() modifiers are affected.

Next, we define a couple of Events:

event Sent(address indexed payee, uint256 amount, uint256 balance);
event Received(address indexed payer, uint tokenId, uint256 amount, uint256 balance);

Our smart contract will emit the:

  • Received event when it receives ether in exchange for one of the non-fungible tokens (e.g.: someone purchased a painting), and the
  • Sent event when funds are withdrawn from the contract’s ether balance (i.e.: artist gotta eat too).

Following up, we define a couple public variables:

ERC721 public nftAddress;
uint256 public currentPrice;

We’ll use the nftAddress variable to hold the address of the non-fungible token contract, and the currentPrice variable to hold the price at which we want to sell the non-fungible tokens (paintings, in our case). Now that we’ve covered this, understanding the constructor should be pretty straight forward:

constructor(address _nftAddress, uint256 _currentPrice) public {
 require(_nftAddress != address(0) && _nftAddress != address(this));
 require(_currentPrice > 0);
 nftAddress = ERC721(_nftAddress);
 currentPrice = _currentPrice;
 }

The constructor sets the initial nftAddress and currentPrice values, and also performs some basic validations.

Now comes the fun part, the actual sales of a non-fungible token:

/**
 * @dev Purchase _tokenId
 * @param _tokenId uint256 token ID (painting number)
 */
function purchaseToken(uint256 _tokenId) public payable whenNotPaused {
 require(msg.sender != address(0) && msg.sender != address(this));
 require(msg.value >= currentPrice);
 require(nftAddress.exists(_tokenId));
 address tokenSeller = nftAddress.ownerOf(_tokenId);
 nftAddress.safeTransferFrom(tokenSeller, msg.sender, _tokenId);
 emit Received(msg.sender, _tokenId, msg.value, address(this).balance);
 }

The purchaseToken function takes only one argument: the _tokenId of the non-fungible token buyer would like to acquire. Note that we also use the payable keyword here which allows the method to receive ether from the message sender (buyer). Also note that here we use the whenNotPaused modifier to ensure the contract hasn’t been paused when performing this action. The function first performs a few validations:

  • It checks that the sender is not the zero address or the sales contract itself,
  • It checks that the sender sent at least the current price (yet lets buyer overpay if he/she/it chooses too),
  • It checks that a token with _tokenId actually exists by calling the exists method implemented by the non-fungible token smart contract, as per ERC721 standard). Note: this is probably not needed and more of a defensive programming approach/choice— the following transfer mechanism will likely check this already.

If those validations pass, the function then gets the current owner of the token by calling the ownerOf method (again, part of ERC721 standard) on the non-fungible token smart contract, and then calls the safeTransferFrom (also implemented on the non-fungible token smart contract, as per ERC721) method to actually send the token to its buyer in exchange for the ether received. Lastly, it emits the Received event as per above.

Tip: always be careful when receiving ether as you can have reentrancy attacks. In this case, we are transferring a unique (non-fungible) token back, so we’re probably safe, but this pattern can be problematic in other cases.

Note that the safeTransferFrom method itself will perform many other validations that my cause the entire execution of this method to revert. One of these validations has to do with whether the seller (in our case, the owner of the token) has authorized the sales contract to sell the token on their behalf.

Following the ERC721 standard, you can achieve this by either calling approve (to give permission to one specific tokens, or a given set of tokens if called multiple times) or setApprovalForAll (to give permission to all tokens owned by owner) on the non-fungible token smart contract (after deploying the sales contract that is — you’ll have to know its address).

As you may have noticed, the funds being received here will stay with the contract, augmenting it’s balance over time. So that you can actually use the proceeds, you’ll need a mechanism to withdraw funds from the contract’s balance. Enter sendTo:

/**
 * @dev send / withdraw _amount to _payee
 */
 function sendTo(address _payee, uint256 _amount) public onlyOwner {

 require(_payee != address(0) && _payee != address(this));
 require(_amount > 0 && _amount <= address(this).balance);
 _payee.transfer(_amount);
 emit Sent(_payee, _amount, address(this).balance);
 }

This method is fairly straight forward: it validates a few things, and then transfers the require _amount to _payee, then emits the Sent event as per above. Note here we use the onlyOwner modifier to ensure only the current contract’s owner can withdraw funds.

Lastly, we also have a way to update the currentPrice variable:

/**
 * @dev Updates _currentPrice
 * @dev Throws if _currentPrice is zero
 */
function setCurrentPrice(uint256 _currentPrice) public onlyOwner {
require(_currentPrice > 0);
 currentPrice = _currentPrice;
 }

This method validates the newly requested price is greater than zero, and then updates the currentPrice variable that will be used in the next purchaseToken invocation. Note the use of the onlyOnwer modifier again for obvious reasons.

Tip: One non-trivial aspect to notice here is that you could experience a race condition between setCurrentPrice and purchaseToken methods. If you want to be absolutely sure no one can purchase a token while you’re updating prices, simply pause the contract first, then set the current price, and then unpause the contract again, waiting for each transaction to be successfully executed and confirmed before executing the next one.

Why multiple contracts?

While you could build this within your NFT contract ifself (and have it all under one single contract), I personally like the separation of concerns and flexibility (easier updates) you get from having the sales functionality in a separate contract. You could even have multiple sales contracts implementing different models for different parties.

Parting thoughts

This is obviously a very simple contract and model where all tokens are offered at the same price at any given time. From here, you could experiment with other models and ideas, for example:

  • adding an auction mechanism (start price, end price, timeframe, etc)
  • adding affiliate payouts (so a third-party can be paid a commission for helping you sell)
  • automatically adjusting prices (based on supply, for example)
  • selling tokens in bundles or sets
  • offering discounts to repeat buyers
  • and many more!

Please USE AT YOUR OWN RISK (and kindly let us know if you find any issues).

If you enjoyed this content, please consider clapping, purchasing a CryptoArte painting, or both :)