solidity – What Does Explicit Conversion from Address to Contract Type Do in Solidity?

constructorcontract-designcontract-developmentsolidity

I'm starting in solidity development and I saw this being used in a contract and I didn't quite understand the meaning of it.

contract A {
    address bAddress;
    constructor(address b){
       bAddress = b;
    }
 
    function callHello() external view returns(string memory){
       B b = B(bAddress); // explicit conversion from address to contract type
       return b.sayHello();
    }
}
contract B {
     string greeting = "hello world";
     function sayHello() external view returns(string memory){
         return greeting;
     }
}

In this example, we have the B b = B(bAddress);. This code is the part that I have doubts about, and I tried to search for any question that could help me, but I didn't find one that exactly explained why the bAddress is passed to the B contract. Is it a hidden param of the constructor? Like, something that it is there and you can always pass it in? Does the address that we put there turn into the contract address of type B?

Sorry if it's a recurrent question, I just couldn't find an answer that would help me understand this. In the example above they do have a comment saying explicit conversion from address to contract type about that line, but I couldn't understand.

Thanks in advance.

Best Answer

In Solidity a contract variable is really just an address under the hood.

You can actually interact with code deployed at a particular address without having a contract variable. address has member functions like .call() for example. These functions are a low-level EVM way to make an external call. At this level functions do not really exist. You specify some call data to be sent to that code and receive back a piece of return data.

Solidity's contract types are an abstraction to make that external code look more like classes you may be familiar with from other languages. Classes can have methods, and in Solidity the compiler simulates this by having the bytecode expect a function ID (selector) in the first 4 bytes of call data and do different things based on which ID was sent. In particular the ID determines how to interpret the data that follows it (i.e. the parameters).

Instead of using the low-level .call() on an address and manually constructing the right call data, you can just call an external function on a contract, and the compiler will convert that for you to a low-level call in the bytecode it generates. To do this, however, it needs to know the address of that contract. You can give it this information by casting the address to the contract type. For convenience, you can store the result of such a cast in a contract variable instead of having to cast it every time.

Apart from external functions, contracts also have internal ones and these work very differently. You probably noticed that you cannot call an internal function on a contract variable. You can only call internal functions from your own contract or from contracts you inherit from. That's because calls to these do not go to a different address and do not need the whole mechanism that simulates external functions. They're actual functions that reside in your contract's own bytecode. Calling them is just a normal jump to a different place in the bytecode.

So, to summarize, contracts contain a mix of internal functions that you use in the contract itself and external functions that you don't call yourself but expect to be called from other contracts. This might be confusing when you're only using the latter because you do not need all that other stuff. You don't even need to know the function body to call it, just the name and the parameters. This is why there exists another kind of contract - interface. Interfaces only define names and parameters of external functions. That's really all you need to know to perform an external call. It's much more common to use an interface rather than actual contract in the situation shown in your example, and I think it makes its purpose clearer.

Contract construction is yet another thing. The constructor is not involved at all when you're casting. The constructor is invoked only when you deploy a contract, and this usually happens in a completely separate transaction. Constructor parameters are included in transaction data in that case.

It's only possible to call the constructor from within a contract in one of two ways:

  1. Passing arguments to a base constructor. This cannot be mistaken for a cast because it never happens inside a function body.
  2. Contract deploying another contract. This uses a different syntax: new B(...) so it's also clearly distinguishable from a cast. Note that you cannot use this with interfaces because they only carry enough information to call the contract they represent, not to recreate it.