Address.functionCall differences between Hardhat/Ganache and a consortium Hyperledger Besu

I’m building a smart contract ERC20 token with multisig capabilities for Mint and Burn. I do this by having two methods, one to start the tx proposal, and one that actually has the logic.

  /// @notice Mints new ERC20 tokens
  /// @dev Proposes a transaction to be confirmed using multisig to mint `amount` of tokens.
  /// @param to the recipient of the tokens
  /// @param amount the amount of tokens
  function mint(address to, uint256 amount)
    public
    multisig(ACTION_MINT, abi.encodeWithSignature('_multisigMint(address,uint256)', to, amount))
  {}

  /// @notice The function that will be executed on completion of the `mint` multisg requirements
  /// @dev Restricted to the multisig contract only
  /// @param to the recipient of the tokens
  /// @param amount the amount of tokens
  function _multisigMint(address to, uint256 amount) public onlyMultisig {
    _mint(to, amount);
  }

Executing the transaction was coded as follows

  function executeTransaction(uint256 transactionId) public {
    require(
      transactionId <= _transactionId.current(),
      'TransactionId does not exist in executeTransaction on MakerCheckerControlled'
    );
    if (isApproved(transactionId)) {
      Transaction storage transaction = _transactions[transactionId];
      address(this).functionCall(transaction.data, 'Transaction execution failed!');
    }
  }

This works perfectly in Hardhat Truffle tests, and when migrating to a Ganache.
But, when migrating a consortium Besu network, it burns through all gas (whatever number I set)

Transaction: 0xf73dc9806fd8cdf12632f105aac6074d286f2ab5e184f63247e6d26845478fe2 exited with an error (status 0) after consuming all gas.
     Please check that the transaction:
     - satisfies all conditions set by Solidity `assert` statements.
     - has enough gas to execute the full transaction.
     - does not trigger an invalid opcode by other means (ex: accessing an array out of bounds).

Using truffle debug I think it goes bad “inside” the revert(errorMessage) in _verifyCallResult in Address.sol on line 185. But let’s call this a guess.

Now when I use regular call, it works (verified by total supply and events)

  function executeTransaction(uint256 transactionId) public {
    require(
      transactionId <= _transactionId.current(),
      'TransactionId does not exist in executeTransaction on MakerCheckerControlled'
    );
    if (isApproved(transactionId)) {
      Transaction storage transaction = _transactions[transactionId];
      address(this).call(transaction.data);
    }
  }

I have no clue why there would be a difference or what it is…

:computer: Environment

  • OpenZeppelin Contracts v4.1.0
  • Truffle v5.3.6
  • Hardhat v2.3.0
  • Node v16.1.0
  • Ganache CLI v6.12.2 (in Docker)
  • Hyperledger Besu 21.1.4
  • solc v0.8.4 optimizer enabled with 200 runs, evmVersion istanbul