Debugging smart contracts
=nil; comes equipped with the Cometa service which is a tool for storing contract metadata and analyzing messages. If a message fails due to a bug in contract logic, Cometa can pinpoint the exact line of Solidity code where the issue that caused the failure has occurred.
Working with the Cometa service involves these steps:
- Creating a JSON file with the description of the compilation task
- Compiling the contract and deploying it
- Registering the contract with the Cometa service
- Using the =nil; developer tools to investigate any failed messages to the contract
Whenever a contract is deployed via the =nil; Playground, it is automatically registered inside the Cometa service.
Draft an example contract
To illustrate how debugging works, this tutorial uses the following contract:
pragma solidity ^0.8.0;
contract CounterBug {
uint256 private value;
event ValueChanged(uint256 newValue);
function increment() public {
require(msg.sender == address(0));
value += 1;
emit ValueChanged(value);
}
function getValue() public view returns (uint256) {
return value;
}
}
Inside the increment()
function, the contract has a require()
statement with a condition that will never evaluate to true
unless the contract is called from the zero address. This is done to deliberately trigger an ExecutionReverted
error.
Create a file with the compilation task
As input, Cometa takes a JSON file storing a compilation task. This task includes the compiler version, settings, and the contract files to be compiled.
See the below example on how this file should be structured:
{
"contractName": "CounterBug.sol:CounterBug",
"compilerVersion": "0.8.28",
"settings": {
"evmVersion": "shanghai",
"optimizer": {
"enabled": false,
"runs": 200
}
},
"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
}
}
}
Make sure that the compiler version in the input file is compatible with the specified EVM target.
Note that the "sources"
key must contain all .sol
files used during contract compilation including any imported contracts.
For locally imported contracts:
"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
},
"Nil.sol": {
"urls": ["path/to/Nil.sol"]
}
}
For contracts imported from packages:
"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
},
"@nilfoundation/smart-contracts/Nil.sol": {
"urls": ["path/to/Nil.sol"]
}
}
Compile the contract, deploy it, and register it
Via the =nil; CLI
To compile the contract, deploy it and register it inside Cometa:
nil wallet deploy --compile-input path/to/counter.json --salt SALT
Alternatively, compile the contract:
solc -o path/to/CounterBug --bin --abi path/to/CounterBug.sol --overwrite --no-cbor-metadata --metadata-hash none"
Deploy the contract separately:
nil wallet deploy path/to/CounterBug.bin --abi path/to/CounterBug.abi --shard-id 2 --salt SALT
Register the contract with the Cometa service:
nil cometa register --address COUNTER_BUG_ADDRESS_SEPARATE --compile-input path/to/counter.json
Via Nil.js
To compile the contract, deploy it and register it inside Cometa:
import {
CometaService,
convertEthToWei,
Faucet,
generateRandomPrivateKey,
HttpTransport,
LocalECDSAKeySigner,
PublicClient,
waitTillCompleted,
WalletV1,
} from "@nilfoundation/niljs";
const cometa = new CometaService({
transport: new HttpTransport({
endpoint: COMETA_ENDPOINT,
}),
});
const client = new PublicClient({
transport: new HttpTransport({
endpoint: RPC_ENDPOINT,
}),
shardId: 1,
});
const faucet = new Faucet(client);
const signer = new LocalECDSAKeySigner({
privateKey: generateRandomPrivateKey(),
});
const pubkey = await signer.getPublicKey();
const wallet = new WalletV1({
pubkey: pubkey,
salt: BigInt(Math.floor(Math.random() * 10000)),
shardId: 1,
client,
signer,
});
const walletAddress = wallet.address;
await faucet.withdrawToWithRetry(walletAddress, convertEthToWei(1));
await wallet.selfDeploy(true);
const counterBugJson = `{
"contractName": "CounterBug.sol:CounterBug",
"compilerVersion": "0.8.28",
"settings": {
"evmVersion": "shanghai",
"optimizer": {
"enabled": false,
"runs": 200
}
},
"sources": {
"CounterBug.sol": {
"urls": ["./CounterBug.sol"]
}
}
}`;
const compilationResult = await cometa.compileContract(counterBugJson);
const { address, hash } = await wallet.deployContract({
bytecode: compilationResult.code,
abi: compilationResult.abi as unknown as Abi,
args: [],
salt: BigInt(Math.floor(Math.random() * 10000)),
feeCredit: 500_000n,
shardId: 1,
});
const receipts = await waitTillCompleted(client, hash);
if (receipts.some((receipt) => !receipt.success)) {
throw new Error("Contract deployment failed");
}
await cometa.registerContractData(compilationResult, address);
const incrementHash = await wallet.sendMessage({
to: address,
functionName: "increment",
abi: compilationResult.abi as unknown as Abi,
feeCredit: 300_000n,
});
await waitTillCompleted(client, incrementHash);
Via the Hardhat plugin
Set the Cometa endpoint inside the project .env
file:
COMETA_ENDPOINT=COMETA_ENDPOINT
Create a new Ignition module:
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
module.exports = buildModule("CounterBugModule", (m: any) => {
const counterBug = m.contract("CounterBug");
return { counterBug };
});
Deploy the contract and register it inside Cometa:
npx hardhat ignition deploy ./ignition/modules/CounterBug.ts
npx hardhat cometa register --contract CounterBug --address ADDRESS
Create a new task for calling the contract:
import { task } from "hardhat/config";
task("increment", "Increment the CounterBug")
.addParam("address", "The address of the CounterBug contract")
.setAction(async (taskArgs, hre) => {
const ContractBug = await hre.ethers.getContractFactory("ContractBug");
const contractBug= ContractBug.attach(taskArgs.address);
const setterTx = await contractBug.increment();
await setterTx.wait(0);
});
Execute the task:
npx hardhat increment --address ADDRESS
Investigate failed messages to the contract
Via the =nil; CLI
To send a message to the increment()
function of the contract:
nil wallet send-message COUNTER_BUG_ADDRESS increment --abi path/to/CounterBug.abi
The command will produce the hash of the failed message to the contract. To investigate this message:
nil debug MESSAGE_HASH
The output of the command should contain the entire message chain as well as the exact line where execution was reverted:
require(msg.sender == address(0));
Via Nil.js
Nil.js
currently does not support the debug API for Cometa.
Via the Hardhat plugin
The Hardhat plugin currently does not support the debug API for Cometa.