Solidity Layout – Pragmas, Importing, and Comments

Following up on the last article, we will take a closer look at other types of pragmas, but also check out how we can import other source files and paths (docs).

After that, we’ll take a break by going through a small section on comments and getting to know them better. It’ll probably be very familiar territory for the experienced ones among us, but it’ll also prove as a useful first encounter for beginners who are yet to become masters of the programming art.

I like to consider programming as a form of art, because, aside from technical knowledge and approaches we apply while programming, there’s also a large space waiting to be filled with creativity and imagination. On that note, let’s roll πŸ˜‰

ABI Coder Pragma

First of all, let’s explain what is ABI, and then what are ABI encoder and ABI decoder.

According to Wikipedia, an ABI is a shorthand for an Application Binary Interface, which is “an interface between two binary program modules. Most often, one of these modules is a library or operating system facility, and the other is a program that is being run by a user.”

Not a very complicated definition, but let’s elaborate on it in more detail.

An ABI allows us, i.e. our smart contracts to access the data structures and functions of another smart contract, which runs as machine code, i.e. bytecode on an EVM.

We know that bytecode is a product of the final phase of the compilation process, and it runs very low, directly on the underlying hardware.

This means that the ABI definition must provide us with instructions or mapping on how to transform our high-level language access to data structures and functions to communicate with an EVM running the bytecode, and also reinterpret the EVM’s response in bytes back to a high-level language data structure.

The process of translating high-level language calls to bytes and from bytes is called ABI encoding and ABI decoding. ABI encoding and decoding are mostly done automatically, by the compilers or wallets, which interact with the blockchain (source).

πŸ’‘ Info: For comparison, but on a different and higher level, an API as a shorthand for Application Programming Interface defines equivalent ways of accessing resources through a source code.

We can select one of the two implementations by stating pragma abicoder v1 or pragma abicoder v2.

An important difference is that the ABI coder v2 can encode and decode arbitrarily nested arrays and structs.

A “price” for that is possibly less optimal code, and it has not been as thoroughly tested as the older version. However, it is not considered to be an experimental version since Solidity v0.6.0. To use ABI coder v2 before Solidity v0.8.0, we have to explicitly select it using the corresponding pragma abicoder v2;, otherwise, ABI coder v1 will be used by default.

The situation changed with Solidity v0.8.0 when ABI coder v2 became the default, and ABI coder v1 became an option by stating pragma abicoder v1;.

The new encoder (v2) supports all the same types as the old one (v1), so the contracts that use the new coder can seamlessly interact with the contracts that use the old coder.

Communication in the other direction is also possible, but only if contracts that use the old coder avoid the calls that are only supported by the new coder. When that’s not the case, the Solidity compiler will show an error, but we can easily fix that (and get rid of the error) by switching to the new coder.

πŸ’‘ Note: The ABI coder pragma is applied to the entire source code file. This means that if we use the old coder in our contract and inherit a code from a contract that uses the new coder, the inherited code can be freely used in our contract, as long as the new types are used internally, but not in external function signatures.

ℹ️ Info: A function signature consists of the function name, function parameters, and sometimes, a return type.

Experimental Pragma

Experimental pragma lets us activate features of a compiler or the Solidity language that are initially disabled. A notable, still experimental pragma is the one regarding the SMTChecker component: pragma experimental SMTChecker;

SMTChecker

We’ll surely remember the article about the Solidity compiler installation, it was quite a lengthy one.

There was an optional component for an installation called SMTChecker. SMTChecker may be included in the resulting compilation output when the Solidity compiler is being compiled, i.e. built.

Regarding the available downloads, some static binaries incorporate SMTChecker, and also some don’t have it included. Following the official documentation, it is available in most static binary versions for the Ubuntu PPA releases (Ubuntu Personal Package Archives).

The Docker images, Windows binaries, or the statically built Linux binaries mainly don’t include SMTChecker. Solidity compiler for Node.js, solc-js can activate SMTChecker via the smtCallback in case we have an SMT solver installed in our development environment and can run solc-js via the node command.

πŸ’‘ An example shows how to use SMTChecker via smtSolver callback (GitHub):

var solc = require('solc');
const smtchecker = require('solc/smtchecker');
const smtsolver = require('solc/smtsolver');
// Note that this example only works via node and not in the browser.

var input = {
  language: 'Solidity',
  sources: {
    'test.sol': {
      content: 'contract C { function f(uint x) public { assert(x > 0); } }'
    }
  },
  settings: {
    modelChecker: {
      engine: "chc",
      solvers: [ "smtlib2" ]
    }
  }
};

var output = JSON.parse(
  solc.compile(
    JSON.stringify(input),
    { smtSolver: smtchecker.smtCallback(smtsolver.smtSolver, smtsolver.availableSolvers[0]) }
  )
);

πŸ’‘ Note: “Using formal verification it is possible to perform an automated mathematical proof that your source code fulfills a certain formal specification. The specification is still formal (just as the source code), but usually much simpler.” (docs).

This was an excerpt from the SMTChecker and Formal Verification section, Solidity documentation.

By using SMTChecker, we can start getting safety warnings internally produced by an SMT solver. We have to have in mind that the SMT solver isn’t yet completely aligned with all features of the Solidity language, and may report a noticeable amount of warnings, stemming from yet unsupported features.

Importing other Source Files

We can organize the source code in Solidity by using import statements. They are similar to the ones JavaScript ES6 and later versions use, but with the notable exclusion of the default export concept, which is not present in Solidity (source).

A general form of an import statement is import <filename>;, where <filename> is a placeholder for a file containing some useful code e.g. a library.

An import statement in this form would import all global symbols defined in the imported file, into the global scope.

This behavior also encompasses the symbols that are imported in the imported file itself. Due to this reason, it is not recommended to import the entire path because it will fill our namespace with lots of different objects, both those that we need and also those we don’t need.

Besides that, when new items are imported into the imported file, they are automatically propagated to all the namespaces below them.

The alternative forms with the same effect are:

import * as symbolName from "filename";

and

import "filename" as symbolName;

We would follow a better approach by selectively importing symbols, and renaming them when a need arises:

import {symbol1 as alias, symbol2} from "filename";

Import paths

Solidity compiler aims to support consistent, reproducible builds on all sensible platforms, and this requires abstracting away the filesystem-specific details about the locations where source files are stored.

This support is implemented by making the import paths use indirect references to the files: source units.

πŸ’Ύ The compiler has an internal database, namely a virtual filesystem, VFS. The VFS holds a record for each source file, a unique source unit name. The import statement (as shown in previous examples) is translated into a source unit name, which is then used to locate the source unit.

πŸ“– Official Solidity Docs: “the recommended way to interface with the Solidity compiler especially for more complex and automated setups is the so-called JSON-input-output interface. The same interface is provided by all distributions of the compiler.”

This interface is called Standard JSON API, and by using it, we can directly provide the names and content of all our source units, i.e. source file paths as the compiler input.

If we want our compiler to automatically find and load source files into the VFS, the source file names have to be declared in a predefined way so that the import callback can locate and load them.

We’ll get back to this subject in an interesting future article on the topic of Import Path Resolution / Virtual File System.

In that context, the command-line compiler supports loading code only from the local filesystem, so the source units will represent file paths. However, some environments are more versatile in providing custom callbacks, like the Remix IDE which lets us “import files from HTTP, IPFS, and Swarm URLs or refer directly to packages in NPM registry”.

Comments

Solidity supports two common types of comments: the single-line comments //, multi-line comments /* ... */, and the third, special type called the NatSpec comment written as a triple slash /// or double asterisk block /** ... **/. The NatSpec comments must be placed just above the function declaration or code statement.

Conclusion

In this article, we learned about or even just reminded ourselves of several neat features of the Solidity programming language.

We continued on the previous article with two more pragma usages and stumbled on a short story about importing other source files.

Then, we stepped into a topic about importing paths and concluded with a dash on comments.

  • First, we clarified (I hope not the opposite) what ABI is and how ABI coder pragma works.
  • Second, we met face to face with an experimental pragma, namely the SMTChecker
  • Third, we discussed what it means to import from other source files.
  • Fourth, we continued our importer’s career and started importing paths.
  • Fifth, we played a short game with comments.

What’s Next?

This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):