Additional Solidity Concepts - Discussion

Hey @berto, hope you are well.

Your contract does work correctly from my side (I deployed it on remix), I think the problem you have comes from the way you are inputing the values to the contract functions.

like updateBalance, you just pass: 1,20000. Which should be “1”, “20000”. Or open the function fields and fill each normally.

image

Carlos Z

Okay, well before I solve this code, I’ve a question about using Remix IDE:

The Remix UI I’m using looks somewhat different than the UI in the video, and I’m not clearly seeing where the values (user Id, balance, etc) are represented whenever I execute a function. I read what is in the terminal, but it does not appear to show any data values being changed, nor do I see this anywhere else in the Remix UI. Hard to fix a problem if I cannot see the results of computations.

Sometimes I see values under the function UI buttons, but sometimes not (???)

How can I see the data values?

Okay I figured out the UI enough to see the data.

Although I’m still wondering how I can see data in the transaction records in the terminal ?
Is it there, but just in hex code?

I’m very happy to see that Solidity includes an event-based programming paradigm.

I was a Flash/ActionScript programmer for more than 10+ years, and I got very used to event-based programming … well before I got into JavaScript or anything else. So I’m enthused to get back into event-based programming with Ethereum smart contacts!

Questions:

1 - emitting an event can also be listened to within a contract AND by other contracts or dApps?That sounds really powerful and full of interesting potentials. I want to learn more about this!

2 - does indexing event data to nodes increase the gas of a contract execution?

3 - by indexing, does this mean that the event data is written permanently into the Ethereum blockchain? if so, then NOT indexing means that data within a contract is never really publicly available … with the exception of token transfers (which are inherently logged on the blockchain?). Correct? I’m just really trying to understand what this indexing really does …

4 - does indexing contribute to on-chain market analysis?

5 - from the Events Quiz, it seemed to me that maybe all 3 of these answers may be correct (?). I’m the least, I thought I heard Fillip say that external contracts and dApps can listen to a contracts events? So isn’t that answer true also?

image

1 Like
pragma solidity 0.7.5;

contract MemoryAndStorage {

    mapping(uint => User) users;

    struct User{
        uint id;
        uint balance;
    }

    function addUser(uint id, uint balance) public {
        users[id] = User(id, balance);
    }

    function updateBalance(uint id, uint balance) public {
         User storage user = users[id];
         user.balance = balance;
    }

    function getBalance(uint id) view public returns (uint) {
        return users[id].balance;
    }

}
1 Like
pragma solidity 0.5.7;

contract MemoryAndUsage {
    
    mapping(uint => User)users;
    
    struct User{
        uint id;
        uint balance;
    }
    
   
    function addUser(uint id, uint balance) public {
        users[id] = User(id, balance);
    }
       
    function updateBalance(uint id, uint balance) public {
        users[id].balance = balance;
    }
    
    function getBalance(uint id) public view returns(uint) {
        return users[id].balance;
    }
    
    
}
1 Like

Hi @jon_m,

I have a question concerning this assignment:

Let’s use the following example values:

  • I execute the function addUser with the values id = 1, balance = 100

  • This means I create a new mapping (key-value pair) with the key equal to 1 and the value equal to a variable of type User with the above-mentioned values for id and balance

  • The new mapping looks like this: users(1 => User(id =1, balance = 100))

  • I understand how the update of the balance works with solution 1 (users[id].balance = balance;)

  • However, I must be missing a point because I do not understand how the second solution works in detail (see below)

function updateBalance(uint id, uint balance) public {
         User storage user = users[id];
         user.balance = balance;

Inserting the values id = 1 and balance = 200 as user input in the updateBalance function does, according to my understanding, the following:

User storage user = users[id];
  • The left side of the equation creates an empty variable of the type User, which is called user, and stores it as a state variable (because I am using storage)
  • Inserting “1” in the right side of the equation retrieves the value of the mapping with the key equal to “1” (which is the variable we created earlier using the addUser function)
user.balance = balance;
  • The second line of the function then updates the balance value of this variable with the user input
  • The following point is my issue:

According to my understanding, the operation of the first line in this function simply creates a new variable of the type User and stores it as a stand-alone state variable and assigns values which we retrieve of an existing mapping which was created before. However, it is not clear to me when the function accesses the mapping users(1 => User(id =1, balance = 100)) and changes the balance to 200. To me, the only thing the function does is that it accesses the newly created variable “user” and changes its balance to 200, but this variable called user is never inserted in the mapping that was created earlier, instead it is saved as a stand-alone state variable.

  • Finally, the function getBalance:
function getBalance(uint id) public view returns(uint) {
        return users[id].balance;
    }

When the function getBalance is executed, the function accesses the mapping with the key equal to 1 and retrieves the corresponding value for balance. However, according to me, we never actually accessed this mapping and updated the balance when the updateBalance function was executed (as explained above), so the value should still be 100 and not 200.

Since this solution provides the desired result, it obviously works, so I must misunderstand/miss something :smiley:

I hope you understand my issue and can help me clarify what I am missing in my approach.

1 Like

Hi @Alex_13,

Absolutely … your question is a very good one, and you’ve explained it very well. It also shows that you’re thinking deeply about what the code is actually doing :muscle:

Let’s move through your example step by step …

From this I can see you have a good understanding of what’s happening here. The mapping does already exist but, before any User instances (of type User) have been added, it is empty. What you are creating is a new User instance and adding it to the mapping. The new User instance is based on the User struct and contains two properties (id and balance).

Correct — this is a good visual representation of the data stored in the mapping at this point.

By assigning users[id] to a local storage variable, we create a local “pointer” called user . The important thing to understand here is that this “pointer” does not create an additional, separate state variable, and it also doesn’t create and store a copy of the User instance mapped to the id (key) input into the function (1 in this example).

Instead, the pointer only references this User instance in the mapping, creating a temporary “bridge” to the mapping during execution of the function.

Any value which is assigned to a property of the local “pointer” (user) …

user.balance = balance;

… is effectively updating that same property of the specific User instance in the mapping which is referenced by the pointer. This enables a specific user’s balance (stored persistently in the mapping) to be reassigned with the new balance (input into the function), before this “bridge” is lost when the function finishes executing.

So hopefully you can also now see why the getBalance function successfully retrieves the new balance whichever method is used.

In fact, even though the code is quite different, the low level operations performed by this method (using a local storage variable as a “pointer”) are effectively the same as those performed by …

users[id].balance = balance;

Hopefully, my explanation above enables you to understand why this is so. As a result, both alternative solutions consume more or less the same amount of gas. And both consume less gas than a solution involving a local memory variable, because neither creates a separate copy of the User instance …

// This alternative solution works, but it is not gas-efficient
function updateBalance(uint id, uint balance) public {
    User memory user = users[id];
    user.balance = balance;
    users[id] = user;
}

So, you may now be wondering, why bother to create a pointer with a local storage variable. From within a function, it is certainly simpler and more concise to just assign data to persistent storage by referencing a mapping directly, as we do with   users[id].balance = balance;

However, sometimes using a local storage variable as a pointer can be a useful intermediate step when performing more complex operations involving multiple properties and their values: it can help us to set our code out into separate logical steps, making it easier to read and understand.

Let me know if anything is still unclear, or if you have any further questions :slight_smile:

1 Like

Hi @jon_m ,

Wow! First of all thank you so much for this comprehensive, comprehensible and detailed response. You provided an answer to all my questions and evend added additional helpful insights (e.g. when a particular approach/solution is more useful than another, storage costs, etc.). This is really awesome :slight_smile:

I need to review the concept and functionality of a pointer in more detail, although I remember it from the C++ course. I will answer to you again in more detail once I can tell for sure whether or not I was able to solve my outstanding questions using your remarks.

Thanks again!

1 Like

Hey Alex,

No problem — take your time working through the different points. These things often take time to digest and fully understand …

1 Like

This is my solution to the MemoryAndStorage problem.

pragma solidity 0.7.5;
contract MemoryAndStorage {

mapping(uint => User) users;

struct User{
    uint id;
    uint balance;
}

function addUser(uint id, uint balance) public {
    users[id] = User(id, balance);
}

function updateBalance(uint id, uint balance) public {
     User storage user = users[id];
     users[id].balance += balance;
}

function getBalance(uint id) view public returns (uint) {
    return users[id].balance;
}

}

1 Like

In the MemoryAndStorage SOLVED video I copied the code users[id].balance = balance; . When you go to get the balance it’s still the same it did not update. Why didn’t it write users[id].balance += balance; instead?

Hi @JonPag,

Your solution code for addBalance() works, but the first line in the function body …

… is redundant and can be removed. You are defining a local storage variable user, but then not referencing it in your second line (i.e. you’re not using it).

The second line …

… does everything that’s needed, anyway. It adds the amount (input into the function) to the user’s existing balance in the mapping (users[id].balance). As this line of code is updating the mapping, the new balance will be stored persistently in the contract state.

Even though the initial idea of this assignment is to update the existing balance by replacing it with the one input, I actually think that it makes more sense to do what you have done, and add an amount to the existing balance using the addition assignment operator += (instead of the assignment operator = ). However, if you add to, instead of replacing, the existing balance, I think your code would be clearer and more readable if you also change the name of the balance parameter to amount, because you are adding an amount to the balance, rather than replacing it with a new balance.

An alternative solution, using the local storage variable you’ve defined in the first line, would be…

function updateBalance(uint id, uint amount) public {
     User storage user = users[id];
     user.balance += amount;
}

By assigning users[id] to a local storage variable, we create a local “pointer” called user.

This points to (references) users[id] ===> the user record in the mapping (users) which is mapped to the id input into the function.

The user records are struct instances with the properties defined in the User struct, and the local “pointer” has these same properties (notice that it has also been declared with the data type User). Any amount added to a property of the local “pointer” (user) is effectively added to that same property of a specific user record in the mapping …

user.balance += amount;

The local variable user creates a temporary “bridge” to the mapping during execution of the function. This enables a specific user’s balance (stored persistently in the mapping) to be increased by the amount input into the addBalance() function, before this “bridge” is lost when the function finishes executing.

Let me know if anything is unclear, or if you have any questions about any of these points.
(I’m going to reply to your other post to answer your question)

Let’s work through an example, with the updateBalance() function coded as follows…

function updateBalance(uint id, uint balance) public {
    users[id].balance = balance;
}
  1. Call addUser with  1, 100
  2. Then, call getBalance with  1
    ==>  the user’s initial balance of 100 should be returned in the Remix panel below the blue getBalance button
  3. Call updateBalance with  1, 300
  4. Then, call getBalance with  1
    ==>  the user’s updated balance of 300 should be returned in the Remix panel below the blue getBalance button. The user’s balance should no longer be 100.

As I mentioned in my feedback, if we use an assignment operator (=), the new balance that is input replaces the original balance; it isn’t added to it. So, the updated balance is 300, not 400 (100 + 300).

… because using the addition assignment operator (+=) will add the amount that is input to the original balance; Instead, the initial idea of this assignment is to update the existing balance by replacing it with a new one (the balance input).

I hope this clarifies things, but just let me know if it doesn’t, or if you have any further questions :slight_smile:

Hello, I really cannot figure out why the line under the “TODO” won’t compile. Could you give me some hints? I wasn’t able to clarify via google. Since I’m passing by reference I would expect to be able to assign x_storage the same way I assign x

pragma solidity 0.7.5;

contract DataLocation {

  string public x = "contract scope: original";
  
  function setX(string memory text) public {
    // this works
    x = "contract scope: assigned with x=";
    

    // passing by reference
    string storage x_storage = x;
    

    // TODO:
    // Understand why this won't compile
    // x_storage = text;
    
    // just tests
    x = "altered"; x = x_storage; // this re-set "altered", which means the reference works
  }
}
1 Like

Hi @fedev,

Very good question! The way I understand it is as follows…

When we create a local storage variable in Solidity, this is called a storage pointer. This is what you’ve created here …

As you have stated in the comment, this creates a reference to the state variable x (not a copy). So, we can only assign by reference, and not by value, which I essentially understand to mean that we cannot replace x but only make changes to values that it “contains”. In Solidity, this is only possible with data structures that are arrays or struct instances, both of which contain other values which can be modified, instead of the data structure itself.

I think that this is, essentially, why the compiler will not allow you to use your local storage pointer x_storage to replace the whole string value saved in the state variable x.

Data structures are complex data types. Strings are also classed as complex data types in Solidity, and that is why we need to declare their data location when we define them as either parameters/arguments, local variables or return values (i.e. when used locally within functions). However, even though it’s possible theoretically, I cannot see any practical reason why we would want to create a local storage pointer to a string state variable, because, as far as I’m aware a string value is immutable in Solidity i.e. we can’t modify any of its internal properties, only reference them e.g. its length. I may be wrong about this particular point (any thoughts @thecil?), but what we definitely can’t do is replace the whole string value using a storage pointer.

What we can do with a storage pointer, however, is change or add a string value stored within an array or struct instance. We can also overwrite a whole struct instance if our storage pointer references the array which contains that struct instance.

I’ve created the following contract to demonstrate this. I would suggest you first do the following, and then experiment with it yourself, changing the code as you see fit…

  1. Call the setStringDirect() function.
  2. Call the blue-button getters, that have been automatically created, to see the changes/additions made by the setStringDirect() function to the various state variables.
  3. Call the setStringPointer() function.
  4. Call the blue-button getters, again, to see the changes/additions made by the setStringPointer() function to the state variables.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.5;

contract DataLocation {

    struct MyStrings {
        string a;
        string b;
    }

    string public x = "contract scope: original";
    MyStrings public stringStore;
    string[] public stringArray;
    MyStrings[] public structArray;
    mapping(address => string) public stringDictionary;
    mapping(address => MyStrings) public stringLog;
  
    // Direct assignment to storage (by value) 
    function setStringDirect(string memory text) public { 
        x = "contract scope: assigned with x=";
        x = text;

        stringDictionary[msg.sender] = text;
        stringDictionary[msg.sender] = "contract scope: assigned with x=";

        stringStore = MyStrings("red", "green"); 
        stringLog[msg.sender] = MyStrings(text, "blue");
        stringArray.push(text);
        structArray.push(MyStrings("white", text));
    }

    // Using storage pointer (assigning by reference)
    function setStringPointer(string memory text) public {    
        MyStrings storage y = stringStore;
        y.a = text;

        string[] storage z = stringArray;
        z.push(text);

        MyStrings storage w = stringLog[msg.sender];
        w.b = text;

        MyStrings[] storage j = structArray;
        j.push(stringStore);
    }
}

Bear in mind that some of code contained in the above contract is purely theoretical, in order to demonstrate what is possible in a concise and clear manner. In practice, you are likely to use, or come across, some examples more than others.

Here is a link to what the Solidity documentation has to say about this …
https://docs.soliditylang.org/en/latest/types.html#data-location-and-assignment-behaviour
However, it’s not the easiest of explanations to wrap your head around!

I hope this helps to answer your question. But do let me know if anything is still unclear, or if you have any further questions :slight_smile:

In terms of not modifying an string variable, it goes in the sense of cant change its internal value, like you could do with an integer by doing something like a =5; a = a -1, you could not do the same with an string (like if you want to remove a character only).

PD: Keep in mind that EVM is not friendly with strings, gas fees will go to the moon, is good to avoid strings or at least minimize its use to very specific case (like URI on erc721/1155).

Carlos Z

1 Like

Hi Jon, thank you for your reply!
You’ve been clear about the possible “workarounds” to avoid this kind of problem with direct assignment via whatever these storage inited variables inside the function body are :smile:
I’ve also read the docs where there is this line…

uint[] storage y = x; // works, assigns a pointer, data location of y is storage

I feel like “pointer” and “reference” in solidity may not behave the same like they do in c++, and that’s why I expected to be able to edit x using y.
Just like this would be legit in c++
( check https://onlinegdb.com/kvdXtDJ1l and click Run )
Since in Solidity we can modify x through y without explicitly accessing the pointer ("*y" in c++)

y.pop(); // fine, modifies x through y

I thought that y in solidity == *y in cpp but well… it’s not! :woozy_face:

@thecil I understood the concept: strings are evil, this is not python :joy:

2 Likes

Hi @fedev,

I don’t have any knowledge of C++ , but from what you’ve shown and said, it looks like you’re right when you say

In the example you’ve quoted from the documentation …

// contract state
uint[] x;     // imagine this already contains some uinsigned integers

// function body
uint[] storage y = x;
y.pop()       // this works
delete y;     // => COMPILER ERROR (this isn't possible)
delete x;     // this works

… we can remove the final value in the storage array x via the local variable and storage pointer y because we aren’t modifying the assignment of the array itself to the state variable x , only its internal “properties/attributes”. However, we can’t delete the whole array via the storage pointer, because this would be modifying the assignment of the state variable’s value.

Similarly, if the state variable x is defined as a string, then the following isn’t possible, because we are again trying to modify a value assigned to a state variable via a storage pointer …

// contract state
string x = "myString";

// function body
string storage y = x;
y = "newString";        // => COMPILER ERROR (not possible)

… we would have to assign the new string value to x directly …

// function body
x = "newString";       // this works
1 Like

As I understand it, the two are compatible: it gets executed every time the function is called but at the very beginning of it.

1 Like