Debugging smart contracts
=nil; comes equipped with the Cometa service which is a tool for storing contract metadata and analyzing transactions. If a transaction 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 transactions 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 smart-account 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 smart-account 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,
HttpTransport,
PublicClient,
generateSmartAccount,
waitTillCompleted,
} 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 smartAccount = await generateSmartAccount({
shardId: 1,
rpcEndpoint: RPC_ENDPOINT,
faucetEndpoint: FAUCET_ENDPOINT,
});
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 smartAccount.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 smartAccount.sendTransaction({
to: address,
functionName: "increment",
abi: compilationResult.abi as unknown as Abi,
feeCredit: 300_000n,
});
await waitTillCompleted(client, incrementHash);
Investigate failed transactions to the contract
Via the =nil; CLI
To send a transaction to the increment()
function of the contract:
nil smart-account send-transaction COUNTER_BUG_ADDRESS increment --abi path/to/CounterBug.abi
The command will produce the hash of the failed transaction to the contract. To investigate this transaction:
nil debug TRANSACTION_HASH
The output of the command should contain the entire transaction 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.