Error when using Defender Relayer to transfer tokens

I get errors when trying to transfer tokens using a Defender Relayer on BNB chain testnet.

:memo: Details

I deployed a ERC2771Forwarder and deployed a token that inherits ERC2771Context, passing the forwarder address into its constructor.

I ran code similar to the example at https://docs.openzeppelin.com/defender/v2/manage/relayers#using-ethers.js

I got this error output:

<ref *1> Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (error={"reason":"execution reverted","code":"UNPREDICTABLE_GAS_LIMIT","method":"estimateGas","transaction":{"from":"0x14f9A42ED153dcD817265f57308FFD11f7FE8DBa","to":"0x6C83356994D60cD88116320B08B43fa198fA5D02","data":"0xa9059cbb000000000000000000000000eb2e452fc167b5bb948c6fc2c9215ce7f4064692000000000000000000000000000000000000000000000000016345785d8a0000","accessList":null},"error":{"code":3,"data":"0xe450d38c00000000000000000000000014f9a42ed153dcd817265f57308ffd11f7fe8dba0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a0000"}}, tx={"to":{},"data":"0xa9059cbb000000000000000000000000eb2e452fc167b5bb948c6fc2c9215ce7f4064692000000000000000000000000000000000000000000000000016345785d8a0000","from":"0x14f9a42ed153dcd817265f57308ffd11f7fe8dba","gasLimit":{},"speed":"fast"}, code=UNPREDICTABLE_GAS_LIMIT, version=@openzeppelin/defender-relay-client)
    at Logger.makeError (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\logger\src.ts\index.ts:269:28)
    at Logger.throwError (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\logger\src.ts\index.ts:281:20)
    at C:\Users\-\Documents\Projects\test-relayer\node_modules\@openzeppelin\defender-sdk-relay-signer-client\lib\ethers\signer.js:155:31
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Promise.all (index 3) {
  reason: 'cannot estimate gas; transaction may fail or may require manual gas limit',
  code: 'UNPREDICTABLE_GAS_LIMIT',
  error: Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (reason="execution reverted", method="estimateGas", transaction={"from":"0x14f9A42ED153dcD817265f57308FFD11f7FE8DBa","to":"0x6C83356994D60cD88116320B08B43fa198fA5D02","data":"0xa9059cbb000000000000000000000000eb2e452fc167b5bb948c6fc2c9215ce7f4064692000000000000000000000000000000000000000000000000016345785d8a0000","accessList":null}, error={"code":3,"data":"0xe450d38c00000000000000000000000014f9a42ed153dcd817265f57308ffd11f7fe8dba0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a0000"}, code=UNPREDICTABLE_GAS_LIMIT, version=providers/5.7.2)
      at Logger.makeError (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\logger\src.ts\index.ts:269:28)
      at Logger.throwError (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\logger\src.ts\index.ts:281:20)
      at checkError (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\providers\src.ts\json-rpc-provider.ts:78:20)
      at DefenderRelayProvider.<anonymous> (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\providers\src.ts\json-rpc-provider.ts:642:20)
      at step (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\providers\lib\json-rpc-provider.js:48:23)
      at Object.throw (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\providers\lib\json-rpc-provider.js:29:53)
      at rejected (C:\Users\-\Documents\Projects\test-relayer\node_modules\@ethersproject\providers\lib\json-rpc-provider.js:21:65)
      at processTicksAndRejections (node:internal/process/task_queues:95:5) {
    reason: 'execution reverted',
    code: 'UNPREDICTABLE_GAS_LIMIT',
    method: 'estimateGas',
    transaction: {
      from: '0x14f9A42ED153dcD817265f57308FFD11f7FE8DBa',
      to: '0x6C83356994D60cD88116320B08B43fa198fA5D02',
      data: '0xa9059cbb000000000000000000000000eb2e452fc167b5bb948c6fc2c9215ce7f4064692000000000000000000000000000000000000000000000000016345785d8a0000',
      accessList: null
    },
    error: Error: execution reverted
        at DefenderRelayProvider.send (C:\Users\-\Documents\Projects\test-relayer\node_modules\@openzeppelin\defender-sdk-relay-signer-client\lib\ethers\provider.js:58:31)
        at processTicksAndRejections (node:internal/process/task_queues:95:5) {
      code: 3,
      data: '0xe450d38c00000000000000000000000014f9a42ed153dcd817265f57308ffd11f7fe8dba0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a0000'
    }
  },
  tx: {
    to: Promise { '0x6C83356994D60cD88116320B08B43fa198fA5D02' },
    data: '0xa9059cbb000000000000000000000000eb2e452fc167b5bb948c6fc2c9215ce7f4064692000000000000000000000000000000000000000000000000016345785d8a0000',
    from: '0x14f9a42ed153dcd817265f57308ffd11f7fe8dba',
    gasLimit: Promise { <rejected> [Circular *1] },
    speed: 'fast'
  }
}

Is there anything wrong in the code below?

:computer: Environment
openzeppelin/defender-sdk v1.10.0
ethers v6.11.1
hardhat 2.20.1
BNB chain testnet

:1234: Code to reproduce
JavaScript using ethers:

  const forwarder = await ethers.deployContract("ERC2771Forwarder", [`TESTERC20ERC2771OZ forwarder`])
  await forwarder.waitForDeployment()
  const forwarderAddress = await forwarder.target
  console.log(`forwarder has been deployed to: ${forwarderAddress}`)

  const tokenContractName = `TESTERC20ERC2771OZ`
  const token = await ethers.deployContract(tokenContractName, [
    `Token 4622`,
    `TOKEN4622`,
    wallet.address,
    forwarderAddress,
  ])
  await token.waitForDeployment()
  const tokenContractAddress = await token.target
  console.log(`token has been deployed to: ${tokenContractAddress}`)

  const { Defender } = require('@openzeppelin/defender-sdk')
  const client = new Defender({ relayerApiKey: process.env.OZ_RELAYER_BSCTEST_API_KEY, relayerApiSecret: process.env.OZ_RELAYER_BSCTEST_API_SECRET })
  const provider = client.relaySigner.getProvider()
  const validUntil = new Date(Date.now() + 120 * 1000).toISOString()
  const signer = client.relaySigner.getSigner(provider, { speed: 'fast', validUntil })
  const token = await ethers.getContractAt(tokenContractName, tokenContractAddress, signer)

  const destinationAddress = `0xEB2e452fC167b5bb948c6FC2c9215ce7F4064692`
  const amount = ethers.parseUnits(`0.1`, `ether`)

  const tx = await token.transfer(destinationAddress, amount)

Token contract in Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";

contract TESTERC20ERC2771OZ is ERC20, ERC20Permit, ERC2771Context {
    bytes public b;

    constructor(
        string memory name,
        string memory symbol,
        address initialHolder,
        address forwarder
    ) ERC20(name, symbol) ERC20Permit(name) ERC2771Context(forwarder) {
        _mint(initialHolder, 1000);
    }

    function _msgSender()
        internal
        view
        override(Context, ERC2771Context)
        returns (address)
    {
        return ERC2771Context._msgSender();
    }

    function _msgData()
        internal
        view
        override(Context, ERC2771Context)
        returns (bytes calldata)
    {
        return ERC2771Context._msgData();
    }

    function _contextSuffixLength()
        internal
        view
        override(Context, ERC2771Context)
        returns (uint256)
    {
        return ERC2771Context._contextSuffixLength();
    }
}

I'm still stuck trying to get Relayers working.

Please check the simple example code above and let me know what I should do.

Thank you.

@emnul @ernestognw @ericglau @marcos.carlomagno @dylkil

I've had a look at example code in https://github.com/OpenZeppelin/defender-sdk/blob/main/examples/relayer-signer-actions/index.js and https://github.com/OpenZeppelin/defender-sdk/blob/main/examples/ethers-signer/index.js but neither include transferring ERC20 tokens.

What could be the problem?

Are Defender 2 relayers actually working for ERC20 token transfers?

Hi could you share the relayerId in use, your tenantId, as well as the contract addresses you deployed?


@emnul I've sent a message to you.

Could you try pulling the latest Defender SDK changes and trying to run your script again? This PR that was merged recently fixes some important ethers v6 compatibility issues

Please publish the updated version on npm so that I can try it.

I still don't see the new version on npm.

While waiting for it I did these:
Cloned https://github.com/OpenZeppelin/defender-sdk
Ran pnpm i --ignore-scripts --prefer-offline && pnpm run build

In my own repository I ran:
yarn remove @openzeppelin/defender-sdk
yarn add -D ../../defender-sdk

When I run the hradhat script I get the error:

Cannot find module '@openzeppelin/defender-sdk'

yarn add -D ../../defender-sdk installed the package under devDependencies in your package.json file. Please reinstall it without the -D flag to use the package.

I tried yarn add ../../defender-sdk and I see that it went into dependencies section of package.json but it didn't make a difference - I still get the Cannot find module '@openzeppelin/defender-sdk' error.

Also, previously I had added @openzeppelin/defender-sdk from npm using yarn add -D @openzeppelin/defender-sdk so it went into devDependencies and it did find the module, but token transfer didn't work as I originally reported. So -D may not be the problem.

Please refer to this stackoverflow post for more information on configuring a project with a local module (cloned Defender repo). We will likely release a new npm version next week.

I think I solved the import issue by adding /packages/defender-sdk to the end of yarn add:

yarn add @openzeppelin/defender-sdk@link:../../defender-sdk/packages/defender-sdk

Now when I run the script I get the error:

contract runner does not support sending transactions (operation="sendTransaction", code=UNSUPPORTED_OPERATION, version=6.11.1)

It happens at the line:

const tx = await token.transfer(destinationAddress, amount)

Hi, please post the entire error log so we can better understand what the issue may be

Error: contract runner does not support sending transactions (operation="sendTransaction", code=UNSUPPORTED_OPERATION, version=6.11.1)
    at makeError (C:\Users\-\Documents\Projects\test-relayer\node_modules\ethers\src.ts\utils\errors.ts:694:21)
    at assert (C:\Users\-\Documents\Projects\test-relayer\node_modules\ethers\src.ts\utils\errors.ts:715:25)
    at send (C:\Users\-\Documents\Projects\test-relayer\node_modules\ethers\src.ts\contract\contract.ts:310:15)
    at Proxy.transfer (C:\Users\-\Documents\Projects\test-relayer\node_modules\ethers\src.ts\contract\contract.ts:352:22)
    at main (C:\Users\-\Documents\Projects\test-relayer\scripts\test-OZRelayer.js:150:39) {
  code: 'UNSUPPORTED_OPERATION',
  operation: 'sendTransaction',
  shortMessage: 'contract runner does not support sending transactions'

Hi are you sure you're using a signer when you created the contract instance that is called here main (C:\Users\-\Documents\Projects\test-relayer\scripts\test-OZRelayer.js:150:39)

Line 150 is

  const tx = await token.transfer(destinationAddress, amount)

You can see how the token contract instance was created in the code included in the original post. i.e. a signer was obtained from a Defender instance.

Is the provided code fully working on your end?

I tried to do a token transfer in an Action via webhook, and it didn't work that way either.

Action code:

const { ethers } = require("ethers");
const { Defender } = require('@openzeppelin/defender-sdk');

exports.handler = async function(credentials) {
  const client = new Defender(credentials);
  
  const provider = client.relaySigner.getProvider()
  const validUntil = new Date(Date.now() + 120 * 1000).toISOString()
  const signer = client.relaySigner.getSigner(provider, { speed: 'fast', validUntil })
  
  const tokenContractAddress = `0x6e1A1B16fae65c2f960203E3F6437BFfa8eF4129` // on BSC test

    const abi = [
    "function transfer(address to, uint amount) returns (bool)",
    "event Transfer(address indexed from, address indexed to, uint amount)"
  ];
  
  const token = new ethers.Contract(tokenContractAddress, abi, signer)

  const destinationAddress = `0xEB2e452fC167b5bb948c6FC2c9215ce7F4064692`
  const amount = ethers.utils.parseUnits(`0.1`, `ether`)
  const txRes = await token.transfer(destinationAddress, amount)
    
  return txRes.hash;
}

Error output:

httpResponse.data: {
  "autotaskRunId": "5111fe96-5b52-415d-917b-8267d10ae48d",
  "autotaskId": "bc526686-1c54-4616-9942-04e4662ea35a",
  "trigger": "webhook",
  "status": "error",
  "createdAt": "2024-03-13T20:35:36.494Z",
  "requestId": "77a7a4b9-673e-4d00-9829-ec0eade89719",
  "encodedLogs": "",
  "message": "cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (error={\"reason\":\"cannot estimate gas; transaction may fail or may require manual gas limit\",\"code\":\"UNPREDICTABLE_GAS_LIMIT\",\"error\":{\"code\":3,\"data\":\"0xe450d38c00000000000000000000000014f9a42ed153dcd817265f57308ffd11f7fe8dba0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a0000\"},\"method\":\"estimateGas\",\"transaction\":{\"from\":\"0x14f9A42ED153dcD817265f57308FFD11f7FE8DBa\",\"to\":\"0x6e1A1B16fae65c2f960203E3F6437BFfa8eF4129\",\"data\":\"0xa9059cbb000000000000000000000000eb2e452fc167b5bb948c6fc2c9215ce7f4064692000000000000000000000000000000000000000000000000016345785d8a0000\",\"accessList\":null}}, tx={\"data\":\"0xa9059cbb000000000000000000000000eb2e452fc167b5bb948c6fc2c9215ce7f4064692000000000000000000000000000000000000000000000000016345785d8a0000\",\"to\":{},\"from\":\"0x14f9a42ed153dcd817265f57308ffd11f7fe8dba\",\"gasLimit\":{},\"speed\":\"fast\"}, code=UNPREDICTABLE_GAS_LIMIT, version=@openzeppelin/defender-relay-client)"
}

Hi the latest version of our Actions dependancies in version v2024-01-18 uses ethers v5 and defender-sdk "1.5.0" so this will not work as defender-sdk version "1.5.0" doesn't have the ethers signer compatibility patch.

I'm working on trying to reproduce the issue you're facing using the local version of defender-sdk 1.11.0 and will get back to you with next steps soon

I need it to work in an Action, because it will be for end users of a dapp. Otherwise I'd have to include the API secret in the code which would be insecure.

Defender relayers would make it possible for new users to not need the blockchain's native currency when they transact in the dapp.

The Action edit interface says we should switch away from defender-relay-client to @openzeppelin/defender-sdk:

image

Anyway, I've just tried the Action using old code adapted from https://docs.openzeppelin.com/defender/v2/module/actions#a-complete-example :

const { ethers } = require("ethers");
const { DefenderRelaySigner, DefenderRelayProvider } = require('defender-relay-client/lib/ethers');

// Entrypoint for the action
exports.handler = async function(event) {
  // Initialize relayer provider and signer
  const provider = new DefenderRelayProvider(event);
  const signer = new DefenderRelaySigner(event, provider, { speed: 'fast' });

  const tokenContractAddress = `0x6e1A1B16fae65c2f960203E3F6437BFfa8eF4129` // on BSC test
 
  const abi = [
    "function transfer(address to, uint amount) returns (bool)",

    // Events
    "event Transfer(address indexed from, address indexed to, uint amount)"
  ];
   
  const token = new ethers.Contract(tokenContractAddress, abi, signer)

  const destinationAddress = `0xEB2e452fC167b5bb948c6FC2c9215ce7F4064692`
  const amount = ethers.utils.parseUnits(`0.1`, `ether`)
  const txRes = await token.transfer(destinationAddress, amount)

  return { tx: txRes.hash };
}

It still fails.

Are Actions with ethers (any version) actually working in production?