Skip to main content

Truffle

warning

Truffle and Ganache reached end of support in 2023 and were sunset in 2024. They receive no fixes and lag behind current Solidity and EVM versions. These notes cover maintaining existing projects — new work should start on Foundry or Hardhat. See Development tools.

Install

npm install -g truffle
npm uninstall -g truffle

# Older releases when a newer one fails on a given Node version
npm install -g [email protected]

Truffle was sensitive to the Node version. If installation or compilation fails on a current Node release, pin an older one with nvm rather than hunting for a Truffle version — Node 16 or 18 is what the late releases were built against.

Commands

truffle -v

truffle init # scaffold a project
truffle dev # console with a built-in dev blockchain
truffle console --network development # console against a running node
truffle compile
truffle migrate # deploy pending migrations
truffle migrate --reset # recompile and redeploy everything
truffle test

truffle dev starts its own chain, so state disappears when the console exits. truffle console --network development attaches to a separately running node, which is what you want when the front end also needs to reach it.

Deploying a range

truffle migrate -f N --to M

Migrations are numbered files, run in order:

1_initial_migration.js
2_deploy_AAA.js
3_deploy_BBB.js
truffle migrate -f 2 --to 3 # run only 2_deploy_AAA.js and 3_deploy_BBB.js

Truffle records the last completed migration on-chain in the Migrations contract, which is how it knows what is still pending. --reset ignores that record.

Network configuration

truffle-config.js. This example uses Infura, with the mnemonic held in an untracked file:

polygon_mainnet: {
provider: () => new HDWalletProvider({
mnemonic: { phrase: mnemonic },
providerOrUrl: "https://polygon-mainnet.infura.io/v3/" + infuraProjectId
}),
network_id: 137,
confirmations: 2,
timeoutBlocks: 200,
skipDryRun: true,
chainId: 137
},

polygon_amoy: {
provider: () => new HDWalletProvider({
mnemonic: { phrase: mnemonic },
providerOrUrl: "https://polygon-amoy.infura.io/v3/" + infuraProjectId
}),
network_id: 80002,
confirmations: 2,
timeoutBlocks: 200,
skipDryRun: true,
chainId: 80002
}

Points on this block:

  • Never commit a mnemonic. Load it from an untracked file or an environment variable, and use a wallet that holds only testnet funds. A mnemonic in a public repository is drained within minutes by bots that scan GitHub.
  • Mumbai (80001) is gone. Polygon shut it down in April 2024; the testnet is now Amoy, chain ID 80002. Any configuration still pointing at polygon-mumbai fails to connect.
  • skipDryRun: true skips the simulated run against a fork. It saves time on a testnet and removes a useful safety check on mainnet — leave it off there.
  • confirmations: 2 waits two blocks between deployment steps, which avoids nonce problems on chains with frequent reorganisations.
npm install --save-dev @truffle/hdwallet-provider

Deployer

module.exports = function (deployer, network, accounts) {
deployer.deploy(MyLibrary);
deployer.link(MyLibrary, MyContract);
deployer.deploy(MyContract);
};

link must come between deploying the library and deploying the contract that uses it — it substitutes the library's address into the contract's bytecode placeholder. Deploying the contract first produces a contract that reverts on every library call.

network and accounts let one migration behave differently per target:

module.exports = async function (deployer, network, accounts) {
const owner = network === "development" ? accounts[0] : process.env.OWNER;
await deployer.deploy(MyContract, owner);
};

Console recipes

// Accounts and balances
const accounts = await web3.eth.getAccounts()
const balance = await web3.eth.getBalance(accounts[0])
web3.utils.fromWei(balance, "ether")

// The instance from the most recent migration
const instance = await Faucet.deployed()

// Read a public state variable (the compiler generated a getter)
const funds = await instance.funds()
funds.toString() // BN, so convert before printing or comparing

// Attach to an already-deployed address
const instance = new web3.eth.Contract(Faucet.abi, "0x7e5FD5f569Fe8903baB6607d3b87d45DA591bf8A")
const funds = await instance.methods.funds().call()

// Send ether; the default unit is wei
web3.eth.sendTransaction({
from: accounts[0],
to: "0x6339248a77e255466A209EED18Fd39032C438611",
value: web3.utils.toWei("10", "ether")
})

// Call a payable function
await instance.addFunds({ value: web3.utils.toWei("2", "ether"), from: accounts[0] })

// Call by raw selector: addFunds() → keccak256 → first 4 bytes → 0xa26759cb
web3.eth.sendTransaction({
from: accounts[0],
to: "0xC138B35FA662D9601f6b12Efa7138157B57b1059",
data: "0xa26759cb",
value: web3.utils.toWei("3", "ether")
})

// Inspect chain state
await web3.eth.getBlock(9)
await instance.funders(0) // element of a public array
await web3.eth.getStorageAt(address, slot) // raw storage, ignores visibility

Prefer web3.utils.toWei("10", "ether") over writing "10000000000000000000" by hand. Counting eighteen zeros is exactly the kind of error that is invisible in review and expensive on mainnet.

Troubleshooting

ReferenceError: ContractName is not defined

The contract artefact has no deployment record for the network you are connected to. Check which network the console is on and whether the migration ran against it:

await web3.eth.net.getId()

A mismatched network ID between truffle-config.js and the node is the usual cause. See Chain ID and network ID.

Number can only safely store up to 53 bits

Token amounts in wei exceed JavaScript's safe integer range, so web3 returns BN objects. toNumber() throws as soon as the value is large enough to lose precision.

userPaid = registrantsPaid.toNumber() // throws for large values
userPaid = registrantsPaid // keep the BN
userPaid = registrantsPaid.toString() // or convert to a string

Do arithmetic with the BN methods (.add, .mul, .cmp), or use native BigInt, which is what ethers v6 and web3 v4 return.

web3.toWei is not a function

The helper moved into web3.utils in web3.js 1.0:

web3.toWei(1, "ether") // web3 0.x
web3.utils.toWei("1", "ether") // web3 1.x and later

Pass the amount as a string. A JavaScript number loses precision before the conversion ever happens.

ParserError: Source not found: File import callback not supported

The import path could not be resolved. Check the path is relative (./MyContract.sol) or resolves under node_modules, and that the file actually exists with that exact case — this bites on Linux and CI after working locally on Windows or macOS.

Migrations could not deploy due to insufficient funds

The deploying account has no balance, or not enough for the gas. Fund it from a faucet on a testnet, and verify the account being used is the one you funded:

(await web3.eth.getAccounts())[0]
await web3.eth.getBalance((await web3.eth.getAccounts())[0])

An HD wallet provider derives the account from the mnemonic, so it is easy to fund one address and deploy from another.