Decrease ERC20 deploy gas usage by removing non-standard methods

OZ ERC20 implementation has several non-standard methods, like increaseAllowance which can increase gas usage when deploying contracts. I was wondering how it's possible to deploy the contract without having these methods, making it cheaper to deploy. Can I easily override them with empty body?

I don't think that overriding a virtual function gets the code of the original function removed from the compilation output (i.e., from the bytecode, in the case at hand).

If the original function is either public or internal, then it is technically possible to call it from a function in the inheriting contract, via super or something, right?

So the compiler cannot choose to remove it.

That being said, if the original function is never called, then perhaps the optimizer can detect that, and avoid from adding it to the output bytecode.

Let's see, shall we?

Given this short snippet:

contract Base {
    uint256 public x;
    function func(uint256 n) public virtual {
        for (uint256 i = 0; i < n; i++) {
            x += i + 1;
        }
        for (uint256 i = 0; i < n; i++) {
            x += i + 2;
        }
        for (uint256 i = 0; i < n; i++) {
            x += i + 3;
        }
    }
}

contract Derived1 is Base {
    function func(uint256 n) public override {
        super.func(n + 1);
    }
}

contract Derived2 is Base {
    function func(uint256 n) public override {
        x = n + 1;
    }
}

The compiler outputs these results:

 ·----------------------|------------------------------|------------------------------·
 | Solc version: 0.8.19 · Optimizer enabled: true      · Runs: 2000                   │
 ·······················|······························|·······························
 | Contract Name        · Deployed size (KiB) (change) · Initcode size (KiB) (change) │
 ·······················|······························|·······························
 | Base                 ·                     0.454 () ·                     0.485 () │
 ·······················|······························|·······························
 | Derived1             ·                     0.476 () ·                     0.507 () │
 ·······················|······························|·······························
 | Derived2             ·                     0.208 () ·                     0.238 () │
 ·······················|······························|·······························

As you can see:

  • The bytecode of Derived1, where the original function is called, has remained as large as (and even slightly larger than) the bytecode of Base
  • The bytecode of Derived2, where the original function is not called, has become much smaller than the bytecode of Base

So my conclusion here, which still needs to be independently verified of course, is that if the original function isn't called, then the compiler (or the optimizer) can detect that, and avoid from adding it to the output bytecode.


Here is another check, this time with optimization disabled:

 ·----------------------|------------------------------|------------------------------·
 | Solc version: 0.8.19 · Optimizer enabled: false     · Runs: 0                      │
 ·······················|······························|·······························
 | Contract Name        · Deployed size (KiB) (change) · Initcode size (KiB) (change) │
 ·······················|······························|·······························
 | Base                 ·                     0.642 () ·                     0.673 () │
 ·······················|······························|·······························
 | Derived1             ·                     0.665 () ·                     0.696 () │
 ·······················|······························|·······························
 | Derived2             ·                     0.394 () ·                     0.425 () │
 ·······················|······························|·······························

And it seems to point to the same conclusion...

So you mean that overriding with empty body can decrease the gas usage? It would be great of OpenZeppelin allowed removing these extra methods somewhere in the new token wizard to reduce the gas usage for cases that these methods are not going to be used.

I mean that overriding a virtual function with an empty-body function (given that no other function in the contract is calling that virtual function on the base contract), can reduce the size of the byte-code, and subsequently - the deployment gas cost (i.e., not the gas cost of any transaction executed on the contract after deployment).