drawer logo

Using the Remix IDE to Show Smart Contract Transaction Fees

Once Blockchain Journal Labs decided to go deep to understand how smart contract fees are both calculated and optimized, we realized that a hands-on approach with the Remix Integrated Development Environment (IDE) was the best way to go. Here's how we did it.

Total Cost of Ownership

DLT Strategy

Smart Contracts

By bob.reselman

Published:January 4, 2023

clock icon

17 min read

In this Story

  • The Remix Integrated Development Environment (IDE) is an extremely impressive toolset for developing smart contracts in Solidity, which is the most popular programming language for coding smart contracts that run on Ethereum Virtual Machines (EVMs), which are present on more than just the Ethereum public distributed ledger.
  •  Remix IDE features the ability to estimate the fees that will be incurred by a smart contract.
  •  In the best interests of a smart contract’s total cost of ownership (TCO) over the long haul, it’s possible for developers to minimize a smart contract’s gas fees by optimizing the variable and function declarations in their Solidity source code. 
  • In this hands-on walk-through, Blockchain Journal Labs shows how it used the Remix IDE to fact-check the differences in gas fees between two different versions of the same Solidity program; one that’s optimized for lower costs and another one that’s not.

This article describes how we used the browser-based version of the Remix IDE for Solidity to estimate the fees for the smart contracts we discussed in the Blockchain Journal's coverage of Why To Optimize Smart Contract Code For Blockchain Fee Reduction. In that article, we analyze and compare two smart contacts in terms of transaction fee optimization. The code in one contract is unoptimized for gas fees and the other is optimized.

In this article, we're going to reveal how we did it; in other words, how we used Remix to examine the smart contracts' source code. Then, we're going to demonstrate how we compiled the source code in Remix and viewed the details about the compilation in terms of storage allocation. We'll show how we deployed each smart contract into the test EVM that comes with the Remix IDE. Finally, we'll explain how Remix reports the various gas fees associated with each smart contract as they're deployed and run.

Let's begin by looking at the source code for each smart contract.

Examining the Source Code

As mentioned above, the point of this article is to demonstrate how we used the Remix IDE to determine the gas fees for two smart contracts written in Solidity. One smart contract is named Unpacked. The other is named Packed. The reason for this naming is that the Unpacked contract is unoptimized. The contract named Packed uses the packing technique to optimize storage allocation and hence, minimize gas fees. Figure 1 shows both contracts. Unpacked is on the left and Packed is on the right.

Figure 1: The source code for the unoptimized and optimized smart contracts.

The Unpacked contract is not optimized because it does not use the packing technique to store contract data on the blockchain efficiently. The way in which data is stored on the blockchain matters because unlike a typical desktop or server-side application where a machine's state and its data (along with how that data is stored) are essentially localized to a system or cluster of systems, the state of an EVM as well as any on-chain data it relies on is replicated across the distributed nodes that are responsible for the operation of the ledger. This means that any variable that describes some aspect of the state of a smart contract needs to be stored on all the computers that make up the blockchain network. As you can imagine, that's a lot of resource-intensive work that has to be done by the EVM that intermediates activities between the smart contract of the underlying blockchain. Thus, the EVM will charge a fee every time it has to change state data.

An example of a state variable can be something such as a variable named owner_address, which is a variable that a developer usually creates to store the address of the ledger account that owns the contract. The logic is that changing the value assigned to the owner_address changes the state of the contract in its entirety. It's similar to a person changing their snail mail address. Because the person's state changes, all the mail that used to go to the old address now goes to the new address and the change in snail mail address will show up on that person's credit report.

Variable Declarations Make a Difference

A variable's data type as well as the order in which variables are declared have a direct impact on how data is stored on the smart contract's underlying blockchain. They can also influence the gas fees charged to the account that owns the smart contract. One of the key features of the Remix IDE is that it allows us to look at a smart contract's data storage layout in exacting detail, as we'll demonstrate in a moment. But, in order to understand how the Remix IDE reports data storage, we first need to describe the way the EVM stores data on its underlying blockchain.

Take a look at the struct named NotOptimized on the left in Figure 1 above, starting at line 5 and shown below. (struct is a Solidity keyword for structure. A structure is a custom type used to organize a collection of variables under a name.)

struct NotOptimized { uint64 number_1; uint256 number_2; uint64 number_3; }

The expression above declares three variables: number_1, number_2, and number_3. Notice that variables number_1 and number_3 are of data type uint64, but that variable number_2 is of type uint256. This makes a difference because of the way the EVM manages storage allocation when running a smart contract.

The Solidity compiler allocates storage in 32-byte groups, with each 32-byte group incurring a distinct fee. Also, Solidity allocates storage according to the order in which variables are declared. Thus, in a situation when the Solidity compiler encounters four variables of type uint64 in order (each uint64 variable is allocated 8 bytes of memory), all four variables will be allocated to a single 32-byte slot of storage (8 bytes + 8 bytes + 8 bytes + 8 bytes = 32 bytes). The code will be charged a fee only for the single 32-byte slot.

Unfortunately, the NotOptimized structure has not been optimized. NotOptimized will be allocated three 32-byte slots of memory because the first variable uint64 number_1 (which is 8 bytes) and the second variable uint256 number_2 (which is 32 bytes), add up to 40 bytes. These 40 bytes exceed the 32-byte slot size memory limit. Therefore, the variable uint64 number_1 will be assigned to a 32-byte slot of memory, the variable uint256 number_2 will be assigned to a 32-byte slot, and uint64 number_3 will be assigned to a third 32-byte storage slot. (This will all be glaringly apparent when we look at the Storage Layout report produced by the Remix IDE.)

The NotOptimized struct is considered an unpacked declaration because the structure has not been optimized in terms of storage allocation.

Now, look at the following declaration for the struct named Optimized:

SOLIDITY
1struct Optimized {
2 uint64 number_1;
3 uint64 number_3;
4 uint256 number_2;
5}

Notice that variables number_1 and number_3 are grouped together in sequence. Since they are declared contiguously, the variables uint64 number_1 (which is 8 bytes) and uint64 number_3 (which is 8 bytes) can "fit" into a single 32-byte storage slot. Also, notice that variable number_2 of data type uint256 comes after the first two variables and will require a single 32-byte storage slot. Thus, the Optimized structure requires only two 32-byte storage slots and will only be charged accordingly. This optimization technique is what is meant by the term packing.

The difference in storage allocation becomes apparent in the Remix IDE when each contract is compiled.

Compiling the Code

In order to run source as a smart contract, we needed to compile it and deploy the resulting bytecode onto a blockchain that supports the EVM. The Remix IDE provides an emulation of the EVM, which allows developers to deploy compiled code immediately and avoid incurring real-world gas fees.

To start, we opened the files we wanted to compile in the Remix IDE. Shown in Figure 2, we clicked the file icon on the left vertical menu and selected the files that we wanted. In this case, we're interested in the files packed.sol and unpacked.sol, which contain the Packed contract (optimized for cost) and the Unpacked (not optimized for cost) contract, respectively.

Figure 2: The file icon on the upper left displays the directories and files that are part of the current workspace.

We selected one of the files that we wanted to compile, unpacked.sol, from the top horizontal bar, as shown at callout (1) in Figure 3.

Figure 3: The steps required to select a file, compile it and then view the compilation details.

Next, we clicked the compilation icon from the left vertical menu, as shown in callout (2) in Figure 3. At callout (3), we clicked the Compile unpacked.sol button. This action compiled the source code.

To access details about the compilation, we clicked Compilation Details at callout (4) in Figure 3, which displays the details pane for the compilation. The details we're interested in are found within the item labeled STORAGE LAYOUT as shown at callout (5).

We'll discuss the information in the STORAGE LAYOUT pane in a moment. But first, to compile the source code for packed.sol, we went back to the top horizontal bar and clicked the file name packed.sol. Then, we clicked the compile icon on the left side and compiled the packed.sol, just as we did for the file named unpacked.sol.

Once the code is compiled, we're ready to deploy the compiled bytecode. But, before discussing the details of deployment, let's look at how the Remix IDE reports the storage layout for state variables declared in both the Unpacked and Packed smart contracts.

Viewing the Storage Layout Details

The Remix IDE provides features that make it possible to do very detailed analysis of a smart contract. One such feature is the ability to inspect the Storage Layout of a given smart contract. As mentioned above, state variables are stored on a blockchain in a particular way depending on the variable's data type and, in the case of a struct, the order in which variables are declared within the given struct.

Take a look at Figure 4 below, which is a side-by-side comparison of the storage allocation for the structures defined in both the Unpacked and Packed smart contracts.

Figure 4: Comparing compilation details for unoptimized and optimized storage allocation.

In Figure 4, the storage layout for the unoptimized smart contract Unpacked is on the left and the Packed smart contract is on the right. Notice that the structure named NotOptimized in the Unpacked smart contract is allocated three 32-byte slots. This makes sense because as described above, the variables number_1 (8 bytes) and number_2 (32 bytes) add up to 40 bytes. They cannot fit into a 32-byte slot. Rather, 2 slots are required. Also, you can see that the numberOfBytes allocated to the NotOptimized structure is 96 bytes as redboxed at the lower, left part of the report.

Now, take a look at the right side of Figure 4.

Notice that the Optimized structure requires only two 32-byte slots. This makes sense due to the order of the variables in the structure. Variable number_1 is a uint64 of 8 bytes. It's followed by variable number_3, which is a uint64 of 8 bytes. Hence, both variables can occupy a single 32-byte slot. Then, variable number_2 follows. Variable number_2 is a uint256, which is 32 bytes in size. Hence, it fits into a single 32-byte slot too. The overall result is that the Optimized structure takes up two 32-byte slots or 64 bytes all together. This 64-byte total is redboxed at the bottom right of the report in Figure 4.

As you can see, just by compiling the smart contracts, the Remix IDE has provided us with important information. Analyzing the expense of storage allocation showed us opportunities to reduce the cost of transaction fees. But, there were even more savings to be realized when we deployed the smart contracts.

Let's take a look.

Deploying the Smart Contracts and Viewing the Gas Fees

After compiling the code for the smart contracts, we needed to deploy them. As mentioned above, in this case we deployed the compiled bytecode for the contracts to the EVM/blockchain emulator that's built into the Remix IDE as shown in Figure 5 below.

Figure 5: The Remix IDE ships with EVM/blockchain emulators.

We used Remix VM (London), which is the sandbox that uses the London fork of Ethereum.

We accessed the Remix IDE deployment tools by clicking the Deploy icon on the vertical menu bar on the left side of the Remix IDE, as shown at callout (1) in Figure 6 below.

Figure 6: Deploying a smart contract and analyzing fees for the contract constructor.

Remix deployed the smart contract that was selected under the file system icon and subsequently compiled it using the compilation tool described above. In this case, as shown in Figure 6, we selected the file unpacked.sol. Then, we clicked the Deploy button as shown at callout (2) in Figure 6 to deploy the compiled smart contract.

Once a smart contract is deployed, the Remix IDE displays all the public variables and functions that are exposed by the smart contract. These items are shown as buttons in the deployment pane, as shown in Figure 6. Both the Unpacked and Packed smart contracts published a single function named fillstruct(). Since no public variables are declared in the smart contracts, none appear as buttons in the deployment pane. The function fillstruct() does nothing more than fill the smart contract's struct with data that is defined within the function.

We clicked on the fillStruct button (callout 3 in Figure 6) to execute the fillStruct() logic. Clicking the button sends output to Remix's terminal pane as shown in callouts (4) and (5) in Figure 6. We can use the information sent to the terminal pane to analyze not only the fee incurred by executing the function, but also the fee incurred simply by loading the smart contract onto the blockchain. (Fees are denominated in gas.)

The following sections discuss the details about these fees and how they are reported in the terminal window.

Comparing Fees

In the previous sections we looked at how we used the Remix IDE to compile source code and then deployed the resulting bytecode into the EVM emulator that's part of the Remix IDE. In this section, we'll discuss the information we gathered. We'll compare the fees incurred by running both the Unpacked and Packed smart contracts.

Figure 7 shows three instances of a terminal window that displays output from deploying the Unpacked smart contract and exercising the fillstruct() function therein.

Figure 7: The gas fees incurred by the unoptimized smart contract.

The first instance at the top is the output generated when we first clicked the Deploy button in the deployment pane to send the smart contract onto the EVM. The second two windows are the result of clicking the fillStruct button. As you might recall, the fillStruct button executes the function fillStruct() that is declared in both the Unpacked and Packed smart contracts.

For clarity, Table 1 below shows the redboxed data as shown in the three terminal windows in Figure 7.

Table 1: The costs incurred from deploying the Unpacked smart contract and executing the fillStruct() function.

Unpacked contract

Action

Output to terminal window

Clicked Deploy (constructor)gas: 129469 gas    
transaction cost: 112581 gas    
execution cost: 112581 gas
Clicked fillStruct button oncegas: 100951 gas     
transaction cost: 87783 gas    
execution cost: 87783 gas
Clicked fillStruct button twicegas: 32296 gas    
transaction cost: 28083 gas     
execution cost: 28083 gas

Take note of the gas, transaction cost and execution cost for each action listed in Table 1. Next, look at Figure 8 and Table 2 below. The figure and table describe the gas, transaction cost and execution cost associated with running the Packed smart contact.

Figure 8: The gas fees incurred by the optimized smart contract.

Table 2: The costs incurred from deploying the Packed smart contract and executing the fillStruct() function.

Packed contract

Action

Output to terminal window

Clicked Deploy (constructor)gas: 129469 gas    
transaction cost: 112581 gas    
execution cost: 112581 gas
Clicked fillStruct button oncegas: 75823 gas     
transaction cost: 65933 gas     
execution cost: 65933 gas
Clicked fillStruct button twicegas: 30053 gas    
transaction cost: 26133 gas    
execution cost: 26133 gas

At first glance, you can see the costs associated with the Packed contract above are less than the costs associated with the Unpacked contract preceding it. The overall cost of the Packed contract is less because the Optimized struct in the Packed contract has been optimized in terms of storage allocation. When you continue to analyze the data in both Tables 1 and 2, you'll see that the costs diminish from the first action, when the contract is deployed, to the second action, which is the first time the fillStruct() function is called, to the third action, when the fillStruct() function is called for the second time. The reason for these diminished costs is as follows.

There is always a cost incurred when a smart contract is initially deployed by the EVM onto the underlying blockchain. Hence, the gas, transaction cost, and execution cost are associated with the first action "Click Deploy" as shown in the tables above.

The initial cost is reflected in a call to the smart contract's constructor. (A constructor is programming logic that's executed when a smart contract is created. Sometimes, constructor behavior is implicit; other times it's explicit. In this case, it's implicit.)

The gas cost is the implied gas limit that Remix and the EVM emulator assign to the action. The gas limit is the amount of gas the contract owner is willing to spend to deploy the smart contract.

The transaction cost is the amount of gas consumed to run the transaction.

The execution cost is the gas consumed to do the computational operations, which are executed as a result of the transaction. In the case of the Unpacked and Packed smart contracts, the transaction cost and execution cost are the same. These contracts do nothing more than define a struct and publish a function that fills the struct. On the other hand, real-world transaction and execution costs can differ in more complex smart contracts.

Things get interesting when we compare the costs of the first and second call to the fillStruct() function, which is the second and third action in the tables above.

Notice the first call to the fillStruct() function is dramatically less than the initial call to the constructor. This makes sense because the smart contract is already deployed to an underlying blockchain. The only computation work that's needed is to run the fillStruct() function. As mentioned above, the fillStruct() function does nothing more than fill the struct within the smart contracts with data.

However, when you look at the second call to the fillStruct() function in each smart contract, you'll notice the costs are less than the first call. The reason for the diminishing cost is that the EVM charges a fee every time a zero value in a smart contract state variable is changed to a non-zero value.

Before we go into the details of this reasoning, let's review the source code for both the Unpacked and Packed smart contracts so that the explanation is easier to understand.

Listing 1 below is an excerpt of code from the Unpacked smart contract. Listing 1 contains the definition of the NotOptimized struct, a declaration of a variable named example (which is an instance of the NotOptimized struct), and the source code for the function fillStruct().

SOLIDITY
1struct NotOptimized {
2 uint64 number_1;
3 uint256 number_2;
4 uint64 number_3;
5}
6NotOptimized example;
7function fillStruct() public {
8 example = = NotOptimized(100, 400, 200);
9}

Listing 1: Declaring the NotOptimized struct, a variable named example of type NotOptimized and the function fillStruct() in the Unpacked smart contract.

Listing 2 below shows a code excerpt from the smart contract Packed. The code is similar to that shown in Listing 1 above; only the code in Listing 2 has the Optimized struct, which as the name implies, is the optimized version of the struct.

SOLIDITY
1struct Optimized {
2 uint64 number_1;
3 uint64 number_3;
4 uint256 number_2;
5}

Optimized example;

SOLIDITY
1function fillStruct() public {
2 example = = Optimized(100, 200, 400);
3}

Listing 2: Declaring the Optimized struct, a variable named example of type Optimized and the function fillStruct() in the Packed smart contract.

Now let's elaborate on the reasoning behind the diminishing costs of executing the fillStruct() functions in each smart contract.

When each smart contract is created and put on the blockchain, the values assigned to the struct's member variables number_1, number_2, and number_3 are set to zero, as shown in the following pseudo-code (my_struct is a fictitious name for a Solidity structure):

SOLIDITY
1my_struct {
2 number_1 = 0
3 number_2 = 0
4 number_3 = 0
5}

The first time the function fillStruct() is called in each smart contract, the function adds non-zero values to the struct's members. In the case of the Unpacked smart contract, the call is as follows:

SOLIDITY
1function fillStruct() public {
2 example = = NotOptimized(100, 400, 200);
3}
4In the case of the Packed smart contract, the call is the following:
5function fillStruct() public {
6 example = = Optimized(100, 200, 400);
7}

Thus, in pseudo-code, the state of the struct after the new values are assigned is:

SOLIDITY
1my_struct {
2 number_1 = 100
3 number_2 = 400
4 number_3 = 200
5}

In either smart contract, adding non-zero numbers to a variable that has a value of zero incurs more gas cost than if the function was adding non-zero values to members that already had a non-zero value.

On the other hand, the second call to

SOLIDITY
1fillStruct()

is adding non-zero values to the struct, but the struct's members already have non-zero values, so no additional fee is incurred.

All this attention to zero and non-zero values might seem like a fine point. But, it's far from trivial. Over time, all of these unwarranted gas costs can add up to a significant increase in fees, particularly when the smart contract is called hundreds if not thousands of times an hour, which is not that unusual for games and betting applications that are implemented using smart contracts running on a blockchain. In the bigger picture, these unnecessary fees (due to lack of Solidity code optimization) will drive up the TCO of any smart contract-based distributed ledger initiative.

If these smart contracts were created using only a simple text editor and then compiled and deployed by executing commands from the command-line interface in a terminal window, we'd have no insight into the potential costs of running our code. On the other hand, using Remix IDE gives us immediate, deep insights into the costs associated with deploying and running the Unpacked and Packed smart contracts. Also, the IDE also allowed us to inspect how the EVM stores smart contract data on the underlying blockchain. These are but a few of the many features the IDE offers, but they really matter.

Putting it All Together

Transaction fees are a prominent part of smart contract development. Unlike coding in a typical business system environment in which computing and storage resources have become amazingly inexpensive, when it comes to writing smart contracts, programmers need to be aware of every byte of storage and every CPU cycle their code consumes. Even the simplest of smart contracts can add up to a significant expense when optimized poorly.

Fortunately, the programming environments that support smart contracts have matured over the years, putting resource consumption front and center in the IDE experience. In fact, there's a good argument to be made that tools such as Remix are a driving force behind the proliferation of smart contract programming among software developers. The Remix IDE allows developers to focus on the logic of their smart contracts yet never lose sight of the fees their code will incur when it's running in the real world. It's a win-win situation for all parties involved and as we can report, IDEs such as Remix are not only free to use, but they also make smart contract programming an engaging, enjoyable experience.

footer logo

© 2024 Blockchain Journal