Skip to content

Unable to use permitWitnessTransferFrom: Signature mismatch in dApp vs Solidity test #267

@Th0rgal

Description

@Th0rgal

Summary

I'm attempting to use permitWitnessTransferFrom in my dApp using the same parameters and EOA as a successful Solidity test, but the generated signature fails with InvalidSigner. The issue appears to be due to an inconsistency in how the EIP712 signature is being generated or hashed.

Context

I adapted the test testPermitTransferFromTypedWitness to use the same values as my frontend (using the same EOA private key), and it passes successfully in Solidity:

function testPermitTransferFromTypedWitness() public {
    vm.chainId(1);
    uint256 nonce = 1;
    MockWitness memory witnessData = MockWitness(10000000, address(5), true);
    address usdcAddress = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    bytes32 witness = keccak256(abi.encode(witnessData));
    ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({
        permitted: ISignatureTransfer.TokenPermissions({token: usdcAddress, amount: 1_000_000}),
        nonce: nonce,
        deadline: 1747147831
    });

    // Signing and asserting success
    bytes memory sig = getPermitWitnessTransferSignature(
        permit, fromPrivateKey, FULL_EXAMPLE_WITNESS_TYPEHASH, witness, DOMAIN_SEPARATOR
    );
    ...
    permit2.permitWitnessTransferFrom(permit, transferDetails, from, witness, WITNESS_TYPE_STRING, sig);
}

In my app, I use Ethers.js to construct and sign the same EIP-712 message. When calling permit2.permitWitnessTransferFrom() I ensured the same permit, transferDetails, userAddress (from), witness and witnessTypeString.

However, despite using the same values, the signatures differ, and the simulation fails with:

Revert data: 0x815e1d64 (InvalidSigner)

In Tenderly, the claimedSigner is decoded as 0x0000000000000000000000000000000000000041, which seems totally off. The expected signer is my EOA: 0x5bb21B30E912871D27182E7b7F9C37C888269cb2.

Link to simulation

Tenderly simulation (fails at claimedSigner != signer)

Typed Data Details

Here’s the exact EIP-712 data I use in the dApp:

const domain = {
    name: "Permit2",
    chainId: connectedChainId, // using 1 (mainnet)
    verifyingContract: "0x000000000022d473030f116ddee9f6b43ac78ba3"
};

const types = {
    PermitWitnessTransferFrom: [
        { name: "permitted", type: "TokenPermissions" },
        { name: "nonce", type: "uint256" },
        { name: "deadline", type: "uint256" },
        { name: "witness", type: "MockWitness" }
    ],
    TokenPermissions: [
        { name: "token", type: "address" },
        { name: "amount", type: "uint256" }
    ],
    MockWitness: [
        { name: "value", type: "uint256" },
        { name: "person", type: "address" },
        { name: "test", type: "bool" }
    ]
};

const message = {
    permitted: {
        token: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // USDC
        amount: "1000000"
    },
    nonce: "1",
    deadline: "1747147831",
    witness: {
        value: "10000000",
        person: "0x0000000000000000000000000000000000000005",
        test: true
    }
};

I generate the signature like this:

const payload = ethers.TypedDataEncoder.getPayload(domain, types, message);
const signature = await provider.send("eth_signTypedData_v4", [
    userAddress,
    JSON.stringify(payload)
]);

When using Rabby Wallet, I see the signing prompt exactly showing:

{
    "types": {
        "PermitWitnessTransferFrom": [
            { "name": "permitted", "type": "TokenPermissions" },
            { "name": "nonce", "type": "uint256" },
            { "name": "deadline", "type": "uint256" },
            { "name": "witness", "type": "MockWitness" }
        ],
        "TokenPermissions": [
            { "name": "token", "type": "address" },
            { "name": "amount", "type": "uint256" }
        ],
        "MockWitness": [
            { "name": "value", "type": "uint256" },
            { "name": "person", "type": "address" },
            { "name": "test", "type": "bool" }
        ],
        "EIP712Domain": [
            { "name": "name", "type": "string" },
            { "name": "chainId", "type": "uint256" },
            { "name": "verifyingContract", "type": "address" }
        ]
    },
    "domain": {
        "name": "Permit2",
        "chainId": "0x1",
        "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
    },
    "primaryType": "PermitWitnessTransferFrom",
    "message": {
        "permitted": {
            "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
            "amount": "1000000"
        },
        "nonce": "1",
        "deadline": "1747147831",
        "witness": {
            "value": "10000000",
            "person": "0x0000000000000000000000000000000000000005",
            "test": true
        }
    }
}

This looks correct to me but there must be something wrong, are there reference examples where a frontend allows to generate a witnesses permit2 and the corresponding code to spend it?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions