The nitty-gritty of Ethereum and Solidity : Smart Contracts (Part 2).

Alberto Molina
Coinmonks
8 min readSep 8, 2023

--

this blog is the second and last part of the blog series “The nitty-gritty of Ethereum : Smart Contracts”. If you want to read Part 1 check this link:

The nitty-gritty of Ethereum and Solidity : Smart Contracts (Part 1).

Smart Contracts Interaction

A very interesting part of smart contracts is how they can be executed.

Smart contracts can be invoked by off-chain users/programs submitting transactions to the blockchain or they can also communicate among them by invoking each other’s methods (following an off-chain invocation off course, smart contracts can not “trigger” themselves).

Smart contract interactions must follow the ABI specifications, which is the set of rules and definitions that standardize smart contracts communication in the ethereum ecosystem.

In this section I will present, in a simplified way, how data must be submitted to the blockchain in order to trigger a smart contract according to the ABI Specifications. Then, I will talk about the different possibilities to invoke smart contracts functions from off-chain and on-chain applications.

Context

Before we start, I will simply clarify which tools I will be using in this blog.

For off-chain entities I will be using the web3 javascript library (web3.js) because it encapsulates the JSON-RPC protocol which is the actual protocol used to communicate with the blockchain. Any other library that can be used for the same purpose would follow the same concepts (ethers.js, …) but the syntax will probably be different.

For on-chain smart contracts I will be using the solidity language but any other EVM compatible programming language would also work.

ABI Specification

The Abi Specification indicates how to build the string of bytes that must be sent as “data” into a “transaction” when invoking a smart contract function. The string of bytes contains 2 main parts:

  • Function selector: first 4 bytes. They indicate the exact function of the smart contract that is been invoked. The function selector is obtained by calculating the hash (keccak256) of the function signature (function name followed by its input parameters data types “func1(bool,uint256,address)”) then simply extracting its first 4 bytes. There can be some collisions since we are just using 4 bytes but it is highly unlikely….

Note: If a “struct” or an “enum” are used as input arguments data types, the selector must be calculated as indicated below

struct ST{
uint256[] list;
}

enum EN{
SMALL,
MEDIUM,
LARGE
}

function f(ST memory _values) external {
...
}

function g(EN _value) external {

}

// selector(f) = bytes4(keccak256("f((uint256[]))"))
// selector(g) = bytes4(keccak256("g(uint8)"))
  • Encoded arguments: from the 5th byte on, we must add the encoded arguments that we are passing as input parameters in the same order they are specified in the function signature. There are two type of arguments, statics (value data types like bool, unit256, …) and dynamics (reference data types like array, …). Static arguments take 32 bytes (at the position they are indicated by the method signature) and they contain the argument’s value (padded with 0s if necessary). Dynamic arguments are encoded in a different way. The 32 bytes reserved to them (at the position they are indicated by the method signature) indicate the position where the argument’s value is actually included (as an offset of bytes to count from the beginning of the encoded argument part). At the indicated position, the first 32 bytes indicate the length of the argument (how many values it contains) then the actual values are listed.

EXAMPLE 1

  • Function : baz(uint32 val, bool check) returns bool
  • Function Signature : baz(uint32,bool)
  • Invocation : baz(69, true)

Orange bytes = function selector. First 4 bytes of keccak256(“baz(uint32,bool)”).

Blue bytes = first encoded parameter, it is a static parameter with value “69” (0x….45).

Red bytes = second encoded parameter, it is a static parameter with value “true” (0x….01).

EXAMPLE 2

  • Function : sam(bytes name, bool check, uint256[] ids)
  • Function Signature : sam(bytes,bool,uint256[])
  • Invocation : sam(“dave”, true, [1,2,3])

Orange bytes = function selector. First 4 bytes of keccak256(“sam(bytes,bool,uint256[])”).

Blue bytes = first encoded parameter, it is a dynamic parameter, we start by indicating its location (byte 0x60). Then at location 0x60, the first byte indicates the length (0x…..04 = 4 bytes since data type is bytes), the second byte indicates the parameter actual value “dave” (0x646176650………0).

Red bytes = second encoded parameter, it is a static parameter with value “true” (0x….01).

Green bytes = third encoded parameter, it is a dynamic parameter, we start by indicating its location(byte 0xa0). Then at position 0xa0, the first byte indicates the length (0x….03 = 3 words since it is a uint256 array), then the three bytes indicating the values ”1”, “2”, “3” (0x…..01, 0x….02, 0x……03).

Off-chain to On-Chain communication

You have a front end or backend application that needs to interact with some ethereum smart contract. I will be using the web3.js library for javascript which will deal with the JSON-RPC protocol and will also generate the abi specification compliant string of bytes that must be submitted to the blockchain.

There are two possible scenarios, you either have the smart contract JSON ABI or you do not.

WITH SMART CONTRACT JSON ABI

The smart contract JSON ABI is a JSON file that is generated by the solidity compiler when you build your smart contracts. The compiler actually generates two documents:

  • ByteCode: The opcodes (EVM operations) that will be deployed on the blockchain and the opcodes from the “constructor” function (if it exists) that will be executed only once, when deploying the smart contract, all in bytes format.
  • The JSON ABI: A json array with the list of public and external functions, events and errors related to your smart contracts. Each function, event and error is a json object within the array and they contain all the necessary information for off-chain entities to interact with them.

The JSON ABI objects contain the following information

Function objects:

  • Type : indicates the type of function, options are “function” (for regular functions), “receive”, “fallback” and “constructor” (for the special ethereum functions).
  • Name : Function name.
  • Inputs : Array of objects containing for each function input argument its name, type and component.
  • Outputs : Just like input but for the function output arguments.
  • State Mutability : the function mutability, options are “view” (only reads from the blockchain), “pure” (neither writes nor read from the blockchain), “nonpayable” (cannot receive ether) and “payable” (can receive ether).

Event objects:

  • Type : always “event”.
  • Name : Event name.
  • Inputs : Array of objects containing for each event parameters its name, type, component and indexed (true or false).
  • Anonymous : true if the event was declared anonymous.

Error objects:

  • Type : always “error”.
  • Name : Error name.
  • Inputs : Array of objects containing for each error parameters its name, type and component).

In order to interact with a smart contract from your off-chain application, you first need to import the JSON Abi file, then instantiate an object providing the JSON Abi and the address pointing to the smart contract. From that moment on you can invoke the contract’s methods directly like for any other object.

Smart contract invocations will be done asynchronously.

// Reference the smart contract
const SmartContract= require(“SmartContract”);
// Retrieve the JSON ABI and address
const SmartContractAbi = SmartContract.abi;
const SmartContractAddress = "0x......"
// Instantiate an object that "encapsulates" the smart contract
const SmartContractObject = new web3.eth.Contract(SmartContractAbi, SmartContractAddress);
// Now you are ready to interact with the smart contract. Functions // invocations will return promises.
SmartContractObject.methods.func1(…).send({from: …, …}).on(…);
SmartContractObject.methods.func2(…).call({from: …}).on(…);

WITHOUT SMART CONTRACT JSON ABI

If you do not have the JSON ABI you can still interact with smart contracts, but it will be a little bit more cumbersome and annoying.

You will have to create the blockchain transaction yourself from the method definition (in json format), the input parameters you wish to submit and send it directly to the smart contract address.

You will be able to either submit a “send” transaction (an actual transaction that will change the state of the blockchain) or a “call” transaction (not an actual transaction from the ethereum perspective since it will only read data).

Transactions will be submitted asynchronously.

// Define the Transaction Data
const TransactionData = web3.eth.abi.encodeFunctionCall({
name: 'myMethod',
type: 'function',
inputs: [{
type: 'uint256',
name: 'myNumber'
},{
type: 'string',
name: 'myString'
}]
}, ['2345675643', 'Hello!%']);
// Now you can either send a transaction or make a call. In both 
// cases you will be dealing with Promises
web3.eth.sendTransaction({from: …, to: …, data: TransactionData, …}).on(…);
web3.eth.call({from: …, to: …, data: TransactionData, …}).on(…);

On-chain to On-Chain communication

You are implementing a smart contract and you would like to invoke another contract’s functionality from your code. I will be using the solidity programming language that offers some in-built functions that generate the abi specification compliant string of bytes.

Just like for the Off-chain to On-chain situation, there are two possible scenarios, you either have the smart contract interface or you do not.

WITH SMART CONTRACT INTERFACE

If you have the interface of the smart contract you wish to invoke solidity will do most of the work for you.

You just need to import the interface into your smart contract file, instantiate an object of the interface type passing the smart contract address and you are ready to go. You are now able to invoke the contract’s methods as for any other object.

// Import the interface and define the contract object using the 
// interface as a data type
import "IContract.sol";
IContract Contract;
// Instantiate the contract with its address
address contractAddress = 0x.......;
Contract = IContract(contractAddress);
// Invoke the contract's methods as defined by the interface
Contract.func1(....);

WITHOUT SMART CONTRACT INTERFACE

If you do not have the contract interface then you will have to build the whole message.

You will need the contract address, the method signature (method name and input parameters types separated by comas) and the input arguments you wish to submit (also separated by comas).

// Contract Address and function signature
address contractAddress = 0x.......;
string memory Method = “func1(uint256,bool)”;
// Define the abi compliant data
bytes memory AbiData = abi.encodeWithSignature(Method, 345223, true);
// Send the message
(bool success, bytes memory data) = contractAddress.call(AbiData);

Warning

It is important to note that, independently of the way you are interacting with a smart contract, if the smart contract address you are using is wrong, you will still be submitting the transaction, there is no check whatsoever. If the smart contract does have a function that matches your call it will be executed, if it does not then the transaction can either fail or succeed if the smart contract has a “fallback()” function… The point is that the consequences can be unexpected and potentially undetectable, which is why you have to be sure to which contract you are sending the transaction, always make sure the contract address is the right one.

--

--