Call solidity function dynamically, based on its bytes4 function selector
Asked Answered
S

3

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!

Stabilizer answered 7/4, 2022 at 2:42 Comment(8)
i don't think i understood it. can you maybe give a more concrete example?Petaloid
Do you mean sth like address(this).call(abi.encodePacked(selector, <func-args>))?Hemphill
@Petaloid yeah, created a concise example here: github.com/rsksmart/demo-code-snippets/blob/c11e373/…Stabilizer
@Hemphill 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).Stabilizer
@Hemphill although I suppose it is indeed a valid answer to my question. If you wanna post it as an answer, I'll ✔-mark itStabilizer
@Stabilizer 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?Hemphill
@Hemphill Yeah, it's looking more an more like: "Yes it is possible, but no it isn't that great an idea."Stabilizer
@Hemphill I have written up an answer expanding on your original suggestion below.Stabilizer
P
6

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.

Petaloid answered 7/4, 2022 at 8:22 Comment(0)
S
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).

Stabilizer answered 8/4, 2022 at 1:10 Comment(0)
M
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.

Monotheism answered 18/11, 2022 at 21:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.