hedera-sdk-js icon indicating copy to clipboard operation
hedera-sdk-js copied to clipboard

Contract call only works with EVM address but not long zero address

Open quiet-node opened this issue 1 year ago • 5 comments

Description

This bug is peculiar: if one of the parameters of a contract call is a long zero address, it results in a CONTRACT_REVERT_EXECUTED error. However, if you switch this parameter to the associated EVM address, the contract call works as expected. I was tracing the transaction through out the Relay but no error was occurred in the Relay. The transaction went straight to the SDK then received the transaction receipt from the SDK. Then looking up that transaction on Hashscan to find that the transaction hit CONTRACT_REVERT_EXECUTED error.

**Original ticket on relay repo: https://github.com/hashgraph/hedera-json-rpc-relay/issues/2466

Steps to reproduce

  1. Contract ABI - it has all other methods but the focus is addMemberFX(...). The _fxContract param is the one causing the problem.
const InterchangeABI = [
  {
    inputs: [
      {
        internalType: 'address',
        name: '_interchangeTokenAddress',
        type: 'address',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'constructor',
  },
  {
    inputs: [
      {
        internalType: 'string',
        name: 'memberName',
        type: 'string',
      },
    ],
    name: 'DuplicateMember',
    type: 'error',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: false,
        internalType: 'bool',
        name: '',
        type: 'bool',
      },
      {
        indexed: false,
        internalType: 'bytes',
        name: '',
        type: 'bytes',
      },
    ],
    name: 'CallResponseEvent',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: false,
        internalType: 'address',
        name: 'contractAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'string',
        name: 'memberName',
        type: 'string',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'memberTokenAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'interchangeTokenAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'buyRate',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'sellRate',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'memberAccount',
        type: 'address',
      },
    ],
    name: 'FXContractNew',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: false,
        internalType: 'address',
        name: 'fromAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'toAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'amount',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'sellQuote',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'buyQuote',
        type: 'uint64',
      },
    ],
    name: 'InterchangeComplete',
    type: 'event',
  },
  {
    inputs: [
      {
        internalType: 'string',
        name: '_memberName',
        type: 'string',
      },
      {
        internalType: 'address',
        name: '_memberToken',
        type: 'address',
      },
      {
        internalType: 'address',
        name: '_fxContract',
        type: 'address',
      },
    ],
    name: 'addMemberFX',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'contract IFX',
        name: 'fromMemberFX',
        type: 'address',
      },
      {
        internalType: 'contract IFX',
        name: 'toMemberFX',
        type: 'address',
      },
      {
        internalType: 'uint64',
        name: 'amount',
        type: 'uint64',
      },
      {
        internalType: 'address',
        name: 'toAddress',
        type: 'address',
      },
      {
        internalType: 'uint64',
        name: 'quote',
        type: 'uint64',
      },
      {
        internalType: 'int64',
        name: 'slippage',
        type: 'int64',
      },
    ],
    name: 'executeInterchange',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [],
    name: 'getInterchangeTokenAddress',
    outputs: [
      {
        internalType: 'address',
        name: 'tokenAddress',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint8',
        name: 'start',
        type: 'uint8',
      },
      {
        internalType: 'uint8',
        name: 'quantity',
        type: 'uint8',
      },
    ],
    name: 'getMemberAccounts',
    outputs: [
      {
        internalType: 'address[]',
        name: '',
        type: 'address[]',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'getMemberCount',
    outputs: [
      {
        internalType: 'uint8',
        name: 'count',
        type: 'uint8',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint8',
        name: 'start',
        type: 'uint8',
      },
      {
        internalType: 'uint8',
        name: 'quantity',
        type: 'uint8',
      },
    ],
    name: 'getMemberFXs',
    outputs: [
      {
        internalType: 'address[]',
        name: '',
        type: 'address[]',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'contract IFX',
        name: 'fromMemberFX',
        type: 'address',
      },
      {
        internalType: 'contract IFX',
        name: 'toMemberFX',
        type: 'address',
      },
      {
        internalType: 'uint64',
        name: 'amount',
        type: 'uint64',
      },
    ],
    name: 'getQuote',
    outputs: [
      {
        internalType: 'uint64',
        name: 'quote',
        type: 'uint64',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    name: 'memberAccounts',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    name: 'memberFXs',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token',
        type: 'address',
      },
      {
        internalType: 'bytes',
        name: 'encodedFunctionSelector',
        type: 'bytes',
      },
    ],
    name: 'redirectForToken',
    outputs: [
      {
        internalType: 'int256',
        name: 'responseCode',
        type: 'int256',
      },
      {
        internalType: 'bytes',
        name: 'response',
        type: 'bytes',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'from',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'to',
        type: 'address',
      },
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'transferFrom',
    outputs: [
      {
        internalType: 'int64',
        name: 'responseCode',
        type: 'int64',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'from',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'to',
        type: 'address',
      },
      {
        internalType: 'uint256',
        name: 'serialNumber',
        type: 'uint256',
      },
    ],
    name: 'transferFromNFT',
    outputs: [
      {
        internalType: 'int64',
        name: 'responseCode',
        type: 'int64',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'function',
  },
];
  1. Retrieve Interchange Contract from testnet
const interchangeContract = new ethers.Contract(
    '0x34dd11157b8c8ef2d6a0a0540286db03792b57ff',
    InterchangeABI,
    signer
  );
  1. Run the fail transaction that has _fxContract param holding the long zero address of an fxContract
try {
  const memberFXContract = await interchangeContract.addMemberFX(
    'Blue Name',
    '0x000000000000000000000000000000000039dbc7', // _memberToken
    '0x0000000000000000000000000000000000429273' // _fxContract
  );

  console.log(memberFXContract.hash);
} catch (error) {
  console.log(error);
}

This will print a transaction hash in the console. Look up this hash on Hashscan and find out this transaction is reverted. (example: https://hashscan.io/testnet/transaction/1716214396.849988785)

  1. Back to the program, run the valid transaction that has _fxContract param holding the associated EVM address of the 0x0000000000000000000000000000000000429273 fxAddress above
try {
  const memberFXContract = await interchangeContract.addMemberFX(
    'Blue Name',
    '0x000000000000000000000000000000000039dbc7', // _memberToken
    '0x9feffbd84e9507273f5212e486a8607c05ab5409' // _fxContract
  );

  console.log(memberFXContract.hash);
} catch (error) {
  console.log(error);
}

This will print out a transaction hash in the console. Look up this hash on Hashscan and find a successful transaction. (example: https://hashscan.io/testnet/transaction/1716214421.935436003)

Additional context

No response

Hedera network

mainnet, testnet, previewnet, other

Version

latest

Operating system

None

quiet-node avatar May 20 '24 14:05 quiet-node

@quiet-node I've encountered something similar several months ago. IIRC the recommendation back then was to exclusively use the EVM address (non-long-zero) whenever interacting with HSCS.

bguiz avatar May 21 '24 00:05 bguiz

Thanks @bguiz! It's strange to me that the _memberToken works fine with long zero address but not _fxContract. Also, we should dig down to see the real problem so it could help people in the future to not run into this again

quiet-node avatar May 21 '24 14:05 quiet-node

@quiet-node can you provide an end to end snippet so we can reproduce it more easily or may be if you have an example repo it would be great.

agadzhalov avatar May 28 '24 15:05 agadzhalov

Hello @agadzhalov, so sorry for the long silence I completely missed the notification of this thread. Anywho, the zip folder below contains the code to reproduce the problem. tiny-contract-call.zip.

All you need to do is just download and unzip the folder, then at line 445, there's a spot for you to put in a Private Key for the signer. After that, just run

node contract_call.js

It should then prints two hashscan transactions, one failure and one pass.

Please let me know if you have any question.

quiet-node avatar Jun 14 '24 19:06 quiet-node

In case you're not a fan of downloading random stuff, here is the plain file

const { ethers } = require('ethers');

const InterchangeABI = [
  {
    inputs: [
      {
        internalType: 'address',
        name: '_interchangeTokenAddress',
        type: 'address',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'constructor',
  },
  {
    inputs: [
      {
        internalType: 'string',
        name: 'memberName',
        type: 'string',
      },
    ],
    name: 'DuplicateMember',
    type: 'error',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: false,
        internalType: 'bool',
        name: '',
        type: 'bool',
      },
      {
        indexed: false,
        internalType: 'bytes',
        name: '',
        type: 'bytes',
      },
    ],
    name: 'CallResponseEvent',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: false,
        internalType: 'address',
        name: 'contractAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'string',
        name: 'memberName',
        type: 'string',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'memberTokenAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'interchangeTokenAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'buyRate',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'sellRate',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'memberAccount',
        type: 'address',
      },
    ],
    name: 'FXContractNew',
    type: 'event',
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: false,
        internalType: 'address',
        name: 'fromAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'address',
        name: 'toAddress',
        type: 'address',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'amount',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'sellQuote',
        type: 'uint64',
      },
      {
        indexed: false,
        internalType: 'uint64',
        name: 'buyQuote',
        type: 'uint64',
      },
    ],
    name: 'InterchangeComplete',
    type: 'event',
  },
  {
    inputs: [
      {
        internalType: 'string',
        name: '_memberName',
        type: 'string',
      },
      {
        internalType: 'address',
        name: '_memberToken',
        type: 'address',
      },
      {
        internalType: 'address',
        name: '_fxContract',
        type: 'address',
      },
    ],
    name: 'addMemberFX',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'contract IFX',
        name: 'fromMemberFX',
        type: 'address',
      },
      {
        internalType: 'contract IFX',
        name: 'toMemberFX',
        type: 'address',
      },
      {
        internalType: 'uint64',
        name: 'amount',
        type: 'uint64',
      },
      {
        internalType: 'address',
        name: 'toAddress',
        type: 'address',
      },
      {
        internalType: 'uint64',
        name: 'quote',
        type: 'uint64',
      },
      {
        internalType: 'int64',
        name: 'slippage',
        type: 'int64',
      },
    ],
    name: 'executeInterchange',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [],
    name: 'getInterchangeTokenAddress',
    outputs: [
      {
        internalType: 'address',
        name: 'tokenAddress',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint8',
        name: 'start',
        type: 'uint8',
      },
      {
        internalType: 'uint8',
        name: 'quantity',
        type: 'uint8',
      },
    ],
    name: 'getMemberAccounts',
    outputs: [
      {
        internalType: 'address[]',
        name: '',
        type: 'address[]',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [],
    name: 'getMemberCount',
    outputs: [
      {
        internalType: 'uint8',
        name: 'count',
        type: 'uint8',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint8',
        name: 'start',
        type: 'uint8',
      },
      {
        internalType: 'uint8',
        name: 'quantity',
        type: 'uint8',
      },
    ],
    name: 'getMemberFXs',
    outputs: [
      {
        internalType: 'address[]',
        name: '',
        type: 'address[]',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'contract IFX',
        name: 'fromMemberFX',
        type: 'address',
      },
      {
        internalType: 'contract IFX',
        name: 'toMemberFX',
        type: 'address',
      },
      {
        internalType: 'uint64',
        name: 'amount',
        type: 'uint64',
      },
    ],
    name: 'getQuote',
    outputs: [
      {
        internalType: 'uint64',
        name: 'quote',
        type: 'uint64',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    name: 'memberAccounts',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
    name: 'memberFXs',
    outputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token',
        type: 'address',
      },
      {
        internalType: 'bytes',
        name: 'encodedFunctionSelector',
        type: 'bytes',
      },
    ],
    name: 'redirectForToken',
    outputs: [
      {
        internalType: 'int256',
        name: 'responseCode',
        type: 'int256',
      },
      {
        internalType: 'bytes',
        name: 'response',
        type: 'bytes',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'from',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'to',
        type: 'address',
      },
      {
        internalType: 'uint256',
        name: 'amount',
        type: 'uint256',
      },
    ],
    name: 'transferFrom',
    outputs: [
      {
        internalType: 'int64',
        name: 'responseCode',
        type: 'int64',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'from',
        type: 'address',
      },
      {
        internalType: 'address',
        name: 'to',
        type: 'address',
      },
      {
        internalType: 'uint256',
        name: 'serialNumber',
        type: 'uint256',
      },
    ],
    name: 'transferFromNFT',
    outputs: [
      {
        internalType: 'int64',
        name: 'responseCode',
        type: 'int64',
      },
    ],
    stateMutability: 'nonpayable',
    type: 'function',
  },
];

(async () => {
  // source code of the interchange contract can be found here https://hashscan.io/testnet/contract/0.0.4321454
  const interchangeAddress = '0x000000000000000000000000000000000041f0ae';

  const memberTokenAddress = '0x000000000000000000000000000000000039dbc7';
  const fxContractAddress = {
    evm_address: '0x857fd6c0e45f88560f1697cefc45b2ac01ebf5c8',
    long_zero_address: '0x000000000000000000000000000000000043dd7d',
  };

  const provider = new ethers.JsonRpcProvider('https://testnet.hashio.io/api');

  // @IMPORTANT: add your account's PK here
  const SIGNER_PK = 'SIGNED_PK';
  const signer = new ethers.Wallet(SIGNER_PK).connect(provider);

  const interchangeContract = new ethers.Contract(
    interchangeAddress,
    InterchangeABI,
    signer
  );

  // Should fail transaction that has _fxContract param holding a long zero address
  try {
    const tx = await interchangeContract.addMemberFX(
      'Random',
      memberTokenAddress, // _memberToken
      fxContractAddress.long_zero_address // _fxContractAddress
    );
    await tx.wait();
  } catch (error) {
    // visit the transaction on hashscan will see the CONTRACT_REVERT_EXECUTED error but no error message was provided
    console.log(
      `https://hashscan.io/testnet/transaction/${error.receipt.hash}`
    );
  }

  //   @IMPORTANT: since successful addMemberFX() transaction will add the signer (msg.sender) into an array on chain, and duplicated senders will throw `DuplicateMember` error
  //               Therefore, every time this test case successfully passes, please update a new signer to sign the transaction.
  //   Should pass transaction that has _fxContract param holding a evm address
  const tx = await interchangeContract.addMemberFX(
    'Random',
    memberTokenAddress, // _memberToken
    fxContractAddress.evm_address // _fxContractAddress
  );
  const receipt = await tx.wait();
  expect(receipt).to.exist;

  // visit the transaction on hashscan will see the transaction is successful
  console.log(`https://hashscan.io/testnet/transaction/${receipt.hash}`);
})();

You just need to install ethers, and run like the instruction I provided above.

quiet-node avatar Jun 14 '24 19:06 quiet-node