Solidity Error Handling

Hey @W1LL, hope you are ok.

Now about the ABI encode.
Now from my understanding, we can use abi.encode(...) OR abi.encodePacked(...) to get the 32 bytes representation of an argument(s), we can use this for accurate comparative or other stuffs.

When you need or not the full length of the argument, encodePacked returns the UTF-8 value of the argument, while encode returns the exact length and detailed decimals of the value. Example: `

abi.encode("test")` 
RESULT IS:

0x0000000000000000000000000000000000000000000000000000000000000020
0x0000000000000000000000000000000000000000000000000000000000000004
0x7465737400000000000000000000000000000000000000000000000000000000
abi.encodePacked("test")
RESULT IS: 

0x74657374

So the 1st one shows the entire data of the value, the 2nd one just “a shorter” representation of the same value.

Basically is on which way you need to get a simpler (encodePacked) representation or the accurate one (encode).

If you have any more questions, please let us know so we can help you! :slight_smile:

Carlos Z.

1 Like

@thecil
Thank you so much for your feedback, man! Appreciate it! :slight_smile: :pray:

Hi @thecil,

your answer made me think of the following question:

if abi.encodePacked(…) will result only in a shorter representation of the same output data as abi.encode(…), then will it actually help with saving on gas consumption?

1 Like

Hi @Wayan!

I haven’t seen any answer to your question, so I will share my understanding of it.

As you pointed out:

Having the creators array allows us to keep track of all keys inside the mapping, thus giving us a way to actually access all the data inside it (without it, in order to be sure we didn’t miss any entry in the mapping, we would have to go through all theoretically possible addresses and check if they give a response).
Having this possibility we create further structures that will make it easier to operate on the data (e.x. a link between Bob’s name and his address).

Does that answer your question?

2 Likes

No, was consumption will remain the same for both functions (32 bytes per argument).

Carlos Z.

1 Like

Hi @lopotras, thanks for replying!

Having this possibility we create further structures that will make it easier to operate on the data (e.x. a link between Bob’s name and his address).

I guess this part does answer my question, as it indicates that we would indeed need additional structures. Maybe deletePerson wasn’t the best use case for the array and that got me confused.
Thanks!

1 Like

Hi Oussama,

I am just a beginner student so I am not sure that my answer is correct, but I think that replacing msg.sender with creator in your example wouldn’t work because in the video, Filip declared the variable in another function. So the creator variable doesn’t exist in the scope of the assert call.

Hi @filip,
In relation to Error Handling - require()… Is there any cost in gas to do any of the checks in require? Because if it fails it returns the gas, so could that be used as a way to spam the network? If for example you kept calling contracts with params that will fail at the require stage…
Thanks!
Brian

2 Likes

I already reply you this question in another topic :slight_smile:

Carlos Z

1 Like

Excellent question @bdoherty!

Yes there is, a small amount, but a cost nonetheless.

It only refunds the remaining gas, which is the total gas limit set LESS the gas consumed on performing any operations up to the point when require() fails and the function reverts. Gas is always consumed for any operations performed including executing the actual require() statement, even if that require() statement fails and the function then reverts. That’s why it’s important to always put require() at the beginning of the function body (or as near to the beginning as makes logical sense in the context) so that no gas is wasted on performing previous operations only to then have them revert, and to lose the gas they consumed, if a subsequent require() statement fails.

The following discussion explains this all quite well, and provides some further details:
https://ethereum.stackexchange.com/questions/46827/is-the-gas-fee-refunded-if-the-transaction-fails

So, the answer is no. There will always be a very small amount of gas consumed and not refunded if a require() statement fails. And this would provide a necessary disincentive for potential spammers to send repetitive function calls with parameters deliberately designed to cause a require() statement to fail.

I hope that answers your questions, but do let us know if anything is still unclear :slight_smile:

1 Like

I’m still trying to wrap my head around this as well. I understand the difference from a code style point-of-view where we all agree to use require() and assert() this way, but what is the technical difference between the two in behaviour?

e.g.: What would happen if I were to switch them around? Wouldn’t this work just the same (technically speaking)?:

function something(uint _number) public pure {
    assert(_number > 50, "Number must be above 50");

    uint oldNumber = number;
    number += _number;

    require(number = oldNumber + _number, "Weird, our code did not correctly update the number");
}

I come from the PHP world where we have different sort of exceptions, for example a “throwable” or “fatal” which have the profound difference that a throwable can be caught at a higher level in code and recovered from while the fatal will terminate execution and can not be recovered from.

So what, on a technical level, is the difference in implementation between require() and assert() which determines the way they are used? Why can an assert() not be used to check inputs or a require() not be used to validate a function’s outcome?

1 Like

@filip Hi Filip I hard coded the assert condition to fail just to see what happens in case it fails.
e.g uint previousBalance = 100;
_transferBalance(msg.sender, receiver, amount);
assert(previousBalance == balance[msg.sender] + amount);

So I was expecting some error which occurred as
"transact to Bank.transferBalance errored: VM error: invalid opcode. invalid opcode The execution might have thrown. Debug the transaction to get more information.

creation of Bank pending… "

So my question is in case of assertion failures, does the transaction revert? or what exactly happens in such as situation?

1 Like

Hi @seer,

Good questions!

The difference is in the gas that they consume when they throw (evaluate to false).

When a function is called, a gas limit has to be allocated to the transaction. If require() throws, this is not considered to be an “exceptional” event, because it is testing the validity of the function’s inputs. We should “expect” callers to sometimes attempt to invoke functions with invalid inputs (either by mistake, or on purpose), or that the callers could themselves be unauthorised to do so.
require() is designed for such non-exceptional events, because when it throws, only the gas consumed up to that point is spent (including the gas required to execute the require function itself), and the gas remaining in the “budget” for that transaction (up to the gas limit) is refunded to the caller (effectively unspent). Because of this functionality in terms of gas consumption, you can proabably see that the earlier in the function require() is placed, the less the cost of gas will be if it throws and the transaction reverts. This seems logical when we consider that require() is meant to throw. While we want to ensure that there is at least some cost associated with calling a function (whether it is successful or not) as a disincentive to spam the network, we don’t necessarily want to penalise those who call the function with invalid inputs by mistake.

In contrast, assert() is not meant to throw, because we obviously aim to only deploy smart contracts that have been coded correctly, logically, and to operate effectively i.e. with no intrinsic flaws in the code. Whereas you are correct in saying that the boolean expressions passed as arguments to both require() and assert() statements could theoretically be used to check either a function’s inputs or its invariants, in contrast to require(), if assert() throws, it causes all of the remaining gas in the “budget” allocated to that transaction (up to the gas limit) to be consumed, and so results in a considerably higher cost. Therefore, it seems logical that we use the more costly assert() to check for conditions which are the least likely to occur (only in “exceptional” circustances).

I hope this answers your question, and clarifies things. But do let us know if you have any further questions.

2 Likes

Thanks for that elaborate answer @jon_m! That makes it very clear what the difference is :smiley:.

Although, from a user perspective, I do wonder, why is it not the other way around? It seems like we are now punishing the user for an unexpected error in our contract while not penalising the user for doing fraud requests (either on purpose or accidentally).

Wouldn’t it be more logical / user friendly to refund the remaining gas to the user when we messed up on our side instead of when they messed up on their side?

Personally, knowing the difference between the two, I would be tempted to never use assert and only use require just to make sure my users never spend more than absolutely necessary for their transaction. :thinking:.

1 Like

Hi @seer

Again, very good questions! I must admit, I was wondering about this exact same thing when I was answering your original post. I’m including some links at the bottom of this post with some more detailed documentation, information and analysis which I think goes a fair way in answering your question, but unfortuantely, not quite as fully, or straightforwardly, as I would have liked. Just to summarise, it seems to me that there are 2 more key differences, as well as the gas consumption, which makes require() and assert() each more appropriate than the other in their separate designated use cases:

  • Using assert() allows developers to perform specific types of analysis at the compile stage in order to detect situations and circumstances when assert would throw. I don’t understand the detailed mechanics of how this works in practice, and the information I’ve linked to doesn’t really explain this either, but this certainly seems to be a key factor in why we use assert() to check for invariants and intrinsic flaws in our smart contract code. The additional information also suggests that the only time assert() would ever actually throw in practice is during the development and testing phase, rather than post launch, so the higher gas fees would probably only ever have to be paid be the developers rather than users. However, this is only my logical deduction, rather than based on any personal practical experience.

  • Using require() allows an error message to be returned to the caller as well as the transaction reverting. This makes require() ideal for placing constraints on specific inputs, and notifying the caller with an appropriate message if these constraints aren’t met.

I think it’s also important to consider that if we were to use assert() to check for invalid inputs, then we would be penalising users who call a function with invalid inputs by mistake much more than when we use require(). Whereas I agree that it would be fair for deliberate spammers to lose all of their remaining gas, I think that using assert() to check for invalid inputs would be more damaging to our users as a whole, because there is no way to distinguish between these two types of callers.

https://docs.soliditylang.org/en/v0.7.5/control-structures.html#id4
It’s also interesting to note that in the latest version of Solidity (v0.8.0) it seems that assert() also now refunds the remaining gas, as well as require(). Whether or not this is to address the very issue that you yourself have raised, though, is a matter for further research…
https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require
See also the 4th bullet point on this page of the documentation:
https://docs.soliditylang.org/en/v0.8.0/080-breaking-changes.html#solidity-v0-8-0-breaking-changes

The following article is from 3 years ago, and there have been quite a few updates to Solidity since it was written, but as long as you bear this in mind when reading it, I think that it still provides a useful summary, and background, regarding require() and assert(). You will notice, where the article mentions that certain functionality will be implemented in the future, that this has already happened.
https://medium.com/blockchannel/the-use-of-revert-assert-and-require-in-solidity-and-the-new-revert-opcode-in-the-evm-1a3a7990e06e

I encourage you to continue your own research on this topic. Do share with us, here in the forum, any additional information and nuances you may discover :slightly_smiling_face:

1 Like

Just another quick follow-up @seer

If you try adding this function to a contract, and then add the missing state variable number, you will notice that your assert() statement generates a compiler error. If you look carefully at the compiler error you will notice that it is because assert() does not allow the 2nd argument. Unlike require(), on evaluating to false, assert() doesn’t allow a message to be returned to the user notifying them of the reason for failure. That’s one of the other reasons I mentioned as to why require() is more suitable for checking for specific invalid inputs.

You’ll also notice that there are some other compiler errors that you need to correct in order for your function to execute. See if you can work out what these are and how to correct them from the error messages. Debugging your own code like this is a good way to learn and practice.

Nice experiment @kHarsh :+1:

You are correct in your assumption of the error you expected:

But you won’t see…

…because, after making the adjustments you’ve detailed in order to force assert() to fail, you first need to deploy your contract (which is when you’ll see creation of Bank pending...)

I did this, then deposited 200 wei in the same account as the address that deployed the contract. This needs to be a different amount to the 100 wei you are fixing previousBalance at, so that the actual previous balance stored in balance[msg.sender] is different to the check amount stored in the local variable previousBalance. I then used the same address to call the transfer function with a different address as the recipient argument, and with an amount argument of 150 wei:

The first require statement doesn’t throw because:
balance[msg.sender] is 200, which is greater than the amount parameter 150 i.e. there is sufficient balance to make the transfer.

The second require statement doesn’t throw because the msg.sender is not the same address as the recipient address.

RESULT = assert() throws with the error message you expected.

Wauw, that is very interesting! And good to see they changed the gas consumption on Assert :smiley:.

So, here I am again, wondering :joy:… Basically, assert() is sort of a unit test right inside your production code then. Now, in this assignment, unit testing has not been shown yet so let’s not get into it in this thread, but I did progress to the unit test course by now, so food for thought:

Why would we still need assert if we have unit tests? :thinking:

As I said though, for this assignment off-topic, so I’ll give it a thought myself and we’ll see in the next topics! Thanks for the very elaborate answers and resources @jon_m!

1 Like

Hi, could anyone help explain more about this question - thanks
Which of the two cases will consume all of the remaining gas in a function call: assert(false) or require(false)?

1 Like

Hi @gwen,

Q3: Which of the two cases will consume all of the remaining gas in a function call: assert(false) or require(false) ?

Remaining gas refers to the difference between:

  • a higher gas limit set for the transaction by the sender
    and
  • a lower actual gas cost of calling the function, and all of the operations that are performed as a result of that, whether or not the transaction completes or not.

Let’s consider three different scenarios when calling a function which has both a require statement and an assert statement.

  1. Your function call is successful (i.e. any assert or require statements evaluate to true):
  • The sender will pay the gas cost of executing all of the code in the function.
  • Any remaining gas will not be consumed.
  1. Your require statement evaluates to false, the transaction isn’t completed, and the contract is reverted to its prior state:
  • The sender will only pay the gas cost of executing the code in the function up to and including the require statement which has failed.
  • Any remaining gas will not be consumed.
  1. Your require statement evaluates to true, but your assert statement evaluates to false: the transaction isn’t completed, and the contract is reverted to its prior state:
  • The sender will pay the gas cost of executing all of the code in the function up to and including the assert statement which has failed,
  • AND THE COST OF ANY REMAINING GAS i.e. the cost of all gas up to the gas limit.
    (but it is also interesting to note that in the latest version of Solidity (v0.8.0), which has only very recently been released, it seems that assert() also now refunds the remaining gas, as well as require(), so this will make the answer to this quiz question valid only up to and including v0.7)

I think you may also find some of the information in this post helpful, and the discussion in the posts that follow.

I hope that clarifies what the question is about. But do let us know if you are still unsure about anything.:slight_smile:

3 Likes