Data Location Assignment

RE

However, your require statement will also not allow an input of 0. As we are replacing the existing balance, and not adding an amount to it, don’t you think we also need to be able to record balances of 0? As you have already identified, if we allow input values of >= 0 (i.e. all unsigned integers) then the require statement is no longer needed.

i) require on “id” allows a balance of zero, however and id value of 0 did not seem sane, and;
ii) the compiler issued a warning on infinite gas with the require statement - so I think generally as a rule of thumb I will avoid these unless it does enforce the contract as it seems it has the potential for an inefficient and possibly expensive contract cost?

Everything else you said makes perfect sense to me.

Thank you.

1 Like

Hi @walter_w,

Nice solution! … the corrected one :wink:
And good questions!

I’ll tackle them in the opposite order…

No… and as far as I am aware, defining a local variable (i.e a variable within a function) with data location storage is only used where we want to create a pointer (a reference) to a specific struct instance in persistent storage. This local variable doesn’t create a separate copy of the struct instance, which it would do if we defined it with data location memory (although this would only be stored temporarily, and lost once the function finished executing).

From within a function, the simplest way to assign data to persistent storage is to reference a state variable or mapping directly, without creating an additional local variable.

The more concise solution to this assignment is to directly assign the new balance to the mapping, by referencing a specific key…

users[id].balance = balance;

However, sometimes using a local variable 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.

With struct instances, we can define a local variable with either:

(1) Data location memory, to temporarily store a local copy of a struct instance, which we assign values to, before assigning it to persistent storage e.g. this would be another alternative solution to the assignment…

User memory user = users[id];
user.balance = balance;
users[id] = user;

OR

(2) Data location storage, to create a pointer, as you have done in your solution. Because this doesn’t create a local copy of the struct instance, it consumes less gas than using the local memory variable. In fact, the gas cost when using a local variable as a pointer is more or less the same as when assigning directly to a state variable or mapping without the pointer. This is because the low-level operations that are performed are essentially the same with either method.


The value or values assigned to the local storage variable (the pointer) will either update/replace pre-existing property values within the referenced struct instance in persistent storage; or it will create a new struct instance with these values if the one referenced does not already exist.

For example, let’s say you already have 5 User instances stored in your users mapping which are mapped to keys 1 to 5 inclusive. The balance property of each instance has a value of 10.

  • If you call your updateBalance function with an id argument of 5, and a balance argument of 20, then the balance of the instance mapped to key 5 will change from 10 to 20.

  • If you then call your updateBalance function with an id argument of 6, and a balance argument of 30, a new instance with a balance of 30 will be created and mapped to key 6 in the mapping. However, any of this new instance’s properties (as defined in the User struct) which are not explicitly assigned a value via the pointer, will be assigned a zero value by default (undefined and null do not exist in Solidity). It would, therefore, be unsuitable to create a new instance in this way in our updateBalance function, because this function doesn’t assign the id parameter to the struct instance’s id property (it only uses it for the key). This means that our new instance mapped to key 6 will have an id property with a value of 0 ! However, we could create a new instance in this way with the addUser function, by replacing the code in its function body with the following:

User storage user = users[id];
user.id = id;
user.balance = balance;

I hope that goes some way towards answering you questions. Let me know if anything is still unclear, or if you have any further questions :slight_smile:

// We need to use storage instead memory because with memory we copy the value of the structure in another variable and any change will not be reflected in the users array. On the other hand, if we use storage, what we do is access the pointer of the users [id], which if we modify the array it will be updated forever.```
pragma solidity 0.8.1;
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

I couldn’t get the problem solved. Anyways, I’m still posting :frowning_face:```
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 memory user = users[id];
     user.balance = balance;
}

function getBalance(uint id) view public returns (uint) {
    return users[id].balan`Preformatted text`ce;
    User memory user = users[id];
    user.updateBalance = balance;
}

}

Hi Joseph,

Essentially, yes. It is the EVM which is actually executing a smart contract’s functions, and so when we talk about the different types of data location, we are referring to the different ways the EVM stores the data which is involved in the computations being performed. This can either be temporary storage (in memory) or persistent/permanent storage (in storage) depending on how each particular piece of data needs to be utilised…

When a successful function call assigns values to a contract’s state variables (including mappings), this changes the current state of the smart contract. It is this current state which needs to be stored persistently, because this “latest position” is the basis for the computations that follow. You are right that the location for this persistent storage of data is the blockchain, because all changes of state are reflected within the transaction data which is included within its blocks. This ensures that the current state of a smart contract persists between each successive function call, and that all previous changes remain permanently recorded on the Ethereum blockchain.


The first word in a variable definition in Solidity is the data type i.e. uint, bool, string etc.

It only starts with a capital letter in the definition we are referring to…

User memory user = users[id];

…because our data type here is the struct that we have defined with the name User and not user. We can define a struct with whatever name we like (within the character constraints provided by the syntax), and so we could use a lower case letter. However, standard practice is to start struct names, event names and contract names with a capital letter (e.g. struct User, event Withdrawal , contract Bank ). This is in contrast to variable names (including mappings and arrays), arguments and parameters, which we usually start with a lower case letter. This contrast is in fact of vital importance in our example, because if it wasn’t for this difference, the struct name and variable name would be exactly the same, and this would result in a compiler error.

user is just the name we have chosen to give to this local variable. We could call it anything that we think would make a suitable identifier (again, within the character constraints provided by the syntax). The variable name user identifies the variable, and can be used to reference whatever value is currently stored within this variable. In our example, this value is users[id] which is a reference to a particular key within our mapping. The users mapping stores instances of the User struct…

mapping(uint => User) users;

These instances are the ones that are created and assigned to the mapping when the addUser function is called:

users[id] = User(id, balance);

I think I’ve already answered that above.

These concepts can be difficult to grasp and assimilate all at once, so don’t be too hard on yourself during these early stages. Don’t get me wrong, it’s really important to notice these things and try to work out what’s really going on with the syntax, so don’t stop questioning these types of things, and keep turning them over in your mind. But due to the nature of everything being inter-related, things will click into place gradually… trust me… I speak from experience :wink:

Just let me know if you have any further questions :slight_smile:

Hi @Maia,

Yes… I can see you’ve had problems here!

The problem is solved by modifying the code within the updateBalance function. You don’t need to add any additional code to the getBalance function. The getBalance function will be able to retrieve a user’s updated balance from the mapping, once the updateBalance function actually assigns their new balance to their “record” in the mapping (saved in persistent storage), and not just to a local memory variable which only stores the new balance temporarily.

If you need some extra help, hints and ideas for these assignments, before you watch the solution video, I strongly recommend that you have a look at some of the other students’ solutions, and the comments and feedback they have received, posted here in the corresponding discussion threads. You should find that this will give you a lot of useful information, and a better idea of what it is you are trying to achieve :slight_smile:

Nice solution @Adrian_Zumba :ok_hand:
… and a very good attempt at an explanation :muscle:

Just a couple of observations…

It’s the users mapping, which is not an array.

Very nearly!.. defining our local variable with data location storage creates a “pointer” (a reference) called user, which points to (references) a specific User instance in the users mapping — the instance which has, as its key, the id parameter input into the updateBalance function. Any value assigned to this local “pointer” variable is effectively updating the instance users[id] stored persistently in the mapping (the instance referenced by the pointer).

So, in your solution…

…assigning balance to user.balance in the second line, effectively assigns the new balance value to the balance property of users[id] in the mapping, and so does exactly the same as the more concise, alternative solution:

users[id].balance = balance;

Let us know if you have any questions :slight_smile:

Hi @GreggyB,

Sorry, my mistake…you are absolutely right! For some reason, when I was commenting on your require statement I was thinking about it as checking the balance parameter instead of the id. I’m going to edit that out of my original post so as not to confuse anyone.

This is strange… for me, your contract (as you’ve posted it here) with the require statement:

…as you have it in your updateBalance function, compiles with no warnings in Remix. Then when I deploy it, call the addUser function to add a couple of users with IDs 0 and 1, and then try to call updateBalance() with an id parameter of 0 and a new balance value, the require statement triggers revert as expected and the error message in the terminal includes the error message you added: ID sane value must be greater than zero. It is perfectly fine to include your require statement for the restriction you want to implement (i.e. id of 0 not allowed) and there is no reason why it should increase the gas cost excessively, or potentially create an infinite gas requirement.

Are you sure you were compiling exactly the same code that you’ve posted? Are you still getting the infinite gas warning after closing and re-entering Remix (to reset anything that may have been triggering this)?

The little test I’ve performed on your contract (outlined above) raises another point. Your require statement preventing an id of 0, would also need to be included in the addUser() function, otherwise a new user can be created with an id of 0, but then their balance can’t be updated with the updateBalance function.

Hello there, so the first solution I came up with was to simply get rid of the User user and instead just use the same mapping users to access the id and change its balance accordingly. It seems to work:

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

And the second solution I found, which is probably the one Filip was hinting to as we’re learning about data locations, is this one. Just changed memory for storage so that the results don’t get lost once the function returns.

    function updateBalance(uint id, uint balance) public {
         User storage user = users[id];
         user.balance = balance;
    }
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

Thanks again.

A mystery to me for now, but I think all will be revealed as I continue my Solidity studies.

I appreciate the reviews, keep up the good work.

1 Like

Hi @Maia,

I’ve deleted your post in the Data Location Assignment thread for the old course, and copied it here (and replied to it) so that it’s in the right place :slight_smile: You’re doing the new Ethereum Smart Contract Programming 101 course (not the old one).

If you read the warning message (hover over the yellow indicator) you will see the problem:
Warning: Unreachable code.
Why is this line of code unreachable?.. because a function is exited after a return statement: this is the same as JavaScript.
Also, as I mentioned in my reply to your other post for this assignment…

If you think about it, this makes sense. All the getBalance function needs to do is reference a specific user’s balance in the mapping (which it does with users[id].balance ) and then return this value to the function caller.

Watch the videos again, and you will see that at no point do we add the line that is generating the warning to the getBalance() function.
User memory user = users[id];  is the first line in the updateBalance function body, in the orginal code given to you with the problem that needs solving. You have solved this problem in your updateBalance function by changing memory to storage, and so when you change your code for the getBalance function back to how it was originally, then you will find that the warning disappears, your code compiles and you can deploy it and do the following successfully:

  1. Add a user by calling the addUser function with id and balance arguments.
  2. Retrieve this user’s initial balance by calling the getBalance function with their id .
  3. Update this user’s balance by calling the updateBalance function with their id and the new balance.
  4. Retrieve this user’s updated balance by calling the getBalance function with their id .

The initial problem is now solved, because when a user’s balance is updated (in step 3) this new balance is now saved persistently in storage, instead of only being saved temorarily in memory and lost when the updateBalance function has finished executing. The proof is that after updating a user’s balance, the balance returned by getBalance is the new balance (and not the old one).

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

1 Like

Thanks Greg, I’m glad you find the reviews helpful.

Yes… sometimes these things are best parked for a while, and then wait for them to reveal themselves in their own time :wink:

Hi @Ernest_SG,

Good to see you back here in forum :smiley:

Both of your solutions are correct and valid :ok_hand:

I actually think your first solution is better in this particular use case because, in my opinion, it is both clearer and more concise. Both solutions actually consume more or less the same amount of gas because, even though the Solidity code itself is different, the low-level operations they perform are actually the same. In both cases we are assigning the new balance parameter to the .balance property of a specific User instance in the mapping (users[id]) — the instance which has, as its key, the id parameter input into the updateBalance function.

Your first solution performs this assignment directly, whereas your second solution performs the assignment via a local “pointer” variable (created by defining it with the data location storage). The important point about this assignment is that we need to write the new balance to persistent storage, and not just save it temporarily in memory (where it will be lost when the function finishes executing). Whether or not we explicitly state the data location storage in our code is only a matter of the required syntax for the method we choose to use. Your first solution is still using storage instead of memory, even though the syntax isn’t requiring you to add the keyword storage.

I hope those extra comments make sense and are helpful. But just let me know if anything is unclear, or if you have any questions :slight_smile:

 function updateBalance(uint id, uint balance) public {
         User storage user = users[id]; //changed from memory to storage
         user.balance = balance;
    }
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;
    }

}

Hi i did my best but for some reason my get balance doesn’t pop up the balance + previous balance. Any tips would be good.

1 Like

Good to see you back here in the forum @Gabriel_Ng :smiley:

Your solution is correct :ok_hand:

Are you inputting the user’s ID when you call the getBalance function? Remember this function takes one uint parameter  getBalance(uint id)

When you deploy your contract, you can test it as follows:

  1. Add a user by calling the addUser function with id and balance arguments
    e.g.   1, 200
  2. Retrieve this user’s initial balance by calling the getBalance function with their id (1)
    returns =>  0: uint256: 200
  3. Update this user’s balance by calling the updateBalance function with their id and the new balance
    e.g.   1, 500
  4. Retrieve this user’s updated balance by calling the getBalance function with their id (1)
    returns =>  0: uint256: 500

The initial problem is now solved, because when a user’s balance is updated (in step 3) this new balance is now saved persistently in storage, instead of only being saved temorarily in memory and lost when the updateBalance function has finished executing. The proof is that after updating a user’s balance, the balance returned by getBalance is the new balance (and not the old one). Note that we are replacing the existing balance, and not adding to it.

I hope you can get this working now, but let me know if you’re still having problems, if anything is unclear, or if you have any further questions :slight_smile:

TQ.
Yes I did earlier and for some reason after updateBalanace and press getBalance it wont update. However I tried again after your reply. It worked :joy:

Thanks

1 Like

Well at least it works now :sweat_smile:

Sometimes, just refreshing, or closing and reopening Remix can magically solve things! Anyway, your code is correct, and if you already executed things as I suggested, then you’re doing everything right :slight_smile:

All I did was change User memory user… to User storage user…

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