0

For testing/mocking purposes, I need to call a contract (A) from another contract (B) that just forwards the calls to (A).

The calls need to be forwarded, not delegated. And I don't want to rewrite every function of A in B, but instead use something generic.

My first approach was taking OpenZeppelin's (transparent) Proxy contract, @openzeppelin/contracts/proxy/Proxy.sol and replace delegatecall with call.


  /**
   * @dev Delegates the current call to `implementation`.
   *
   * This function does not return to its internall call site, it will return directly to the external caller.
   */
  function _delegate(address implementation) internal virtual {
    assembly {
      // Copy msg.data. We take full control of memory in this inline assembly
      // block because it will not return to Solidity code. We overwrite the
      // Solidity scratch pad at memory position 0.
      calldatacopy(0, 0, calldatasize())

      // Call the implementation.
      // out and outsize are 0 because we don't know the size yet.
      // let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
      let result := call(gas(), implementation, 0, calldatasize(), 0, 0)

      // Copy the returned data.
      returndatacopy(0, 0, returndatasize())

      switch result
      // delegatecall returns 0 on error.
      case 0 {
        revert(0, returndatasize())
      }
      default {
        return(0, returndatasize())
      }
    }
  }

But it doesn't compiles, I get TypeError: Function "call" expects 7 arguments but got 6

Do you know the parameters of function call in assembly mode, or how can I implemented this forward non-transparent proxy?

5
  • the parameters to call() are described in Solidity Inline Assembly documentation page
    – Nulik
    Commented Aug 26, 2021 at 0:13
  • docs.soliditylang.org/en/v0.5.4/assembly.html
    – Nulik
    Commented Aug 26, 2021 at 0:16
  • Thanks @Nulik for you help with call() documentation. Commented Aug 26, 2021 at 15:31
  • I made this change: // let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) by let result := call(gas(), implementation, 0, 0, calldatasize(), 0, 0) And it's working fine. Commented Aug 27, 2021 at 11:27
  • RicardoS is right, to also forward the ETH value, third parameter should be callvalue() Commented Aug 16, 2022 at 13:19

2 Answers 2

0

Finally I found the solution replacing delegatecall with call.

Solution (inheriting from OpenZeppelin's Proxy)

//SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol";

/**
 * @dev This contract provides a fallback function that forwards all calls to another contract using the EVM
 * instruction `call`.
 *
 * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a
 * different contract through the {_delegate} function.
 *
 * The success and return data of the delegated call will be returned back to the caller of the proxy.
 */
contract ForwardProxy is Proxy {
  address internal _forwardTo;

  constructor(address forwardTo) {
    _forwardTo = forwardTo;
  }

  /**
   * @dev Delegates the current call to `implementation`.
   *
   * This function does not return to its internall call site, it will return directly to the external caller.
   */
  function _delegate(address implementation) internal virtual override {
    // solhint-disable-next-line no-inline-assembly
    assembly {
      // Copy msg.data. We take full control of memory in this inline assembly
      // block because it will not return to Solidity code. We overwrite the
      // Solidity scratch pad at memory position 0.
      calldatacopy(0, 0, calldatasize())

      // Call the implementation.
      // out and outsize are 0 because we don't know the size yet.
      // let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
      let result := call(gas(), implementation, 0, 0, calldatasize(), 0, 0)

      // Copy the returned data.
      returndatacopy(0, 0, returndatasize())

      switch result
      // delegatecall returns 0 on error.
      case 0 {
        revert(0, returndatasize())
      }
      default {
        return(0, returndatasize())
      }
    }
  }

  /**
   * @dev This is a virtual function that should be overriden so it returns the address to which the fallback function
   * and {_fallback} should delegate.
   */
  function _implementation() internal view virtual override returns (address) {
    return _forwardTo;
  }

  function setForwardTo(address forwardTo) external {
    _forwardTo = forwardTo;
  }
}
0

Guillermo answer has an issue, namely that if msg.value > 0 all ETH of the transaction gets stuck in the proxy and isn't forwarded.

We need to forward msg.value by doing this

// callvalue() is msg.value
let result := call(gas(), implementation, callvalue(), 0, calldatasize(), 0, 0)

Full answer:

  function _delegate(address implementation) internal virtual override {
    // solhint-disable-next-line no-inline-assembly
    assembly {
      // Copy msg.data. We take full control of memory in this inline assembly
      // block because it will not return to Solidity code. We overwrite the
      // Solidity scratch pad at memory position 0.
      calldatacopy(0, 0, calldatasize())

      // Call the implementation.
      // out and outsize are 0 because we don't know the size yet.
      let result := call(gas(), implementation, callvalue(), 0, calldatasize(), 0, 0)

      // Copy the returned data.
      returndatacopy(0, 0, returndatasize())

      switch result
      // delegatecall returns 0 on error.
      case 0 {
        revert(0, returndatasize())
      }
      default {
        return(0, returndatasize())
      }
    }
  }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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