5

I have followed this Hardhat tutorial. But I'm modifying the config file to make it work on RSK, and have uencountered some unexpected behaviour with addresses. ​

const { expect } = require('chai');
​
describe('Token contract', () => {
  it('Deployment should assign the total supply of tokens to the owner', async () => {
    const [owner] = await ethers.getSigners();
    console.log('Smart contract owner: ', owner.address);
      
    const Token = await ethers.getContractFactory('Token');
​
    const hardhatToken = await Token.deploy();
    
    const ownerBalance = await hardhatToken.balanceOf(owner.address);
    expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
  });
});

​ Here is the hardhat.config.js setup I am using: ​

require('@nomiclabs/hardhat-waffle');
const fs = require('fs');
​
const minimumGasPriceTestnet = 65164000;
const TESTNET_GAS_MULT = 1;
const mnemonic = fs.readFileSync('.secret', 'utf8').toString().trim();
if (!mnemonic || mnemonic.split(' ').length !== 12) {
  throw new Error('unable to retrieve mnemonic from .secret');
}
​
module.exports = {
  solidity: '0.7.3',
  defaultNetwork: 'rsktestnet',
  networks: {
    hardhat: {},
    rsktestnet: {
      chainId: 31,
      url: 'https://public-node.testnet.rsk.co/',
      gasPrice: Math.floor(minimumGasPriceTestnet * TESTNET_GAS_MULT),
      gasMultiplier: TESTNET_GAS_MULT,
      accounts: {
        mnemonic,
        initialIndex: 0,
        // Ref: https://developers.rsk.co/rsk/architecture/account-based/#derivation-path-info
        path: "m/44'/37310'/0'/0",
        count: 10,
      },
    },
  },
};

​ In a file .secret I store a mnemonic phrase I am also using in a web browser wallet (Metamask).

I run the test and it fails with the following error: ​

Token contract
Smart contract owner:  0x266f1533e637F88C8022251d5Ec9221E9.......
    1) Deployment should assign the total supply of tokens to the owner
​
​
  0 passing (5s)
  1 failing
​
  1) Token contract
       Deployment should assign the total supply of tokens to the owner:
     ProviderError: the sender account doesn't exist
      at HttpProvider.request (node_modules/hardhat/src/internal/core/providers/http.ts:49:19)
      at HDWalletProvider.request (node_modules/hardhat/src/internal/core/providers/accounts.ts:181:36)
      at processTicksAndRejections (internal/process/task_queues.js:97:5)
      at EthersProviderWrapper.send (node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:20)

​I have noticed that the contract owner address differs from the Metamask generated. Why does Hardhat and Metamask produce different account addresses. Since I'm using the same seed phrase, shouldn't both have the same addresses?

1 Answers1

4

MetaMask uses a derivation path value of m/44'/60'/0'/0, which is technically only correct for Ethereum, and incorrect for RSK.

The technically correct solution here would be to configure MetaMask to use the RSK derivation path as well. Unfortunately, MetaMask currently (March 2022) does not support custom derivation paths in their network config.

In lieu of the "proper" solution, you can use a workaround: Use the Ethereum derivation path in the hardhat config.

To do so, in hardhat.config.js, replace:

path: "m/44'/37310'/0'/0",

with

path: "m/44'/60'/0'/0",

Why does this work?

To truly understand what is going on above, and how it works, one needs to understand the BIP-44 technical specification for Multi-Account Hierarchy for Deterministic Wallets. The path attribute in the hardhat config file above refers to the the heirarchical derivation path described in BIP-44.

However the full inner workings of that aren't necessary, and instead it suffices to think of it as a black box, and just consider it as a function with inputs and outputs.

Think of the full proccess described in BIP-44 as a black box, in a function named hdPath. This function takes in two inputs, the seed phrase, and the derivation path. This function produces a single output, which is a set of accounts.

hdPath(seedPhrase, derivationPath) -> set of accounts

An account consists of a private key, which is then used to generate a public key, which is then used to generate an address:

account : privateKey -> publicKey -> address

So if you use the same seed phrase (in both hardhat and metamask), but use a different derivation path (m/44'/37310'/0'/0 in hardhat, but m/44'/60'/0'/0 in metamask); you overall still have different inputs to the hdPath function, and therefore it will generate a different set of private keys, which in turn means a different set of public keys, which is turn means a different set of addresses; which is what you've observed.

bguiz
  • 27,371
  • 47
  • 154
  • 243