2

I would like to half implement ERC223 for business reasons. This means being able to notify other contracts when they receive tokens, but not revert if the recipient is a contract that doesn't implement the tokenFallback function. So the following situations should work:

  1. User transfers token to an address. No call to tokenFallback (as it's just an address), transfer succeeds.
  2. User transfers token to a contract that doesn't implement tokenFallback. No call, transfer succeeds.
  3. User transfers token to a contract that does implement tokenFallback. Function is called, transfer succeeds.

Here are my findings thus far:

  • I can call by doing TokenRecipient(address).tokenFallback(msg.sender, value, 0) and this works for 1 and 3, but not 2 because I can't find a way to check if the contract conforms with the ERC223 interface.
  • I can call by doing to.call(<parameters>), and this works if I omit the bytes parameter. But the problem is the ERC223 call includes this bytes parameter, and even their reference implementation doesn't seem to actually work this way: https://github.com/Dexaran/ERC223-token-standard/issues/51 The behaviour I'm seeing is that as soon as there's a bytes parameter at all, call() starts just returning false and not doing anything.
  • I could theoretically use inline assembly to do the call, but I can't find any documentation on how I should correctly encode the bytes parameter. I know it's possible because the first option here works cross-contract, I just need to know how to actually do the assembly call.

Can anyone provide any guidance on the best way to go here? Is there a fix for call(), or a way to stop the normal call from reverting if the function doesn't exist? Or alternatively can someone point me to some docs on how to prepare the call from inline assembly?

EDIT: Here's what I've tried for the assembly. Doesn't seem to work as the function isn't called when it exists:

function _internalNoThrowTokenFallback(address contractRecipient, address from, uint value, bytes data)
    internal
{
    // We need to do this in assembly because:
    //   1. Calling the function as normal would result in a revert if the destination contract doesn't
    //      implement tokenFallback. We want this case to be ignored.
    //   2. We could use contractRecipient.call() but that doesn't work with bytes parameters: 
    //      https://github.com/ethereum/solidity/issues/2884
    //   3. So we need to do the call the same way 
    assembly {
        // Get some free memory to copy our params over to.
        let free_ptr := mload(0x40)

        // Tee up the function selector in the memory we've gotten
        mstore(free_ptr, 0xc0ee0b8a)

        // Load our calldata across to the new call, except for
        //  - The function selector (4 bytes)
        //  - The first address parameter (64 bytes when padded)
        //
        // Thus making a total of 68 bytes we want to ignore, and we want to copy the
        // data to our free_ptr after our function selector
        calldatacopy(add(free_ptr, 4), 68, sub(calldatasize, 68))

        // Ok, call the function without allowing the other contract to alter our state
        let result := staticcall(
            30000,                  // Give the other contract 30k gas to work with
            contractRecipient,      // The other contract's address
            free_ptr,               // We've prepared the inputs at free_ptr.
            sub(calldatasize, 64),  // They're the same length as our inputs were minus the address parameter.
            0,                      // There's no return from the function
            0
        )

        // Don't check the result or revert if the call failed. We want to proceed regardless.
    }
}
0

After working with the core team in this issue I've been able to solve this. I'm sharing the solution for anyone that stumbles upon this in the future.

Context

  • call() has a bug where it corrupts function parameters by not correctly encoding them if they're dynamic types, e.g. bytes.
  • In solidity v0.5 using the version of call() that encodes parameters will no longer be allowed and will not compile. call() will only take a single bytes argument which is the pre-encoded function call with selector and all arguments as per the abi spec.
  • abi.encodeWithSignature() has been introduced for this purpose, which does not have the bug that call() had and correctly encodes function parameters, even dynamic ones.

Solution

So, if you wanted to optionally call this function on another contract:

function tokenFallback(address from, uint amount, bytes data) public {
    // Do stuff here
}

You have these options:


Normal Call: Cast the receiving contract to a type with this function signature in the abi and call the function normally, e.g.

CallReceiver receiver = CallReceiver(to);
receiver.tokenFallback(msg.sender, value, data);

In this case, if the receiver doesn't implement tokenFallback the entire transaction will revert, making it not very optional.


Optional Call: call(), e.g.

to.call(abi.encodeWithSignature(
    "tokenFallback(address,uint256,bytes)", 
    msg.sender,
    value,
    data
));

This will return true or false depending on whether the call succeeded or not, which allows you to ignore failure and continue on with execution. Note that even though the function is declared with a uint parameter, it's uint256 when computing the function selector. The canonical type is always used for parameters, and there's no space between types in the function signature. More info in the abi spec.


Assembly Call: You could use inline assembly for this, but given that I was able to find a solution without doing this, I've opted not to go that route. The first problem with the assembly above is that it moves the static parameters and doesn't update the dynamic pointer to the bytes parameter, but I'm not sure if that's the only problem because I didn't bother to work this through since call() with abi.encodeWithSignature() works.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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