(image)

Programming Hotmoka
A tutorial on Hotmoka and smart contracts in Takamaka

6.2 Non-fungible tokens (ERC721)

A non-fungible token is implemented as a ledger that maps each token identifier to its owner. Ethereum provides the ERC721 specification [8] for non-fungible tokens. There, a token identifier is an array of bytes. Takamaka uses, more generically, a BigInteger. Note that a BigInteger can be constructed from an array of bytes by using the constructor of class BigInteger that receives an array of bytes. In the ERC721 specification, token owners are contracts, although the implementation will check that only contracts implementing the IERC721Receiver interface are added to an IERC721 ledger, or externally owned accounts.

The reason for allowing externally owned accounts is probably a simplification, since Ethereum users own externally owned accounts and it is simpler to use such accounts directly inside an ERC721 ledger, instead of creating contracts of type IERC721Receiver. In any case, no other kind of contracts is allowed in standard ERC721 implementations.

The hierarchy of the Takamaka classes for the ERC721 standard is shown in Fig. 6.2.

(-tikz- diagram)

Figure 6.2: The hierarchy of the ERC721 token implementations.

As in the case of the ERC20 tokens, the interface IERC721View contains the read-only operations that implement a ledger of non-fungible tokens: the ownerOf() method yields the owner of a given token and the balanceOf() method returns the number of tokens held by a given account. The snapshot() method yields a frozen, read-only view of the latest state of the ledger.

The IERC721 subinterface adds methods for token transfers. Please refer to their description given by the Ethereum standard. We just say here that the transferFrom() method moves a given token from its previous owner to a new owner. The caller of this method can be the owner of the token, but it can also be another contract, called operator, as long as the latter has been previously approved by the token owner, by using the approve() method. It is also possible to approve an operator for all one’s tokens (or remove such approval), through the setApprovalForAll() method. The getApproved() method tells who is the operator approved for a given token (if any) and the isApprovedForAll() method tells if a given operator has been approved to transfer all tokens of a given owner. The view() method yields a read-only view of the ledger, that reflects all future changes to the ledger.

The implementation ERC721 provides standard implementations for all the methods of the interfaces IERC721View and IERC721, adding metadata information about the name and the symbol of the token and protected methods for minting and burning new tokens. These are meant to be called in subclasses, such as ERC721Burnable. Namely, the latter adds a burn() method that allows the owner of a token (or its approved operator) to burn the token.

As we have already said in Sec. 6.1.2, the owners of the tokens are declared as contracts in the IERC721View and IERC721 interfaces, but the ERC721 implementation actually requires them to be IERC721Receiver‘s or externally owned accounts. Otherwise, the methods of ERC721 will throw an exception. Moreover, token owners that implement the IERC721Receiver interface get their onReceive() method called whenever new tokens are transferred to them.

The ERC721 standard requires onReceive() to return a special message, in order to prove that the contract actually executed that method. This is a very technical necessity of Solidity, whose first versions allowed one to call non-existent methods without getting an error. It is a sort of security measure, since Solidity has no instanceof operator and cannot check in any reliable way that the token owners are actually instances of the interface IERC721Receiver. The implementation in Solidity uses the ERC165 standard [24] for interface detection, but that standard is not a reliable replacement of instanceof, since a contract can always pretend to belong to any contract type. Takamaka is Java and can use the instanceof operator, that works correctly. As a consequence, the onReceive() method in Takamaka needn’t return any value.

6.2.1 Implementing our own ERC721 token

(See the io-hotmoka-tutorial-examples-erc721 project in https://github.com/Hotmoka/hotmoka)

Let us define a ledger for non-fungible tokens that only allows its creator the mint or burn tokens. We will call it CryptoShark. As Fig. 6.2 shows, we plug it below the ERC721 implementation, so that we inherit that implementation and do not need to reimplement the methods of the IERC721 interface. The code is almost identical to that for the CryptoBuddy token defined in Sec. 6.1.1.

Create in Eclipse a new Maven Java 21 (or later) project. Use for this project the name io-hotmoka-tutorial-examples-erc721. You can do this by duplicating the previous project io-hotmoka-tutorial-examples-family. Use the following pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">


  <modelVersion>4.0.0</modelVersion>
  <groupId>io.hotmoka</groupId>
  <artifactId>io-hotmoka-tutorial-examples-erc721</artifactId>
  <version>|\hotmokaVersion{}|</version>


  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>21</maven.compiler.release>
  </properties>


  <dependencies>
    <dependency>
      <groupId>io.hotmoka</groupId>
      <artifactId>io-takamaka-code</artifactId>
      <version>|\takamakaVersion{}|</version>
    </dependency>
  </dependencies>


  <build>
    <plugins>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
         <version>3.11.0</version>
      </plugin>
    </plugins>
  </build>


</project>

and the following module-info.java:

module erc721 {
    requires io.takamaka.code;
}

Create package io.hotmoka.tutorial.examples.erc721 inside src/main/java and add the following CryptoShark.java source inside that package:

package io.hotmoka.tutorial.examples.erc721;


import static io.takamaka.code.lang.Takamaka.require;


import java.math.BigInteger;


import io.takamaka.code.lang.Contract;
import io.takamaka.code.lang.FromContract;
import io.takamaka.code.tokens.ERC721;


public class CryptoShark extends ERC721 {
    private final Contract owner;


    public @FromContract CryptoShark() {
        super("CryptoShark", "SHK");
        owner = caller();
    }


    public @FromContract void mint(Contract account, BigInteger tokenId) {
        require(caller() == owner, "Lack of permission");
        _mint(account, tokenId);
    }


    public @FromContract void burn(BigInteger tokenId) {
        require(caller() == owner, "Lack of permission");
        _burn(tokenId);
    }
}

The constructor of CryptoShark takes note of the creator of the token. That creator is the only that is allowed to mint or burn tokens, as you can see in methods mint() and burn().

You can compile the file:

cd io-takamaka-code-examples-erc721
mvn clean install
cd ..

Then you can install that jar in the node and create an instance of the token exactly as we did for the CryptoBuddy ERC20 token in Sec. 6.1.1.