I'm trying to call a smart contract on Base L2 that has an ecrecover function, but when I sign a message and pass it my r,s,v values (contract accepts 4 params, all uint256, presumably hashedmessage, r, s, v), it never returns my address? I'm using ethers and the code below, am I doing something wrong?

const ethers = require('ethers');

async function main(message) {
    const provider = new ethers.providers.JsonRpcProvider('https://goerli.base.org');

    const wallet = new ethers.Wallet('private-key', provider);
    console.log('Signing wallet address:', wallet.address);
    const contractAddress = 'addr';
    const messageHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(message));
    const signature = await wallet.signMessage(ethers.utils.arrayify(messageHash));
    const sig = ethers.utils.splitSignature(signature);
    const functionSelector = 'functionname';
    const arg0 = messageHash.slice(2);
    const arg1 = ethers.utils.hexZeroPad(ethers.utils.hexlify(sig.v), 32).slice(2);
    const arg2 = sig.r.slice(2);
    const arg3 = sig.s.slice(2);
    const data = functionSelector + arg0 + arg1 + arg2 + arg3;
    const rawTransaction = {
        to: contractAddress,
        value: ethers.utils.parseEther("0"),
        gasPrice: ethers.utils.parseUnits("1", "gwei"),
        gasLimit: ethers.utils.hexlify(1000000),
        nonce: await wallet.getTransactionCount(),
        data: '0x' + data,
        chainId: 84531  // Base Testnet Chain ID
    const signedTransaction = await wallet.signTransaction(rawTransaction);
    try {
        const txResponse = await provider.sendTransaction(signedTransaction);
        console.log('Transaction sent:', txResponse.hash);
        const receipt = await txResponse.wait();
        console.log('Transaction confirmed:', receipt.transactionHash);

    } catch (error) {
        console.error('Error:', error);

main('Hello World');

Contract decompiled code:

def unknown7e392050(uint256 _param1, uint256 _param2, uint256 _param3, uint256 _param4) payable: 
  require calldata.size - 4 >=′ 128
  require _param1 == _param1
  require _param2 == uint8(_param2)
  require _param3 == _param3
  require _param4 == _param4
  signer = erecover(_param1, _param2 << 248, _param3, _param4) # precompiled
  if not erecover.result:
      revert with ext_call.return_data[0 len return_data.size]
  log 0xe1f54da2: _param1, addr(signer)
  if addr(signer) != caller:
      revert with 0, 'Signer must be the message sender'
  return 5
  • can you share the smart contract code you're interacting with?
    – ISMAIL S.
    Commented Oct 18, 2023 at 15:26
  • @ISMAILS. added to the body of the question, thanks!
    – McD
    Commented Oct 18, 2023 at 20:59

1 Answer 1


Let's start with the fact that the function in the smart contract works correctly if you pass it valid values.

Judging by the code, you are using ethers version 5.6(7).+. The inconsistency of the signature check on messageHash is that wallet.signMessage adds its prefix = "\x19Ethereum Signed Message:\n" to your messageHash string. enter image description here

There are two solutions.

You can generate the hash separately instead of arg0 to send to rawTransaction

Or, what I recommend, upgrade to ethers 6.7 and use the following code to sign the message:

    const signer = new ethers.SigningKey(privateKey);
    const messageHash = await ethers.solidityPackedKeccak256([ "string", "uint" ], [ message, message.length ] );
    const signature = signer.sign(messageHash);

//    console.log(`messageHash: ${messageHash}`);
//    console.log(`v: ${sign.v}`);
//    console.log(`r: ${sign.r}`);
//    console.log(`s: ${sign.s}`);

  • thank you! upgrading to v6.7 and using your code worked, much appreciated!
    – McD
    Commented Oct 19, 2023 at 15:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.