5

I have 2 interacting smart contracts which I am developing/testing in Hardhat and deploying to RSK. One of them is an ERC1363 payable token with transferAndCall(address,uint256,bytes) function, and the second one is a token receiver whose buy(address,uint,uint,bytes3) function call I need to encode off-chain and send to the token's transferAndCall function bytes parameter. The ERC1363 contract transfers tokens from sender's account to the receiver smart contract's account and then within the same transaction calls receiver's onTransferReceived(address,address,uint256,bytes), where the last bytes parameter should be encoded buy function call.

This is my receiver smart contract:

contract TokenReceiver is IERC1363Receiver {
  IERC1363 acceptedToken;
    
  constructor(IERC1363 _acceptedToken) {
    acceptedToken = _acceptedToken;
  }
  event PurchaseMade(address indexed sender, uint tokensPaid, uint productAmount, bytes3 color);
    
  function buy(address sender, uint tokensPaid, uint productAmount, bytes3 color) public {
    // allowed to be called only via the accepted token
    require(msg.sender == address(acceptedToken), "I accept purchases in Payable Tokens");
    emit PurchaseMade(sender, tokensPaid, productAmount, color);
  }

  function onTransferReceived(address operator, address sender, uint256 tokensPaid, bytes calldata data) external override (IERC1363Receiver) returns (bytes4) {
      // TODO: decode calldata and call `buy` function
    return this.onTransferReceived.selector;
  }
}

This is how I assemble the calldata by encoding the signature and params of the buy function together:

  it('buyer should be able to pay tokens and buy products in one transaction', async () => {
    // TokenReceiver `buy` function signature hash: 0x85f16ff4
    const buySigHash = tokenReceiver.interface.getSighash('buy');
    // providing some product properties to the TokenReceiver
    const productAmount = 99;
    const color = '0x121212';
    // packing `buy` signature and the properties together
    const calldata = ethers.utils.defaultAbiCoder.encode(
      ['bytes4', 'uint256', 'bytes3'],
      [buySigHash, productAmount, color],
    );
    // pay tokens and buy some products in one tx
    const transferAndCallTx = payableToken
      .connect(buyer)
      ['transferAndCall(address,uint256,bytes)'](tokenReceiver.address, tokenAmount, calldata);
    await expect(transferAndCallTx)
      .to.emit(tokenReceiver, 'PurchaseMade');
  });

My question is:

  • How do I decode the calldata inside the receiver's onTransferReceived function?
  • How do I extract the function signature and the other 2 encoded params, and then call the corresponding function on the receiver?
bguiz
  • 27,371
  • 47
  • 154
  • 243
Aleks Shenshin
  • 2,117
  • 5
  • 18

2 Answers2

5
//Define struct within the contract, but outside the function
struct BuyParams {
        bytes4 buySigHash;
        uint256 productAmount;
        bytes3 color;
}

// within onTransferReceived: decode the calldata:
BuyParams memory decoded = abi.decode(
    data,
    (BuyParams)
);


//Now use as function arguments by passing:
decoded.buySigHash,
decoded.productAmount,
decoded.color,


Addition: You might be able to save gas by changing the order of the calldata:

from:
        bytes4 buySigHash;
        uint256 productAmount;
        bytes3 color;
to:
        uint256 productAmount;
        bytes4 buySigHash;
        bytes3 color;
Derawi
  • 430
  • 4
  • 12
  • Thanks, brilliant idea! Let's say, now I have a function signature and the params. How can I then call a function with a dynamic name (decoded signature)? Is it possible to do smth like `buySigHash(productAmount, color, ...);` ? – Aleks Shenshin Jul 01 '22 at 18:20
  • try: ```this.call(buySigHash, productAmount, color, ...)``` – Derawi Jul 01 '22 at 18:30
  • sorry made a small mistake, did you try ```address(this).call()```? – Derawi Jul 01 '22 at 20:45
5

You could make use of inline assembly to decode your bytes data. Create a pure helper function with the following contents: ​

function decode(bytes memory data) private pure returns(bytes4 selector, uint productAmount, bytes3 color) {
    assembly {
      // load 32 bytes into `selector` from `data` skipping the first 32 bytes
      selector := mload(add(data, 32))
      productAmount := mload(add(data, 64))
      color := mload(add(data, 96))
    }
}

​ here mload(0xAB) loads a word (32 bytes) located at the memory address 0xAB, and add(0xAB, 0xCD) summs two values ​ See this article for more on inline assembly in solidity. ​ Next, this is how you can utilise the created function in you contract: ​

(bytes4 selector, uint productAmount, bytes3 color) =
  decode(data);

​ Since you have the selector and other parameters, you can construct the function call data ​

bytes memory funcData =
  abi.encodeWithSelector(selector, sender, tokensPaid, productAmount, color);

​ Now you can make a low level call to invoke the corresponding function ​

(bool success,) = address(this).call(funcData);
require(success, "call failed");

Warning: Keep in mind that using the above method allows an attacker to be able to call any function in your contract. Be careful using low level calls. ​ To avoid this, validate the function selector, before calling it, like this: ​

if (selector == this.buy.selector) {
    buy(sender, tokensPaid, productAmount, color);
}

​ Thus, your onTransferReceived function may look something like this: ​

function onTransferReceived(address operator, address sender, uint256 tokensPaid, bytes calldata data) external override (IERC1363Receiver) returns (bytes4) {
    require(msg.sender == address(acceptedToken), "I accept purchases in Payable Tokens");
​
    (bytes4 selector, uint productAmount, bytes3 color) =
        decode(data);
​
    if (selector == this.buy.selector) {
      buy(sender, tokensPaid, productAmount, color);
    }
​
    return this.onTransferReceived.selector;
  }