arctools for Arc

arc-lint

The differences that matter compile cleanly.

Arc is EVM-compatible, so existing contracts deploy unchanged. That is exactly the risk: the places where its semantics diverge produce no compiler warning and no local test failure. arc-lint catches them by reading source.

Using it

npm install --save-dev arc-evm-lint

npx arc-evm-lint contracts/ scripts/
npx arc-evm-lint --rules                         # list every rule
npx arc-evm-lint --foundry                       # read foundry.toml
npx arc-evm-lint --sarif --out arc-lint.sarif    # GitHub code scanning
npx arc-evm-lint --github                        # PR annotations

Published as arc-evm-lint: arc-lint on npm is Egor Galkin's linter, which covers the same Solidity hazards with solc AST analysis.

Bad.sol
  17:59   error   block.prevrandao is always 0 on Arc  arc/no-prevrandao
          │ uint256 seed = uint256(keccak256(abi.encodePacked(block.prevrandao, …)));
          → Arc has no beacon-chain RANDAO mix, so block.prevrandao is hardcoded
            to 0. Any contract deriving randomness from it is fully predictable.
            Use an oracle or VRF instead.
          https://docs.arc.io/arc/references/evm-differences

9 error · 3 warning across 2 of 3 file(s)

Exit code is 1 on any error-severity finding, so it drops into CI as-is. Use --fail-on warning to tighten that, or --fail-on never to report without blocking.

The rules

Each maps to a documented protocol-level divergence, and carries the doc reference that justifies it — so a finding can always be traced back to the spec.

RuleSeverityCatches
arc/decimals-mixerrorAn 18-decimal literal in a file that also uses the 6-decimal ERC-20 USDC interface
arc/no-prevrandaoerrorblock.prevrandao / block.difficulty, both hardcoded to 0
arc/no-assembly-prevrandaoerrorThe same read from inline assembly
arc/no-blob-opcodeserrorblobhash, blobbasefee — blob transactions are rejected
arc/no-beacon-rootserrorEIP-4788 beacon roots; the contract is not deployed on Arc
arc/burn-to-zero-addresserrorNative value sent to address(0), which reverts
arc/gas-fee-floorerrormaxFeePerGas below Arc's 20 Gwei floor
arc/wrong-usdc-decimalserrorparseEther / parseUnits(x, 18) on a 6-decimal ERC-20 call
arc/selfdestruct-value-ruleswarningSELFDESTRUCT under Arc's extra native-value rules
arc/balanceof-zero-is-not-emptywarningbalanceOf(x) == 0, which truncation makes unreliable
arc/ether-unit-is-usdcwarningThe ether unit, which denominates USDC here
arc/hardcoded-eth-rpcwarningLocalhost RPCs that cannot reproduce Arc semantics
arc/unnecessary-weth-wrapperinfoA wrap/unwrap layer Arc does not need
arc/prefer-multicall3frominfoMulticall3, where Arc's Multicall3From preserves msg.sender

It runs where you already work

Foundry

Foundry has no plugin API, so integration means being a well-behaved CLI: read the same config forge reads, lint exactly what it compiles, and skip the dependency tree in libs.

npx arc-evm-lint --foundry

--foundry reads src, script, test, and libs from foundry.toml, honouring FOUNDRY_PROFILE.

Hardhat 3

// hardhat.config.ts
import arcLint from "hardhat-arc-lint";

export default { plugins: [arcLint] };
npx hardhat arc-lint
npx hardhat arc-lint --format sarif --out arc-lint.sarif
npx hardhat arc-lint --fail-on warning

The task reads Hardhat's own config.paths rather than guessing directory names. Hardhat 3's paths has no entry for deploy scripts — and the script-language rules fire almost exclusively there — so scripts/, ignition/modules/, and deploy/ are added when they exist, and the task says when it did that.

A lint failure throws HardhatPluginError, so it prints as a plugin result rather than as a Hardhat crash with a stack trace and a bug-report link.

GitHub Actions

- uses: ilkermanap/arctools/packages/arc-lint@main
  with:
    foundry: "true"
    format: sarif        # or github (default) / text
    fail-on: error

format: github annotates the pull request diff inline and writes a step summary grouped by rule. format: sarif emits a SARIF 2.1.0 report for github/codeql-action/upload-sarif, so findings land in the Security tab as code scanning alerts. Step outputs expose errors, warnings, findings, and sarif-file.

Suppression

// arc-lint-disable-next-line arc/decimals-mix
uint256 amount = 1e18;

Omit the rule id to mute every rule on that line, or use arc-lint-disable-line for the current one. Whole paths go in .arclintignore; a pattern containing * is anchored to the full path, one without matches as a substring.

How it works, and what it cannot do

Comments and string literals are blanked in place — preserving byte offsets, so line and column stay exact — and rules match against that. Solidity rules see strings blanked, which keeps revert messages from tripping them. Script rules see strings intact, because the values they check (parseGwei("5"), RPC URLs) live inside strings. String literals are always parsed even when kept, so a URL containing // is never mistaken for a comment.

Regex, not AST — and that has a cost

Rules match against comment-stripped source rather than a compiled AST. That keeps arc-lint compiler-free and instant, but it is coarse: arc/decimals-mix flags every 1e18 in a file that touches IERC20, instead of following one value across assignments. AST rules via solc --standard-json are the next step.

Tests

33 tests, fully offline. Bad.sol is expected to trip ten rules exactly once each and Good.sol must stay clean; the rest pin the SARIF and annotation encodings and the foundry.toml reader.