0

I am new to solidity and having difficulty understanding polymorphism and address typecasting of contracts.

I am passing the address of child_1 contract to getChild2Value(address adr) which type cast address to child_2 contract. Should it not raise an error or exception as I am sending child_1 address and type casting it to child_2 but instead I am getting return from child_1 function.

Thanks.

pragma solidity ^0.8.0;

contract parent{   
  
    function getValue( 
    ) public view virtual returns(uint) {   
        return 10;   
    }   
}   
  
contract child is parent{   
      
    function getValue( 
    ) public view override returns(uint) {   
        return 15;   
    }   
}   
    
contract child_2 is parent{   
      
    function getValue( 
    ) public view override returns(uint) {   
        return 20;   
    }   
}   
  
contract ContractPolymorphism {   
    function getChildValue(address adr) public view returns(uint){
        return child(adr).getValue();
    }
    function getChild2Value(address adr) public view returns(uint){
        return child_2(adr).getValue();
    }
}

enter image description here

1

I am passing the address of child_1 contract to getChild2Value(address adr). Should it not raise an error or exception as I am sending child_1 address and type casting it to child_2

I think you're expecting a type-safe response but the deployed contract instances don't support that. In other words, the deployed contracts are not cast as distinct types. They're just addresses with bytecode. In other words, type-safe is a compile-time feature that does not extend to deployed contracts.

This says, return the response from function "getValue" from the "child" contract at address "adr":

return child(adr).getValue();

It makes it through child(adr) without complaint because it will accept any address and assign it the child type defined in your contract. Your internal description of this Type need not be complete and is not necessarily accurate. Inaccuracies will start to surface when you try to interact with the contract.

It makes it through the function call without trouble.

child(adr).getValue()

From the perspective of ContractPolymorphism the only pertinent information about this external contract is the function signature and the contract address. Everything it needs to know is captured in this interface description which the compiler worked out:

function getValue() external view returns(uint);

The identical function is present in both versions of child so the message is sent and the response is received, as expected.

To illustrate how inheritance and composition work, consider this set of contracts:

interface IERC20 { ... // minimal
contract ERC20 is IERC20 { ... // everything important

contract TokenA is ERC20 { ... // instances
contract TokenB is ERC20 { ...
contract TokenC is ERC20 { ...

contract WorkWithAnyToken {

  // when a contract is a function param, externally it is an address
  // and internally, it is cast as a contract.

  function withFromAny(IERC20 token, uint amount) public ... 
    token.transfer(msg.sender, amount);
  }
}

Breaking it down:

  1. The interface has undefined functions. It lays out the external interface.
  2. The implementation fulfills the commitment. By inheriting the interface, we know it won't deploy unless all of the interface's functions are defined.
  3. Making tokens is easy. They can all have unique properties but must implement the same basic functions and we know there are no departures from the essential interface.
  4. We can make a function that works with the interface abstraction (it doesn't need all the code for an actual token - just the interface). We'll need an address to instantiate the token. We can go function foo(address bar) ... { IERC20 token = IERC20(bar); or just cast it as IERC20 in the function arguments. Externally, there is no difference. It is always an address because the ABI does not support a complex type like a contract. It is internally type-safe - checked at compile-time.

Importantly, there is no check that the address passed in is one of the three defined tokens or even someone else's ERC20 contract. Yes, you can use IERC20 to instantiate an ERC20. You're simply saying "this is how to talk to the contract at the address." So, if you want to limit the inputted addresses to "trusted" contracts, then you're contract needs a whitelist and your function needs to check before interacting.

Problems will arise at the interaction stage if there is no such function. If you blindly trust and it turns out it's not an instance of the expected type then a runtime error results.

For giggles, consider this arrangement that sort of rolls up these points. Deploy a Parent and a Family. then bang on the Parent to deploy children. See the child addresses in the event log. Inspect them with Family:

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.7.4;

interface IChild {
   function birthday() external view returns(uint);  
}

contract Child is IChild {
    
    uint public override birthday;
    
    constructor() {
        birthday = block.timestamp;
    }
}


contract Parent {
    
    event NewChild(Child child);
    
    function newChild() public returns(Child child) {
        child = new Child();
        emit NewChild(child);
    }
}

contract Family {
    
    // The interface is more compact than the implementation which makes this contract smaller. 
    // We're only concerned with the message format so the contracts can talk. 
    
    function getBirthday(IChild child) public view returns(uint birthday) {
        return child.birthday();
    }
}

Hope it helps.

2
  • So, from what I understand is that typecast verification is only supported at compile time when it checks if the typecast has a function with such a signature, but on the run time it uses the contract address to call the function of the address contract.
    – Ahsan Ali
    Jan 31 at 15:32
  • Yes. At compile time it will check types, so you need to do explicit conversion. At runtime, where external interactions are concerned, the caller is using the type it has to encode arguments and function signatures and decode responses. You have a lot of latitude when defining interfaces for the contracts you want to talk to because the compiler doesn't check the on-chain state. You get runtime errors if there is an unexpected result. Jan 31 at 21:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.