Should modifiers be included in the interface

Hi guys, just wondering if a modifier like onlyOwner for a public function should be included in the interface as well. See the following code for an example.

contract Test {
    uint256 val;
    function getVal() public view onlyOwner returns(uint256) {
        return val;
    }
}

interface ITest {
    function getVal() external view returns(uint256);
}
1 Like

Modifers are not allowed on unimplemented functions. This will not compile:

interface ITest {
    function getVal() external view onlyOwner returns(uint256);

    modifier onlyOwner() { _; }
}

contract Test is ITest {
    uint256 val;
    function getVal() public view override onlyOwner returns(uint256) {
        return val;
    }
}
Error: Functions in interfaces cannot have modifiers.
 --> test.sol:2:5:
  |
2 |     function getVal() external view onlyOwner returns(uint256);
  |   
2 Likes

Actually, it’s pretty weird that it’s even possible to define modifiers in interfaces. I think it’s an oversight and it should be disallowed: https://github.com/ethereum/solidity/issues/11557.

2 Likes

Gotcha. Good to know this is an undocumented area. Here is a related question about the use of interfaces.

I’ve seen interfaces are used to call a deployed contract literally as an interface, and I’ve also seen interfaces are somehow used as a road map or blue print, i.e., a contract can inherit from an interface and all unimplemented functions in the interface must be implemented in the contract. In the former case, a smart contract exists prior to an interface, whereas in the later case, a smart contract comes after ITS interface. Conceptually these are two things. Wondering if this is intentional or not. Thanks.

contract Test is ITest {
    function doSth() public override {
    // doSth();
    }
}
1 Like

The first case is like casting an object to a particular base class. The other is like implementing that base class. This is the same way in most object oriented languages. These are basically two different faces of the same coin. The language allows you to use interfaces for both contracts the compiler knows the source code for and contracts for which only the compiled and deployed bytecode is known.

The only quirk is that in the first case, the compiler cannot really verify if the deployed contract actually implements the interface so there are runtime checks (in the external contract) instead of compile-time checks. Kinda similar to dynamic_cast in C++.

3 Likes

The reason I’m raising this up is that I’m getting an impression that Solidity is getting more and more explicit about motivations and purposes in coding, like memory, calldata and storage have to be explicitly labeled and expressed. I’m wondering about the same for interfaces, i.e., interface for calling and interface for implementing, etc. Just a thought.

1 Like

Interesting, you’re the second person I see mentioning this in a short time so you might be on to something :-). Any specific ideas on how you’d want them to work? I guess you’re probably thinking about a separate kind of interface meant only internal functions - is there anything that can’t already be achieved with abstract contracts?

1 Like

Hey @cameel, thanks for asking. I’m no expert in any programming language, and I have been studying and developing with Solidity for 98 days (according to OZ forum counters), so I can only speak from my own experience.

Initially I got confused about why a contract would inherit from an interface, and one has to deal with mutability, virtual, override warnings, etc. Later I realize that people may start from designing an interface and then continue implementing logics in the contract according to the interface. This is the blue print side of an interface. I got it.

In other cases, people may go straightforward to a contract design and implementation, which may or may not be a good practice, and interfaces are primarily considered and used as means of communication among contracts. I don’t see the blue print side of it in these scenarios, although they could be forced to be combined as one. Take IERC20Metadata and ERC20 for example, to be honest even up to today, I am still very confused about why ERC20 contract would inherit from IERC20Metadata, other than for checking supportsInterface (Later on, I realize it is a way of enforcing the ERC20 standard. For non-standard applications, no such enforcement is required). Clearly there is a mix of purposes and motivations in this practice.

These said, I don’t have a very clear idea about the potential change or if there would ever be any at all. I just hope I have made my experience with interfaces more explicit.

1 Like

Thanks for your feedback! Just curious - what other languages besides Solidity are you familiar with?

I see what you mean and I think this might be just a difference of perspective. For me for example it feels completely natural to combine these two aspects. You define an interface that describes an object and then use it different ways. You can use it as a “blueprint” and inherit from it. This way you say that your contract is something with this particular set of functions. And you can also take an existing contract instance and view it strictly as something providing these functions, ignoring anything beyond that and this way making it interchangeable with anything else that provides them.

At least I do not see a strong reason to have different kinds of interfaces for these two usages because you’d just be repeating yourself and defining the same thing multiple times. For example it would be completely redundant if in Java or C++ you had to define a separate interface to inherit from it and another one to cast a variable to it.

2 Likes

I spent a good amount of effort studying C++ at graduate school but never got into any serious developing profession. Later I primarily used Python.

In addition, the OOP paradigm seems to have a difference between C++ and Solidity. In the case of C++, different class objects for the same project or software are stored at different memory locations but still belong to the same project, isolated from the outside. However, for Solidity, different contracts, even though belonging to the same project at different addresses, can meanwhile be contributing to other projects and hence have an open and independent existence (Take Uniswap Router for example). The significance for an interface being a means of communication is stronger for Solidity than that for C++. And yes, you are right, maybe not strong enough to be named differently. However, it is on that edge similar to debating whether Pluto is a planet or not.

One more point to add: take IERC20 for example. The functions in it are labeled external for visibility, which indicates the functions would be used externally, however, the interface is later somehow used as a standard enforcement for ERC20 contracts, and in inherited contracts, the functions that are considered to be called externally, would need to be changed to public to incorporate internal calls. To summarize, interfaces are designed for external usage, and yet practically used as internal standard enforcment. This is somewhat self-contradicting.

However, for Solidity, different contracts, even though belonging to the same project at different addresses, can meanwhile be contributing to other projects and hence have an open and independent existence (Take Uniswap Router for example).

I don’t see it as fundamentally different from e.g. using a shared library in C++. A library could give you a pointer to an object it created and you could cast it to particular class. It’s actually really similar because when the object comes from the binary compiled from separate source code you can never be sure it really matches the class defined in your headers. You’re just assuming it does and bad things can happen if it does not, just like when you cast an EVM address to an interface it does not support.

The functions in it are labeled external for visibility, which indicates the functions would be used externally, however, the interface is later somehow used as a standard enforcement for ERC20 contracts, and in inherited contracts, the functions that are considered to be called externally, would need to be changed to public to incorporate internal calls.

It’s fully consistent with the concept of an interface. An interface represents only a single facet of an object, not the whole of it. Inheriting from IERC20 really means that your contract provides at least the stuff from IERC20. But it can provide more. A public function is a very good example - it’s not a separate entity incompatible with external. It’s really just a shortcut that tells the compiler to generate two functions under the hood - an external and internal one. Same concept applies when you override a function that’s view with a pure one - the pure one is at least as strict as view when it comes to storage access. The implementation can do more than is specified in the interface as long as it upholds all the guarantees of that interface. Otherwise it would not be possible to inherit from two interfaces at the same time.

2 Likes

Hey @cameel thanks for the explanations. The public function example nailed it, and I got a much better view towards interface. Many thanks.