Calling functions of other contracts on Solidity
By: Graphicaldot, Saurav Verma
Tags: Blockchain, Solidity, Cryptocurrency, Ethereum

Bytecode & ABI

As Ethereum uses EVM(Ethereum Virtual Machine) as a core component of the network, smart contract code written in high-level languages needs to be compiled into EVM bytecode to be run. EMV Bytecode is an executable code on EVM and Contract ABI is an interface to interact with EVM bytecode. For example, if you want to call a function in a smart contract with your JavaScript code, ABI plays a role as an intermediary between your JavaScript code and EVM bytecode to interact with each other. The below diagram shows the architecture of Contract ABI, EVM bytecode, and outside components(dApp and network). The left side is a process of compiling and the right side is interacting.

Smart contract languages like Solidity cannot be executed by the EVM directly. Instead, they are compiled to low-level machine instructions (called opcodes). In order to efficiently store opcodes, they are encoded to bytecode.

More about EVM can be found here.

In Ethereum, Contract ABI is an interface that defines a standard scheme of how to call functions in a smart contract and get data back. Contract ABI is designed for external use to enables application-to-contract and contract-to-contract interaction. For example, if you want to call a smart contract function from your dApp, you call via Contract ABI. Contract ABI is represented with JSON format like below.

[{"inputs":[],"stateMutability":"payable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"_guess","type":"uint256"}],"name":"guess","outputs":[],"stateMutability":"nonpayable","type":"function"}]

Contract ABI defines function names and argument data types. It is used to encode contract calls for the EVM and to read data out of transactions. There is a clear specification of how to encode and decode Contract ABI.

You need to compile your code into an application binary interface (ABI) and bytecode (bin) for deploying onto the blockchain.

Interacting with Smart contracts

If you have the function signatures or the actual source code of the contract — already deployed on the Blockchain — with whom you want to interact, you can interact with its address on the Blockchain.

contract AlreadyDeployedContract { function Test(uint _num1, uint _num2) public returns (..return values) {} } contract TestABI {



Deployed dc;



function Existing(address _t, uint _num1, uint _num2) public {

dc = AlreadyDeployedContract(_t); //Here _t is the address

// of the contract

// already deployed on the Blockchain.

dc.Test(_num1, _num2);

}

This is easy and actually the recommended way to interact with deployed contracts. However, what if we don’t have the ABI of the deployed contract?

We have four options at our disposal to explore if we don't have the ABI of the deployed contract:

call : Execute code of another contract

: Execute code of another contract delegatecall : Execute code of another contract, but with the state(storage) of the calling contract. When a third contract calls a delegate call to some function in callee on behalf of the caller and storage changes are made in the caller’s value and nothing is reflected in callee’s storage. This means that a contract can dynamically load code from a different address at runtime. Storage, current address, and balance still refer to the calling contract, only the code is taken from the called address.

: Execute code of another contract, but with the state(storage) of the calling contract. When a third contract calls a delegate call to some function in callee on behalf of the caller and storage changes are made in the caller’s value and nothing is reflected in callee’s storage. This means that a contract can dynamically load code from a different address at runtime. Storage, current address, and balance still refer to the calling contract, only the code is taken from the called address. callcode : (deprecated)

: (deprecated) staticcall : This is basically the same as a call, but will revert if the called function modifies the state in any way.

Note: DELEGATECALL was a new opcode that was a bug fix for CALLCODE which did not preserve msg.sender and msg.value . If Alice invokes Bob who does DELEGATECALL to Charlie, the msg.sender in the DELEGATECALL is Alice (whereas if CALLCODE was used the msg.sender would be Bob).

Note: Prior to version 0.5.0, .call, .delegatecall and .staticcall only returned the success condition and not the return data.

They all take a single bytes memory parameter and return the success condition (as a bool ) and the returned data ( bytes memory ). The functions abi.encode , abi.encodePacked , abi.encodeWithSelector and abi.encodeWithSignature can be used to encode structured data.

To understand the different encoding techniques available in solidity, Please look at the different encoding and decoding schemes.

abi . encode(...) returns (bytes memory) : abi-encodes the given arguments

: abi-encodes the given arguments abi.encodePacked(...) returns (bytes memory) : Performs packed encoding of the given arguments. Note that packed encoding can be ambiguous!

: Performs packed encoding of the given arguments. Note that packed encoding can be ambiguous! abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory) : ABI-encodes the given arguments starting from the second and prepends the given four-byte selector

: ABI-encodes the given arguments starting from the second and prepends the given four-byte selector abi.encodeWithSignature(string memory signature, ...) returns (bytes memory) : Equivalent to abi .encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)`

: Equivalent to abi.decode(bytes memory encodedData, (...)) returns (...) : ABI-decodes the given data, while the types are given in parentheses as second argument. Example: (uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))

Example:

To call the function Test with arguments 12, 35 on AlreadyDeployedContract deployed at address addr.

bytes memory payload = abi.encodeWithSignature("Test(uint, uint)", 12, 35);

(bool success, bytes memory returnData) = address(addr).call(payload);

require(success);

It is possible to adjust the supplied gas with the gas modifier:

address(addr).call{gas: 1000000}(abi.encodeWithSignature("Test(uint, uint)", 12, 35));

Similarly, the supplied Ether value can be controlled too:

address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("Test(uint, uint)", 12, 35));

Lastly, these modifiers can be combined. Their order does not matter:

address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("Test(uint, uint)", 12, 35));

In a similar way, the function delegatecall can be used: the difference is that only the code of the given address is used, all other aspects (storage, balance, …) are taken from the current contract. The purpose of delegatecall is to use library code which is stored in another contract. The user has to ensure that the layout of storage in both contracts is suitable for delegatecall to be used.

Warning: All these functions are low-level functions and should be used with care. Specifically, any unknown contract might be malicious and if you call it, you hand over control to that contract which could in turn call back into your contract, so be prepared for changes to your state variables when the call returns.

Note: Most of the things explained in this blog have been copied from other impressive and astounding blogs.