4

Let's say I have a contract called contractA.

It has the function:

function bla(address test) {
    // way 1. test.call(abi.encodeWithSignature("first4bytes", arg1, arg2))
    // way 2. testAddrInterface(test).functName()
}

As you can see, in the way 2 example, I just use the interface so that I can call functName on it directly instead of using way 1.

I've seen lots of places that people use way 1. Why ? what's wrong with Way 2? if you tell me way 2 contains more code and is more gas costly, I will ask: are the interfaces still included in the final code and is this the only reason why Way 1 is better ?

15
  • 4
  • 1
    The title of that other question is not the same as yours, but I believe that it essentially refers to the same issues. In either case, you can read my answer there, which lists a couple of reason for using call. Commented Dec 29, 2020 at 20:20
  • 1
    No, you are passing the function prototype (name and input argument types) as string. You can even let an external user (for example) pass it as input to a function in your contract. So no, it is definitely does not correspond with "known during compile time". Commented Dec 29, 2020 at 20:24
  • 1
    In strongly-typed languages, any function which is known during compilation time is immediately replaced by the compiler with a jump to the address of the function in the code (and I guess that something similar to that happens in solidity). When you call a function by its string prototype, the actual decision of "where to jump" occurs only during runtime. Commented Dec 29, 2020 at 20:26
  • 1
    No you don't. The function signature consists of only its name and the types of input arguments. That is why using call allows you to actually execute a function without knowing its return-value type (as you can read at the bottom of the answer to the question that I linked above). Commented Dec 29, 2020 at 20:26

2 Answers 2

2

The more important difference is that if funcName reverts then for case 2 bla will also revert.

For case 2 calls returns a boolean indicating if the call has failed, and have the posibility to ignore the failure.

Using raw call is low level and discouraged because it is easy to make mistakes. Since solc 0.6 you can use try/catch

    try myContract.funcName(param1, param2, ..) returns (uint retValue) {
        /* Do something with retValue */
    } catch Error(string memory err) {
        /* Do something with the error message and revert */
    }

Another place where case 1 is the only option is when making arbitrary calls since at compile time you don't know the function name nor the parameters, for example proxy contracts, multisig wallets, etc.

-1

Effectively, there is no difference.

Using an interface provides compile time safety checks on arguments used. And simplifies the usage of return values the same way.

This is a quick, simple example, but basically these code lines are equivalent.

<Type> returnTuple = address.call( <payload as byyes> );

<Type> returnTuple = address.call( abi.encodeWithSelector( <Function Selector>, <Arguments . . .>);

<Type> returnTuple = address.call( abi.encodeWithSignature( <Function Selector string>, <Arguments . . .>);

<Type> returnTuple = address.f( <arguments> );

All would revert if the return tuple doesn't fit. When using call, you would normally accept the raw bytes. And then work with it from there. Such as returning, forwarding, or parseing.

11
  • That's not correct. There are several notable differences, see this answer. Commented Dec 29, 2020 at 20:20
  • Not in this example. Since the selector is hard coded, this code doesn't allow for dynamic execution. Commented Dec 29, 2020 at 20:30
  • And the answer you cite is a longer way of saying compile time type safety checks. So in the context of the question, which is concerning the technical difference in executing the function call to another contract, there is no difference. The function call is executed the same way. Both simply use the low level call( bytes payload ); The interface just layers the payload encoding providing type safety checks during compile time. Commented Dec 29, 2020 at 20:36
  • Here is one difference: if the 2nd option reverts for an internal implementation reason (i.e., not because the function doesn't exist), then the 1st option will not revert. In other words, the 1st option allows you to handle failures "your way" (kinda like try/catch in other languages). So stating 'no differences', IMO is wrong. Commented Dec 29, 2020 at 20:39
  • In addition, in the 1st option, the compiler generates code which expects (bool, bytes memory) to return. In the 2nd option, the compiler generates code which expects whatever is defined in the interface to return. So by no means are these two options equivalent in terms of compilation or runtime. Commented Dec 29, 2020 at 20:44

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.