Skip to content

Making an ERC721 contract

In this tutorial we are going to implement a subset of the functionality of an ERC721 contract in Etch. We will be using UInt256 for identifiers and SHA256 to generate the identifiers of the initial tokens.

We will need two records: an Address record stating which tokens an address holds and a token record keeping track of the owner of the token.

For the first of these tasks, we will use the State object in Etch, and for the second we will use ShardedState.

Initialisation function

Assuming that we have defined an owner and a total_supply, the initialise function will do three things:

  1. It generates a list of token ids.
  2. It creates a record of each token owner.
  3. It creates a record of the tokens that an owner has.

The dual relationship is there to make lookups efficient, but comes at the price of twice the book keeping.

As the first step we create the list of token ids:

  // Genereating tokens
  var token_id = UInt256("hello world");
  for(i in 0:tokens.count()-1)
    var hasher = SHA256();
    hasher.update(token_id);
    token_id = hasher.final();
    tokens[i] = token_id;
  endfor

Next we assign an owner:

  // Assigning owner
  var owner_state = ShardedState< Address >("tokens.owner");
  for(i in 0:tokens.count()-1)
    var tid = tokens[i];
    owner_state.set(toString(tid), owner);
  endfor
and finally, we store the list of tokens on the creators address:
  var objects_state = State< Array< UInt256 > >(owner);

  // Storing the tokens on the owners address
  objects_state.set(tokens);

Queries

In this section we will focus on the two functions which are needed to implement a wallet overview and token details view, namely balanceOf and ownerOf. Both of these functions are short and easy to implement. We first make it possible to query the balance:

@query
function balanceOf(owner: Address) : UInt256
  var objects_state = State< Array< UInt256 > >(owner);
  var tokens = objects_state.get( Array< UInt256 >(0) );
  var ret = UInt256( toUInt64(tokens.count()) );

  return ret;
endfunction
Next, we make it possible to query the token owner. This is also a straight forward function to implement:
@query
function ownerOf(token_id: UInt256) : Address
  var owner_state = ShardedState< Address >("tokens.owner");
  return owner_state.get(toString(token_id)); 
endfunction
With these two query functions, it is possible to implement a ERC-721 wallet on top of the smart ledger. It is possible to make several optimisations for these functions. For instance, by storing number of tokens separately, there would be no need to deserialize the full array.

Actions

The standard ERC-721 contract has a number of different functions to transfer funds from one party to another. We will only implement one of these as they are all essentially variations of the same mechanism with more or less error checking built into them.

We implement a single transfer function here:

function transferFrom(from: Address, to: Address, token_id: UInt256)
  if(!from.signedTx()) 
    panic("Invalid signature from owner.");
  endif

  var owner_state = ShardedState< Address >("tokens.owner");
  var owner = owner_state.get(toString(token_id));

  if(owner != from)
    panic("Owner does not actually own the token");
  endif

  var from_state   = State< Array< UInt256 > >(from);
  var from_objects = from_state.get( Array< UInt256 >(0) );
  var found = false;
  var position : Int32;


  for(i in 0:from_objects.count()-1)
    var tid = from_objects[i];
    if(tid == token_id)
      if(found)
        panic("Contract broken - token is only supposed be represented once.");
      endif

      found = true;
      position = i;
      break;
    endif
  endfor

  if(!found)
    panic("Contract is fundamentally broken - owner has not been updated correctly");
  endif

  from_objects[position] = from_objects[from_objects.count() - 1];
  from_objects.popBack();

  var to_state   = State< Array< UInt256 > >(to);
  var to_objects = to_state.get( Array< UInt256 >(0) );
  to_objects.append(token_id);

  // updating sender
  from_state.set(from_objects);

  // Updating receiver
  to_state.set(to_objects);

  // Updating owner
  owner = to;
  owner_state.set(toString(token_id), owner);
endfunction
The above implementation only requires sender to sign the transaction, but could easily be extended to also requiring the receiver to sign.

The full contract can be found here.