8

In a smart contract, let's say I have a function which wants to invoke another function dynamically, based on some internal logic. Here it obtains the function selector as a bytes4 variable.

After which it is possible to use branching logic to invoke one of the target functions. See: (A)

However, is it possible to avoid that and invoke the function selector directly? See: (B)

function myDynamicFunc(uint256 someParam) public {
    bytes4 selector = /* ... some internal logic ... */

    if (selector == this.myFuncA.selector) {
      myFuncA(someParam);
    } else if (selector == this.myFuncB.selector) {
      myFuncB(someParam);
    }
    // (A) instead of something like this ^ branching logic (which works)

    selector.invoke(someParam);
    // (B) can something like this ^ instead by calling the selector directly instead (does not work)
}


Details

  • myDynamicFunc is public and myFuncA+myFuncB are also public.
  • All 3 functions are implemented in the same smart contract.

Notes

I have written up an answer expanding on @kj-crypto's suggestion in the comments. If there is another way to accomplish the above without using address(this).call(...), I'm all ears!

Yilmaz
  • 35,338
  • 10
  • 157
  • 202
bguiz
  • 27,371
  • 47
  • 154
  • 243
  • i don't think i understood it. can you maybe give a more concrete example? – keser Apr 07 '22 at 03:58
  • 1
    Do you mean sth like `address(this).call(abi.encodePacked(selector, ))`? – kj-crypto Apr 07 '22 at 06:36
  • @keser yeah, created a concise example here: https://github.com/rsksmart/demo-code-snippets/blob/c11e373/invoke-function-dynamically/example.sol#L37-L51 – bguiz Apr 07 '22 at 08:00
  • 1
    @kj-crypto just tried to use your suggestion (see `myDynamicFunc2`, in same link above), and looks like: (1) the return value is `bytes memory`, which can't be easily typecast to intended type, and (2) requires an additional `require()` ... which is less than ideal from a gas point of view (should experiment to verify, but that's another question). – bguiz Apr 07 '22 at 08:06
  • 1
    @kj-crypto although I suppose it is indeed a valid answer to my question. If you wanna post it as an answer, I'll ✔-mark it – bguiz Apr 07 '22 at 08:08
  • @bguiz When you use `pure` or `view` function then, they can be called off-chain via `eth_call` so in this case there is no need for gas optimization. However, I assume that here is no such case. Am I right? – kj-crypto Apr 07 '22 at 10:57
  • @kj-crypto Yeah, it's looking more an more like: "Yes it is possible, but no it isn't that great an idea." – bguiz Apr 07 '22 at 13:25
  • 1
    @kj-crypto I have written up [an answer](https://stackoverflow.com/a/71790602/194982) expanding on your original suggestion below. – bguiz Apr 08 '22 at 01:15

3 Answers3

5

Regarding option B:

  • Using call will return a bytes object, which then should you convert to appropiate type, in this case to an integer. (extra gas usage)
  • To use call, you need to pack the selector and the parameters (extra gas usage)

As long as you are using a function in the same contract, there is no point to use its abi specification, because you already now where the function is, how is it defined and you can call it without any hassle.

keser
  • 2,472
  • 1
  • 12
  • 38
4

Expanding on @kj-crypto's comment above:

Do you mean sth like address(this).call(abi.encodePacked(selector, <func-args>))?

... and created this implementation:

  function myDynamicFunc(uint256 someParam)
    public
    // pure // --> (1)
    returns (bytes memory result) // --> (2)
  {
    bytes4 selector =
      /* ... some internal logic ... */
      this.myFuncA.selector;

    (bool success, bytes memory resultBytes) =
      address(this).call(abi.encodePacked(selector, someParam));

    require(success, "failed to call selector"); // --> 3
    result = resultBytes;
  }

To summarise, the answer is: "Yes it is possible, but no it isn't that great an idea."

Reasons:

(1) - If you need the function to be pure, it cannot be, unfortunately, because address(this).call(...) potentially modifies state.

(2) - The return type will default to bytes memory, as this is the return type of address(this).call(...). You can cast it, but this adds additional complexity to the code, which is against the grain of the original motivation.

(3) - To properly handle address(this).call(...), need to do something with the bool returned in the tuple. For example using require(). This also against the grain of the original motivation, as it simply shifts the branching logic from one form to another (if ... else to require()), and a more expensive one at that.

(4) - Overall, the gas costs of the original function appear to be less than, and thus advantageous, over this suggested form. Note that this has not been verified with experimentation, and if anyone would like to give it a go, here's the (full solidity file).

bguiz
  • 27,371
  • 47
  • 154
  • 243
2

selector is bytes4 type and has no method to call a function or invoke a function.

bytes4 private constant SELECTOR = bytes4(keccak256(bytes("transfer(address,uint256)")));

or it is return data value of:

  nonPayableAddress.call(abi.encodeWithSignature("transfer(address,uint256)", 0xaddress, amount))

the only thing is available to call another contract's function using selector is

(bool success, bytes memory data) = contractAddress.call(
        abi.encodeWithSelector(SELECTOR, to, value)
    );

call, delegateCall, callcode methods are available for address, transfer and send methods are available for payable address type.

Yilmaz
  • 35,338
  • 10
  • 157
  • 202