Solidity Error Handling

@pedromndias @filip@anyoneelse :wink:

While I’m on a roll, here are the other questions I have about this section on Error Handling:

Question 1

Quiz question: When should you use assert() ?
Answer:      To avoid conditions which should never, ever be possible.
I put to avoid compilation errors, because assert() can throw if there is an error in our code which does not comply with an invariant. However, is my answer wrong, because compilation errors refer to syntax and so won’t actually trigger assert() , which is instead only triggered by coding errors which would result in an output which should be impossible (i.e. one that fails to meet an invariant condition)?

Question 2

In our createPerson function…

people[msg.sender] should always be equal to newPerson   (this is our invariant)

… so I understand why we need to combine the properties of each of these into single hex values with abi.encodePacked() in order to be able to compare them.
But why does Solidity further require us to hash these hex values? Surely, if the unhashed hex values equal each other, this is enough to prove that our invariant condition has been met?

Question 3

In the deletePerson function we have the following assert function

assert(people[creator].age == 0);

I extended this as follows, and as expected it also works:

assert(
   people[creator].age == 0 &&
   people[creator].height == 0 &&
   people[creator].senior == false
);

However, why can’t we also use the following, and how could we get it to work?

assert(people[creator].name == "");

Also, if we have deleted the person from our mapping, why do we still have a struct which contains the properties: age 0, height 0, senior false? Surely, if the person has been deleted, an assert function with the above conditions for invariants should still fail, because the properties age, height and senior shouldn’t even exist? Instead, it seems like we still have a person, but one with no name, height 0, age 0 (and not a senior).
This anomoly is also evident when getPerson is called for the same account which has just had its person deleted — it returns a person with no name, age 0, height 0, and senior false.
This seems strange to me — what’s actually happening here?

Question 4

Following on from Question 3, if the whole Person struct isn’t actually deleted, but its properties just effectively set to falsy values, could we improve the getPerson function by including a require function that throws and cancels execution if either:
(i) no person has been created for that address yet; or
(ii) the person created has since been deleted?
It would make sense to me that in these circumstances we should receive an error message such as “You must create a person before you can get it”, rather than getPerson returning a person with no name, age 0, height 0, and senior false, as it does at the moment.

In an attempt to create something along these lines, I’ve added the following to the getPerson function body:

require(
   people[msg.sender].height != 0,
   "You must create a person before you can get it"
);

This works, but it’s hardly ideal. Can we not ensure that the whole Person struct is deleted from the mapping, and then have a require function with the condition that a Person struct exists for this address (no matter what property values it contains), and failing if it doesn’t?

2 Likes

Hi @jon_m

Look at the third post, it explains really well the differences.

https://ethereum.stackexchange.com/questions/5812/what-is-the-difference-between-transaction-cost-and-execution-cost-in-remix


I wrote some examples to show the differences, to run your tests you need to do it with the tesnet environment as the javascript vm info are not correct (the javascript env is their to test functionality, not gas cost)


Wrong info

javascript VM

Creation

to HelloWorld.createPerson(string,uint256,uint256) 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
gas 3000000 gas
transaction cost 153362 gas
execution cost 131066 gas

Delete

to HelloWorld.deletePerson(address) 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
gas 3000000 gas
transaction cost 23703 gas
execution cost 24725 gas


Correct info

inject web3 (testnet ropsten)

Creation

  • With the correct account

to HelloWorld.createPerson(string,uint256,uint256) 0x28c0F27b8Eca9D069Ab54CC6D9888eFDE3B4f635
gas 152854 gas
transaction cost 152854 gas


Delete

to HelloWorld.deletePerson(address) 0x28c0F27b8Eca9D069Ab54CC6D9888eFDE3B4f635
gas 46157 gas
transaction cost 23079 gas

The execution cost will be the difference between the gas (total gas used) and the transaction cost


Get

to HelloWorld.getPerson() 0x28c0F27b8Eca9D069Ab54CC6D9888eFDE3B4f635
gas -> no info free
transaction cost -> no info free


  • With an other account

Delete

to HelloWorld.deletePerson(address) 0x28c0F27b8Eca9D069Ab54CC6D9888eFDE3B4f635
gas 3000000 gas
transaction cost 22625 gas

But if you look a the transaction no gas has been taken from my address

https://ropsten.etherscan.io/tx/0x8ddb49eda4779145ff0f62df46b76e3fdf8fddc95aaa9bd08752a4002f09145f#statechange

! Here i only pay the transaction cost, not the execution cost as the transaction as been revert


  • By making the assert fail

Delete

to HelloWorld.deletePerson(address) 0xb127C94DB708c342e4306Ac9799F516BeEaeb2CA
gas 3000000 gas
transaction cost 3000000 gas

https://ropsten.etherscan.io/tx/0x23d27db112254cf2a53f1f4bea5237388079d5ad93977ee6023ca4687b0664f5#statechange

Here i payed the gas limit but as remix check that the transaction will failed it sends the max amount of gas


  • By making the assert fail

Delete

Here i change the gas limit to 2000000 and all the gas is taken

https://ropsten.etherscan.io/tx/0xe335db0845253e8649e58441db439d7a9f6ed180d46a189d7ffdc35aac62898a#statechange

This is a remix issue, i think you need to check if it does the same thing by deploying the contract your self and interacting with it from the truffle console


So to answer your question

Scenario 1
Read the stackexchange post, transaction and execution are not the same you ll see why

scenario 2

Yes you are right for your 2 first assemptions.

You will not see a refund, the evm try to execute all your code (see it as it s in the ram) then when it is done it takes your eth (write the eth taken into the blockchain), if the revert is trigger during the execution (in the ram) the eth is not taken.

I m not sure why the transaction cost is not the same in this case i can’t find info about it.

I guess is related to on of this rule:

  • the cost for every zero byte of data or code for a transaction.
  • the cost of every non-zero byte of data or code for a transaction.

scenario 3

See my tests above on the testnet , this is related to remix as you send more gas (the max limit) and the assert is trigger all the gas is taken.

The remaining gas is when you send more gas than enought to execute the transaction so this gas is not used.

Question 1

The compilation error happened when the contract is deploy not during execution

Question 2

Not really sure to understand your question, if you don’t hash them you will have to test 4 cases in your assert, one for each test, so it ll cost you more gas.

Question 3

You can add

assert(people[creator].name == "");

But you will have to check that this value is provided when you create a person. Assert is used to check condition which should never happen, so you will check only one case which shouldn’t happen instead of all of them ?

The default value for a mapping is 0, you have created a mapping of type struct so if you try to get a person with a different address (without a Person created) or with a deleted person the result will be the same 0 for each element of the struct

Question 4

That the limitation of a mapping, and this is why an array is better in this situation, if the Person has been deleted you ll not find his address in the array.
Your require is a good idea, the contract in the course is not advance you can add multiples checks

1 Like

Hey Jon, I’m not @gabba but I’ll try some answers:
Question 1: Like Filip said, an assert() is used for internal errors and to check for invariants. A well written contract should never trigger assert(), it is used more to avoid critical errors like “if we withdraw all our tokens the balance should be zero”. An assert() will check if this is working as a concept, more than just correcting your syntax (because you can have good syntax and still “double spend” tokens). It is a very important complement to require() and on Unit Testing (Eth SC 201) you will use it all the time.
Question 2:
Solidity can’t compare 2 structs with “==”. But if the values are exactly the same, their hash should be the same. And solidity “likes” hashes so yeah, it’s just the way it is. Compare those gases and make sure the properties are in the same order to hash them.
Question 3:
I thought that by putting a triple “===” it would work because it is a string (I guess I heard it in the course) but it is showing an error…
Oh, wait, I see @gabba already answered all of your questions, so let’s learn!
Take care!

2 Likes

It’s always good to have answers from different people :+1:
Great point

Solidity can’t compare 2 structs with “==”.

Thank for your inputs @pedromndias :v:

2 Likes

Hey @gabba and @pedromndias !

Thanks so much for your replies! I had a day off yesterday as I was feeling brain dead lol, so I’m working through it all now. I think the transaction/execution gas costs will take me a while longer to get my head around, but first of all, here goes with my follow-up on the other questions:

Questions 1 & 4 resolved! :partying_face:

I thought that abi.encodePacked() converted each set of 4 property values into just 1 combined hex number, before hashing with keccak256(). That’s why it didn’t make sense to me, as I thought we already had just 2 hex numbers to compare before hashing. But you seem to be saying that abi.encodePacked() generates a separate hex number for each of the 4 property values in each struct. Is that correct?.. and is this therefore why we also need the hash function to combine each set of 4 hex numbers into just 1?

Question 3

OK, got it… I imagine that the default value is 0 only for type integer?..and for a string such as Person.name the default value is an empty string, and for a boolean such as Person.senior the default value is false ?

I don’t understand what you mean here. The following all compile correctly if I use them for assert() to check that the person has been deleted correctly:

assert(people[creator].age == 0); // this is the one used by Filip in the video
assert(people[creator].height == 0);
assert(people[creator].senior == false);

But when I try to use…

assert(people[creator].name == "");

… which, like the other assert functions above, also checks for a default value, but this time for the string property name — I get the following compiler error:

TypeError: Operator == not compatible with
types string storage ref and literal_string ""

Do strings need to be hashed before they can be compared? I’ve tried the following and it actually works…

assert(
   keccak256(abi.encodePacked(people[creator].name))
   ==
   keccak256(abi.encodePacked(""))
);

Is this a valid solution?
I was like :astonished: when it actually worked!! lol
I’m not sure why we need bothabi.encodePacked()and the hash function here, as unlike the 4 property values compared before, this time we’re only comparing 1?

1 Like

Hi @jon_m

I m sure all your questions will help a lot of people, you dive really deep into it :+1:

Question 2
Sorry i read it again now and i get what you mean:

I thought that abi.encodePacked() converted each set of 4 property values into just 1 combined hex number, before hashing with keccak256()

This is correct ! I didn’t express myself well i thought you didn’t get the point to use encodePacked. Your question make a lot of sense. This is a common thing to use this two function together.

I guess this is a way to save gas, as the two hash will get store in a register before getting compared, they will always have a fix length. If you don’t hash them and let’s say you have 4 strings the abi.encodePacked() can generate a lot of bytes.
But is it better in this case ?
With 1 bool, and 2 uint. I am not sure :man_shrugging:

Question 3

Exactly


I forgot i has the same issue when i did this course the first time

I think this is the best solution, i had found a library which compare string but it cost so much gas

https://forum.toshitimes.com/t/solidity-error-handling-assignment/7293/29

2 Likes

cc @pedromndias

I hope so!.. and that they don’t confuse people more at this early stage in the course! :crossed_fingers:

Just one thing to confirm now…

Ok, so as I have only just started with Solidity and don’t want my brain to explode :wink: is it probably best for me to just accept for now that when comparing strings or multiple property values, we always use the functions keccak256(abi.encodePacked()) together? I do understand your point about saving gas, though, because the two hash values will always have the same length of 256 bits: so, does the compiler throw an error if we just try to use abi.encodePacked()not because it wouldn’t work, but because it is forcing us to use a more efficient method?

Stay tuned for my transaction/execution gas costs follow-up :wink:
— but I think this won’t be until tomorrow now, as I’m already feeling saturated :joy:

Thanks for your time and patience! Much appreciated :smiley:

3 Likes

The compiler will not throw an error when using only abi.encodePacked() .
You will progress really fast by analyzing things in detail but yeah be careful with your :exploding_head: eheh. Wait a bit before codding smart contract with assembly :stuck_out_tongue:

But when I remove keccak256() and just use abi.encodePacked() as follows…

assert(
   abi.encodePacked(people[creator].name)
   == 
   abi.encodePacked("")
);

…my Remix Solidity compiler throws the following error:

TypeError: Operator == not compatible with types bytes memory and bytes memory

Instead, it only works when I include keccak256() as follows…

This is why I thought…

Have I misunderstood something you’ve tried to explain to me? :thinking:

cc @pedromndias

Following up on the transaction/execution gas costs…

I’ve had a good think about what you’ve explained and the information in the link you provided. The following is a summary of what I’ve understood — any comments/corrections?

  • I shouldn’t use the figures generated in the Remix JavaScript VM to try to work out what’s actually happening with gas costs — the 3 gas costs (gas, transaction cost and execution cost) and the amount of ETH deducted from the account address — because these figures aren’t correct and don’t reflect what would actually happen when run on the EVM. Instead, I should wait until I’m ready to start experimenting using the testnet environment, and use the gas figures generated there.

  • Transaction cost = gas to send contract data to the blockchain. On contract deployment this will include fixed gas costs of 32000 (for deployment) + 21000 (base cost). For transactions after deployment it will only include the base cost. In addition to these fixed costs, the transaction cost also includes per zero byte and per non-zero byte gas costs for all the bytes in the whole contract (effectively a variable cost based on the contract’s size).

  • Execution cost = sum of gas to execute each individual operation in a particular transaction i.e. only for those operations executed in the functions which are called (including any helper functions called internally).

  • The total execution cost is added to the total transaction cost and the sum of both is converted into ETH and deducted from the ETH account balance of the msg.sender .
    EXCEPT when…
    (i)  require() is triggered (e.g. a non-owner address tries to delete data):
    In this case, only the transaction cost (and not the execution cost) is deducted in ETH from the unauthorised msg.sender's account balance;
    (ii) assert() is triggered:
    In this case, as well as the total of both transaction and execution costs, the remaining gas (up to the gas limit set) is deducted in ETH from the msg.sender's account balance.
    However, I’m still not sure whether you are saying that the remaining gas, which is consumed when assert() is triggered, is only up to the gas limit set when we are using Remix. So, could the remaining gas (in the following quiz answer) refer to something different if the contract is actually run on the EVM? In this case, could remaining gas simply be the same total execution cost charged when assert() isn’t triggered and the function successfully executed?

  • In your examples using the testnet:
    (i)   gas figure (in your examples) = transaction cost + execution cost (both as I’ve descibed above in the previous bullet points);
    (ii)  transaction cost (in your examples) = transaction cost (as I’ve described above);
    (iii) gas figure – transaction cost (in your examples) = execution cost (as I’ve described above).
  • The gas cost refunded (i.e. not charged) in my above quote, is only the execution cost — the transaction cost is still charged and deducted.
    Even though the require() function in the 1st line of the function body is executed, there is no execution cost for this if it fails, so the Remix figures from my Scenario 2 are definitely wrong as they show a small execution cost, as follows:

Why is there no execution cost (and only a transaction cost) when the createPerson function is called? Surely there is also an execution cost of performing the operations contained within that function?


While we’re discussing gas, I’d also like to check the following:

  • I assume that the gas costs paid by the msg.sender are only paid to the nodes when they successfully mine a block of transactions? i.e. nodes only get paid when they successfully solve and mine a block (so non-mining nodes never get paid for running the EVM and taking part in the consensus mechanism).

  • When we talk about storage being the most expensive operation in terms of gas cost, what exactly are we referring to?
    Amending storage by reassigning a state variable’s value?
    The initial defining of a state variable on deployment?
    Something else?

3 Likes

@filip,

Hi filip, I think I broke ethereum with our code :).

After asserting that the deleted mapping with age first == 10 and then 0, all the ethereum adresses in remix were emptied. They all show 0 ether and I cant run the contract anymore.

What happened?

1 Like

Hi @Kraken
It happens sometime when you are switching to an other environment in remix try to reload the page, or login to metamask again :slight_smile:

Did you figure out what happened?

I understand what require and assert do, but I don’t really see a difference between the two. You mentioned that assert is meant to check “invariants”, but what is the practical difference? Could we use require and assert interchangeably in princple?

Hi @letscrypto,

No, there is an important difference:

require()restricts what the function allows as its inputs, and/or ensures that only authorised addresses can execute it. It is a filter at the start of the function’s code block, before it has begun to run. So it checks that the transaction is performed with the right data, and by the right person.

assert()  on the other hand, is like the function’s internal auditor performing a reconciliation of the final output in order to check that it is what the function’s code intended and should produce (the invariant) i.e. that it makes sense and no bug has crept in. It performs this validation check at the end of the code block, after all of the function’s computations have been completed. In this way it checks everything that has happened since require() ran the initial "entry" check.

I hope this makes things clearer, but just let us know if you’re still unsure about anything.

6 Likes

I’m wondering, why wouldn’t a company just make sure the code is working instead of programming something that checks for internal errors? Doesn’t the “assert()” function cost extra gas every single time the contract is executed?

2 Likes

Hi @Martijncvv

Because many companies made sure their code was working , they also run audits by externals companies and at the end they get hacked because humans are not bullet proof at coding :wink:

3 Likes

Thank you for your reply.
So it is not seen as a: “I’m not 100% sure my code is working as it is supposed to do”.
Or do they only implement it in very very advanced contracts.

1 Like

Filip, how is the require-function different from an ordinary if-then-function? Is it the link to the revert-function that makes it more convenient to use or is there some other reason that makes it actually impossible to replace ‘require’ by ‘if-then’?

1 Like

Not at all… these sorts of checks and balances are good practice (indeed expected) in all industries where risk management is important (which is pretty much all of them): especially ones that involve finance, legal, health and safety etc. The fact that once smart contracts are deployed they are immutable makes these checks and balances in their design even more of an absolute necessity.
Good risk management looks to avoid improbable negative events, as well as possible ones (even including ones thought impossible — as things at first thought impossible have still been known to happen!) Obviously there has to be a balance between implementation costs v probability of risk occuring, but in terms of just adding some extra code to make the smart contract more robust (even if it’s never needed and so becomes superfluous), the cost/benefit analysis becomes a no brainer.

3 Likes