Assignment - ERC721

  1. Constant time complexity
  2. Linear time complexity
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 => mapping(uint256 => bool)) ownershipToken;  // owner address => (kitty id => true / false) )
    mapping (address => uint256[]) ownershipToken;  

    

    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){
        return ownershipToken[_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]++;
        ownershipToken[_to].push(_tokenId);

        kittyIndexToOwner[_tokenId] = _to;

        if (_from != address(0)) {
            ownershipTokenCount[_from]--;
            for(uint256 i = 0; i<ownershipToken[_from].length; i++) {
                if(ownershipToken[_from][i] == _tokenId) {
                    ownershipToken[_from][i] = ownershipToken[_from][ownershipToken[_from].length - 1];
                    ownershipToken[_from].pop();
                    break;
                }
            }
        }

        // 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

Assignment ERC721.

1. In my opinion the complexity of creating new cats is constant as we are just adding to  kitties array each time and increasing storage.

2.  getAllCats is iterating the whole length of the kitties array each time, and will increase in length, and therfore consider this to be linear time complexity as the number of cats increases.

3. We can make the getAllCats function constant by replacing the iterative loop based on the number cats owned to a new mapping as below.

This will reduce the time element and gas cost but we need new storage for the mapping, this could offset the gas cost gain.  

Also you  need to add/remove Id's from the mapping array in the _transfer function as shown below ( the remove code has noy been implemented). However this operation will have additional gas cost.


Please comment.


Change getAllCats to use a new mapping from owner address to array of Token Id's .

mapping (address=> unit256[])  ownerCatsIdArray
  
From -----  

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;
    }

To -----

function getAllCatsFor(address _owner) external view returns (uint[] memory cats){

	return  ownerCatsIdArray[_owner];

}


Change _transfer function to add/remove Id from ownerCatsIdArray note (remove code not implemented)

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

        kittyIndexToOwner[_tokenId] = _to;
        ownerCatsIdArray[_to].push(_tokenId);

        if (_from != address(0)) {
            ownershipTokenCount[_from]--;
           // remove from ownerCatsIdArray to be coded 
        }

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

   
  1. Constant, O(1)
  2. Linear, O(N)
  3. Change the mapping for ownership to map to an array which will contain the IDs. The return of the list will be quick since the array itself can be returned (the array is copied since the method returns an array in memory from storage). The lookup for the ID within the list is still linear, but the data set is reduced to per user for complexity O(N).
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[]) ownershipTokenList;

    function balanceOf(address owner) external view returns (uint256 balance){
        return ownershipTokenList[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){
        cats = ownershipTokenList[_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 _findIndex(uint256[] storage list, uint256 _tokenId) private view returns (uint256) {
        for (uint256 index = 0; index < list.length; index++) {
            if (list[index] == _tokenId) {
                return index;
            }
        }
        require(false, "Token not found in list");
        return 0;
    }
    
    function _removeIndex(uint256[] storage list, uint256 _index) private {
        require(_index < list.length, "Attempt to remove element outside of array bounds");
        list[_index] = list[list.length-1]; // doesn't matter if its the end of the list, no harm done
        list.pop();
    }
    
    function _transfer(address _from, address _to, uint256 _tokenId) internal {
        if (_from != address(0)) {
            uint256 index = _findIndex(ownershipTokenList[_from], _tokenId);
            _removeIndex(ownershipTokenList[_from], index);
        }
        ownershipTokenList[_to].push(_tokenId);
        kittyIndexToOwner[_tokenId] = _to;

        // 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

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

What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
this function is linear as we loop through an array

How could the storage design be changed to make the getAllCats function constant?
by calling the balanceOf ( ownersCats) as this part of the contract that has been initialized first and will save gas from doing a loop through an array (getAllCatsFor) function.

1 Like

1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
Constant - instantiate variables then add to the end of the list.

2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
linear - has to iterate through the entire 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.
store a Mapping to an array ( address => “array of owners kitties”). The getAll cats function becomes constant but then the transfer function increases in complexity because you have to update your array of kitties.

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

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

  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.

// MY solution for getAllCatsFor function
mapping (address => uint256[]) public ownersKitties;
function getAllCatsFor(address _owner) external view returns (uint[] memory){
   return ownersKitties[_owner];
}

My solution would be not to store owners by token id and number of tokens by owners. I would use mapping of list, which would allow to find all tokens that user owns by inputting address and giving a list of tokens ids as an output.

1 Like
  1. Constant
  2. Linear
  3. Ok, so here I would create a mapping that maps an address to an array of Kitty IDs:
mapping (address => uint[]) numberOfKittiesPerOwner;

Then I would insert a push functionality into the _createKitty() function:

numberOfKittiesPerOwner[_owner].push(newKittenId);

And then I would re-write the getAllCatsFor() function like this:

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

These two lines of code will map all the kitties (via their ids) to there respective address. I checked the code and it works.

But here’s the snag: the transfer of ownership will become incredibly complex. You would have to delete the kitten out of your array by setting the last kitten in your array equal to the kitten you want to transfer and then popping the last kitten out of your array and then you would have push that kitten to the receiving address and I’m getting confused just typing this out…
Although it’s not impossible to do this, it is very inefficient and complicated and the golden rule of code is to keep things as simple as possible. That’s why in this case I think it is better to keep the array, even if it is more time consuming and expensive.

1 Like

1 is constant
2 is linear
3 getAllCats function would become constant with a mapping of an array of ids from the kitties

mapping (address => uint[]) thisAddressKitties;

where we can push the ids into and then get the array as the result, but to transfer ownership would be a lot more complicated and expensive if we store that information like this.

1 Like
  1. O(1)
  2. O(n)
  3. We could store a mapping of the owner’s address to an array of his cats:
    mapping(address=>uint256[])
    This would make the time complexity of getAllCats O(1). The drawbacks are: it requires more storage space, and it complexifies the _transfer function.
1 Like
  1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
    Ans: Constant (just append at the end of the array).

  2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
    Ans: Linear (loop through array).

  3. How could the storage design be changed to make the getAllCats function constant? Discuss benefits and drawbacks with your implementation.
    Ans: In this case I implemented a new mapping ownerToKitty, as shown in the code, as well as below changes:

In function _createKitty():
// New
ownerToKitty[_owner] = newKittenId;

// Modified function getAllCatsFor()
function getAllCatsFor(address _owner) external view returns (uint256){
return ownerToKitty[_owner];
}

The benefits are constant execution time and efficiency for cats creation and retrieval, but the drawback is it can only hold one record of cat per owner.

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;

    // New
    mapping (address => uint256) public ownerToKitty;


    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);
    }
    
    // Modified
    function getAllCatsFor(address _owner) external view returns (uint256){
        return ownerToKitty[_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;

        // New
        ownerToKitty[_owner] = 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. What is the time complexity of creating new cats? (constant or linear w nr of cats).
    Constant

  2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
    Linear, the larger the array, the higher the cost

  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.
    On a side note, I deleted the first mapping (kittyIndexToOwner) and placed the owner in the struct. The ownerOf function can then request it by “return kitties[_tokenId].owner;”

I used

mapping (address => uint256[]) kittyOwnerToKittyArray;
function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
        return kittyOwnerToKittyArray[_owner];
 }

It makes for a much smaller array that’s cheaper to use (unless 1 address starts owning hundreds of cats)

Ofcourse this makes for a bit longer transfer fucntion but by using while it only looks until it found the right cat, replaces it with the last in the array and then pop the last one

            uint256 lastKitty = kittyOwnerToKittyArray[_from].length-1;
            uint i=0;
            while(kittyOwnerToKittyArray[_from][i] != _tokenId){
                i++;
            }
            kittyOwnerToKittyArray[_from][i]= kittyOwnerToKittyArray[_from][lastKitty];
            kittyOwnerToKittyArray[_from].pop();
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;
        address owner;
    }

    Kitty[] kitties;

    mapping (address => uint256[]) kittyOwnerToKittyArray;

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

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

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

    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 kittyOwnerToKittyArray[_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),
            owner: address(msg.sender)
        });
        
        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 {
        kittyOwnerToKittyArray[_to].push(_tokenId);
        
        kitties[_tokenId].owner = _to;
        
        if (_from!=address(0)){
            uint256 lastKitty = kittyOwnerToKittyArray[_from].length-1;
            uint i=0;
            while(kittyOwnerToKittyArray[_from][i] != _tokenId){
                i++;
            }
            kittyOwnerToKittyArray[_from][i]= kittyOwnerToKittyArray[_from][lastKitty];
            kittyOwnerToKittyArray[_from].pop();
        }
        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
      return kitties[_tokenId].owner == _claimant;
    }
}
1 Like

1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
Complexity O(1). Push to array is a constant operation

2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
Complexity O(n). We loop through all catID’s

3. How could the storage design be changed to make the getAllCats function constant? Discuss benefits and drawbacks with your implementation.
To make getAllCatsFor(_owner) constant we could add a:
mapping(address => uint[]) catsOfOwner;
the tradeoff is bigger storage usage and therefore bigger gas costs

1 Like
  1. Constant, as new cats are only pushed to the end of the array.

  2. Linear, as the always growing array has to be search from start to finish for each call.

  3. One possibility is to add the following: mapping (address => uint256[]) ownersToKittens;
    This will hold all kittens for specific owners. It will add storage costs and complexity when transferring kittens between accounts since we’ll need to traverse the kittens array to remove the transferred kitten.

1 Like
  • The time complexity of creating new cats is constant as it doesn’t matter how many already exist in storage.

  • The time complexity of getAllCatsFor() is linear as the size of the list/ array matters because it will take more time to loop through.

  • The storage design could be changed to make getAllCatsFor() constant by adding a mapping to an address of an array of cats owned.

// added mapping array of cats owned
    mapping (address => uint256[]) kittiesOwned;

My changes to getAllCatsFor()

function getAllCatsFor(address _owner) external view returns (uint[] memory cats){
       /*
       ****
       * without the added mapping above, this makes the time complextity - linear
       ****
       
       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;
       */
       
       /*
        ****
        * with the mapping added above, this makes the time complexity- constant
        ****
        */
        return kittiesOwned[_owner];
   }
1 Like

The time complexity for creating new cats is not dependant on the number of cats existant. It is constant.
Each new cat simply get’s pushed to the end of the array. This doesn’t require more work, as the array expands.

The time complexity for the getAllCatsFor function is linear w/ the nr of cats.
We’re looping though the entire array, meaning one more loop for every cat created.

If I wanted to create a function with constant performance I’d do this mapping:

constant getAllCatsFor.sol
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);
    }
    
    function getAllCatsFor(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;

        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;
        catsOwned[_to].push(_tokenId); 
        
        uint256 arrayCatPosition;
        uint256 endPos = catsOwned[_from].length - 1;
        
        for(uint256 i=0; i < catsOwned[_from].length; i++){ //delete token from the previous owner
            if(catsOwned[_from][i] == _tokenId) {
                catsOwned[_from][i] = catsOwned[_from][endPos];
                catsOwned[_from].pop();
            }
        }
        

        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;
  }
}

The drawbacks are: Transfering performs linear with the # of cats owned, because we now have to find the position of the cat ID in the ownership-array. And it takes up more memory, because of the extra arrays stored.

1 Like
  1. The time is constant (O(1)) as the creation process is independent of the existing number of cats.

  2. The time is linear (O(n)) as the creation process is dependent on the existing number of cats.

  3. “My” solution.

// SPDX-License-Identifier: GPL-3.0
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;
        // added
    mapping (address => uint256[]) ownedCats;

    

    function balanceOf(address owner) external view returns (uint256 balance){
        // changed
        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){
        //changed
        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;
        
        //added
        ownedCats[_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 {

        kittyIndexToOwner[_tokenId] = _to;
        //added
        ownedCats[_to].push(_tokenId);

        if (_from != address(0)) {
            //added
            removeCat(_from, _tokenId);
        }

        emit Transfer(_from, _to, _tokenId);
    }
    
    //added
    function removeCat(address _owner, uint256 _tokenId) internal {
        for(uint256 i=0; i<ownedCats[_owner].length;i++){
            if(ownedCats[_owner][i]==_tokenId){
                ownedCats[_owner][i]=ownedCats[_owner].length-1;
                ownedCats[_owner].pop;
                break;
            }
        }
        
    }

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

}

The previus getAllCatsFor() add to go though all the existing cats to search for the ones belonging to the user making it inficient and expensive. While we also rely on a for loop in the new solution to removeCat() in the _transfer() the iteration is made on a much smaller array with only the ownedCats from the address used.

1 Like

Just watched the solution video. It looks like the original solution was the best because of the use of the view keyword in getAllCatsFor().

1 Like

Here are my answers and proposed solution to ERC721 assignment.

  1. The time complexity of creating new cats is constant.

  2. The time complexity of the getAllCats function is linear since we must search the entire length of our array.

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[])ownedKitties;

    

    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){
        cats = ownedKitties[_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]++;

        kittyIndexToOwner[_tokenId] = _to;
        ownedKitties[_to].push(_tokenId);

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

        // Emit the transfer event.
        emit Transfer(_from, _to, _tokenId);
    }
    
    function _nolongerOwner(address _from, uint _tokenId) internal
        {
           uint oldCat = ownedKitties[_from][ownedKitties[_from].length-1]; 
           for(uint i = 0; i < ownedKitties[_from].length; i++)
            {
                if(ownedKitties[_from][i] == _tokenId)
                    {
                        ownedKitties[_from][i] = oldCat;
                        ownedKitties[_from].pop();
                    }
            }
        }

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

}
1 Like

1. What is the time complexity of creating new cats? (constant or linear w nr of cats).
The time is constant. It doesn’t need to run over the kitties array to create a new cat.

2. What is the time complexity of the getAllCatsFor function? (constant or linear w nr of cats).
It is linear. The function checks for each Kitty in kitties[] if kittyIndexToOwner[i] == _owner.

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.

CryptoKitties.sol
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[]) ownerToKittyArray;


    function balanceOf(address owner) external view returns (uint256 balance){
        return ownerToKittyArray[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){
        cats = ownerToKittyArray[_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 {
        // add KittenId to new owner array
        ownerToKittyArray[_to].push(_tokenId);
        
        kittyIndexToOwner[_tokenId] = _to;

        if (_from != address(0)) {

            uint kittyToKeep = ownerToKittyArray[_from][ownerToKittyArray[_from].length - 1];
            
            for (uint i; i < ownerToKittyArray[_from].length; i++){
                if (ownerToKittyArray[_from][i] == _tokenId) {
                    ownerToKittyArray[_from][i] = kittyToKeep;
                    ownerToKittyArray[_from].pop();
                    break;
                }
            }
        }

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

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

}

This solution reduces the cost of getting the cats of a specific address but increases the cost of transferring the token, however, the loop tends to run over a smaller array.

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

It is pretty much constant since the operations that the function has to perform are always the same. I did notice minor variances in the gas execution cost depending on the number of digits that one inputs for the genes unit, the bigger the number it cost slightly more, but overall the discrepancy is too little and I assume that is related to the size of the unit and how it is stored. But overall the time complexity is constant O(1).

Also, can someone explain to me why adding the first element to an array is always a more expensive transaction than the rest?

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

It is a linear time complexity or of O(n), this is because the function will always have to iterate one more time per element added to the array. In terms of gas consumption, I’ve recorded that the execution cost increments by 3019 gas for every new cat added to the array.

  1. How could the storage design be changed to make the getAllCats function constant? Discuss benefits and drawbacks with your implementation.

We could implement a mapping that links an address to the collection of tokens that it owns. That way whenever we need to query all the cats that an address has, the execution cost will be constant because it will not have to loop through the entire array of tokens to find the desired one.

I manage to do part of the assignment on my own, but couldn’t because I was trying to avoid having to use a loop at all, only to find out that there no other way of deleting the desired ID. But it was very interesting to really grasp the core understanding that time complexity cost only matters whenever we are adding information to the blockchain.

Benefits:
It allows us to have a constant time complexity function.

Drawnbacks:
It makes a lot more expensive the creation of tokens and the transfer of tokens.

I’ll leave the code down below:

pragma solidity ^0.8.0;

contract kittyContract{
    
    // DECLARATIONS
    string public constant name = "TestKitties";
    string public constant symbol = "TK";
    
    mapping (uint => address) public kittyIndexToOwner;
    mapping (address => uint) public ownershipTokenCount;
    mapping (address => uint[]) catsOwnedBy;
    
    struct Kitty{
        uint256 genes;
        uint64 birthTime;
        uint32 mumID;
        uint32 dadID;
        uint16 generation;
        
    }
    
    Kitty[] kitties;
     
    
    // EVENT 
    event Transfer(address indexed from, address indexed to, uint indexed tokenIndex);
    event Birth(address owner, uint256 kittenID, uint256 mumID, uint256 dadID, uint256 genes);
    
    // FUNCTIONS
    function balanceOf(address _owner) external view returns(uint256){
       return ownershipTokenCount[_owner];
    }
    
    function totalSupply() public view returns(uint256){
        return kitties.length;
    }
    
    function owns(address _claimant, uint256 _tokenID) internal view returns(bool){
       return kittyIndexToOwner[_tokenID] == _claimant;
    }
    
    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;
        
        _transfer(address(0), _owner, newKittenID);
        
        emit Birth(_owner, newKittenID, _mumID, _dadID, _genes);
        return newKittenID;
    }
    
    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){
        return catsOwnedBy[_owner];
    }
    
    // INTERNAL/PRIVATE FUNCTIONS
    
    function _transfer(address _from, address _to, uint256 _tokenID) internal {
        ownershipTokenCount[_to] ++;
        kittyIndexToOwner[_tokenID] = _to;
        catsOwnedBy[_to].push(_tokenID);
        
        if(_from != address(0)){
            ownershipTokenCount[_from]--;
            _deleteCat(_from, _tokenID);
            
        }
        emit Transfer(_from, _to, _tokenID);
    }
    
    function _deleteCat(address _owner, uint256 _tokenID) internal {
        
        uint256 lastID = catsOwnedBy[_owner][catsOwnedBy[_owner].length - 1];   
        
        for (uint256 i = 0; i < catsOwnedBy[_owner].length; i++){
            if (catsOwnedBy[_owner][i] == _tokenID){
                catsOwnedBy[_owner][i] = lastID;
                catsOwnedBy[_owner].pop();
            } 
        }
    }
}
    


1 Like