Assignment - ERC721

1. What is the time complexity of creating new cats? (constant or linear w nr of cats).

The time complexity of creating new cats is constant. This is because a new cat is simply just added to the end of the array.

2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).

The time complexity of the getAllCatsFor function is linear due to the iteration over the entire kitty array.

3. How could the storage design be changed to make the getAllCats function constant?
We could add another mapping that would map the owner address to a uint[] of all the cats owned by the owner. This would require more memory and be a little more expensive to call _createKitty and to call _transfer cats, but would give us a constant time complexity to access all cats owned by a specific address.


pragma solidity ^0.8.0;
 
 

 
 
contract Kittycontract {
 

 
 
   string public constant name = "TestKitties";
 
   string public constant symbol = "TK";
 
  
 
   event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
 

 
 
   event Birth(
 
       address owner,
 
       uint256 kittenId,
 
       uint256 mumId,
 
       uint256 dadId,
 
       uint256 genes
 
   );
 

 
 
   struct Kitty {
 
       uint256 genes;
 
       uint64 birthTime;
 
       uint32 mumId;
 
       uint32 dadId;
 
       uint16 generation;
 
   }
 

 
 
   Kitty[] kitties;
 

 
 
   mapping (uint256 => address) public kittyIndexToOwner;
 
   mapping (address => uint[]) ownerCat;
 

 
 
  
 

 
 
   function balanceOf(address owner) external view returns (uint256 balance){
 
       return ownershipTokenCount[owner];
 
   }
 

 
 
   function totalSupply() public view returns (uint) {
 
       return kitties.length;
 
   }
 

 
 
   function ownerOf(uint256 _tokenId) external view returns (address)
 
   {
 
       return kittyIndexToOwner[_tokenId];
 
   }
 

 
 
   function transfer(address _to,uint256 _tokenId) external
 
   {
 
       require(_to != address(0));
 
       require(_to != address(this));
 
       require(_owns(msg.sender, _tokenId));
 

 
 
       _transfer(msg.sender, _to, _tokenId);
 
   }
 
  
 
   function getAllCatsFor(address _owner) external view returns (uint[]){
return ownerCat[_owner]; 
 
 
 
 
 
 
 
 
 
 
 
 
 
           }
 
       }
 
       return result;
 
   }
 
  
 
   function createKittyGen0(uint256 _genes) public returns (uint256) {
 
       return _createKitty(0, 0, 0, _genes, msg.sender);
 
   }
 

 
 
   function _createKitty(
 
       uint256 _mumId,
 
       uint256 _dadId,
 
       uint256 _generation,
 
       uint256 _genes,
 
       address _owner
 
   ) private returns (uint256) {
 
       Kitty memory _kitty = Kitty({
 
           genes: _genes,
 
           birthTime: uint64(block.timestamp),
 
           mumId: uint32(_mumId),
 
           dadId: uint32(_dadId),
 
           generation: uint16(_generation)
 
       });
 
      
 
       kitties.push(_kitty);
 

 
 
       uint256 newKittenId = kitties.length - 1;
 

 
 
       emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);
 

 
 
       _transfer(address(0), _owner, newKittenId);
 

 
 
       return newKittenId;
 

 
 
   }
 

 
 
   function _transfer(address _from, address _to, uint256 _tokenId) internal {
 
       ownershipTokenCount[_to]++;
 

 
 
       kittyIndexToOwner[_tokenId] = _to;
 

 
 
       if (_from != address(0)) {
 
           ownershipTokenCount[_from]--;
 
       }
 

 
 
       // Emit the transfer event.
 
       emit Transfer(_from, _to, _tokenId);
 
   }
 

 
 
   function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
 
     return kittyIndexToOwner[_tokenId] == _claimant;
 
 }
 

 
 
}

 
 
 

1 Like

Hi,

My answers:

1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
It is constant because creating a new cat always takes the same amount of time, computations.

2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
This one is linear, because the time to gather all cats, for a given address, grows proportionally to the size of kitties, this is due to the loop inside de gatAllCatsFor function
that has size kitties.length.

3. How could the storage design be changed to make the getAllCats function constant? Implement your idea. Then discuss the benefits and drawbacks of your implementation and post everything in the forum.

I used a mapping to get a constant time. Introduced the mapping

mapping(address => uint[]) public catsOwned;

then in _createKitty function introduced

catsOwned[_owner].push(newKittenId);

Code below.
Note: just saw solution video and realized that did not update cats after transfers… ooops.

pragma solidity ^0.8.0;

contract Kittycontract {

    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }

    Kitty[] kitties;

    mapping (uint256 => address) public kittyIndexToOwner;
    mapping (address => uint256) ownershipTokenCount;
    mapping(address => uint[]) public catsOwned;
    

    function balanceOf(address owner) external view returns (uint256 balance){
        return ownershipTokenCount[owner];
    }

    function totalSupply() public view returns (uint) {
        return kitties.length;
    }

    function ownerOf(uint256 _tokenId) external view returns (address)
    {
        return kittyIndexToOwner[_tokenId];
    }

    function transfer(address _to,uint256 _tokenId) external
    {
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));

        _transfer(msg.sender, _to, _tokenId);
    }
    
    function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
        uint[] memory result = new uint[](ownershipTokenCount[_owner]);
        uint counter = 0;
        for (uint i = 0; i < kitties.length; i++) {
            if (kittyIndexToOwner[i] == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
    
    
    function getAllCatsForV2(address _owner) external view returns (uint[] memory cats){
        return catsOwned[_owner];
    }
    
    function createKittyGen0(uint256 _genes) public returns (uint256) {
        return _createKitty(0, 0, 0, _genes, msg.sender);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns (uint256) {
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });
        
        kitties.push(_kitty);

        uint256 newKittenId = kitties.length-1;
        
        catsOwned[_owner].push(newKittenId);

        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);

        _transfer(address(0), _owner, newKittenId);

        return newKittenId;

    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        ownershipTokenCount[_to]++;

        kittyIndexToOwner[_tokenId] = _to;

        if (_from != address(0)) {
            ownershipTokenCount[_from]--;
        }

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
      return kittyIndexToOwner[_tokenId] == _claimant;
  }

}
1 Like

1.) The time complexity of creating new cats is constant since it takes a fixed amount of operation and does not involve iterations.

2.) The Time complexity of the getAllCatsFor function is linear since we have to loop through an array and the longer it is, longer it takes and it is of linear order.

3)I am unsure how to implement this to make it funcation. Currently the only way to make getAllCatsFor function requires an array looping to make it work correctly because if we use mapping, we can only have one cat linked to address. Looking at other’s submission also leads to believe that this is the case.

1 Like
  1. What is the time complexity of creating new cats?
    The time complexity of creating new cats is constant because creating mappings and adding a new element to an array are quick operations that do not depend on storage size or looping through an ever expanding array.

  2. What is the time complexity of the getAllCatsFor function?
    The getAllCatsFor function, however, has a time complexity that grows linearly with the number of cats stored in the array. Each cat added increases the length of the array and the amount of time it takes to loop through all the values in that array.

  3. How could the storage design be changed to make the getAllCats function constant? Discuss benefits and drawbacks with your implementation.
    The storage design could be changed by adding another mapping to keep track of how many cats are owned by each address. This adds complexity to our contract and increases the amount of code in our functions but the trade off is worth it. In return the getAllCatsFor function no longer increases linearly in time complexity as more cats are added. Instead, it is now constant in time complexity.

Implementation:

pragma solidity ^0.8.0;

contract Kittycontract {

    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }

    Kitty[] kitties;

//        adding a new mapping to keep track of how many tokens each address ownes to speed up getAllCatsFor function and improve storage design
    mapping (address => uint256[]) public tokensOwned;
    
    mapping (uint256 => address) public kittyIndexToOwner;
    mapping (address => uint256) ownershipTokenCount;

    

    function balanceOf(address owner) external view returns (uint256 balance){
        return ownershipTokenCount[owner];
    }

    function totalSupply() public view returns (uint) {
        return kitties.length;
    }

    function ownerOf(uint256 _tokenId) external view returns (address)
    {
        return kittyIndexToOwner[_tokenId];
    }

    function transfer(address _to,uint256 _tokenId) external
    {
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));

        _transfer(msg.sender, _to, _tokenId);
    }
    
//        change getAllCatsFor function to simply return value stored in mapping instead of looping through entire array
    function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
        return tokensOwned[_owner];
    }
        //uint[] memory result = new uint[](ownershipTokenCount[_owner]);
        //uint counter = 0;
        //for (uint i = 0; i < kitties.length; i++) {
            //if (kittyIndexToOwner[i] == _owner) {
                //result[counter] = i;
                //counter++;
            //}
        //}
        //return result;
     //}
    
    function createKittyGen0(uint256 _genes) public returns (uint256) {
        return _createKitty(0, 0, 0, _genes, msg.sender);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns (uint256) {
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });
        
        kitties.push(_kitty);

        uint256 newKittenId = kitties.length - 1;

        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);

        _transfer(address(0), _owner, newKittenId);

        return newKittenId;

    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        ownershipTokenCount[_to]++;
        kittyIndexToOwner[_tokenId] = _to;
//            adding token Id to tokensOwned mapping for recieving address
        tokensOwned[_to].push(_tokenId);
        if (_from != address(0)) {
            ownershipTokenCount[_from]--;
//                calling _changeOwner function to remove token Id from senders tokensOwned mapping
            _changeOwner(_from, _tokenId);
        }
        emit Transfer(_from, _to, _tokenId);
    }

//        function needed to remove token Ids from new mapping as tokens are transfered.
//        using same idea as storage design example : mapping_w_Index_and_Delete.sol
    function _changeOwner(address _from, uint _tokenId) internal {
        uint tokenIdToMove = tokensOwned[_from][tokensOwned[_from].length-1];
        for (uint i = 0; i < tokensOwned[_from].length; i++) {
            if (tokensOwned[_from][i] == _tokenId) {
                tokensOwned[_from][i] == tokenIdToMove;
                tokensOwned[_from].pop();
            }
        }
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
      return kittyIndexToOwner[_tokenId] == _claimant;
  }

}
  1. What is the time complexity of creating new cats? (constant or linear w nr of cats).

seems to be constant beyond a one time increase from 139207 the first time to 105007 thereafter. presumably the difference is in creating the array versus interacting with an existing array? writing to memory as opposed to read/write thereafter.

  1. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).

linear as the content is stored in an array and as the array gets bigger so too do the gas costs of interacting with it.

  1. How could the storage design be changed to make the getAllCats function constant?
    A mapping that would input an address and return an ID.
1 Like
  1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
  2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
  3. How could the storage design be changed to make the getAllCats function constant? Discuss benefits and drawbacks with your implementation.

1. The time complexity to create new cats is constant because we rely on the function .push (kitties.push(_kitty);) to increase the array by 1 element. It doesn’t require to much processing power because it just uses the length of the array and pushes the new cat on the next index (it doesn’t require looping through the array or something like that).

Testing the gas cost:

Execution cost to create kitty with index 3:

Decoded output: “0”: “uint256: 3”
Execution cost: 105019 gas

Execution cost to create kitty with index 8:

Decoded output: “0”: “uint256: 8”
Execution cost: 105007 gas

(The results shows that the processing complexity to create new Kitties is constant)

2. The time complexity to run the function getAllCatsFor is linear with the nr of cats, because it loops through the entire array of kitties, so the larger the array, more processing power is required to loops through that, to get the kitties owned by the address. (It will always loop thought the entire array)

for (uint i = 0; i < kitties.length; i++) {
    if (kittyIndexToOwner[i] == _owner) {
        result[counter] = i;
        counter++;
    }
}

3. Instead of looping through the array to get all the cats for the address, we could use the mapping (mapping (address => uint256) ownershipTokenCount;) to get the number of kitties (value) that the address has by inputting the address (key) that we want to check.
That would make the function constant.

The benefits of this approach are:

  • We get the balance of kitties for that address instantaneously;

The cons of this approach are:

  • We can’t get the Kitty ID of every single cat owned by the address. Intead we just get the number of kitties owned.
  • Because we are dealing with NFTs, getting the “raw balance” of kitties is a huge drawback. That’s because they are not fungible, meaning that one cat can worth more than the other. So the user is more interested on knowing the different kitties he has instead of the number of kitties! (that can be only achieved by getting the kittenId that will be related with it’s array index).
1 Like

Just saw the solution video and realized that my 3rd answer is wrong, instead the mapping that we should use is:

mapping (address => uint256[]) ownerToCats;

As referred in the video, it will raise the costs of functions that write data on the blockchain while optimizing functions that can be runned off-chain for free (view function). So this approach is worse than looping through the array!

Hi, All!

  1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
    Constant as we add in the end of list new element (kitties.push(_kitty); )
  2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
    Linear. Each time we have to check whole list of elements. We have time for checking one elemnt multiple on length of rrray. ( for (uint i = 0; i < kitties.length; i++) { )
  3. How could the storage design be changed to make the getAllCats function constant? Implement your idea. Then discuss the benefits and drawbacks of your implementation and post everything in the forum.
    Solution :
mapping (address => uint256) ownershipTokenCount; // old code
mapping (address => uint256[]) ownershipTokenCountA; // new code
ownershipTokenCountA[msg.sender].push(_toAdd); // adding id of cat  // new code
// new code
ownershipTokenCountA[msg.sender].length; 
//old code
           for (uint i = 0; i < kitties.length; i++) {
               if (kittyIndexToOwner[i] == _owner) {
                   result[counter] = i;
                    counter++;
           }
       }

Here we keep in ownershipTokenCountA id of each Kitties which owners have. On the other hand we dont need to look through all list of kitties.

1 Like
  1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
    The time complexity in this case is constant since the creation of a new cat is just a simple push at the end of the array (plus some other basic operations) and it an instantaneous operation.

  2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
    The time complexity in this case is linear since to get the list of cats for an owner you have to always iterate through the all array and the more the array grows the more time you have to spend to iterate the full list of cats.

  3. How could the storage design be changed to make the getAllCats function constant? Implement your idea. Then discuss the benefits and drawbacks of your implementation and post everything in the forum.
    You can implement a mapping from the owner address to an array cointaining all the cats that he/she owns. Doing this when you call the getAllCatsFor an address you just instantaneously return the array containing all the cats that is pointed in the mapping by the address. Here below the pieces of code added/changed:

mapping (address => uint256[]) listOfCats;

    function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
         return listOfCats[_owner];
    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        ownershipTokenCount[_to]++;

        kittyIndexToOwner[_tokenId] = _to;

        listOfCats[_to].push(_tokenId);
        
        if (_from != address(0)) {
            for (uint i=0; i<listOfCats[_from].length;i++){
                if (listOfCats[_from][i]==_tokenId){
                    listOfCats[_from][i]=listOfCats[_from][listOfCats[_from].length-1];
                    listOfCats[_from].pop();
            }
        }

        }

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

PROS and CONS
For sure now the getAllCatsFor function became constant in terms of time complexity, but we needed to introduce the dropping of array items into the _transfer function in order to avoid duplication of the cats. In the previous solution to execute the getAllCatsFor function it was always mandatory to iterate all the array of existing cats while in this solution you have to deal with shorter ownership arrays when yu iterate them in the _transfer function. One might argue that it also depends from how many times the getAllCatsFor function is run in comparison with the _transfer. So all in all the answer is not stright forward, maybe a research on the contract usage is recommendable before to implement one or the other.

1 Like

I just reviewed the solution from Filip and I realized I missed the thinking around the fact that getAllCatsFor is view and most likely called by external contracts while the _transfer is internal and our contract has always to pay gas for it. So defenetly the first solution is better! Thanks @filip for the learnings!!! :grimacing:

1 Like

What is the time complexity of creating new cats?

Constant, when a new cat is created it is just added to the end of the cats array.
There is no significant increase in execution cost.

What is the time complexity of the getAllCatsFor function?

Linear, when someone wants to find the cats owned by an address, the function needs
to loop through the entire array until it can give an answer to the parameter given
and the longer the array, the costlier the execution cost.

How could the storage design be improved upon to make the getAllCatsFor function constant?

Currently, an address is taken as a parameter and the function loops
through an array to find which cat IDs the address given owns.

Possibly try take an address that points to its array. An address is taken as a
parameter and points to its array, within the array are the IDs the address owns.

The transfer function must remove a specific _tokenId from the
_from address array and push it into the _to address array.

Possibly use a separate function to remove _tokenId from the array.
The array must be shortened once _tokenId has been removed.

Got everything besides forgetting to move the _tokenId to the end of
the array before using the .pop() function on the array.

Full solution below:

pragma solidity ^0.8.0;
 
contract kittyContract {
 
    string public constant name = "TestKitties";
    string public constant symbol = "TK";
 
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
 
    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }

    Kitty[] kitties;
 
    mapping(uint256 => address) public kittyIndexToOwner;
 
    mapping(address => uint256) ownershipTokenCount;
 
    mapping(address => uint256[]) kittiesOwned;
 
    function balanceOf(address owner) external view returns(uint256 balance){
        return ownershipTokenCount[owner];
    }

    function totalSupply() public view returns(uint){
        return kitties.length;
    }

    function ownerOf(uint256 _tokenId) external view returns(address){
        return kittyIndexToOwner[_tokenId];
    }

    function transfer(address _to,uint256 _tokenId) external {
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));
 
        _transfer(msg.sender, _to, _tokenId);
    }

    function getAllCatsFor(address _owner) external view returns(uint256[] memory){
        return kittiesOwned[_owner];
    }

    function createKittyGen0(uint256 _genes) public returns(uint256){
        return _createKitty(0, 0, 0, _genes, msg.sender);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns(uint256){
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });

        kitties.push(_kitty);
 
        uint256 newKittenId = kitties.length - 1;
 
        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);
 
        _transfer(address(0), _owner, newKittenId);
 
        return newKittenId;
    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        ownershipTokenCount[_to]++;
        kittiesOwned[_to].push(_tokenId);
        
        kittyIndexToOwner[_tokenId] = _to;
        
        if (_from != address(0)) {
            ownershipTokenCount[_from]--;
            _renounceOwnership(_from, _tokenId);
        }
        emit Transfer(_from, _to, _tokenId);
    }

    function _renounceOwnership(address _owner, uint256 _tokenId) internal {
        uint256 lastId = kittiesOwned[_owner][kittiesOwned[_owner].length - 1];
        
        for(uint i = 0; i < kittiesOwned[_owner].length; i++){
            if(kittiesOwned[_owner][i] == _tokenId){
                kittiesOwned[_owner][i] = lastId;
                kittiesOwned[_owner].pop();
            }
        }
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns(bool){
      return kittyIndexToOwner[_tokenId] == _claimant;
    }
}

1.) Its constant. Creating a new Cat is always the same process.

2.) Its linear. Because we have a loop whose execution is more effort the more cats have to be enumerated through

3.) This took quite a while. But the part that makes the function linear is the loop. Therefor we have to get rid of it.

I’ve replaced it with an additional Mapping.

mapping (address => uint256[]) catsOwned;

now we can return this Array but it think we have to also fill the Array which happens during creation of cats and transfering them.
I’ve added the following to the …

transfer,

catsOwned[_to].push(_tokenId);

createKittyGen0

catsOwned[msg.sender].push(_genes);

and _createKitty functions

catsOwned[msg.sender].push(newKittenId);

The loop can be removed and the getAllCatsFor function is simplified

function getAllCatsFor(address _owner) external view returns (uint[] memory){
        return catsOwned[_owner];
    }

Entire Code:

pragma solidity ^0.8.0;

contract Kittycontract {

    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }
    
    Kitty[] kitties;

    mapping (uint256 => address) public kittyIndexToOwner;
    mapping (address => uint256) ownershipTokenCount;
    mapping (address => uint256[]) catsOwned;
    

    function balanceOf(address owner) external view returns (uint256 balance){
        return ownershipTokenCount[owner];
    }

    function totalSupply() public view returns (uint) {
        return kitties.length;
    }

    function ownerOf(uint256 _tokenId) external view returns (address)
    {
        return kittyIndexToOwner[_tokenId];
    }

    function transfer(address _to,uint256 _tokenId) external
    {
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));

        _transfer(msg.sender, _to, _tokenId);
        catsOwned[_to].push(_tokenId);
    }
    
    function getAllCatsFor(address _owner) external view returns (uint[] memory){
        return catsOwned[_owner];
    }
    
    function createKittyGen0(uint256 _genes) public returns (uint256) {
        return _createKitty(0, 0, 0, _genes, msg.sender);
        //
        catsOwned[msg.sender].push(_genes);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns (uint256) {
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });
        
        kitties.push(_kitty);
        

        uint256 newKittenId = kitties.length - 1;

        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);

        _transfer(address(0), _owner, newKittenId);
        //
        catsOwned[msg.sender].push(newKittenId);
        
        return newKittenId;

    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        ownershipTokenCount[_to]++;

        kittyIndexToOwner[_tokenId] = _to;

        if (_from != address(0)) {
            ownershipTokenCount[_from]--;
        }

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
      return kittyIndexToOwner[_tokenId] == _claimant;
  }

}

1 Like
  1. Constant. Always pushed to the end of the array
  2. Linear, have to loop through the array to search for the cats

I would implement a mapping from address to int256 array which contains all the kitties the owner of the address has. GetAllCats would then simply return the array for the address specified and that would make it constant. Many functions would become linear, but the good thing is that you would be looping through the array of the owners kitties but not all of the kitties.

Here’s my implementation:

pragma solidity ^0.8.0;

contract KittyContract {

    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }

    Kitty[] kitties;

    mapping (uint256 => address) public kittyIndexToOwner;
    mapping(address => uint256[]) public ownerToKitties;
    

    function balanceOf(address owner) external view returns (uint256 balance){
        return ownerToKitties[owner].length;
    }

    function totalSupply() public view returns (uint) {
        return kitties.length;
    }

    function ownerOf(uint256 _tokenId) external view returns (address)
    {
        return kittyIndexToOwner[_tokenId];
    }

    function transfer(address _to,uint256 _tokenId) external
    {
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));

        _transfer(msg.sender, _to, _tokenId);
    }
    
    function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
        return ownerToKitties[_owner];
    }
    
    function createKittyGen0(uint256 _genes) public returns (uint256) {
        return _createKitty(0, 0, 0, _genes, msg.sender);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns (uint256) {
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });
        
        kitties.push(_kitty);

        uint256 newKittenId = kitties.length - 1;

        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);

        _transfer(address(0), _owner, newKittenId);

        return newKittenId;

    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        kittyIndexToOwner[_tokenId] = _to;
        
        ownerToKitties[_to].push(_tokenId);

        if (_from != address(0)) {
            for(uint i = 0; i < ownerToKitties[_from].length; i++){
                if(ownerToKitties[_from][i] == _tokenId){
                    ownerToKitties[_from][i] = ownerToKitties[_from][ownerToKitties[_from].length-1];
                    
                    ownerToKitties[_from].pop();
                    
                    return;
                }
            }
        }

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
      return kittyIndexToOwner[_tokenId] == _claimant;
  }

}
1 Like
  1. Time complexity of creating new cats is constant, because we are only adding to the array - we don’t have to loop through it.

  2. Time complexity of getAllCatsFor function is linear, with number of Cats, because we have to loop through - longer array more time ang gas it costs.

  3. We could instead of mapping ownershipTokenCount mapping that maps address to array of unsigned integers (cat ids) like this:

pragma solidity ^0.8.0;

contract Kittycontract {

    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }

    Kitty[] kitties;

    mapping (uint256 => address) public kittyIndexToOwner;
        /*It is mapped from id to address because in this way 1 address can have many kitties.
        In other way (address to id) it would just overwrite!*/
    mapping (address => uint256[]) ownerOfKitties;

    


    function balanceOf(address owner) external view returns (uint256 balance){
        return ownerOfKitties[owner].length;
    }


    function totalSupply() public view returns (uint) {
        return kitties.length;
    }


    function ownerOf(uint256 _tokenId) external view returns (address){
        return kittyIndexToOwner[_tokenId];
    }


    function transfer(address _to,uint256 _tokenId) external{
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));

        _transfer(msg.sender, _to, _tokenId);
    }
    
    
    function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
        return  ownerOfKitties[_owner];
    }
    
    
    function createKittyGen0(uint256 _genes) public returns (uint256) {
        return _createKitty(0, 0, 0, _genes, msg.sender);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns (uint256) {
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });
        
        kitties.push(_kitty);

        uint256 newKittenId = kitties.length - 1;

        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);

        _transfer(address(0), _owner, newKittenId);

        return newKittenId;

    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        kittyIndexToOwner[_tokenId] = _to;

        ownerOfKitties[_to].push(_tokenId);

        if (_from != address(0)) {
            removeKitty(_from, _tokenId);
        }

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
        return kittyIndexToOwner[_tokenId] == _claimant;
    }
    
    function removeKitty(address _owner, uint256 _tokenId) internal {
        for (uint i = 0; i < ownerOfKitties[_owner].length; i++) {
            //Checks if kitty id is the same to the kitty id we want to delete
            if (ownerOfKitties[_owner][i] == _tokenId) {
                //Sets kitty id which we want to delete to last one
                ownerOfKitties[_owner][i] = ownerOfKitties[_owner][ownerOfKitties[_owner].length-1];
                //We pop the last element out of the array (kitty we want to delete)
                ownerOfKitties[_owner].pop();
                return;
            }
        }
    }
}

Pros of this solution:
getAllCatsFor function is now instant - we just return array of cat ids for owner. We save on gas and time.
Cons of this solution:
Transfer function now costs more gas and time, when deleting ownership of cat for sender - we have to delete cat id from array (we have to loop through) of all cat ids that owner (sender) has.

1 Like
  1. Constant
  2. Linear
  3. We should mapping the address to array of ids
1 Like

Hello, can somebody explain these questions? I don’t really remember if I learned about “time complexity” or "constant or linear w nr " stuff. Would really appreciate for the explanation! :slight_smile:

1. What is the time complexity of creating new cats? (constant or linear w nr of cats).

The time complexity is constant 0(1), the push goes directly to the end of the array. Is always the same procedure, therefore is always the same time.

2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
The time complexity will depend on the length of the array, so its a 0(n).

3. How could the storage design be changed to make the getAllCats function constant?
I will need to create a new mapping named onwerToKitties, that maps address => unit[]

Solution

` mapping (address => uint[]) ownerToKitties;
function getAllCatsFor2(address _owner) external view returns (uint[] memory ){

    return ownerToKitties[_owner];
}`

This solution brings speed, but having an extra mapping, will have gas consumption implications, like delete, storing, etc. I think will be better to wait on a free function with less total gas consumption in the whole contract. But it will depend on the use case.

  1. Constant complexity
  2. Linear complexity
  3. Beside the solution which was already mentioned in the forum (with mapping (address => uint256[]) ownersCats;). I figured out one more solution:
pragma solidity ^0.8.0;

contract Kittycontract {

    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }

    struct TokenIds {
        uint256[] contractTokenIds;
        mapping(uint256 => uint256) ownersTokenId; //mapping contractTokenId to ownersTokenId
    }

    Kitty[] kitties;

    mapping (uint256 => address) public kittyIndexToOwner;
    mapping (address => TokenIds) ownershipTokenIdxs;


    function balanceOf(address owner) external view returns (uint256 balance){
        return ownershipTokenIdxs[owner].contractTokenIds.length;
    }

    function totalSupply() public view returns (uint) {
        return kitties.length;
    }

    function ownerOf(uint256 _tokenId) external view returns (address)
    {
        return kittyIndexToOwner[_tokenId];
    }

    function transfer(address _to,uint256 _tokenId) external
    {
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));

        _transfer(msg.sender, _to, _tokenId);
    }
    
    function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
        uint[] memory result;
        for (uint i = 0; i < ownershipTokenIdxs[_owner].contractTokenIds.length; i++) {
            result[i] = ownershipTokenIdxs[_owner].contractTokenIds[i];
        }
        return result;
    }
    
    function createKittyGen0(uint256 _genes) public returns (uint256) {
        return _createKitty(0, 0, 0, _genes, msg.sender);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns (uint256) {
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });
        
        kitties.push(_kitty);

        uint256 newKittenId = kitties.length - 1;

        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);

        _transfer(address(0), _owner, newKittenId);

        return newKittenId;

    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        uint256 tokenToTransfer = ownershipTokenIdxs[_from].ownersTokenId[_tokenId]; // element (ownerId) which should be transeferd
        uint256 lastToken = ownershipTokenIdxs[_from].ownersTokenId[ownershipTokenIdxs[_from].contractTokenIds.length-1]; // last element of ownershipTokenCount[_from]

        ownershipTokenIdxs[_to].contractTokenIds.push(_tokenId);
        ownershipTokenIdxs[_to].ownersTokenId[_tokenId] = ownershipTokenIdxs[_to].contractTokenIds.length-1;

        kittyIndexToOwner[_tokenId] = _to;

        if (_from != address(0)) {
            ownershipTokenIdxs[_from].contractTokenIds[tokenToTransfer] = ownershipTokenIdxs[_from].contractTokenIds[lastToken];
            ownershipTokenIdxs[_from].contractTokenIds.pop();
        }

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
      return kittyIndexToOwner[_tokenId] == _claimant;
  }
}

In this solution getAllCatsFor storage complexity is limited to owner’s tokens. Moreover there is no need to iterate all tokens in order to remove one of them. Of course getAllCatsFor is view function and the best solution seems to be the original approach with linear complexity of this function because it is free anyway.

1 Like
  1. What I understand from the code is that there is no loading (for loop) in the functions needed for creation so the time should be constant with the number of cats.

  2. The get all cats function has a loop in it, thus the more cats there are the longer the time will take and thus is linear.

  3. My changed code to get the getAllCatsFor function constant.

The benefit is that the getAllCatsFor function has become constant and thus the gas costs are lower.
The disadvantage is that a new linear function for deleting cats is added in transfers. Thus if someone gets more cats, transfers require more gas. This however is way cheaper than wjat the getAllCats function would cost if there were many cats.

Here is my changed code:

pragma solidity ^0.8.0;

contract Kittycontract {

    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    event Birth(
        address owner, 
        uint256 kittenId, 
        uint256 mumId, 
        uint256 dadId, 
        uint256 genes
    );

    struct Kitty {
        uint256 genes;
        uint64 birthTime;
        uint32 mumId;
        uint32 dadId;
        uint16 generation;
    }

    Kitty[] kitties;

    mapping (uint256 => address) public kittyIndexToOwner;
    mapping (address => uint256 []) ownedCats;

    

    function balanceOf(address _owner) external view returns (uint256){
        return ownedCats [_owner].length;
    }

    function totalSupply() public view returns (uint) {
        return kitties.length;
    }

    function ownerOf(uint256 _tokenId) external view returns (address)
    {
        return kittyIndexToOwner[_tokenId];
    }

    function transfer(address _to,uint256 _tokenId) external
    {
        require(_to != address(0));
        require(_to != address(this));
        require(_owns(msg.sender, _tokenId));

        _transfer(msg.sender, _to, _tokenId);
    }
    
    function getAllCatsFor(address _owner) external view returns (uint[] memory cats){

        return ownedCats [_owner];
    }
    
    function createKittyGen0(uint256 _genes) public returns (uint256) {
        return _createKitty(0, 0, 0, _genes, msg.sender);
    }

    function _createKitty(
        uint256 _mumId,
        uint256 _dadId,
        uint256 _generation,
        uint256 _genes,
        address _owner
    ) private returns (uint256) {
        Kitty memory _kitty = Kitty({
            genes: _genes,
            birthTime: uint64(block.timestamp),
            mumId: uint32(_mumId),
            dadId: uint32(_dadId),
            generation: uint16(_generation)
        });
        
        kitties.push(_kitty);

        uint256 newKittenId = kitties.length - 1;

        emit Birth(_owner, newKittenId, _mumId, _dadId, _genes);

        _transfer(address(0), _owner, newKittenId);

        return newKittenId;

    }

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        
        kittyIndexToOwner[_tokenId] = _to;

        if (_from != address(0)) {
           // Deletes the cat from the owner
           _deleteKittyOwnership (_from, _tokenId);
        }
        
        // Add the token to the new owner
        ownedCats [_to].push (_tokenId);

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
      return kittyIndexToOwner[_tokenId] == _claimant;
  }
  
  function _deleteKittyOwnership (address _owner, uint _tokenId) internal {
      for (uint i = 0; i < ownedCats [_owner].length; i ++) {
          
          if (ownedCats [_owner] [i] == _tokenId) {
              ownedCats [_owner] [i] = ownedCats [_owner] [ownedCats [_owner].length-1];
              ownedCats [_owner].pop ();
              return;
          }
      }
  }

}
1 Like
  1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
    -the function is constant since you can use the function at any time within any interval of time
  2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
    -linear since you are iterating through an array which is happening at a certain pace.
  3. How could the storage design be changed to make the getAllCats function constant? Discuss benefits and drawbacks with your implementation.
    -if we used an mapping to the id/ place in array of the cat, you could call the id of the cats at whatever pace. but this would be annoying since you would have to manually do all of this unlike iterating through an array w one button
1 Like