drawer logo

Monitoring the Cost of a Smart Contract Using Logging and the Remix IDE

In the course of developing BCJ’s series on optimizing blockchain fees to lower the TCO of a DLT project, we discovered a powerful logging feature in the Remix IDE that can help developers peel apart gas and transaction fees. From our Labs, Bob Reselman explains.

Smart Contracts

Integrated Development Environments (IDE)

By Bob Reselman

Published:January 4, 2023

clock icon

13 min read

In this Story

  • Smart contracts are the means by which businesses can, with innovatively-developed business logic, programmatically unlock the unique value proposition and capabilities of Distributed Ledger Technology (DLT).
  • As with all software development, smart contract source code relies on variables to temporarily store certain values. Such smart contract variable declaration results in blockchain fees that are above and beyond the fees associated with any transactions that are automated by the smart contract.
  • These fees are not exclusively specific to the order in which the variables are declared (although the order of declaration matters). Rather, as revealed by a powerful logging feature in the Remix Integrated Development Environment (IDE) for developing Solidity-based smart contracts, the fees vary depending on whether a variable is initialized (or reinitialized) to zero or non-zero values.
  • At scale, a smart contract with code that is not optimized to minimize the fees it incurs will unnecessarily increase the total cost of ownership (TCO) of any blockchain initiative.
  • Developers are encouraged to leverage the full power of the Remix IDE and the plug-ins designed to work with it in order to optimize their code for cost savings.

Logging is a critical, necessary part of any application. Without it, software developers and system administrators are flying blind. This is particularly true when it comes to distributed applications in general and smart contracts–the blockchain equivalent of a distributed application.

Distributed applications operating on a distributed ledger are different from standalone applications. Unlike a typical desktop or server-side application where an application's state and its data (along with how that data is stored) are essentially localized to a system or cluster of systems, a smart contract and its data are replicated across the distributed machines (aka nodes) that are responsible for the operation of the ledger.

Thus, when something goes wrong, it can be hard to pin down which node or event is causing problems unless you have a way to view activities on all of the machines supporting the smart contract. Logging provides this bird's-eye view.

As mentioned above, smart contracts are computer programs (basically, automated business logic) that run on Distributed Ledger Technology (DLT) and require the presence of a virtual machine in order to run. In the DLT world, the virtual machine that's found on more chains than any other and the language that's used to program it are the Ethereum Virtual Machine (EVM) and the Solidity programming language. Also mentioned above, when a smart contract is deployed to its native blockchain, a copy of the smart contract is distributed among all the nodes on the DLT network that are responsible for the operation of that network. But there's more. Smart contracts can be programmed to interact with other smart contracts similar to the way that distributed business applications interact with other services hosted on machines throughout the network. In the same way that application developers use logs to troubleshoot issues on distributed systems, smart contract developers can also use logs to monitor and troubleshoot behavior on their smart contracts. In addition, logs are an indispensable tool for helping developers analyze and anticipate the fees that will be charged to run a smart contract (see Blockchain Journal's Using the Remix IDE to Show Smart Contract Transaction Fees.

Hence, the purpose of this article.

In this article we'll explain how the EVM supports smart contract logging. Then we'll demonstrate how we used logging under the Remix IDE to illustrate a rule about gas consumption in smart contract programming. The rule is that changing a variable's value from a zero to non-zero amount consumes more gas than changing a non-zero value to another non-zero value. We'll show a series of log entries that prove the rule.

But, before we go into the nitty-gritty of log analysis, it's important to understand how smart contracts use events as the mechanism to log data.

Using Events to Log Smart Contract Activity

Solidity and the EVM support logging in smart contracts by way of events. An event is a programming term that describes a specific, named occurrence that's defined by a programmer. Events are typically used by software developers to trigger the execution of some business logic. A simple example of such an event is when the stock price for a publicly traded company changes (the event), which triggers the business logic that updates a ticker symbol and price that scrolls by on the bottom of some television broadcasts. The name assigned to an event is arbitrary, but typically describes the meaning of the event. For example, an event that occurs before setting the value of a numeric variable might be named BeforeSetMyNumber. The event that occurs after setting the value might be named AfterSetMyNumber. (You'll see BeforeSetMyNumber and AfterSetMyNumber events implemented and discussed in a moment.)

An event can have data associated with it. For example, imagine a scenario in which a bill is being paid. A developer defines two events for that scenario, BeforePay and AfterPay. The developer makes it so that the invoice_id and the amount being paid accompany the BeforePay event. In such a case, the Solidity code will look like the following:

SOLIDITY
1event BeforePay(uint256 invoice_id, uint256 invoice_amount);

Also, the developer supplies the invoice_id and a receipt_id with the AfterPay event like so:

event BeforePay(uint256 invoice_id, uint256 receipt_id);

In a Solidity smart contract, event data is written to the contract's underlying distributed ledger and is therefore available to all the nodes that are responsible for the chain's operation and the instances of the EVM running on them. You can think of the blockchain as the central, yet distributed location where event data is stored.

Events are generated in a Solidity smart contract using the emit command. For example, the code to emit the fictitious BeforePay event described can be as follows:

SOLIDITY
1emit BeforePay(893871384090029302, 250);

WHERE

  • 893871384090029302 is the fictitious invoice_id as described above.
  • 250 is the invoice_amount as described above. In this case, the value 250 represents the currency that is native to the given DLT.

Now that we've covered event declaration and emission, let's take a look at actual code that defines and emits events.

Listing 1 below shows a smart contract named EventLoggingExample. The purpose of the contract is twofold. The first is to demonstrate how to use events to log data. The second purpose is to demonstrate the gas fees incurred when changing a variable that has a value of zero to a non-zero value. (You'll see these demonstrations later in this article.)

SOLIDITY
1// SPDX-License-Identifier: GPL-3.0
2pragma solidity 0.8.7;
3/***********************************
4 * @author Blockchain Journal
5 * @dev An example of a smart contract the uses events
6 * to log data to the underlying DLT
7 ***********************************/
8contract EventLoggingExample {
9 uint64 mynumber;
10 event BeforeSetMyNumber(uint64 oldValue);
11 event AfterSetMyNumber(uint64 newValue);
12 /***********************************
13 * A setter function for the private
14 * member variable mynumber
15 *
16 * @param uint64 num, the value to assign to the private
17 * variable
18 ***********************************/
19 function setMyNumber(uint64 num) public {
20 emit BeforeSetMyNumber(mynumber);
21 mynumber = num;
22 emit AfterSetMyNumber(mynumber);
23 }
24}

Listing 1: The source code for the EventLoggingExample smart contract.

Notice that the events BeforeSetMyNumber and AfterSetMyNumber are defined at lines 12 and 13 in the figure, respectively.

Also notice that the BeforeSetMyNumber event is emitted at line 23 in Listing 1 and the AfterSetMyNumber event is emitted at line 25. In addition, where the events are defined at lines 12 and 13, both the BeforeSetMyNumber and AfterSetMyNumber events are declared with a single parameter that represents a uint64 (8-byte) number that will be fed to the event when it's emitted.

The BeforeSetMyNumber event's only declared parameter – oldValue – indicates a number that is an old value. In contrast, the AfterSetMyNumber event's only declared parameter – newValue – indicates a number that is a new value.

Next, take a look at the function setMyNumber() starting a line 22 in Listing 1. The function setMyNumber() has a parameter named num that represents a number that gets passed to the function when it is executed. The function setMyNumber() does nothing more than change the value of the global variable uint64 mynumber declared at line 10 to the value passed to the function's num parameter. Thus the following call…

SOLIDITY
1setMyNumber(4)

… will initialize the variable mynumber to the value 4.

However, notice also that there are two events emitted within the setMyNumber() function. The event BeforeSetMyNumber is emitted at line 12. Emitting BeforeSetMyNumber(mynumber) will log the value of the variable mynumber to the underlying DLT where it will be reported within the Remix IDE console as part of the transaction receipt that's returned to the IDE when a transaction completes. Emitting AfterSetMyNumber(mynumber) will also log the value of the variable mynumber to the underlying DLT where it will be subsequently reported within the Remix IDE after the update takes place.

Viewing Events and Logs in the Remix IDE

As explained above, events are logged to the EVM's underlying distributed ledger. These logs can be viewed in the Remix IDE as shown in Figure 1 at callout (2). In the case of the smart contract EvenLoggingExample mentioned previously, the BeforeSetMyNumber and AfterSetMyNumber events are emitted within the smart contract's setMyNumber() function.

Figure 1: The setMyNumber button and terminal window in the Remix IDE.

Figure 1 is a screenshot that shows the result of deploying the EvenLoggingExample contract to the testing DLT that's provided by Sepolia, which is a project that provides testing resources for smart contract developers and can be integrated into the Remix IDE.

Notice that the IDE publishes a setMyNumber button and a textbox. This setMyNumber button corresponds to the setMyNumber() function published by the smart contract. Since the setMyNumber() function was declared with a single uint64 parameter (num), the textbox to the right of the button takes a uint64 value that gets passed into the setMyNumber() function as that num parameter. (To learn more about the details of deploying and running smart contracts in the Remix IDE, refer to the Blockchain Journal article, Using the Remix IDE to Show Transaction Fees.)

For our demonstration purposes, we're going to click the update button three times. The first click passes a value of zero (0) to the setMyNumber() function. The second click passes a value of 37 and the third click passes a value of zero again (see Figure 2).

Figure 2: Passing values to the setMyNumber() function using the setMyNumber button in the Remix IDE.

Snippets of the log data from those three clicks along with the gas fees incurred with each click are shown in the listings below. The output in the listing includes information and logged data that have been sent to the Remix IDE.

Listing 2 shows the gas cost and log data from the first function call to setMyNumber(0). The BeforeSetMyNumber information is shown at lines 7 through 11. The AfterSetMyNumber information is shown at lines 16 through 20.

Notice the event field describes the event being reported. Also, notice that the args field reports the name of the parameter associated with the event along with the parameter's value.

SOLIDITY
1transaction cost: 25886 gas
2logs: [
3 {
4 "from": "0xedAeCd99EfB3FDde69ea232b01ea6A275E297B7f",
5 "topic": "0x2e393af8228b1fe9c414a2e3f3e1027836c40b2bb0e925d45edfb7d741ee623c",
6 "event": "BeforeSetMyNumber",
7 "args": {
8 "0": "0",
9 "oldValue": "0"
10 }
11 },
12 {
13 "from": "0xedAeCd99EfB3FDde69ea232b01ea6A275E297B7f",
14 "topic": "0x113104e570132530fcf51efa1b3d21da957fce4bd18f4ca4d009678464520af0",
15 "event": "AfterSetMyNumber",
16 "args": {
17 "0": "0",
18 "newValue": "0"
19 }
20 }
21]

Listing 2: The log output when applying a zero value to a variable that has a zero value.

Listing 3 shows the gas cost and log data from the second function call to setMyNumber(37). The BeforeSetMyNumber information is shown at lines 7 through 11. The AfterSetMyNumber information is shown at line 16 through 20.

SOLIDITY
1transaction cost: 45798 gas
2logs: [
3 {
4 "from": "0xedAeCd99EfB3FDde69ea232b01ea6A275E297B7f",
5 "topic": "0x2e393af8228b1fe9c414a2e3f3e1027836c40b2bb0e925d45edfb7d741ee623c",
6 "event": "BeforeSetMyNumber",
7 "args": {
8 "0": "0",
9 "oldValue": "0"
10 }
11 },
12 {
13 "from": "0xedAeCd99EfB3FDde69ea232b01ea6A275E297B7f",
14 "topic": "0x113104e570132530fcf51efa1b3d21da957fce4bd18f4ca4d009678464520af0",
15 "event": "AfterSetMyNumber",
16 "args": {
17 "0": "37",
18 "newValue": "37"
19 }
20 }
21]

Listing 3: The log output when applying a non-zero value to a variable that has a zero value.

Listing 4 shows the gas cost and log data from the third function call to setMyNumber(0). As before, the BeforeSetMyNumber information is shown at lines 7 through 11 and the AfterSetMyNumber information is shown at lines 16 through 20.

SOLIDITY
1transaction cost: 23886
2logs: [
3 {
4 "from": "0xedAeCd99EfB3FDde69ea232b01ea6A275E297B7f",
5 "topic": "0x2e393af8228b1fe9c414a2e3f3e1027836c40b2bb0e925d45edfb7d741ee623c",
6 "event": "BeforeSetMyNumber",
7 "args": {
8 "0": "37",
9 "oldValue": "37"
10 }
11 },
12 {
13 "from": "0xedAeCd99EfB3FDde69ea232b01ea6A275E297B7f",
14 "topic": "0x113104e570132530fcf51efa1b3d21da957fce4bd18f4ca4d009678464520af0",
15 "event": "AfterSetMyNumber",
16 "args": {
17 "0": "0",
18 "newValue": "0"
19 }
20 }
21]

Listing 4: The log output when reapplying a zero value to a variable that has a non-zero value

Each listing shows how the variable mynumber changes from button click to button click, as reflected in the oldValue and newValue parameters associated with their respective events in the logs. Also, you'll see how the gas costs fluctuate.

Table 1 below shows the base costs for gas costs for the three calls to setMyNumber().

Table 1: A comparison of costs when running the setMyNumber() function.

Function call

Gas cost

First click, update(0)transaction cost: 25886 gas
Second click, update(37)transaction cost: 45798 gas 
Third click, update(0)transaction cost: 23886 gas

As you can see, gas costs vary significantly from function call to function call. The difference in gas cost is due to the rule about changing zero-values to non-zero values when running a smart contract. Let's look at the details.

Using Log Data to Observe Gas Costs of Changing Zero Values to Non-Zero Values

As mentioned at the beginning of this article, a rule in Solidity/EVM programming states that when a variable that has a zero value is changed to a non-zero value, the EVM will charge a gas fee. However, when a non-zero value is reverted to zero value, no additional gas fee is charged. In fact, the cost of incrementing the variable value to a non-zero value is refunded.

The log data shown in the previous demonstrations illustrated the rule. The first time the function setMyNumber() is called, mynumber, which started as zero (a uint64 variable has a default value of 0 when initialized), does not change. A zero value is being applied to a variable that already has zero value. The transaction cost is 25886 gas.

However, the second time the function setMyNumber() is called, the value of mynumber increases to 20. Hence, a greater gas fee is charged due to changing a zero value to a non-zero value. The transaction cost is now 45798 gas, which is 19912 more gas, including the cost of changing a zero value to a non-zero value.

The third time the setMyNumber() function is called, the transaction fee is reduced to 23886. The 21912 gas difference from the 45798 gas cost incurred previously reflects the lower gas fee for changing a non-zero value to zero. This confirms the reduced cost that goes with changing a non-zero value to zero.

We can do the analysis for gas cost because we can observe the contract's runtime behavior using the Remix IDE's logs. But, just logging isn't enough. Getting the information we needed to do the analysis required planning.

We defined the events that we found interesting and then made sure the smart contract emitted the events, with the right data, at the right time. It's not magical. We had to anticipate the events and information that were useful for our observation and troubleshooting purposes, then program our logging implementation accordingly.

Understanding the Trade-offs

Events add an important dimension to smart contract programming, but as with just about everything that has to do with a smart contract running on a blockchain, events come with a price.

Table 2 below is a comparison between not using and using events in the EventLoggingExample contract. The Remix IDE also has a feature that reports estimated gas costs upon compilation. The column on the left shows the gas cost of estimates without events as reported by Remix. The column on the right reports the cost with events.

Table 2: The gas cost of the smart contract without and with events according to estimates provided at a smart contract's compile time.
Without EventsWith Events
SOLIDITY
1{
2 "Creation": {
3 "codeDepositCost": "39000",
4 "executionCost": "93",
5 "totalCost": "39093"
6 },
7 "External": {
8 "setMyNumber(uint64)": "24466"
9 }
10}
SOLIDITY
1{
2 "Creation": {
3 "codeDepositCost": "64600",
4 "executionCost": "117",
5 "totalCost": "64717"
6 },
7 "External": {
8 "setMyNumber(uint64)": "28694"
9 }
10}

As you see, events cost gas (which makes sense because events require more compute resources). Every time an event is used in code, a gas fee is incurred. Thus, care needs to be taken at design time when implementing logging in a smart contract. The trick is to use events in a way that is meaningful and cost effective.

As far as minimizing costs goes, the questions the development team needs to ask are: What is our overall logging policy? And, is every gas cost incurred by our policy essential?

Under-logging events can be risky because there may not be enough information to adequately monitor smart contract behavior and perform minimal troubleshooting when the time comes. Over-logging could result in undesirable gas costs. However, when observing a smart contract's behavior, it's important to consider that logging is not the only way to gather information. An alternative is to use the Remix Debugger.

The Remix Debugger is a plug-in that a developer can add to the IDE. Once the Debugger plug-in is installed, developers can step through code one line at a time at design time to gather information. As shown in Figure 3, part of the information reported by the Debugger is the gas cost consumed and available as the code executes.

Figure 3: The Remix Debugger plug-in enables developers to step through code one line at a time. Still, when it comes to logging, there's always a trade-off.

There is no one-size-fits-all solution to implementing logging architecture in a smart contract. Each smart contract has different needs. The important thing is to be aware of the costs involved and plan accordingly.

Putting It All Together

This article covered a lot. We described the relationship between events and logs. We went over the basics of using events in a smart contract. Also, we showed how to use events to generate meaningful log data. In addition, we demonstrated how to use logs to explain the gas costs incurred when changing a variable with a zero value to a non-zero value and vice versa. Finally, we provided a brief introduction to the expense incurred when using events in a smart contract.

As stated at the beginning of this article, logging is a critical aspect of distributed application architecture for smart contracts as well as for mainstream business applications that run at enterprise scale. Without such logging, developers are essentially flying blind. However, when used effectively, developers can rely on logs to not only minimize the fees associated with smart contract execution, but to help manage the overall TCO of an organization's DLT strategy. But as we demonstrated, smart contract logging also comes with price itself. The trick to cost-effective logging in smart contracts is taking the time to plan ahead and to determine the events a smart contract needs to emit in order to make maintenance and troubleshooting as effortless as possible.

footer logo

© 2024 Blockchain Journal