Ethereum Virtual Machine (EVM) Message Calls – Solidity Smart Contracts

You can scroll through the presentation as you watch the video here:

In this article, we’ll hold the depth from the last article regarding the Ethereum Virtual Machine through topics such as message calls and special variants of calls, logs, callcode/delegatecall, libraries, logs, special operations for smart contract code creation/removal, and finally, precompiled contracts.

This is Part 6 of our Solidity Crash Course:

🌍 Full Course Curriculum and Overview: Solidity Crash Course

Message Calls

Message calls are a mechanism to enable inter-contract communication, such as calling other contracts or sending Ethereum to non-contract accounts.

Message calls are similar to transactions (exchange of messages) in that they also use parameters source, target, data payload (input data), Ether, gas, and return data.

In terms of implementation, every transaction is nested (https://nestingdolls.co/blogs/posts/meaning-symbolism-nesting-dolls), or more commonly said, wrapped, in a top-level message call that can also create further message calls, e.g. in a situation where additional messages need to be exchanged and calculations performed before a transaction can finalize.

The contract has an additional role, and that is to set the amount of gas that will be deducted from the outer (upper) message call before the remaining gas gets forwarded to the inner (lower) message call. We have previously explained what an out-of-gas exception is.

If any type of exception happens in an inner message call, the exception will put an error value onto the stack, and only the amount of gas that was forwarded to the inner message call is used.

In Solidity, the calling contract follows up on such situations by throwing an exception by default, causing the exceptions to “bubble up” or emerge up the call stack.

Each time the called contract, i.e. a function gets called, it will be assigned with an instance of (working) memory that lasts until the call ends. Such an instance is usually known as a stack frame, activation frame, or activation record (https://www.techopedia.com/definition/22304/stack-frame).

The memory instance will also receive the call payload, i.e. the (function) call arguments via a separate memory area, the calldata. The called contract can return the data to a memory location in the caller’s memory, which is preallocated by the caller.  All message calls are synchronous, meaning the caller contract (function) will wait until it receives a reply from a called contract (function).

💡 Note: A stack frame exists only during the contract (function) call and holds all the information needed for a called function to execute and return to the caller. It enables some important programming approaches, such as recursion.

Message calls can go 1024 levels in depth (that’s 1024 stack frames), meaning that we shouldn’t use them for iterating over an array or performing more complex operations. When we do require complex operations, loops are a tool of choice.

Besides that, there is also a limit on how much gas we can forward to a message call (63/64 parts), practically limiting us further, to about 1000 levels in depth.

callcode and delegatecall and Libraries

Besides ordinary message calls, as described in a previous chapter, we have special message calls available, named delegatecall and callcode.

These specific message calls might seem a bit strange, so we’ll go through step by step.

A callcode message call is very similar to an ordinary message call, except for one crucial difference: the code from the called contract (at the target address) is executed in the context of the calling contract (at the source address).

What this means is that the executed code behaves just like it was an integral part of the calling contract.

Additionally, a delegatecall message call is identical to callcode, but it keeps the variables msg.sender (current address) and msg.value (account balance) unchanged.

delegatecall and callcode message calls enable our contract to dynamically load the code from a different address at runtime, effectively enabling us to import a remote contract as a reusable library code and use it in our contract’s storage, e.g. for building complex data structures.

Logs

Logs are a specially indexed data structure that provides us with logging functionality, with logs saved in the Ethereum blockchain.

In one of the previous articles, we mentioned that logs are used by Solidity to implement events.

The log data in the blockchain is partly stored in Bloom filters (https://llimllib.github.io/bloomfilter-tutorial) unavailable to contracts, however, it can be accessed from outside the blockchain, by the so-called light clients.

💡 Light clients are network peers that do not download the whole blockchain but still can search for log data stored in bloom filters in an efficient and cryptographically secure way.

Create

We have previously mentioned a catalog of EVM instructions, called the instruction set.

Each instruction is represented by an instruction mnemonic, which is “a word or acronym used in assembly language to represent a binary machine instruction operation code”

🌍 (https://www.webster-dictionary.org/definition/instruction+mnemonic).

As the definition mentioned, there is also an operation code, or short, an opcode. An opcode is a number that EVM uses to recognize specific instructions.

Among other opcodes, there is a special opcode create, that a contract can use to create other contracts.

Of course, there is a small difference between ordinary message calls and those invoked by create calls: the payload data is treated as code, therefore it gets executed. Upon execution, the caller (creator) contract receives the return data, i.e. the address of the newly created contract on the stack.

Self-destruct and Deactivate

We went through the creation of a contract, but what about its destruction?

Well, the contract can also be taken off duty, so to say, by removing its code from the blockchain.

The code is removed when the contract at a certain address executes a selfdestruct instruction, which will take care of the remaining Ether stored at that address, i.e. it will move it to a specified receiver.

A call to selfdestruct may originate directly from the contract, or indirectly by using instructions delegatecall or callcode.

In the next step, the contract’s storage and code are removed from the contract’s state.

Although the contract removal can also be done, it’s not recommended because if Ether is mistakenly sent to a removed contract’s address, it will become lost and unavailable.

A better approach to deactivating contracts is disabling them by changing some of the internal states which will cause all functions to revert. This way, such a contract will be unusable, but the Ether will be immediately returned, instead of lost.

💡 Note: selfdestruct instruction is not the same thing as the delete command we all know and love (?), because a contract removed by calling selfdestruct will remain a part of the blockchain’s history.

Precompiled Contracts

As a final chapter of this article, we will mention a special group of contracts, the precompiled contracts.

They occupy a small range of reserved contract addresses between 1 and 8 (inclusive), available for calling just as with any other contract.

However, these precompiled contracts’ behavior and gas consumption are not defined by our code at these addresses. In fact, these addresses do not contain any code. Their behavior and gas consumption are implemented in the EVM runtime environment.

💡 Note: although all (future) precompiled contracts in the Ethereum main chain are expected to land in the reserved address range between 1 and 0xffff (inclusive), there is a possibility that different, but EVM-compatible chains may deviate, that is, use a different set of precompiled contracts.

The Conclusion

In this article, we have gone through the EVM structure and gained a very good insight into its core concepts, structure, and melody.

First and most important, Carly Rae Jepsen has a nice song like made for our main topic, “Call Me Maybe”. It’s a great and cheerful song, as well as our message calls.

Second, we found out how a contract may dress in borrowed feathers – by codecall and delegatecode! And while it was at it, it also joined the library.

Third, every self-respected lumberjack knows his logs. Now we do too.

Fourth, what’s the use of the logs, if we don’t know how to put them to good use? That’s why we learned how to create – contracts! Although, digital, not paper ones… But they’re smart!

Fourth, not the happiest part of the process, we also learned how to deactivate and self-destruct. T-800 would be so proud of us because we’ll be back.

Fifth, if nothing else, “we’ll always have Paris”, that is, precompiled contracts. Some of them will surely be in Paris. And now we share some beautiful memories with them.


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):


Also, check out our full Solidity course on the Finxter Computer Science Academy: