I was tasked to create ethereum smart contracts in my previous job.
the library most recommended by other developers to create
smart contracts was hardhat
.
to create and debug the smart contracts, I developed them using test-driven-development methods. The tests gave me a way to print out and inspect variables.
just as I was done with the development I read somewhere that hardhat has a
console.log()
utility. You import it with import hardhat/console.sol
and
use it like JS’s console.log
. I wondered how it worked. Upon testing, it
seemed to only allow a maximum amount of parameters.
so i decided to look at hardhat’s internals and find out how they do it.
the first step was to find the console.log()
utility function.
this is their repository https://github.com/NomicFoundation/hardhat
1$ ls
2config CONTRIBUTING.md crates docs LICENSE package.json packages README.md scripts yarn.lock
im not familliar with their file structure conventions. But I can guess that
the source code is not in config/
, docs/
, or scipts/
.
1$ ls crates packages
2crates:
3rethnet_evm_napi
4
5packages:
6common hardhat-core hardhat-foundry hardhat-solhint hardhat-truffle4 hardhat-waffle
7eslint-plugin hardhat-ethers hardhat-network-helpers hardhat-solpp hardhat-truffle5 hardhat-web3
8hardhat-chai-matchers hardhat-etherscan hardhat-shorthand hardhat-toolbox hardhat-vyper hardhat-web3-legacy
i didn’t know what crates/
were so I also tried it.
anyway, packages/
contains hardhat-core/
. Inside it we’ll find
console.sol
.
let’s see the code.
1$ cat packages/hardhat-core/console.log
link to it: https://github.com/NomicFoundation/hardhat/blob/main/packages/hardhat-core/console.sol
opening it, we find 1513 lines of repetitive, possibly auto-generated code.
here’s an interesting part of the code:
1$ cat packages/hardhat-core/console.sol -n | tail -n 30
21503
31504 function log(address p0, address p1, bool p2, string memory p3) internal view {
41505 _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,string)", p0, p1, p2, p3));
51506 }
61507
71508 function log(address p0, address p1, bool p2, bool p3) internal view {
81509 _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,bool)", p0, p1, p2, p3));
91510 }
101511
111512 function log(address p0, address p1, bool p2, address p3) internal view {
121513 _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,address)", p0, p1, p2, p3));
131514 }
141515
151516 function log(address p0, address p1, address p2, uint256 p3) internal view {
161517 _sendLogPayload(abi.encodeWithSignature("log(address,address,address,uint256)", p0, p1, p2, p3));
171518 }
181519
191520 function log(address p0, address p1, address p2, string memory p3) internal view {
201521 _sendLogPayload(abi.encodeWithSignature("log(address,address,address,string)", p0, p1, p2, p3));
211522 }
221523
231524 function log(address p0, address p1, address p2, bool p3) internal view {
241525 _sendLogPayload(abi.encodeWithSignature("log(address,address,address,bool)", p0, p1, p2, p3));
251526 }
261527
271528 function log(address p0, address p1, address p2, address p3) internal view {
281529 _sendLogPayload(abi.encodeWithSignature("log(address,address,address,address)", p0, p1, p2, p3));
291530 }
301531
311532 }
this looks fascinating to me. I’m less familiar with statically-typed languages, so I don’t know how common this method is. I’ll explain why it intrigues me.
most of the file consist of functions with the name log
. In python or
probably any dynamically-typed languages, this would not work. For example, if log()
is
called, The interpreter would not know which log
, to use out of the many log
s defined.
11524 function log(address p0, address p1, address p2, bool p3) internal view {
21525 _sendLogPayload(abi.encodeWithSignature("log(address,address,address,bool)", p0, p1, p2, p3));
31526 }
41527
51528 function log(address p0, address p1, address p2, address p3) internal view {
61529 _sendLogPayload(abi.encodeWithSignature("log(address,address,address,address)", p0, p1, p2, p3));
71530 }
in solidity, this is a completely valid use case. Although the function
names are the same, the compiler can determine which log
to use by looking at
the data types of the arguments, and finding which log
function has the
correct parameter data types.
11524 function log(address p0, address p1, address p2, bool p3) internal view {
21528 function log(address p0, address p1, address p2, address p3) internal view {
For example, these two function definitions are almost the same, except the
fourth parmeter. Line 1524
handles log
calls with specifically 4 parameters
with respective types: address
, address
, address
, and bool
.
Whereas Line 1528
handles log
calls that pass exactly 4 address data types.
11524 function log(address p0, address p1, address p2, bool p3) internal view {
21525 _sendLogPayload(abi.encodeWithSignature("log(address,address,address,bool)", p0, p1, p2, p3));
31526 }
This design means that only a fixed number of arguments can be accepted by hardhat’s console.log.
Console.log will not work if it’s supplied with arguments that are not pre-defined in console.sol
.
I later found the script that generates this code. It’s in
hardhat/packages/hardhat-core/scripts/console-library-generator.js
.
The parameters p0
to p3
, are passed to
abi.encodeWithSignature
which is then passed to _sendLogPayload
.
1$ cat packages/hardhat-core/console.log -n | head -n 14
2 1 // SPDX-License-Identifier: MIT
3 2 pragma solidity >= 0.4.22 <0.9.0;
4 3
5 4 library console {
6 5 address constant CONSOLE_ADDRESS = address(0x000000000000000000636F6e736F6c652e6c6f67);
7 6
8 7 function _sendLogPayload(bytes memory payload) private view {
9 8 uint256 payloadLength = payload.length;
10 9 address consoleAddress = CONSOLE_ADDRESS;
1110 assembly {
1211 let payloadStart := add(payload, 32)
1312 let r := staticcall(gas(), consoleAddress, payloadStart, payloadLength, 0, 0)
1413 }
1514 }
At first glance, I can deduce that Hardhat sends the messages from console.log
to a dedicated public address. It then scans the address for messages and
outputs any messages that are sent to it to the terminal.
So that’s Hardhat’s console.log. We looked at its internals and found mysterious code that turned out to be a smart solution to the problem.