Skip to content

"Send Tokens" UI doesn't work (even after whitelisting the ERC20 transfer() function) #1335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
SecurityIsHard69420666 opened this issue Apr 10, 2025 · 1 comment
Assignees

Comments

@SecurityIsHard69420666
Copy link

SecurityIsHard69420666 commented Apr 10, 2025

edit: maybe it does work if you just ignore the "Function not Allowed" error and click submit (idk why I didn't try that earlier). in any case it's a UI error. also when I ignored the error and clicked submit anyways, the gas estimate was $50 in for the send xD. I didn't go through with that (so idk tbh) because the scripted version is really what I want.

Doesn't work:
Zodiac Pilot GUI -> "Send Tokens" -> https://app.pilot.gnosisguild.org/tokens/send

Image

I verified that the problem isn't me by crafting an ethers script that calls erc20 transfer() through zodiac's executeTransactionWithRole().... and it works!

Here's my permissions.ts for permissions-starter-kit:

export default [
  allow.mainnet.weth.transfer("0xRECIPIENT-ADDRESS-GOES-HERE")
] satisfies Permissions;

edit: i also waited ~24 hours after applying the permission to my avater/role-modifier thinking maybe "the cloud just needs to synchronize or something", but it still doesn't work.

and here's that ethers erc20-transfer-via-zodiac-executeTransactionWithRole script that works:

import { ethers } from "ethers";
import type { TransactionRequest } from "ethers";

const path = require("path");
const dotenv = require("dotenv");
dotenv.config({ path: path.join(__dirname, ".env") });

function sleep(): Promise<void>
{
    return new Promise(resolve => setTimeout(resolve, 2000));
}
function debugOutput(...args: unknown[]): void
{
    console.error(...args);
}
interface OutputJson {
    success: boolean;
    broadcastedTransaction?: string;
    blockNumber?: number;
    txStatus?: number;
    txHash?: string;
    error?: string;
}
(async function main(): Promise<void>
{
    let outputJson: OutputJson = { success: false };
    try
    {
        if (process.argv.includes("--help") || process.argv.length < 8 || !process.env.PRIVATE_KEY)
        {
            debugOutput("Usage: node script.js tokenAddress destination amount roleKey zodiacRoleContract providerRpc");
            debugOutput("       also be sure to set up PRIVATE_KEY= in your .env file");
            throw new Error("Invalid Arguments");
        }

        const [
            tokenAddress,
            destinationAddress,
            amountStr,
            roleKey,
            zodiacRoleContract,
            providerRpc
        ] = process.argv.slice(-6);

        // amountStr is in base units (wei for ERC-20):
        const amountToTransfer = BigInt(amountStr);

        const provider = new ethers.JsonRpcProvider(providerRpc);
        const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

        // The Zodiac Role modifier function: executeTransactionWithRole(...)
        // Function signature: 0xc6fe8747
        const operation = 0;
        const shouldRevert = true;

        // The ERC-20 transfer(address,uint256) signature is 0xa9059cbb
        const abi = ethers.AbiCoder.defaultAbiCoder();
        const subCall = "0xa9059cbb"
            + abi.encode(["address", "uint256"], [destinationAddress, amountToTransfer]).slice(2);

        // Encode final call to executeTransactionWithRole(...)
        const callData = "0xc6fe8747"
            + abi.encode(
                ["address", "uint256", "bytes", "uint8", "bytes32", "bool"],
                [tokenAddress, 0n, subCall, operation, roleKey, shouldRevert]
            ).slice(2);

        debugOutput("getting gas fee data...");
        const feeData = await provider.getFeeData(); await sleep();
        if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas)
            throw new Error("Provider did not return EIP-1559 gas data");

        const bump = 1.2;
        const maxFee = feeData.maxFeePerGas * BigInt(Math.floor(bump * 100)) / 100n;

        debugOutput("getting nonce...");
        const nonce = await wallet.getNonce(); await sleep();

        const tx: TransactionRequest = {
            type: 2,
            chainId: 1,
            nonce,
            to: zodiacRoleContract,
            value: 0n,
            data: callData,
            gasLimit: 455_793n,
            maxFeePerGas: maxFee,
            maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
        };

        debugOutput("Sending ERC-20 Transfer transaction via Zodiac Role...");
        const txResponse = await wallet.sendTransaction(tx); await sleep();
        debugOutput("Transaction sent!");
        debugOutput(`Hash: ${txResponse.hash}`);
        debugOutput("Waiting for confirmation...");
        const receipt = await txResponse.wait();
        if (receipt == null)
            throw new Error("null tx receipt");
        debugOutput(`Confirmed in block ${receipt.blockNumber}`);
        debugOutput(`Status: ${receipt.status === 1 ? "Success" : "Failed"}`);

        outputJson.broadcastedTransaction = txResponse.hash;
        outputJson.blockNumber = receipt.blockNumber;
        outputJson.txStatus = receipt.status!;
        outputJson.txHash = txResponse.hash;
        outputJson.success = (receipt.status === 1);
    }
    catch (err)
    {
        debugOutput(err);
        outputJson.error = err instanceof Error ? err.message : String(err);
    }
    finally
    {
        console.log(JSON.stringify(outputJson, (_, v) => typeof v === "bigint" ? v.toString() : v));
        process.exit(outputJson.success === true ? 0 : 1);
    }
})();

@jfschwarz jfschwarz self-assigned this Apr 10, 2025
@jfschwarz
Copy link
Collaborator

Hey @SecurityIsHard69420666, this is quite strange. I can't reproduce it with my accounts. Can you maybe share the account address & route you're using, e.g. by copy&pasting the url of Pilot's route editing page?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants