Move vs Solidity — A game of NFTango

Overmind
Overmind_xyz
Published in
5 min readJun 7, 2023

The best way to learn a new language is to translate it from a familiar one. Following the close of the popular NFTango quest, we translated it into Solidity.

In order to replicate not only use case, a NFT betting platform, but also internal execution of the functions, what was 1 Move contract turned into 3 Solidity contracts. While this does not necessarily indicate that Move is more efficient than Solidity, it starkly contrasts the philosophies by which these languages approach storage on the blockchain and treatment of data. This article shows code snippets of prominent comparative points but you can find the full code, in Move and Solidity, here.

Contracts Structure

NFToken.sol simply calls the ERC721 implementation from OpenZeppelin. This serves as the NFTs that will be minted and used for testing our NFTango platform. This is not required in Move because Move treats NFTs as an object, a resource in Move terminology, which can be created in a line of code and the minting and transferring is controlled by capabilities and defined module functions. Solidity is contract orientated and to simulate NFT betting, we would have needed to use an already deployed contract or created our own NFT contract which is far preferable since we have control over the contract. Move is resource orientated and the token.move module is treated as an object with certain functionalities instead of a standard to implement in a contract. This difference represents a mentality shift needed when learning Move from a Soldity background.

Solidity is contract centric. Move is resource centric and focused on the ownership and secure MOVEment of these resources.

The NFTango.sol contract defines the structure of the NFTangoStore object along with some assertions and getter / setter functions. The NFTangoManager contract is responsible for creation and game logic. In essence, The NFTango.sol contract encapsulates a single object and exposes ways to manipulate the object state while the NFTangoManager is the control center. Think of the NFTango contract as a Move resource and the Manager contract as a Move resource account plus module.

Resources and Resource Accounts

Technically, we could have moved the NFTangoStore object directly into the NFTangoManager.sol contract and then we could forego the whole NFTango.sol contract all together. In fact, the Move contract has it all together, so why did we separate it in Solidity?

Taking a look at the init_game function in the Move contract, we see that in order to start a game, three things happen:

  1. We create a resource account by hashing a random seed phrase and the address of the creator (signer) of the betting game.
  2. The creator of the game pays an NFT to the function.
  3. The function generates a NFTangoStore resource from the NFT token data, moves the NFT into the resource account and moves the NFTangoStore resource under the game creator’s address.

When a second player comes along, they will join the game by looking for the NFTangoStore resource under the game creator’s address. The NFTangoStore resource manages the game state per individual creator and the resource account handles the NFT betting. This is a very common framework set up in Move — user addresses store their respective states and resource accounts handle interactions.

In Solidity’s contract centric world, it is possible to generate contracts programmatically as well. These types of contracts are called Factories or Managers. Hence, we have an NFTangoManager contract to generate NFTango games every time init_game is called. Still, we could have maintained a mapping of addresses to NFTangoStore objects directly in the Manager contract, so why encapsulate the NFTangoStore object in a NFTango contract? In a nutshell, we wanted to emulate a Move resource account, even imperfectly.

In Move, a resource is defined in a module. This module will contain the fields of the struct, the abilities the struct has, and the functions related to the struct. All of this together forms a resource. A resource account, on the other hand, is able to hold, manage and transfer these resources. Notice that the NFT data was stored inside the NFTangoStore object but physically moved to the resource account. In the Manager, we maintain a mapping of game creator addresses to the NFTango.sol objects because Solidity does not store things individually in terms of x resource under y address but stores an overall state under the Managing contract.

Secondly, if we tried to have NFTango.sol control the NFT transfers, we would need a function in NFTangoManager.sol to have the user pay the NFTango.sol contract. The players of this game is still approving the Manager contract to have control of their NFTs instead of the NFTango.sol contract. Likewise, when it is time to pay the winning player, the NFTango contract has to approve the Manager contract to make the transfer happen. In conclusion, the power ultimately remains with the Manager contract. Move resource accounts are able to handle the NFTs much more independently because we can store the account’s Signer Capability as a value inside the NFTangoStore and fire off a transfer in the name of the resource account much more easily. We will deep dive in Part 2 — Ownership and Capabilities

Global Storage

The above operational difference also alludes to the difference by which Move and Solidity accesses their global data. Without in going into details about Merkle Trees and Root Hashes, Solidity stores everything via contract addresses. The NFTangoManager stores all the existing NFTango contracts which in turn stores individual game state. In Move, the NFTangoStore is created and immediately moved under game creator addresses to track individual game state. The centralized vs decentralized approach to storage means access to global variables means a host of new functions in Move to access global data.

A good illustration of the different storage philosophies lies in the assert_nftango_store_exists function.

In the nftango.move file

assert!(exists<NFTangoStore>(account_address), error::invalid_state(ERROR_NFTANGO_STORE_DOES_NOT_EXIST));

In NFTangoManager.sol

require(address(Games[creator]) != address(0), CODE_1);

As shown, the NFTangoStore resource is stored in Global Storage and we can directly check its existence via an “exists” call and inputting a specific user’s address. In Solidity, it is not that easy to check if a NFTango contract exists and is tracked via a centralized mapping in the NFTangoManager contract. In fact, Solidity has a tendency to initialize everything as zeros instead of allowing a null state to exist.

Lastly, an additional perk found in Move when throwing errors, Move has an error library and can wrap error codes to sort them into error categories much like how HTTP errors are sorted by the hundreds digit — 400s for client errors, 500s for server errors.

In conclusion…

Solidity revolves around contracts. Move packages things into resources and moves them to different user or resource accounts. Solidity centralizes asset transfers and interactions into the declaring contract whereas asset interactions in Move is handled by type abilities, global storage and resource accounts.

--

--

Overmind
Overmind_xyz

The first web3 solve-to-earn platform where developers compete on coding puzzles to earn prizes and on-chain credentials. Live on #Aptos.