0

Let's suppose I have the two following contracts, Dest and Proxy:

pragma solidity ^0.5.0;

contract Dest {
  function foo(string memory name, string memory symbol, uint256 decimals, uint256 totalSupply, address sender, uint256 value) public {
    /*
     * Logic code Here
     */
    }

  function bar(address a, address b, address c, address d, uint256 e, address f, address sender, uint256 value) public {
    /*
     * Logic code Here
     */
  }
}

contract Proxy {
  address private _owner;
  address private _dest;

  constructor(address dest) public {
    _owner = msg.sender;
    _dest = dest;
  }

  function getDest() public view returns(address) {
    return _dest;
  }

  function setDest(address dest) public {
    require(msg.sender == _owner);
    _dest = dest;
  }

  // Invoke takes msg.sender and msg.value and concatenates both to payload before to call the correct method
  function invoke(string memory method, bytes memory data) public payable {
    bytes memory payload = abi.encodePacked(bytes4(keccak256(bytes(method)))), data, abi.encode(msg.sender), abi.encode(msg.value));
    (bool ok, bytes memory response) = _dest.call(payload);
  }
}

Both foo and bar methods in Dest contract have address sender, uint256 value as final input parameters.

The logic is that users will call Proxy passing the complete method signature and all abi encoded parameters except for the last two.

Proxy sequentially packs together method signature, partial call parameters, msg.sender and msg.value in a unique bytes packed payload, then invokes the call method on the Dest address using the packed payload as input.

The problem is that when the foo method is called, the value of sender input parameter is not the original msg.sender address, but contains some inconsistent value.

But everything works fine when the bar method is called.

My suspicion is that the problem is related to the fact that in the head of the input parameters of the foo method there are strings, which have variable length, while in bar there are only parameters with fixed length.

Any suggestion on how to build a general purpose solution?

1

In general, you cannot just concatenate partial ABI encodings, as ABI encoding splits variable length parameters into fixed and variable parts. For function Foo the correct encoding would be:

selector (4 bytes)
name offset (32 bytes)
symbol offset (32 bytes)
decimals (32 bytes)
totalSupply (32 bytes)
address (32 bytes)
value (32 bytes)
name length (32 bytes)
name body (? bytes)
symbol length (32 bytes)
symbol body (? bytes)

When user encodes all Foo parameters except for the last two, the encoding probably looks like this:

selector (4 bytes)
name offset (32 bytes)
symbol offset (32 bytes)
decimals (32 bytes)
totalSupply (32 bytes)
name length (32 bytes)
name body (? bytes)
symbol length (32 bytes)
symbol body (? bytes)

When you append msg.sender and msg.value, you get:

selector (4 bytes)
name offset (32 bytes)
symbol offset (32 bytes)
decimals (32 bytes)
totalSupply (32 bytes)
address (32 bytes)
value (32 bytes)
name length (32 bytes)
name body (? bytes)
symbol length (32 bytes)
symbol body (? bytes)
msg.sender (32 bytes)
msg.value (32 bytes)

So the result is obviously incorrect.

Correct solution would be to first decode all the parameters into separate local variables using abi.decode, and then repack the back together with msg.sender and msg.value using abi.encodeWithSignature. Something like this:

string memory name;
string memory symbol;
uint256 decimals;
uint256 totalSupply;

(name, symbol, decimals, totalSupply) = abi.decode (
  data,
  (string, string, uint256, uint256));
bytes memory payload = abi.encodeWithSignature (
  method, name, symbol, decimals, totalSupply, msg.sender, msg.value);
  • Got it, extremely useful. Thanks. Because of I cannot know what payload contains (can be foo params, bar params or other method params if dest address changes in future), is it possible to call abi.decode without knowing parameters? – Marco Vasapollo Dec 23 '19 at 9:35
  • No, it is not possible as ABI encoding is not self-describing. However what I could recommend is to make parameters that you want to substitute in Proxy to go before any other parameters, i.e. in the beginning of the encoding, and require the user to pass some (arbitrary) values for them, and Proxy will then overwrite these values in place. – Mikhail Vladimirov Dec 23 '19 at 10:00
  • In first release, sender and value were at start, but didn't worked, probably because of string offset mismatch. But now I understood your suggestion: pass address(0) and 0 as placeholders at start, then splice first 64bits and attach correct data. Hope to not have contract size problems (dest logic is really complicated). Will let you know, thanks. – Marco Vasapollo Dec 23 '19 at 10:11
  • Yes, you need placeholders to preserve offsets. Also, placeholders will allow users to use function signature as is without stripping replaceable parameters. – Mikhail Vladimirov Dec 23 '19 at 10:14
0

Thans to @Mikhail Vladimirov suggestions I solved the problem like this:

First of all, sender and value were placed as first and second input parameters in both foo and bar functions of the Dest contract.

pragma solidity ^0.5.0;

contract Dest {
  function foo(address sender, uint256 value, string memory name, string memory symbol, uint256 decimals, uint256 totalSupply) public {
    /*
     * Logic code Here
     */
    }

  function bar(address sender, uint256 value, address a, address b, address c, address d, uint256 e, address f) public {
    /*
     * Logic code Here
     */
  }
}

Then the new logic in the invoke method of the Proxy contract replaces the first 64 bytes of the bytes memory data input parameters (32 for address sender, and 32 for uint256 value) with msg.sender and msg.value.

contract Proxy {
  address private _owner;
  address private _dest;

  constructor(address dest) public {
    _owner = msg.sender;
    _dest = dest;
  }

  function getDest() public view returns(address) {
    return _dest;
  }

  function setDest(address dest) public {
    require(msg.sender == _owner);
    _dest = dest;
  }

  // Invoke takes msg.sender and msg.value and concatenates both to payload before to call the correct method
  function invoke(string memory method, bytes memory data) public payable {
    require(data.length >= 64, 'Insufficient space in payload');
    assembly {
        mstore(add(data, 0x20), caller())
        mstore(add(data, 0x40), callvalue())
    }
    bytes memory payload = abi.encodePacked(bytes4(keccak256(bytes(method)))), data);
    (bool ok, bytes memory response) = _dest.call(payload);
  }
}

Of course, this means that, in the off-chain logic, the user must include a dummy address and dummy uint256 as the first two parameters of data field when packaging input, to let the abi payload be coherent with offsets. Values are not important, because the SmartContract logic will discard them and replace with correct values.

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.