Solidity interface: Explained through Javascript lens
Table of contents
Introduction using javascript
Interfaces are important building blocks in the smart contract world to make it composable (combining multiple smart contracts to implement complex logic). Lets me explain the idea through analogies from Javascript world.
Suppose we have an object in JS
const obj = {
methodValid: (val) => val + '2'
}
We can call it using obj.methodValid(1)
, and it will return 12
.
But as JS is not a compiled language, it won't check whether the method exists on obj
. Nothing prevents us from calling obj.methodNonExistent()
, and we won't see any errors until running the program.
Using typescript interface
If we move to Typescript, we can define an interface for the object
interface IObj {
methodValid: (val: number) => number;
}
We need to attach the interface with the object
const obj: IObj = {
methodValid: (val) => val + '2'
}
And we will instantly get an error while compiling.
Type 'string' is not assignable to type 'number'.
Changing the interface definition or the function return statement would fix the issue, and we can run the program afterward.
Coming to solidity
In solidity, we can call functions from another contract.
Let me define a sample program for explanation
contract Parent {
string public element;
constructor() {
element = "a";
}
function setElement(string memory _element) external {
element = _element;
}
}
contract Child {
function getElementDirect(address _parentContractAddress) external view returns (string memory) {
return Parent(_parentContractAddress).element();
}
}
After deploying both contracts, we can fetch data from Parent contracts from Child. (An getter function is automatically created for each public variables)
The above code is looking up the blockchain storage and fetching Parent contract files from the address. After that we can call the function.
But it is a compiled language, so it needs to know details of the Parent contract before calling the element()
function.
There is no chance for runtime error as we are sure that element()
exists.
Using interface
For the above setup we need both contracts in same file. But it's not always feasible. Source contract can be big, or oftentimes we call contracts made by other developers, eg. Uniswap. Interfaces enable this use case.
A Solidity contract interface is a list of function definitions without implementation. And we don't need details of all functions. We just need definitions for function that we are using.
So lets define an interface for Parent contract
interface IParent {
function element() external view returns (string memory);
}
We can fetch data using the interface now
contract Child {
function getElementDirect(address _parentContractAddress) external view returns (string memory) {
return Parent(_parentContractAddress).element();
}
function getElementProper(address _parentContractAddress) external view returns (string memory)
{
return IParent(_parentContractAddress).element();
}
}
We can cheat using interface
I said before "There is no chance for runtime error as we are sure that element()
exists."
Well, unless we are sure about the authenticity of interface definition. Lets explain using an example.
interface IParent {
function getElement() external view returns (string memory); // This doesn't exist in Parent
}
contract Child {
function getElementFaulty(address _parentContractAddress) external view returns (string memory) {
return IParent(_parentContractAddress).getElement();
}
}
There is no getElement()
function in Parent contract. So it will throw error.
We can use any name for the interface
We have used IParent
as interface name for Parent
contract.
It's just a convention to use I
prefix to contract name. Logically we can use any random name.
Same interface can be used for multiple contracts with same function signature
Lets define another contract
contract Parent {
string public element;
constructor() {
element = "a";
}
function setElement(string memory _element) external {
element = _element;
}
}
contract AnotherParent {
string[] public elements;
constructor() {
elements.push("z");
}
function element() external view returns (string memory) {
return elements[0];
}
function setElement(string memory _element) external {
elements[0] = _element;
}
}
interface Interface {
function element() external view returns (string memory);
}
contract Child {
function getElement(address _parentContractAddress) external view returns (string memory)
{
return Interface(_parentContractAddress).element();
}
}
Parent and AnotherParent are using string and array as storage, respectively. But the function signature is same here so we can reuse the interface.
Popular example of this pattern is IERC20 interface from Openzeppelin. There are lots of tokens following the ERC20 convention, so we can use same interface to interact with all tokens.
Changing address to other contract
In the above code snippet if we change _parentContractAddress
parameter to address of different deployment it would work fine.
It would fetch the contract from storage during runtime.
All declared functions must be external
Let me modify the code to change Child contract function to public
visibility.
interface Interface {
function element() external view returns (string memory);
}
contract Child {
function getElement(address _parentContractAddress) public view returns (string memory)
{
return Interface(_parentContractAddress).element();
}
}
It would enable the getElement
method to be called from another method inside Child contract.
Interface only focused on functions that can be called from outside world. So we don't care about that use case at the interface level.
Comparison with JSON ABIs
ABIs are used in Frontend applications to interact with blockchain. It works similar to interfaces.
Both are API of the contract, and their purpose is to define "what" the contract does without explaining the "how".
The whole example code can be found here gist.github.com/admiralrohan/a5dee7dab17c18..