Exchange monsters or How to implement transfers of NFTs between contracts

in #eos6 years ago (edited)

Market for Monsters

In the last monstereos post, I wrote about making monsters of the tamagochi game monstereos.io non-fungible tokens (NFTs) that can be exchanged between users with a simple method call. Now, let's bring in some value. The NFT should only be transferred if a certain amount of tokens were transferred to the owner and thereby create a market for monsters.

imageedit_23_3254665918.gif

To setup 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 claimpet action 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 send 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.cpp and 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.

Conclusion

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.