MonsterEOS and the value, transfer, and implementation of non-fungible tokens (NFTs)

MonsterEOS
MonsterEOS
Published in
9 min readSep 20, 2018

The Game

MonsterEOS was named the first game implemented on the EOS blockchain. It began as a Tamagotchi styled game where the user has to care for their monster. There are no creation fees for new monsters. The user has just to pay for the storage (< 400 bytes). Have a go, and try it out!

With your monsters, you can now also play in a battle arena. You meet with another user, everyone chooses one of the monsters to play and then you start attacking each other. Depending on the properties of the monster you have a different choice of attack methods. If you know the properties of the other monsters you have higher chances to win the game. There are 109 different types of monsters and each monster becomes unique by its name and its id. The monster can there be identified globally as monstereosio:pets:2558 (first the contract account, then the place of storage/table name, then the id).

Non-fungible tokens

With the global identifier, monsters become non-fungible tokens. On the Ethereum blockchain, the properties of NFTs are defined in EIP-721 and the standard requires the following methods:

  • balanceOf(owner)
  • ownerOf(tokenId)
  • *transferFrom(from, to, tokenId)
  • .. and more methods about approved addresses

ownerOf

Non-fungible tokens (NFTs) have only one owner and you can find the owner by using e.g.

cleos get table monstereosio monstereosio  pets -L 2558 -l 1

This means one row (-l 1) starting from primary key with id 2558 (-L 2558) is queried.

Hence, the owner can be found by querying the table and looking for property owner in the result of get table.

tokenOfOwner

On the other hand, the NFTs of a owner can be found with

cleos get table monstereosio monstereosio  pets -L owner  -U ownerX --key-type i64 --index  2

This call assumes that the second index of the table pets is the index by owner. For the upper limit (-U) a name should be used that follows ownerand preceeds the next user. As names are limited to 12 characters appending one letter to owner like ownerX is a good choice to just receive the NFTs of the owner.

transfer

When transferring a monster between users without payment it does not need more than an update of the table row in the transfernft(owner, newowner, tokenId) action.

pets.modify(itr_pet, 0, [&](auto &r) {
r.owner = newowner;
});

The code is taken from github.com/leordev/monstereos

The Ricardian contract for that action could contain a part like the following:

### Intent
The intent of the `{{ transfernft }}` action is to transfer ownership of the given digital asset to {{ newowner }}. Consequently move the cost for RAM of storing to the asset to {{ newowner }}. {{actor}} confirms that {{newowner}} has agreed to pay for these costs and adhere to the contracts associated with the digital asset.

### Term
This Contract expires after the code was executed.

The text was adapted from github.com/friedger/monstereos

Implementation

Non-fungible tokens can be implemented using multi_index tables with the following conventions:

  • Tokens are identified by contract:table_name:id
  • The token table has a column owner
  • The second index of the token table is by column owner
  • The contract has a method transfernft

For Monstereos, the Ricardian contract of the createpet action defines the rights and obligations of a monster owner. If and how this is applicable to the general concept of NFTs needs to be discussed.

Furthermore, NFTs should be transferable with a value associated to the token. This becomes more tricky on the EOS blockchain. The NFT should only be transferred if a certain amount of tokens were transferred to the owner and thereby create a market for monsters.

To set up the monster market a new contract was created (monstereosmt). It contains the actions for asking and bidding for monsters. Without going into the aspects of finding matching offers and bids let’s assume that an offer (created by the monster owner with the offerpet action) was accepted by a bidder and the owner updated the offer to include the name of the new owner, i.e. the bidder.

Simple Transfer

For the simple transfer discussed in the previous post (without value), the market contract needs to ask the game contract to update the ownership of the monster. This is because tables are world-readable but only writable by the owning contract. So, an action needs to be added to the game contract that the market contract can call. For MonsterEOS, the claimpetaction of the market can be called by the new owner. That action should then call the transferpet action in the game contract. This is the code:

action(permission_level{_self, N(active)}, N(monstereosio), N(transferpet), std::make_tuple(pet.id, newowner)).send();

The action is sent in the name of the contract, hence the permission level with self. The action to perform is transferpet of contract monstereosio. Finally, the parameters are added. Sending an action from a contract needs the eosio.code permission for the market account. This is done once with cleos like this:

cleos set account permission monstereosmt active \
'{"threshold": 1,
"keys": [{
"key": "'${EOS_KEY}'",
"weight": 1
}],
"accounts": [{
"permission": {"actor": "monstereosmt",
"permission": "eosio.code"},
"weight": 1
}]}' owner -p monstereosmt

On the other side, in the game contract the transferpet method was added. However, it is important that the method is not added to the ABI, otherwise, every contract could call this method. And then in the method, the authorization of the market contract needs to be verified:

require_auth(N(monstereosmt));

Transfer with Value

Monsters that have been well fed and have played many battles are valuable and the new owner might be prepared to send some tokens in exchange for the monster. In order, to add value to a transfer the following steps are needed:

  1. Transfer to escrow: The bidder uses the token contract to transfer the agreed amount to the game escrow account (monstereosio in this case)
  2. Verify offer and transfer: The game contract validates the transferred amount, token symbol, and the identities of the owner and bidder with the offer.
  3. Change ownership: The game contract changes ownership.
  4. Transfer funds from escrow: The game contract transfers the tokens to the previous owner of the owner.
  5. Tidy up: The offer is deleted by the market contract which finalizes the transfer

For 1., it is just a call to the transfer action of the eosio.token contract, sending from the new owner to the game account. However, now it becomes interesting. What is needed to react on the transfer?

As described in EOS stackexchange, the market contract needs to adjust the apply method in the EOSIO_ABI macro. Currently, it is only possible to define your own EOSIO_ABI_EX accepting actions from the token contract by adding an other if-condition:

code == N(eosio.token)

This means all actions of the token contract that are implemented in the market contract are called in the market contract as well. In this case, it is only the transfer method.

[Protip: Use a base file like pet.cpp and two main files like petcode.cppand petabi.cpp to compile the wasm code using the EOSIO_ABI_EX and to generate the abi file using EOS_ABI (see github for details)]

In monstereosio, the transfer method was also used to update the account balance of the user, keeping up to date the amount the user transferred to the game. Now, the transfer method is extended with the handling of transferring ownership, bundled in method _handletransf.

In this method, the memo of the transfer is inspected and the contained offer id extracted. With the offer id the details are retrieved from the table of the market contract using the type _tb_offers defined in the headers file (and abi) of the market contract.

_tb_offers offers(N(monstereosmt), N(monstereosmt))

The second parameter is N(monstereosmt) as there is only one global table. Some contracts us one table for each user. The call should then be like this:

_tb_offers offers(N(monstereosmt), N(pet.owner))

After the offer was retrieved from the table, the amount, symbol, owner and offer type is validated. Once, the transfer can go ahead the token is transferred to the old owner of the monster, the ownership of the monster table is updated. For transferring the asset the game contract needs to have the permission to send actions. As described above, this is done with a single cleos call but this time for account monstereosio:

cleos set account permission monstereosio active \
'{"threshold": 1,
"keys": [{
"key": "'${EOS_KEY}'",
"weight": 1
}],
"accounts": [{
"permission": {"actor": "monstereosio",
"permission": "eosio.code"},
"weight": 1
}]}' owner -p monstereosio

Finally, the balance of the new owner account is decreased as it was increased at the beginning of the transfer of ownership. However, this is not necessarily needed for other use cases of NFTs.

Notes

Sending an action from one contract to another can be done with action(...).send() if the contract account has permission eosio.code. This works even if the action is not included in the ABI. The action is executed with the authority of the sending account.

Auto-feed

There are a few requests in the MonsterEOS channel, how to feed monsters while on vacation or away from the internet. Ideas mentioned were monster caring services or handing over private keys. Both is not recommended — you never know who is on the other side.

When buddy.works, a continuous integration service announced support for EOS it was clear that this could be the solution to the feeding problem. So, I created a new EOS account noctambulist and transferred around 1 EOS, enough to run the monster actions.

In the online service https://apps.buddy.works do the following:

  1. Create a new project
  2. Add a new pipeline, choose a name, select recurrent, choose every 12h
  3. Add a new action from Blockchain, choose Build EOS
  4. In the Run commands text area past the following lines, replacing the pet_id 3673with your monster id and the account noctambulist with your account name.
cleos wallet create
cleos wallet import --private-key ${PRIVAT_KEY}
cleos -u https://eu.eosdac.io push action monstereosio feedpet '{"pet_id": 3673}' -p noctambulist

Finally, go back to the pipeline page and add a new variable PRIVAT_KEYwith the private key of the account (use the active key, not owner key). Make sure that encryption is enabled! Note that during the execution the private key is never shown. However, while pasting the private key into the text field I felt reassured that only 1 EOS is held in the account.

Now, you just have to wait for 12h or hit Run pipeline button to see it work.

Warning

Many things can go wrong! There is no guarantee that the monster is fed. The buddy.works service could break down, the transaction could be lost due to network problems, etc. As a user, you are not notified. Furthermore, the warnings that are usually given about the monster already being fed, or not awake or so are not shown.

Having said that, the monster #3673, named Sleepy1 is still alive and happy at the time of writing.

Value of NFTs

This leads to the question of how to determine the value of NFTs. In some games it is scarcity, in others, it is time-based or money invested or all of it. For collectibles like NFTs, in most cases, the value is not rationally justified.

In MonsterEOS, there is only a time-based aspect of it as creation is free and no shops are available yet. With the introduction of auto-feeding, it becomes even more unclear, how to determine the value of monsters. If auto-feeding is against the rules is still to be discussed, and if so, how this can be detected as the chain does not distinguishes between actions from humans or bots.

Conclusion

It is easy to set up a feeding service if you are prepared to enter your private key into a website.

Written by: Friedger Muffke

Developer for MonsterEOS

Twitter: https://twitter.com/fmdroid

— — Editor: Jenny Calpu

--

--