Problems with the interaction between the relayer and the multisig wallet

Hello OpenZeppelin community!
Im trying to create an transaction proposal to burn some tokens on my multisig wallet through relayer (code below).
But i find out that core.safe doesnt have save service for Avalanche Fuji (list of available services below)

But defender somehow creates proposals etc.
Can anyone suggest how I can create proposals in a different way?


const { ethers } = require('ethers');
const {
  DefenderRelayProvider,
  DefenderRelaySigner,
} = require('defender-relay-client/lib/ethers');
const Safe = require('@safe-global/safe-core-sdk').default;
const EthersAdapter = require('@safe-global/safe-ethers-lib').default;
const SafeServiceClient = require('@safe-global/safe-service-client').default;

const CONTRACT_ADDRESS = '0xFAA27935213A149c5f81442C30BaaD36b15863A9';
const USER_WALLET_ADDRESS = '0x663653284888bf19d7ed0d5b76c9f6f6217fd0b7';
const AMOUNT_TO_BURN = ethers.utils.parseUnits('100', 18);

const API_KEY = 'XXXXX';
const API_SECRET =  'XXXXX';

async function tryToBurnFunds() {
  // Create signer:
  const credentials = { apiKey: API_KEY, apiSecret: API_SECRET };
  const relayerProvider = new DefenderRelayProvider(credentials);
  const signer = new DefenderRelaySigner(credentials, relayerProvider, {
    speed: 'fast',
  });

  // Generate transaction data:
  const burnFunctionHash = ethers.utils.keccak256(
    ethers.utils.toUtf8Bytes('burn(uint256)'),
  );
  const encodedArguments = ethers.utils.defaultAbiCoder.encode(
    ['uint256'],
    [AMOUNT_TO_BURN],
  );
  const transactionData = burnFunctionHash + encodedArguments.slice(2);
  const safeTransactionData = {
    to: CONTRACT_ADDRESS,
    data: transactionData,
    value: 0,
  };

  // Create safe factory:
  const ethAdapter = new EthersAdapter({
    ethers,
    signerOrProvider: signer,
  });
  const safeSdk = await Safe.create({
    ethAdapter,
    safeAddress: USER_WALLET_ADDRESS,
  });
  const safeTransaction = await safeSdk.createTransaction({
    safeTransactionData,
  });

  // Try to send transaction:
  const safeTxHash = await safeSdk.getTransactionHash(safeTransaction);
  const senderSignature = await safeSdk.signTransactionHash(safeTxHash);

// url for Avalanche mainnet is https://safe-transaction-avalanche.safe.global/
  const txServiceUrl = 'HERE MUST BE SAFE SERVICE URL';
  const safeService = new SafeServiceClient({
    txServiceUrl,
    ethAdapter,
  });
  const result = await safeService.proposeTransaction({
    safeAddress: USER_WALLET_ADDRESS,
    safeTransactionData: safeTransaction.data,
    safeTxHash,
    senderAddress: await signer.getAddress(),
    senderSignature: senderSignature.data,
  });

  console.log('result', result);
}

tryToBurnFunds();

If you want to create proposals on a network not supported by Gnosis Safe App, you could use an app such as Defender (there may be other alternatives as well) or interact directly with the Gnosis Safe and provide the appropriate signatures, params, etc. Though this does take some advanced understanding of how to craft the transaction.

1 Like

I deployed a safe transaction service locally by this guide.
However, I ran into another problem.
If i use DefenderRelaySigner, the service refuses to accept a request to create an offer and returns a 422 error (Error while proposing transaction: Unprocessable Entity). Here is my code:

import { ethers, utils } from 'ethers';
import SafeApiKit from '@safe-global/api-kit';
import EthersAdapter from '@safe-global/safe-ethers-lib';
import Safe from '@safe-global/safe-core-sdk';
import { SafeTransactionDataPartial } from '@safe-global/safe-core-sdk-types';
import { DefenderRelayProvider, DefenderRelaySigner } from 'defender-relay-client/lib/ethers';

const txServiceUrl = 'http://localhost:8000/txs/';
const safeAddress = '0x512560bB71b98a60bC7e87719D2B8573C935Ff58';
const tokenAddress = '0xFAA27935213A149c5f81442C30BaaD36b15863A9';
const abi = 'erc-20 token abi...';
const amount = '100'; // 100 erc-20 tokens to burn

const {
    API_KEY,
    API_SECRET,
} = process.env;

// Create defender signer:
const credentials = { apiKey: API_KEY!, apiSecret: API_SECRET! };
const relayerProvider = new DefenderRelayProvider(credentials);
const signerOrProvider = new DefenderRelaySigner(credentials, relayerProvider, {
    speed: 'fast'
});
/*
if i use this instead of DefenderRelaySigner, the code will work!!!
const { DEVELOPER_WALLET_KEY, RPC_URL } = process.env;
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const signerOrProvider = new ethers.Wallet(DEVELOPER_WALLET_KEY!, provider); */

// Create safeService instance:
const ethAdapter = new EthersAdapter({
    ethers,
    signerOrProvider
});
const safeService = new SafeApiKit({
    txServiceUrl,
    ethAdapter
});

// Create transactoin description:
const contractInterface = new utils.Interface(abi);
const data = contractInterface.encodeFunctionData('burn', [ethers.utils.parseEther(amount)]);

const safeTransactionData: SafeTransactionDataPartial = {
    to: tokenAddress,
    value: '0',
    data,
    operation: 0
};

// Create transaction:
const safeSdk = await Safe.create({
    ethAdapter,
    safeAddress
});
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData });

// Try to propose transaction:
const safeTxHash = await safeSdk.getTransactionHash(safeTransaction);
const senderSignature = await safeSdk.signTransactionHash(safeTxHash);
await safeService.proposeTransaction({
    safeAddress,
    safeTransactionData: safeTransaction.data,
    safeTxHash,
    senderAddress: await signerOrProvider.getAddress(),
    senderSignature: senderSignature.data
});

console.log('DONE!!!!');

Has anyone encountered a similar problem?

The problem was that Defender provides relayer addresses in non-checksummed form.
Safe API requires exactly this address format.
So, if you replace the code with this, then everything works perfectly:

import Web3 from 'web3';
import { ethers, utils } from 'ethers';
import SafeApiKit from '@safe-global/api-kit';
import EthersAdapter from '@safe-global/safe-ethers-lib';
import Safe from '@safe-global/safe-core-sdk';
import { SafeTransactionDataPartial } from '@safe-global/safe-core-sdk-types';
import { DefenderRelayProvider, DefenderRelaySigner } from 'defender-relay-client/lib/ethers';

const txServiceUrl = 'http://localhost:8000/txs/';
const safeAddress = '0x512560bB71b98a60bC7e87719D2B8573C935Ff58';
const tokenAddress = '0xFAA27935213A149c5f81442C30BaaD36b15863A9';
const abi = 'erc-20 token abi...';
const amount = '100'; // 100 erc-20 tokens to burn

const {
    API_KEY,
    API_SECRET,
} = process.env;

// Create defender signer:
const credentials = { apiKey: API_KEY!, apiSecret: API_SECRET! };
const relayerProvider = new DefenderRelayProvider(credentials);
const signerOrProvider = new DefenderRelaySigner(credentials, relayerProvider, {
    speed: 'fast'
});

// Convert address to checksummed:
let senderAddress = await signerOrProvider.getAddress();
if (!Web3.utils.checkAddressChecksum(senderAddress)) {
    senderAddress = Web3.utils.toChecksumAddress(senderAddress);
}

// Create safeService instance:
const ethAdapter = new EthersAdapter({
    ethers,
    signerOrProvider
});
const safeService = new SafeApiKit({
    txServiceUrl,
    ethAdapter
});

// Create transactoin description:
const contractInterface = new utils.Interface(abi);
const data = contractInterface.encodeFunctionData('burn', [ethers.utils.parseEther(amount)]);

const safeTransactionData: SafeTransactionDataPartial = {
    to: tokenAddress,
    value: '0',
    data,
    operation: 0
};

// Create transaction:
const safeSdk = await Safe.create({
    ethAdapter,
    safeAddress
});
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData });

// Try to propose transaction:
const safeTxHash = await safeSdk.getTransactionHash(safeTransaction);
const senderSignature = await safeSdk.signTransactionHash(safeTxHash);
await safeService.proposeTransaction({
    safeAddress,
    safeTransactionData: safeTransaction.data,
    safeTxHash,
    senderAddress,
    senderSignature: senderSignature.data
});

console.log('DONE!!!!');