StarkNet ERC721 Workshop: Exercise 2

Note: This article was updated on Sep 20th, 2022 to use Cairo 0.10

Creating Token Attributes

  • Call ex2a_get_animal_rank() to get assigned a random creature to create.
  • Read the expected characteristics of your animal from the Evaluator
  • Create the tools necessary to record animals characteristics in your contract and enable the evaluator contract to retrieve them trough get_animal_characteristics function on your contract (check this)
  • Deploy your new contract
  • Mint the animal with the desired characteristics and give it to the evaluator
  • Call submit_exercise() in the Evaluator to configure the contract you want evaluated
  • Call ex2b_test_declare_animal() to receive points (2 pts)

Looking for exercise 1? Click here.

Solution

We start as the exercise suggests by calling the function ex2a_get_animal_rank of the Evaluator (0x06d2…) using Voyager and Argent X (you can also use Braavos as an alternative).

Creating random characteristics for NFT
Creating random characteristics for NFT

To know which characteristics were randomly generated we have to call the view functions assigned_legs_number, assigned_sex_number and assigned_wings_number from the Evaluator (0x06d2…) providing the address of our test account on Argent X (0x0113…).

Randomly assigned number of wings for NFT
Randomly assigned number of wings for NFT

After checking those three functions we are informed that our soon to be created NFT should have assigned the number 2 for sex, 4 for wings and 8 for legs

Before we move on with the exercise, I’m going to use again the ERC721MintableBurnable.cairo from OpenZeppelin as the base for our ERC721 smart contract so I’m just going to copy the code over to a new file called ERC721_ex2.cairo inside the src folder.

At this point in time our project’s folder structure should look like this:

$ tree .
>>>
.
├── comp
│   └── ERC721_ex1.json
├── src
│   ├── ERC721_ex1.cairo
│   └── ERC721_ex2.cairo
└── utils.py

The exercise tells us that the Evaluator will check to see if our NFT has the desired characteristics by calling the function get_animal_characteristics on our ERC721 smart contract so we need to implement the function. The exercise also provides us a link to an interface file so we know the function signature.

@contract_interface
namespace IExerciseSolution {
    // Breeding function
    func is_breeder(account: felt) -> (is_approved: felt) {
    }
    func registration_price() -> (price: Uint256) {
    }
    func register_me_as_breeder() -> (is_added: felt) {
    }
    func declare_animal(sex: felt, legs: felt, wings: felt) -> (token_id: Uint256) {
    }
    func get_animal_characteristics(token_id: Uint256) -> (sex: felt, legs: felt, wings: felt) {
    }
    func token_of_owner_by_index(account: felt, index: felt) -> (token_id: Uint256) {
    }
    func declare_dead_animal(token_id: Uint256) {
    }
}

From the interface file we can see that the function get_animal_characteristics expects to be passed a token_id and will return the value for the three characteristics. The other interesting signature is declare_animal as it will allow us to create an animal with the desired characteristics in the first place.

In its current state, our ERC721 smart contract allows us to create a new NFT by directly exposing the function mint that expects the tokenId and the address of the owner (to) of the NFT to be passed as arguments.

src/ERC721_ex2.cairo

@external
func mint{
        pedersen_ptr: HashBuiltin*,
        syscall_ptr: felt*,
        range_check_ptr
    }(to: felt, tokenId: Uint256) {
    Ownable.assert_only_owner();
    ERC721._mint(to, tokenId);
    return ();
}

Because mint is not part of the ERC721 standard we can get rid of it and replace it with declare_animal where we can define the characteristics of our NFT. 

@external
func declare_animal{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
    sex : felt, legs : felt, wings : felt
) -> (token_id : Uint256) {
    alloc_locals;
    Ownable.assert_only_owner();
}

Our new function is only callable by the owner of the smart contract and instead of expecting the tokenId to be provided, it will keep track of the latest tokenId and auto increment its value when a new animal is minted. While we are at it, I’m going to move away from the camel case convention and use snake case instead for our variables as it’s the convention in Cairo.

from starkware.starknet.common.syscalls import get_caller_address
from starkware.cairo.common.uint256 import Uint256, uint256_add
...

@storage_var
func last_token_id() -> (token_id : Uint256) {
}
...

@external
func declare_animal{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
    sex : felt, legs : felt, wings : felt
) -> (token_id : Uint256) {
    alloc_locals;
    Ownable.assert_only_owner();

    // Increment token_id by 1
    let current_token_id : Uint256 = last_token_id.read();
    let one_as_uint256 = Uint256(1, 0);
    let (local new_token_id, _) = uint256_add(current_token_id, one_as_uint256);

    let (sender_address) = get_caller_address();

    // Mint NFT and update token_id
    ERC721._mint(sender_address, new_token_id);
    last_token_id.write(new_token_id);

    return (token_id=new_token_id);
}

Using the uint256 library we can define values similar to Solidity and perform simple arithmetic operations like add or subtract. To track the latest value of token_id we have created a new storage variable called last_token_id that we take care of updating every time a new NFT is minted. To make sure that our new storage variable is properly initialized, let’s add some additional functionality to the constructor.

...
@constructor
func constructor{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
    name : felt, symbol : felt, owner : felt
) {
    ERC721.initializer(name, symbol);
    Ownable.initializer(owner);
    token_id_initializer();
    return ();
}

...
func token_id_initializer{pedersen_ptr : HashBuiltin*, syscall_ptr : felt*, range_check_ptr}() {
    let zero_as_uint256 : Uint256 = Uint256(0, 0);
    last_token_id.write(zero_as_uint256);
    return ();
}

By default, the owner of a new animal NFT will be the owner of the smart contract acting as the caller. The only thing missing now is to care about storing the characteristics of our NFTs. For that purpose we can create an Animal data type using a struct.

...
struct Animal {
    sex : felt
    legs : felt
    wings : felt
}

@storage_var
func animals(token_id : Uint256) -> (animal : Animal) {
}
...

@external
func declare_animal{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
    sex : felt, legs : felt, wings : felt
) -> (token_id : Uint256) {
    alloc_locals;
    Ownable.assert_only_owner();

    // Increment token id by 1
    let current_token_id : Uint256 = last_token_id.read();
    let one_as_uint256 = Uint256(1, 0);
    let (local new_token_id, _) = uint256_add(current_token_id, one_as_uint256);

    let (sender_address) = get_caller_address();

    // Mint NFT and store characteristics on-chain
    ERC721._mint(sender_address, new_token_id);
    animals.write(new_token_id, Animal(sex=sex, legs=legs, wings=wings));

    // Update and return new token id
    last_token_id.write(new_token_id);
    return (token_id=new_token_id);
}

Our function declare_animal is now completed, we can switch our attention to developing the @view function get_animal_characteristics.

from starkware.cairo.common.uint256 import Uint256, uint256_add, uint256_check
...
@view
func get_animal_characteristics{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
    token_id : Uint256
) -> (sex : felt, legs : felt, wings : felt) {
    with_attr error_message("ERC721: token_id is not a valid Uint256") {
        uint256_check(token_id);
    }
    let animal = animals.read(token_id);
    let animal_ptr = castt(&animal, Animal*);
    return (sex=animal_ptr.sex, legs=animal_ptr.legs, wings=animal_ptr.wings);
}

Note: Weirdly, the code editor of my blog breaks when using the word cast so I’m using the misspelled version castt to prevent my blog from crashing.

At the very beginning of our function we validate that the provided token_id is in fact a valid uint256 number as it’s always a good practice to sanitize our inputs to prevent from unexpected hacks. After that the function is straightforward except for the highlighted line. The value that comes out of the animals storage variable is simply a memory address, not a pointer. To turn that memory address into a pointer we have to use the & operator. By default the compiler wouldn’t know what type of pointer this is so to inform it of the type we have to use the function cast and provide the type as the second argument (Animal*).

Our new ERC721 smart contract is completed so we are ready to compile it and deploy it to Goerli.

$ starknet-compile src/ERC721_ex2.cairo --output comp/ERC721_ex2.json
$ starknet deploy --contract comp/ERC721_ex2.json \
  --inputs 71942470984044 4279881 4862… --network alpha-goerli --no_wallet
>>>
Deploy transaction was sent.
Contract address: 0x0581c5f6b97da0132759a6502156624d4b893c75eaca11e333dde1a4383f608d
Transaction hash: 0x211ca20951f29e1ff32fefd7a2aeab1969f4e01aa19afcbbfb46236c2cf84d0

To mint the NFT we can interact with our smart contract on Voyager (0x0581…) and call the function declare_animal passing the values requested by the Evaluator for our animal.

Minting an animal NFT
Minting an animal NFT

Once the transaction is completed we can verify that our NFT was created by calling the view function get_animal_characteristics with the token_id 1.

Reading the characteristics of an NFT
Reading the characteristics of an NFT

Note: There’s a step missing. After the NFT has been minted you have to transfer it to the Evaluator smart contract invoking the function transferFrom using your Argent X wallet address as the from field, otherwise the submit_exercise function will fail.

In theory, everything is working fine so we can move to the next step and submit our ERC721 smart contract to the Evaluator (0x06d2…) using the submit_exercise function.

Submitting exercise 2 for evaluation
Submitting exercise 2 for evaluation

Once the transaction is processed we can verify if we completed the exercise by calling the function ex2a_get_animal_rank.

Validating that Evaluator can fetch the animal characteristics
Validating that Evaluator can fetch the animal characteristics

If the Evaluator was able to get the information about the NFT from our smart contract we should now be awarded an additional two points on the Pointer Counter smart contract (0x00a0…).

Exercise 2 validated successfully
Exercise 2 validated successfully

Indeed now our test wallet on Argent X has 8 points, 2 more than before. We have successfully completed exercise 2.

To see the final implementation of the ERC721 token for exercise 2 go to my GitHub repo.

To continue the tutorial go to Exercise 3: Minting NFTs.

11 thoughts on “StarkNet ERC721 Workshop: Exercise 2”

  1. great tutorial!

    however, when i try to ex2b_declare_animal with a token_id of 1 i get the error:

    Error at pc=0:10:
    Got an exception while executing a hint.
    Cairo traceback (most recent call last):
    Unknown location (pc=0:228)
    Unknown location (pc=0:214)

    Error in the called contract (0x2efdbbca923eec1823465082b642dd2988cbeb6646ed123dbd3e36146cf6599):
    Error at pc=0:96:
    Got an exception while executing a hint.
    Cairo traceback (most recent call last):
    Unknown location (pc=0:1163)
    Unknown location (pc=0:1107)
    Unknown location (pc=0:650)
    Unknown location (pc=0:706)
    Unknown location (pc=0:728)

    Error in the called contract (0x2d15a378e131b0a9dc323d0eae882bfe8ecc59de0eb206266ca236f823e0a15):
    Error message: Token 1 doesn’t belong to the evaluator
    Error at pc=0:2600:
    An ASSERT_EQ instruction failed: 1274519388635697963204543407605438159849675014318520616686163352039693617685 != 1328418714381046312097288842157507459852428051700220238512456108458803946905.
    Cairo traceback (most recent call last):
    Unknown location (pc=0:1883)
    Unknown location (pc=0:1872)

    any idea why this is?

    1. the key of the error can be found here:

      1274519388635697963204543407605438159849675014318520616686163352039693617685 != 1328418714381046312097288842157507459852428051700220238512456108458803946905.

      If you transform those felts into hex this is what you get:

      0x2d15a378e131b0a9dc323d0eae882bfe8ecc59de0eb206266ca236f823e0a15 != 0x2efdbbca923eec1823465082b642dd2988cbeb6646ed123dbd3e36146cf6599

      The first address is the address of the Evaluator while the second I presume is the address of your wallet. I think the problem is that the NFT with id 1 is owned by your wallet instead of by the Evaluator as the exercise requires.

  2. Hi suraj,
    I fixed that issue by added more parameter into declare_animal method:
    @external
    func declare_animal{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
    sex : felt, legs : felt, wings : felt, to_: felt
    ) -> (token_id : Uint256) {
    alloc_locals;
    Ownable.assert_only_owner();

    // Increment token_id by 1
    let current_token_id : Uint256 = last_token_id.read();
    let one_as_uint256 = Uint256(1, 0);
    let (local new_token_id, _) = uint256_add(current_token_id, one_as_uint256);

    //let (sender_address) = get_caller_address();
    let to = to_;
    // Mint NFT and update token_id
    ERC721._mint(to, new_token_id);
    animals.write(new_token_id, Animal(sex=sex, legs=legs, wings=wings));

    // Update and return new token id
    last_token_id.write(new_token_id);

    return(token_id=new_token_id);

    }

  3. I’m realising that I forgot to document a crucial step and that’s why you are all having issues trying to validate the exercise. Please check my comment in blue at the middle of the article.

So, what do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.