Would it cause any problems to label an erc20 standard function `external` instead of `public`?

consider the erc20 standard function

function balanceOf(address _owner) public view returns (uint256 balance)

suppose i don't need to call this internally. would it cause any incompatibility with the erc20 standard to change it to external like this

function balanceOf(address _owner) external view returns (uint256 balance)

I would first consider why this is considered desirable - bytecode reasons? Compiler yelling? Normally sticking to the standard is a good practice.

Thereafter, if you plan to inherit from another ERC20 contract with public in the signature, you will get a:

TypeError: Overriding function visibility differs.

You can run this example through Remix yourself:

contract BaseContract {
    function f() public pure virtual returns (bool) {
        return true;
    }
}
contract Contract is BaseContract {
    function f() external pure override returns (bool) {
        return false;
    }
}

Unfortunately I picked the wrong example to make my case. It probably doesn't matter when the function is view, but consider a function which modifies data such as transfer or approve. marking these functions as public uses more gas than external because public functions automatically write the parameters to memory, instead of allowing them to just remain in calldata.

Hello @nm77

THe reason we make these function public, is that we want other contract, that inherit from something like ERC20, to be able to call the user facing version (in addition to the internal version).

This is something you can see in action here. This wouldn't be possible if the functions were external. As always, I want to point out that OZ is targeting maximum reusability/modularity, and sometimes its makes use lean toward choices that are not the most gas effective, but that we still believe are in the best interest of the community.

2 Likes

There's several reasons why this is not true, or not necessarily true.

  1. Value types like address, uint, etc., do not get copied to memory. They are loaded on the stack for both public and external functions.
  2. public functions can also have calldata arguments for reference types (arrays, structs), at least since Solidity 0.7.0.
  3. Calldata arguments may in theory be cheaper than memory arguments, but this is something you would need to measure to be absolutely sure. We've seen cases before where many calldata arguments resulted in a lot more code than calldata arguments, which generally means more expensive.

Always measure gas claims!

2 Likes

@frangio Thank you. I was confused about these points 1-3, that makes sense to me now.

I'd also add that just making it public without changing anything else about the function will not make your contract more expensive until you actually make an internal call to the function from your code. If you don't, the internal version will simply get optimized out of your code without adding even a single byte to it. Only the external one will remain. It's only when you change between memory and calldata or change the types that you get different bytecode.

3 Likes

thanks @cameel this is the direct answer to my original question