StarkNet ERC721 Workshop: Exercise 5

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

Adding Permissions and Payments

  • Use dummy token faucet to get dummy tokens
  • Use ex5a_i_have_dtk() to show you managed to use the faucet (2 pts)
  • Create a function to allow breeder registration.
  • This function should charge the registrant for a fee, paid in dummy tokens (check registration_price)
  • Add permissions. Only allow listed breeders should be able to create animals
  • Deploy your new contract
  • Call submit_exercise() in the Evaluator to configure the contract you want evaluated
  • Call ex5b_register_breeder() to prove your function works. If needed, send dummy tokens first to the evaluator (2pts)

Looking for exercise 4? Click here.

Solution

We start as always by duplicating the smart contract created on the previous exercise to use it as the base for this one.

$ cp src/ERC721_ex4.cairo src/ERC721_ex5.cairo

To get dummy tokens we have to go to Voyager (0x052e…) and invoke the function faucet using Argent X (you can also use Braavos as an alternative) with the test account we have been using since the beginning of the workshop (0x0113…).

Getting dummy token from faucet
Getting dummy tokens from faucet

If this function was executed successfully we should have been granted some amount of dummy tokens on our test account. To check the amount of tokens received, we can call the function balanceOf of the Dummy Token smart contract (0x052e…) passing the address of our test account.

Checking dummy token balance
Checking dummy token balance

Considering that the smart contract is using 18 decimal places for the balance as it’s the standard, we can see that we have been awarded 100 dummy tokens.

Before we invoke the function ex5a_i_have_dtk from the Evaluator (0x06d2…), let’s take a look at what the function does.

Evaluator.cairo

@external
func ex5a_i_have_dtk{...}() {
    alloc_locals;
    // Reading caller address
    let (sender_address) = get_caller_address();
    // Reading sender balance in dummy token
    let (dummy_token_address) = dummy_token_address_storage.read();
    let (dummy_token_init_balance) = IERC20.balanceOf(
        contract_address=dummy_token_address, account=sender_address
    );

    // Verifying it's not 0
    // Instanciating a zero in uint format
    let zero_as_uint256 : Uint256 = Uint256(0, 0);
    let (is_equal) = uint256_eq(dummy_token_init_balance, zero_as_uint256);
    with_attr error_message("Caller should own some DTK") {
        assert is_equal = 0;
    }

    // Checking if player has validated this exercise before
    let (has_validated) = has_validated_exercise(sender_address, 51);

    if (has_validated == 0) {
        // player has validated
        validate_exercise(sender_address, 51);
        // Sending points
        distribute_points(sender_address, 2);
        return ();
    else {
        return ();
    }
}

The Evaluator function is simply calling the function balanceOf of the Dummy Token smart contract passing the address of the caller, exactly what we did before. Let’s invoke this function on Voyager (0x06d2…).

Validating receiving dummy tokens
Validating receiving dummy tokens

If the validation completed successfully we should have been awarded two more points on the Points Counter smart contract (0x00a0…).

Checking the score after proving getting dummy tokens
Checking the score after proving getting dummy tokens

Sweet, two more points awarded.

We can now move on to the second part of the exercise that requires executing the Evaluator’s function ex5b_register_breeder. Let’s take a look at what the function does.

Evaluator.cairo

@external
func ex5b_register_breeder{...}() {
    alloc_locals;
    // Reading caller address
    let (sender_address) = get_caller_address();
    // Retrieve exercise address
    let (submited_exercise_address) = player_exercise_solution_storage.read(sender_address);
    // Get evaluator address
    let (evaluator_address) = get_contract_address();
    // Is evaluator currently a breeder?
    let (is_evaluator_breeder_init) = IExerciseSolution.is_breeder(
        contract_address=submited_exercise_address, account=evaluator_address
    );
    with_attr error_message("Evaluator shouldn't be a breeder for now") {
        assert is_evaluator_breeder_init = 0;
    }
    // TODO test that evaluator can not yet declare an animal (requires try/catch)

    // Reading registration price. Registration is payable in dummy token
    with_attr error_message("Couldn't read the registration price") {
        let (registration_price) = IExerciseSolution.registration_price(
            contract_address=submited_exercise_address
        );
    }

    // Reading evaluator balance in dummy token
    let (dummy_token_address) = dummy_token_address_storage.read();
    let (dummy_token_init_balance) = IERC20.balanceOf(
        contract_address=dummy_token_address, account=evaluator_address
    );
    // Approve the exercise for spending my dummy tokens
    IERC20.approve(
        contract_address=dummy_token_address,
        spender=submited_exercise_address,
        amount=registration_price,
    );

    // Require breeder permission.
    with_attr error_message("Couldn't register the Evaluator as a breeder") {
        IExerciseSolution.register_me_as_breeder(contract_address=submited_exercise_address);
    }

    with_attr error_message("Couldn't check that the evaluator is a breeder") {
        // Check that I am indeed a breeder
        let (is_evaluator_breeder_end) = IExerciseSolution.is_breeder(
            contract_address=submited_exercise_address, account=evaluator_address
        );
    }
    with_attr error_message("Evaluator is not a breeder") {
        assert is_evaluator_breeder_end = 1;
    }

    // Check that my balance has been updated
    let (dummy_token_end_balance) = IERC20.balanceOf(
        contract_address=dummy_token_address, account=evaluator_address
    );
    // Store expected balance in a variable, since I can't use everything on a single line
    let evaluator_expected_balance : Uint256 = uint256_sub(
        dummy_token_init_balance, registration_price
    );
    // Verifying that balances where updated correctly
    let (is_evaluator_balance_equal_to_expected) = uint256_eq(
        evaluator_expected_balance, dummy_token_end_balance
    );
    with_attr error_message(
            "Actual registration cost is not the one returned by registration_price") {
        assert is_evaluator_balance_equal_to_expected = 1;
    }

    // Checking if player has validated this exercise before
    let (has_validated) = has_validated_exercise(sender_address, 52);

    if (has_validated == 0) {
        // player has validated
        validate_exercise(sender_address, 52);
        // Sending points
        distribute_points(sender_address, 2);
        return ();
    else {
        return ();
    }
}

We can summarize the actions performed by the function as follows:

  1. Check that the Evaluator is not a breeder
  2. Get our ERC721 breeder registration cost
  3. Approve our ERC721 to use the Evaluator’s dummy tokens for payment
  4. Register the Evaluator as a breeder by paying dummy tokens
  5. Check that the Evaluator is a breeder
  6. Check that the Evaluator was charged the registration fee

In particular, the Evaluator attempts to interact with our ERC721 smart contract by using the functions is_breeder, registration_price and register_me_as_a_breeder. The expected signature of those functions was given at the beginning of the workshop and it’s shown below.

IExerciseSolution.cairo

%lang starknet

from starkware.cairo.common.uint256 import Uint256

@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) {
    }
}

In a previous exercise we created a breeder registration system although not exactly following that interface. Let’s see what we currently have and how it compares to the expected function signatures.

src/ERC721_ex5.cairo (before)

@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 ();
}

Let’s make some changes to those functions to make them compliant with the interface.

src/ERC721_ex5.cairo (after)

@storage_var
func _is_breeder(account : felt) -> (is_approved : felt) {
}

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

@external
func register_me_as_breeder{...}() -> (is_added : felt) {
    // let’s work on this later
}

@external
func unregister_me_as_breeder{...}() -> (is_added : felt) {
    let (sender_address) = get_caller_address();
    _is_breeder.write(account=sender_address, value=0);
    return (is_added=0);
}

We will work on the implementation of register_me_as_breeder in a moment. For now notice that besides the changes in the function signature we also changed who has permission to register and unregister as a breeder. Before this was only available to the owner of the smart contract. Now it is open to any smart contract to add or remove itself from the breeder list.

One function that we don’t currently have is registration_price so let’s add it to our smart contract.

src/ERC721_ex5.cairo

@view
func registration_price{...}() -> (
    price : Uint256
) {
    let one_as_uint256 = Uint256(1, 0);
    return (price=one_as_uint256);
}

Because the exercise doesn’t require us to create a method for dynamically setting the registration price I’m going to hard code the value to 1.

We have now all that’s required to work on the implementation of the function register_me_as_breeder.

src/ERC721_ex5.cairo

from starkware.starknet.common.syscalls import get_caller_address, get_contract_address
from openzeppelin.token.erc20.IERC20 import IERC20

@external
func register_me_as_breeder{...}() -> (is_added : felt) {
    let (sender_address) = get_caller_address();
    let (erc721_address) = get_contract_address();
    let (price) = registration_price();
    let (dummy_token_address) = _dummy_token_address.read();

    let (success) = IERC20.transferFrom(
        contract_address=dummy_token_address,
        sender=sender_address,
        recipient=erc721_address,
        amount=price,
    );
    with_attr error_message("ERC721: unable to charge dummy tokens") {
        assert success = 1;
    }
    _is_breeder.write(account=sender_address, value=1);
    return (is_added=1);
}

The function first attempts at charging the registration fee to the caller and then, if successful, adds the caller to the breeder list. For this function to work we need to store the address of the dummy token smart contract.

src/ERC721_ex5.cairo

@storage_var
func _dummy_token_address() -> (dummy_token_address : felt) {
}

@constructor
func constructor{...}(
    name : felt, symbol : felt, owner : felt, dummy_token_address : felt
) {
    ERC721.initializer(name, symbol);
    Ownable.initializer(owner);
    token_id_initializer();
    _dummy_token_address.write(dummy_token_address);
    return ();
}

When we deploy the smart contract we will need to take care of passing the address of the Dummy Token (0x052e…) to the constructor as a felt.

$ python -i utils.py
>>> hex_to_felt("0x052ec5de9a76623f18e38c400f763013ff0b3ff8491431d7dc0391b3478bf1f3")
2344204853301408646930413631119760318685682522206636282740794249760712946163

We have now everything to pass the exercise 5 so let’s move on to compiling and deploying our smart contract.

$ starknet-compile src/ERC721_ex5.cairo --output comp/ERC721_ex5.json

$ starknet deploy --contract comp/ERC721_ex5.json \
  --network alpha-goerli --no_wallet \
  --inputs 71942470984044 4279881 680769605472490446995541710352012140980533076999125541840625342975082521171 2344204853301408646930413631119760318685682522206636282740794249760712946163

>>>
...
Contract address: 0x00479d5289649617a6d73d53f70cdbe29c027b6c5ae1548bc2796a97ddcf6f3e
...

On Voyager we can now head out to the Evaluator (0x06d2…) and submit our smart contract by invoking the function submit_exercise.

Submitting our ERC721 smart contract for evaluation
Submitting our ERC721 smart contract for evaluation

We are ready now to invoke the function ex5b_register_breeder to validate our smart contract.

Checking if our solution works
Checking if our solution works

Finally, if everything went well, we should be able to verify that we have been awarded 2 additional points on the Points Counter (0x00a0…) by calling the function balanceOf and passing the address of our Argent X account.

Checking the score after completing exercise 5
Checking the score after completing exercise 5

We have been awarded 2 more points. Exercise 5 completed successfully.

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

To continue the tutorial go to Exercise 6: Claiming an NFT.

1 thought on “StarkNet ERC721 Workshop: Exercise 5”

So, what do you think?

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