1

On my frontend app I am trying to implement EIP-3009 with USDC using Ethers v6.13.4, Web3Auth (using Web3AuthNoModal) and Alchemy as the RPC provider. The implementation itself seems to be very straightforward, here is the code:

import { WalletContext } from "../contexts/wallet"; ... const { provider, web3auth } = useContext(WalletContext); ... const ethersProvider = new ethers.BrowserProvider(provider as any); const web3AuthSigner = await ethersProvider.getSigner(); const address = await web3AuthSigner.getAddress(); const amountToTransfer = ethers.parseUnits(amount.toString(), 6); // Generate deadline (1 hour) const now = new Date(); const deadline = new Date(now.getTime() + 10 * 60 * 1000); // Generate a random nonce const randomBytes = ethers.randomBytes(32); const nonce = ethers.hexlify(randomBytes); const chainId = parseInt(process.env.CHAIN_ID, 16); // ABI for the authorizationState function in the USDC contract const usdcContract = new ethers.Contract( erc20Address, [ { inputs: [ { internalType: "address", name: "authorizer", type: "address" }, { internalType: "bytes32", name: "nonce", type: "bytes32" }, ], name: "authorizationState", outputs: [{ internalType: "bool", name: "", type: "bool" }], stateMutability: "view", type: "function", }, ], web3AuthSigner ); // Check if the nonce has already been used const alreadyAuthorized = await usdcContract.authorizationState(address, nonce); if (alreadyAuthorized) { throw new Error("Nonce has already been used."); } // Define the domain and message types for the signed data console.info(process.env.CHAIN_ID); console.info(parseInt(process.env.CHAIN_ID)); const domain = { name: "USDC", version: "2", chainId: parseInt(process.env.CHAIN_ID), verifyingContract: erc20Address, }; const types = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }; // Message to sign (EIP-3009 structure) const message = { from: address, to: toAddress, value: amountToTransfer, validAfter: 0, validBefore: deadline.valueOf(), nonce: nonce, }; console.info({ message }); let signature; try { // Sign the typed data with the user's wallet signature = await web3AuthSigner.signTypedData(domain, types, message); console.info("Signature: ", signature); } catch (error) { console.error("Error signing the authorization:", error); throw error; } 

I've copied this implementation from a number of different projects, however, in this project, it always fails despite the same version of Ethers, using Web3Auth and even using Alchemy as the RPC provider. Here is the error message when applying it on Ethereum Sepolia (I've omitted the addresses used, but I can assure that they are always the expected addresses):

could not coalesce error (error={ "code": -32603, "message": "Cannot read properties of null (reading 'ok')" }, payload={ "id": 6, "jsonrpc": "2.0", "method": "eth_signTypedData_v4", "params": [ "0x...", "{\"types\":{\"TransferWithAuthorization\":[{\"name\":\"from\",\"type\":\"address\"},{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"validAfter\",\"type\":\"uint256\"},{\"name\":\"validBefore\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"bytes32\"}],\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}]},\"domain\":{\"name\":\"USDC\",\"version\":\"2\",\"chainId\":\"0xaa36a7\",\"verifyingContract\":\"0x1c7d4b196cb0c7b01d743fbc6116a902379c7238\"},\"primaryType\":\"TransferWithAuthorization\",\"message\":{\"from\":\"0x...\",\"to\":\"0x...\",\"value\":\"100000\",\"validAfter\":\"0\",\"validBefore\":\"1734784620899\",\"nonce\":\"0xa27be55ed87a2b4a2e38db6d4864816f76d45dc27b9cae4b2b0415ef1ef2523e\"}}" ] }, code=UNKNOWN_ERROR, version=6.13.4) 

This error only happens at the end of the script, while using signTypedData(domain, types, message), which makes an error with code 32603 even stranger, as I previously check on-chain if the nonce has already been used or not. I've tried to change the keys to the provider but to no avail.

Any lights on the problem would be greatly appreciated. Thank you in advance!


P.S.:

Since the issue is most likely on the way I create the provider, I'll share the way I instantiate it. Since I am using React, I am only sharing the relevant bits for this context. These snippets are not all contained within the same function. Regarding the React logic itself, the only thing relevant is that I have useState for the variable provider that I will add to a context script that allows me to use it throughout the whole app.

I am using Web3AuthNoModal and EthereumPrivateKeyProvider in the following way on a different script:

const [provider, setProvider] = useState<IProvider | null>(null); ... const chainConfig = { chainNamespace: CHAIN_NAMESPACES.EIP155, chainId: process.env.CHAIN_ID, rpcTarget: process.env.ALCHEMY_RPC_TARGET, }; const privateKeyProvider = new EthereumPrivateKeyProvider({ config: { chainConfig }, }); ... const web3auth = useMemo( () => new Web3AuthNoModal({ clientId: process.env.WEB3AUTH_CLIENT_ID, web3AuthNetwork: process.env.WEB3AUTH_NETWORK, privateKeyProvider: privateKeyProvider, }), [] ); ... web3auth.configureAdapter(openloginAdapter); await web3auth.init(); setProvider(web3auth.provider); ... return ( ... <WalletContext.Provider value={{ provider, setProvider, ... }} > ... </WalletContext.Provider> 

Here is the context script for the wallet:

export interface IContextValues { provider: IProvider | null; setProvider: Dispatch<SetStateAction<IProvider | null>>; ... } export const WalletContext = createContext<IContextValues>({ provider: null, setProvider: () => null, ... }); 

Finally, the way I import this context and use it is on the first piece of code that I've shared.

2 Answers 2

0

I executed your exact code on my end using JsonRpcProvider, and it worked completely fine, so the problem seems to be with how you've defined your ethersProvider.

If you're using it in the browser environment, replace this line:

const ethersProvider = new ethers.BrowserProvider(provider as any); 

with:

const ethersProvider = new ethers.BrowserProvider(window.ethereum); 

If you want to use the JsonRpcProvider (specifically in a script or a non-browser environment), you need to define the signer using a hardcoded private key (stored as an environment variable).

Your ethersProvider and web3AuthSigner variables should be defined as follows:

const ethersProvider = new ethers.JsonRpcProvider(process.env.JSON_RPC_URL); const web3AuthSigner = new ethers.Wallet(process.env.PRIVATE_KEY, ethersProvider); 

Using AlchemyProvider, it would be like:

const ethersProvider = new ethers.AlchemyProvider("sepolia", process.env.ALCHEMY_API_KEY); 

Since, you’re using Web3Auth, so you’ve to get the provider from the corresponding instance, like:

const web3Auth = new Web3AuthNoModal({ clientId: process.env.WEB3AUTH_CLIENT_ID, web3AuthNetwork: process.env.WEB3AUTH_NETWORK, privateKeyProvider: privateKeyProvider, }); const ethersProvider = new ethers.BrowserProvider(web3Auth.provider); const web3AuthSigner = await ethersProvider.getSigner(); 
4
  • Thank you very much for your reply! The issue seems to come from the provider, but I haven't been able to pinpoint what, exactly, since the previous transaction for checking the nonce works fine. Regarding your answer, I need to use the provider from Web3Auth. Here is how I instantiate it: ``` new Web3AuthNoModal({ clientId: process.env.WEB3AUTH_CLIENT_ID, web3AuthNetwork: process.env.WEB3AUTH_NETWORK, privateKeyProvider: privateKeyProvider, }) ``` Commented Dec 23, 2024 at 12:11
  • You’re welcome. Instead of instantiating provider like that, you’ve to define/instantiate another variable (say web3Auth. And, then use web3Auth.provider as the argument to ethers.BrowserProvider(), while defining ethersProvider. Commented Dec 23, 2024 at 12:59
  • I’ve added the same in my answer. Please check. Commented Dec 23, 2024 at 13:04
  • That is what I am doing, unfortunately. I've added more context to my question Commented Dec 23, 2024 at 19:13
0

Apparently the Web3Auth signer, in the way that I use it, does not support operations of the type eth_signTypedData_v4. The way that I got around this issue was by creating an Ethers Wallet and doing the signature with it:

const privateKey = await provider.request({ method: "eth_private_key" }); const ethersProvider = new ethers.BrowserProvider(provider as any); let wallet; if (typeof privateKey === "string") { wallet = new ethers.Wallet(privateKey, ethersProvider); } else { throw Error('Private key was not fetched correctly'); } ... signature = await wallet.signTypedData(domain, types, message); 

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.