StarkNet ERC721 Workshop: Exercise 3

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

Minting NFTs

  • Create a function to allow breeders to mint new animals with the specified characteristics
  • Deploy your new contract
  • Call submit_exercise() in the Evaluator to configure the contract you want evaluated
  • Call ex3_declare_new_animal() to get points (2 pts)

Looking for exercise 2? Click here.

Solution

To start, let’s make a copy of the ERC721 smart contract we created on exercise 2 as the base for this exercise.

$ cp src/ERC721_ex2.cairo src/ERC721_ex3.cairo

Because now only breeders can mint new NFTs, we need to keep track of addresses registered as breeders along with a mechanism to add and remove them from the registry.

from starkware.cairo.common.math import assert_not_zero

@storage_var
func is_breeder(breeder_address : felt) -> (is_breeder : felt) {
}

@view
func get_is_breeder{...}(
    address : felt
) -> (is_true : felt) {
    with_attr error_message("ERC721: the zero address can't be a breeder") {
        assert_not_zero(address);
    }
    let (is_true : felt) = is_breeder.read(address);
    return (is_true);
}

@external
func add_breeder{...}(
    breeder_address : felt
) {
    Ownable.assert_only_owner();
    with_attr error_message("ERC721: the zero address can't be a breeder") {
        assert_not_zero(breeder_address);
    }
    is_breeder.write(breeder_address, 1);
    return ();
}

@external
func remove_breeder{...}(
    breeder_address : felt
) {
    Ownable.assert_only_owner();
    with_attr error_message("ERC721: the zero address can't be a breeder") {
        assert_not_zero(breeder_address);
    }
    is_breeder.write(breeder_address, 0);
    return ();
}

Because storage variables are not accessible from outside the smart contract, we have added the view function get_is_breeder that would allow us to verify if an address has been registered as a breeder.

We have limited the use of the functions add_breeder and remove_breeder to only the owner of the smart contract for security reasons, we don’t want strangers adding themselves to the registry and then minting NFTs as they wish.

Additional protection has been added to prevent a caller from passing the zero address as a breeder as it has a special meaning in the system and could be used as a vector of attack exploiting an edge case.

Analyzing the source code from the Evaluator smart contract we can see that when invoking the function ex3_declare_new_animal it calls in turn the function declare_animal on our smart contract using the same signature we defined on exercise 2.

@external
func ex3_declare_new_animal{...}() {
    ...
    let (created_token) = IExerciseSolution.declare_animal(
        contract_address=submited_exercise_address,
        sex=expected_sex,
        legs=expected_legs,
        wings=expected_wings,
    );
    ...
}

Our current implementation of declare_animal needs only one minor change. Instead of restricting its usage to only the owner of the smart contract we need to restrict its usage to addresses registered as breeders.

@external
func declare_animal{...}(
    sex : felt, legs : felt, wings : felt
) -> (token_id : Uint256) {
    alloc_locals;
    assert_only_breeder();
    ...
}

func assert_only_breeder{...}() {
    let (sender_address) = get_caller_address();
    let (is_true) = is_breeder.read(sender_address);
    with_attr error_message("Caller is not a registered breeder") {
        assert is_true = 1;
    }
    return ();
}

This in theory should be all that is needed to complete the exercise so we can move to compiling, deploying and testing our smart contract.

To gain familiarity with StarkNet’s CLI, I will avoid using Voyager and Argent X (you can also use Braavos as an alternative) to interact with the smart contract as much as possible and instead rely on the call and invoke commands of the CLI and my default wallet.

If you followed the steps outlined here to create a default account that is accessible by the CLI, you’d have a json file somewhere on your home folder with information about the account.

$ cat ~/.starknet_accounts/starknet_open_zeppelin_accounts.json
>>>
{
    "alpha-goerli": {
        "__default__": {
            "private_key": "...",
            "public_key": "...",
            "address": "0x1814d4c1404a8fed9dccfc20f7aaf2aebd96c8f0a1f8e594829f51611d46253"
        }
    }
}

Because I would like to make this account the owner of the ERC721 smart contract that I’m about to deploy, I have to use the utils.py library once more to derive the felt representation of the account address.

$ python -i utils.py 
>>> hex_to_felt("0x1814d4c1404a8fed9dccfc20f7aaf2aebd96c8f0a1f8e594829f51611d46253")
680769605472490446995541710352012140980533076999125541840625342975082521171

As a result of wanting to use the CLI to interact with the smart contract we are going to deploy, we need to make sure to generate the Application Binary Interface (ABI) along with the compiled version of the code.

$ mkdir abis
$ starknet-compile src/ERC721_ex3.cairo \
  --output comp/ERC721_ex3.json --abi abis/ERC721_ex3.json

As a result of running this command, our folder structure should look like this:

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

We are ready now to deploy our smart contract passing the felt representation of our default wallet address as the third argument of the constructor.

$ starknet deploy --contract comp/ERC721_ex3.json --network alpha-goerli --no_wallet \
--inputs 71942470984044 4279881 680769605472490446995541710352012140980533076999125541840625342975082521171
>>>
Deploy transaction was sent.
Contract address: 0x0364446a67e0bd77d2ec702431bcedb7395f000033ad898cfaff2f9a500af6f8
Transaction hash: 0x30fd7e814306c4cb95b17c8e451460d4647254a02d1327e22691863f785605b

To make the commands shorter, I’m going to set up some environment variables so I don’t have to repeat the same values over and over every time I use the CLI.

$ export STARKNET_WALLET="starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount"
$ export CONTRACT_ADDRESS="0x0364446a67e0bd77d2ec702431bcedb7395f000033ad898cfaff2f9a500af6f8"
$ export STARKNET_NETWORK="alpha-goerli"

The first test we will make is to validate if the function declare_animal rejects an invocation from a wallet address that is not yet registered as a breeder.

$ starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function declare_animal \
    --inputs 2 2 2
>>>
...{"code": "StarknetErrorCode.TRANSACTION_FAILED", "message": "...Error message: Caller is not a registered breeder"}...

The invocation failed which means that the authorization mechanism we added to the function worked as expected. Let’s try now adding our default wallet as a breeder by invoking the function add_breeder.

$ starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function add_breeder \
    --inputs 680769605472490446995541710352012140980533076999125541840625342975082521171
>>>
Sending the transaction with max_fee: 0.000009 ETH.
Invoke transaction was sent.
Contract address: 0x0364446a67e0bd77d2ec702431bcedb7395f000033ad898cfaff2f9a500af6f8
Transaction hash: 0x4a8b0441951257c22acac1ee4f052036bf034332a05c0fcb84d2debd33ff2a9

To check the progress of the transaction we can use the tx_status command from the CLI.

$ starknet tx_status --hash 0x4a8b0441951257c22acac1ee4f052036bf034332a05c0fcb84d2debd33ff2a9
>>>
{
    "block_hash": "0x572374345822adb40215a6de296f27bc4aab4e20195c15264cbb1ed3458c405",
    "tx_status": "ACCEPTED_ON_L2"
}

Now that we know that the transaction was executed, let’s verify if our default wallet address was added to the list of breeders by calling the view function get_is_breeder.

$ starknet call \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function get_is_breeder \
    --inputs 680769605472490446995541710352012140980533076999125541840625342975082521171
>>>
1

The number 1 in this case represents the boolean true so we know that we have succeeded at registering our default wallet as a breeder. We can now try again invoking the function declare_animal.

$ starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function declare_animal \
    --inputs 2 2 2
>>>
Sending the transaction with max_fee: 0.000021 ETH.
Invoke transaction was sent.
Contract address: 0x0364446a67e0bd77d2ec702431bcedb7395f000033ad898cfaff2f9a500af6f8
Transaction hash: 0x168a6fb929154eed1f1cddf56d46ad470c819f1886f6ff4530ed14f50a050c2

This time the transaction didn’t fail so we should be able to verify if the newly created NFT with token id 1 has the provided characteristics.

$ starknet call \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function get_animal_characteristics \
    --inputs 1
>>>
Error: AssertionError: Expected at least 2 inputs, got 1.

Because the input token_id was defined as an Uint256 instead of felt, we need to pass the inputs 1 0 as this is how the number 1 in Uint256 is created using the built in library.

let one_as_uint256 = Uint256(1, 0)

Calling the function again with the right inputs will give us back the same characteristics we previously defined.

$ starknet call \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function get_animal_characteristics \
    --inputs 1 0
>>>
2 2 2

Now that we know that our smart contract works as expected we can focus on passing the evaluation of the exercise. Because the Evaluator will try to call our function declare_animal, we need to add that smart contract to the list of breeders.

As always, we have to first derive the felt value from the hexadecimal value that describes the Evaluator address.

$ python -i utils.py
>>> hex_to_felt("0x06d2c2a6948a67b445ef16ea726ebac9b4535259b1f5d763b6985230c258c675")
3086258404888638876219097282085579162243564028072194906443891907322397116021

Let’s call the function add_breeder providing the Evaluator address as a felt.

$ starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function add_breeder \
    --inputs 3086258404888638876219097282085579162243564028072194906443891907322397116021
>>>
Sending the transaction with max_fee: 0.000007 ETH.
Invoke transaction was sent.
Contract address: 0x0364446a67e0bd77d2ec702431bcedb7395f000033ad898cfaff2f9a500af6f8
Transaction hash: 0x90b1ad34393fa054d3417e61cb87d94936dab18bac1359bf782513f1808cb2

It is time now to transfer ownership of the smart contract over to the Argent X wallet before submitting the result to the Evaluator. This step is required only because I want the points awarded for completing each exercise to go to the same wallet as before.

$ starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi abis/ERC721_ex3.json \
    --function transferOwnership \
    --inputs 486246126474359946192348700142268263967120013078464126154508728538516568533
>>>
Sending the transaction with max_fee: 0.000007 ETH.
Invoke transaction was sent.
Contract address: 0x0364446a67e0bd77d2ec702431bcedb7395f000033ad898cfaff2f9a500af6f8
Transaction hash: 0x3f16687733ae75877288d28ed1c282ffda34e1b70df1cd4f876be9b3326248e

Let’s now head out to Voyager and call the function submit_exercise on the Evaluator (0x06d2…) providing the address of our ERC721 smart contract (0x0364…).

Note: The address displayed on the screenshot doesn’t correspond to the address we got during deployment just because I had to make some changes to the smart contract after it was already verified by the Evaluator and was forced to deploy a new instance with a different address.

Submitting the ERC721 smart contract to the Evaluator
Submitting the ERC721 smart contract to the Evaluator

Next we invoke the Evaluator’s function ex3_declare_new_animal so the Evaluator is able to create its own NFT with any characteristics it wants.

Allowing the Evaluator to create an NFT
Allowing the Evaluator to create an NFT

If everything has gone according to plan, we should have been awarded an additional 2 points on our Argent X wallet address (0x0113…) on the Points Counter smart contract (0x00a0…).

Points collected after completing exercise 3
Points collected after completing exercise 3

We have now 10 points, 2 more than before which means that we have successfully completed exercise 3.

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

To continue the tutorial go to Exercise 4: Burning NFTs.

So, what do you think?

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