Creating a storage factory smart contract in Solidity and interacting with it (notes from Freecodecamp)

Creating a storage factory smart contract in Solidity and interacting with it (notes from Freecodecamp)

I'm following Freecodecamp's wonderful web3 course on Youtube (link here => youtube.com/watch?v=gyMwXuJrbJQ&ab_chan..), and to make sure that I remember what I've learnt, I like to take notes. Here, I'll post my notes from Lesson 3: Remix Storage Factory. I'll make my disorganized notes human readable so it will look like a tutorial.

Please note that all the credit goes to Patrick Collins and incredible Freecodecamp team/community, I am just delivering what I've seen in written format so that I won't have to rewatch the tutorial in the future. I hope you too can benefit from it.

As this is the 3rd lesson, there are some fundamentals missing here. It also takes the SimpleStorage smart contract introduced to us in Lesson 2 of the Freecodecamp course. You might want to check it out before reading this post, although I'll try to explain what's going on in the SimpleStorage contract. If I'm mistaken at any point, please please feel free to correct as it's been more than a week since I've watched the Lesson 2, so I might have forgotten some of the reasons why we're doing things as we do :)

We will use Remix IDE for this post, so make sure you go to https://remix.ethereum.org/ and be ready for hacking!

Note: You can find the code for all the contracts here in Patrick's GitHub repo, here => github.com/PatrickAlphaC/storage-factory-fcc

Simple storage smart contract

Let's start by looking at the SimpleStorage smart contract .

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {

    uint256 favoriteNumber;

    struct People {
        uint256 favoriteNumber;
        string name;
    }
    // uint256[] public anArray;
    //create an array of type People, which is a struct and name it people. It is also public.
    People[] public people;

    mapping(string => uint256) public nameToFavoriteNumber;

    function store(uint256 _favoriteNumber) public {
        favoriteNumber = _favoriteNumber;
    }

    function retrieve() public view returns (uint256){
        return favoriteNumber;
    }

    function addPerson(string memory _name, uint256 _favoriteNumber) public {
        people.push(People(_favoriteNumber, _name));
        nameToFavoriteNumber[_name] = _favoriteNumber;
    }
}

All Solidity smart contracts start with a SPDX-License-Identifier. This is a license that the smart contract is under. In this case, it is MIT. If I'm not mistaken, that means it's open source.

Then, we have to define the Solidity compiler version. ^0.8.0 means "anything above 0.8.0" is okay.

Then, we use the contract keyword to let the compiler know that we're creating a smart contract. Our contract is named SimpleStorage.

Since Solidity is a typed language, we need to define the type of the variable we're creating. I'm a Javascript developer normally, so my mind works in terms of JS. This is how I see it: Instead of saying let favoriteNumber or const favoriteNumber, we say uint256 favoriteNumber. If we were to define a string, we'd have to say string favoriteNumber.

Now we have our favoriteNumber variable ready, we're creating a struct of People. Structs are like objects in Javascript. They take key/value pairs. In this case, they're uint256 favoriteNumber and string name.

Then, we create a dymamic array named people with the type People struct. I don't know Typescript or any other typed language other than Solidity yet, so this part was (and still is) quite confusing for me. It's like, this people array can only take People structs as values. It cannot take a single string or an array of strings or uints or anything other than People structs. It can't take other type of structs also if I'm not mistaken; it can only and only take People structs as values and that's all.

Also, with this syntax People[] public people, we're telling the compiler that this is a dynamic array of People structs. That is to say, the length of the array is not defined-because you can do that in Solidity, i.e. you can have arrays with predefined length.

On more thing about the array, you'd notice the public keyword here. It means that this variable is public, i.e. it can be seen and called outside of the contract. If we'd say private instead of it, you couldn't access it from outside of the contract.

Then we have a mapping of type string to uint256. Mappings too confused me a lot in Solidity. They're like objects in JS, but instead of taking multiple values like structs, they take only one key/value pair and they work similar to arrays in Javascript. They come in quite handy though, especially when mapping addresses.

Our first function, store is a public function that takes a single parameter _favoriteNumber of type uint256 and changes the favoriteNumber variable to the value of _favoriteNumber parameter. The underscore (_) is just a convention in Solidity, it's for parameters.

Then, we have a function retrieve that is public and view. This means that it can be seen and called from outside of the contract. It returns a uint256 value and it DOES NOT cost gas. That's because it doesn't change the state of EVM (Ethereum Virtual Machine). Snce there's no change, there's no gas cost.

Then, we have a function addPerson that is public. It takes two parameters _name and _favoriteNumber of type string and uint256. Now, the first line inside this function, people.push(People(_favoriteNumber, _name));, does the following: it takes the parameters and creates a People struct with them and pushes this new People struct into the people array.

The second line, nameToFavoriteNumber[_name] = _favoriteNumber;, does the following: it takes the parameters and creates a key/value pair with them and puts this new key/value pair into the nameToFavoriteNumber mapping. You see, in the mapping mapping(string => uint256) public nameToFavoriteNumber; we have a string and uint256, so the _name goes as the string, and _favoriteNumber goes as the uint256.

That's all for the SimpleStorage.sol contract. You can paste this contract to remix, deploy and play with it. You'll notice that so far we can only retrieve the favoriteNumber as we only created a getter function for that.

StorageFactory smart contract

For this part, we'll create a new contract in Remix IDE, named StorageFactory.sol. The idea in this one is to create a new contract that can create other smart contracts. Yes, smart contracts can do that. "The ability for contractsto seamlessly interact with each other is known as composability. "

Let's check out the final version of StorageFactory smart contract:


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./SimpleStorage.sol";

contract StorageFactory {

    SimpleStorage[] public simpleStorageArray;

    function createSimpleStorageContract() public {
        SimpleStorage simpleStorage = new SimpleStorage();
        simpleStorageArray.push(simpleStorage);
    }

    function sfStore(uint256 _simpleStorageIndex, uint256 _simpleStorageNumber) public {

        simpleStorageArray[_simpleStorageIndex].store(_simpleStorageNumber);
    }

    function sfGet(uint256 _simpleStorageIndex) public view returns (uint256) {

        return simpleStorageArray[_simpleStorageIndex].retrieve();
    }
}

As you can see, after our pragma solidity line, we have this import statement => import "./SimpleStorage.sol";. Importing works like Javascript. We can either copy paste the code, or import it like this to make it much more manageable.

Now, with SimpleStorage[] public simpleStorageArray; we create a public array named simpleStorage (minuscule) with the type of SimpleStorage (majuscule) contract that we've just imported. So, this array will contain only and only SimpleStorage contracts. Cool, right?

The public function createSimpleStorageContract does two things: In the first line, it creates a variable called simpleStorage (minuscule) with the type of SimpleStorage (majuscule) contract. It does this with the new keyword. When Solidity sees the 'new' keyword here in this context, it says "aha! We're going to deploy a new SimpleStorage contract." In the second line, it pushes this new contract into the simpleStorageArray array.

The function sfStore ("sf" stands for "storageFactory") takes two uin256 parameters: the index of the contract just created and pushed into the array, and the favorite number that was in the simpleStorage contract.

Remember, the store function that stored the favorite number in simpleStorage.sol was as such:

function store(uint256 _favoriteNumber) public{
favoriteNumber = _favoriteNumber;
}

Then, with the line simpleStorageArray[_simpleStorageIndex].store(_simpleStorageNumber); it stores the favorite number given in uin256 _simpleStorageNumber parameter to the simpleStorageArray at the index given in the parameter _simpleStorageIndex. It does so by calling the store function in simpleStorage.sol that I've shown above.

I know it sounds complex, and maybe it is, it's just, we're writing a function so that we can choose whatever SimpleStorage we've created using the createSimpleStorageContract function using its index in the array so that we assign it a favorite number.

The next and the last function in this contract, sfGet ("sf" standing for "storageFactory" again) is a public getter function and we know that it does not cost us any gas because it contains the view keyword in it. It takes the index of the simpleStorage contract we've created via createSimpleStorageContract function and returns the favorite number that was in that contract by callng the retrieve function in simpleStorage.sol contract. That retrieve function was as such:

//retrieve function in simpleStorage.sol
    function retrieve() public view returns (uint256){
        return favoriteNumber;
    }

    ```

Now say, if I opened the Remix IDE, compiled and deployed the StorageFactory.sol contract, and then I called the sfGet function, create a bunch of contracts using the createSimpleStorageContract function, and say, called the sfStore function with the parameters 0,22 and then called the sfGet function with the parameter 0, I would get 22 as the favorite number. If I called the sfStore function with the parameters 2,378 and then subsequently call the sfGet function with the parameter 2, I would get 378 as the favorite number.

That's it. Now, we have one more thing to learn in this post, inheritance.

Inheritance

In the tutorial Patrick shows us how we can create a coype of a contract and even overriding it in this or that way. For that, we need to create a new contract called ExtraStorage.sol. Now, if we import SimpleStorage.sol contract in this new ExtraStorage.sol contrat and define it as contract ExtraStorage is SimpleStorage instead of just declaring contract ExtraStorage as we'd normally do, this new ExtraStorage contract will have all the logic the variables, the functions and all in SimpleStorage contract.

But he goes even further. What if we wanted to change some things in our copy of SimpleStorage (which is our ExtraStorage contract) and we wanted to add some more functionality to it?

Then we'd need to override it. Check this snippet out:


// SPDX-License-Identifier: MIT

pragma solidity 0.8.8;

import "./SimpleStorage.sol";

contract ExtraStorage is SimpleStorage {
    function store(uint256 _favoriteNumber) public override {
        favoriteNumber = _favoriteNumber + 5;
    }
}

You see, we're overriding the store function in SimpleStorage.sol contract. This is done by the override keyword. We're just adding the number 5 to everyone's favorite number for heuristic purposes.

But this can't work by itself. It wouldn't override like so. We need one more extra step so that it would work seamlessly. We need to go back to our original SimpleStorage.sol contract and add the virtual keyword to the function we want to override later on. In this case, it is the store function in SimpleStorage.sol.

So, we open the SimpleStorage.sol contract, and change the store function to this:


    function store(uint256 _favoriteNumber) public virtual {
        favoriteNumber = _favoriteNumber;
    }

Now, this store function is "overrideable". We can create a copy of SimpleStorage.sol and override its store function as we please.

That's all folks! I hope you enjoyed this post. I hope to continue publishing my notes if everything goes according to the plan. Yet I highly suggest that you check the original tutorial on Freecodecamp's Youtube channel, it is phenomenal.

Keep calm & happy coding!