agentleFS
OpenZeppelinOpenZeppelin/openzeppelin-contracts-upgradeable/.claude/skills/testing/SKILL.md

testing

Skill1.2k starsChanged 24 days agoMITView on GitHub

Testing conventions for openzeppelin-contracts. Use when writing or modifying tests, mocks, or formal verification specs. Covers hardhat-exposed $-wrappers, when manual mocks are warranted, Hardhat+Chai patterns (loadFixture, shouldBehaveLike, multi-target loops), Foundry fuzz, Halmos symbolic execution, Certora rule-based verification, and the changeset rule.

194 other projects use a version of this.

Trending

More talk lately.about the whole repository

Dec 2025: 0 mentionsJan 2026: 0 mentionsFeb 2026: 0 mentionsMar 2026: 2 mentionsApr 2026: 2 mentionsMay 2026: 0 mentionsJun 2026: 3 mentionsJul 2026: 1 mentionAug 2026: 0 mentionsSep 2026: 2 mentionsOct 2026: 4 mentionsNov 2026: 6 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the OpenZeppelin/openzeppelin-contracts-upgradeable repository that our weekly crawl found. There is no adoption data for this file yet.

Most recently taken up by

Who's talking about OpenZeppelin/openzeppelin-contracts-upgradeable

The biggest 5 of 51 mentions linking to the repository that our crawl found. Each line is their own headline.

See all 51

Talked about on GitHub and Stack Overflow (51 times).

On GitHub · 50

45 more on GitHub

Used with

Projects with it also use bundle-dependency-prs, update-library-template and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • MicrosoftSkill

    bundle-dependency-prs

    Fix broken dependency update PRs and aggregate the ones that work into one PR.

    Also in 7 projects that use this

  • MicrosoftSkill

    update-library-template

    Merges the latest Library.Template into this repo (at position of HEAD) and resolves conflicts.

    Also in 7 projects that use this

  • zetaloopSkill

    assign-copilot

    Assigns a GitHub issue to the Copilot coding agent, optionally specifying a custom agent. Use this when asked to assign an issue to Copilot or delegate an issue to CCA.

    Also in 3 projects that use this

  • NVIDIA-NeMoSkill

    build-and-dependency

    Dev environment setup for Megatron Bridge — container-based development, uv package management, lockfile regeneration, adding dependencies, Slurm container usage, and common build pitfalls.

    Also in 3 projects that use this

  • timmo001Skill

    changeset-scope

    Keep all scoped code work contained to the user-defined changeset. Use for implementation, fixes, diagnosis, refactoring, cleanup, and review when explicit instructions, named files, diffs, branches, pull requests, or injected work scopes define the boundary.

    Also in 3 projects that use this

  • JetBrainsSkill

    module-dependencies

    Add or modify IntelliJ module dependencies in `.iml` files.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

6 rules. 0 are common, 3 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 173 edited versions we read keep these.

  • 3

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Default to Hardhat+Chai unit tests, add Halmos for invariants you can express symbolically, escalate to Certora when state-machine coverage …

    line 17
  2. Very specific:

    Fixtures: use loadFixture from @nomicfoundation/hardhat-network-helpers.

    line 56
  3. Very specific:

    Multi-target loops: when the same suite should run against several wrappers (e.g.

    line 81
  4. Very specific:

    Use assertEq, assertGt, etc.

    line 113
  5. Distinctive:

    Use Certora when a property is too complex for Halmos, or when a state machine needs deeper rule coverage than fuzzing provides.

    line 140
  6. Distinctive:

    Every PR that changes contract behavior needs a changeset.

    line 144

“Common” is measured: kept by at least 6 in 10 of the 173 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Testing

When to use each approach

ApproachRequired when
Formal verification (Certora)Contract includes a state machine or access-control rules complex enough that fuzzing won't cover the cases
Symbolic execution (Halmos)A property is expressible as a Foundry test with symbolic inputs — fast, no external credentials
Fuzzing (Foundry)Math-heavy code, data structures, complex invariants not suited for FV
Edge unit cases (Hardhat)All contracts: golden path + pathological/border cases. This is the baseline.

Default to Hardhat+Chai unit tests, add Halmos for invariants you can express symbolically, escalate to Certora when state-machine coverage requires it.

A bug fix must add a minimal test that reproduces the bug, committed alongside the fix. The objective is 100% branch coverage per PR.

Accessing internal functions: hardhat-exposed

$-prefixed wrapper contracts are auto-generated by hardhat-exposed. The plugin creates a $ContractName for every contract in contracts/, with every internal _function re-exposed as an external $_function. Output goes to contracts-exposed/ (gitignored). Use them directly:

const token = await ethers.deployContract('$ERC20', ['Name', 'SYM']);
await token.$_mint(holder, 1000n);

Do not write $-prefixed wrappers by hand.

Manual mocks in contracts/mocks/

Manual mocks live in contracts/mocks/ (not test/) and mirror the contracts/ directory layout. Write a manual mock only when hardhat-exposed cannot give you what you need:

  • Constructor-level setup that auto-generation can't provide — e.g. ERC721ConsecutiveMock calling _mintConsecutive during construction.
  • Multi-extension composition to test how extensions compose — e.g. ERC721Consecutive + ERC721Pausable + ERC721Votes.
  • Injected behavior that overrides a hook to simulate an edge case — e.g. ERC20Reentrant firing a reentrant call inside _update.
  • Adversarial behavior with no analogue in the base — e.g. ReentrancyAttack, ERC20ReturnFalseMock.
  • A plain public wrapper for an internal operation where the auto-generated signature is unsuitable — e.g. ERC20Mock.mint.

Even inside a manual mock, prefer the auto-generated $_ functions over redeclaring the same exposure:

// prefer (reuses auto-generated exposure):
await token.$_mint(holder, 1000n);

// avoid (redundant — hardhat-exposed already provides this):
contract ERC20Mock is ERC20 {
    function __mint(address account, uint256 value) external { _mint(account, value); }
}

Hardhat + Chai (.test.js)

Fixtures: use loadFixture from @nomicfoundation/hardhat-network-helpers. Declare the fixture with a named function, not an inline arrow — loadFixture keys its cache on the function reference, so an anonymous function (especially an inline loadFixture(async () => {...})) re-runs on every test instead of restoring the snapshot.

async function fixture() {
  const [holder, recipient] = await ethers.getSigners();
  const token = await ethers.deployContract('$ERC20', [name, symbol]);
  await token.$_mint(holder, initialSupply);
  return { holder, recipient, token };
}

beforeEach(async function () {
  Object.assign(this, await loadFixture(fixture));
});

Structure: describe('ContractName') > describe('methodName') > it('description').

Shared behavior: reuse via shouldBehaveLike* functions in sibling .behavior.js files. Mount with this-context state set in the fixture:

const { shouldBehaveLikeERC20 } = require('./ERC20.behavior');
// ...
shouldBehaveLikeERC20(initialSupply, { forcedApproval });

Multi-target loops: when the same suite should run against several wrappers (e.g. the auto-generated $ERC20 and a manual $ERC20ApprovalMock), iterate at the top:

const TOKENS = [{ Token: '$ERC20' }, { Token: '$ERC20ApprovalMock', forcedApproval: true }];
for (const { Token, forcedApproval } of TOKENS) {
  describe(Token, function () {
    /* … */
  });
}

Assertion style:

  • bigint literals (100n) for uint values — not ethers.BigNumber.
  • Custom errors: .revertedWithCustomError(contract, 'ErrorName').withArgs(...).
  • Panics: .revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW).
  • Events: .emit(contract, 'EventName').withArgs(...).
  • Balance changes: .changeTokenBalance(token, account, delta) / .changeTokenBalances(token, [a, b], [d1, d2]).

Foundry (.t.sol)

Foundry tests live next to their Hardhat counterparts in test/. Under HH3 they are picked up by npm test (via the @nomicfoundation/hardhat-mocha solidity runner) alongside the JS tests. Standalone forge test still works but uses forge defaults for fuzz. Two distinct purposes:

Fuzzing: function name starts with test, parameters are typed. Foundry generates random inputs.

function testFuzzAdd(uint256 a, uint256 b) public pure {
  vm.assume(a < type(uint256).max - b); // restrict if needed
  assertEq(Math.add(a, b), a + b);
}

Use assertEq, assertGt, etc. from forge-std/Test.sol. Fuzz settings live in hardhat.config.ts under test.solidity.fuzz (runs = 5000, maxTestRejects = 150000) and are applied when running via npm test. Standalone forge test uses forge defaults.

Symbolic execution (Halmos): function name starts with symbolic or testSymbolic — that's what the CI halmos job matches (--match-test '^symbolic|^testSymbolic'). These run as regular Foundry fuzz tests as well.

function testSymbolicTransfer(address to, uint256 amount) public {
  // Halmos treats inputs as fully symbolic (all values).
  // Use vm.assume(...) for preconditions.
}

Halmos runs on every PR, needs no credentials. Reach for it when a property can be expressed as a Foundry test.

Pragma note: Foundry compiles with solc 0.8.31 (foundry.toml); contract pragmas in contracts/ may still be ^0.8.20. New .t.sol files should match the contract's pragma minimum unless they specifically need a newer feature.

Certora (rule-based formal verification)

Certora specs live in fv/specs/ as .conf + .spec pairs. CI runs one job per affected config, on any PR whose diff reaches a spec, a harness, a contract one of them verifies, or an fv/diff patch applied to one -- no label needed. Label a PR formal-verification-force-all to run every config instead. Requires a CERTORAKEY, and the certora environment gates the job behind a manual approval. Selection runs the base branch's copy of scripts/list-dependencies.js over the PR's files, so a PR cannot change which configs it is checked against. The matrix job names carry their config, so verified is the aggregate job to require in branch protection. Deleting a .conf fails selection, since a config that no longer exists cannot be selected and its verification would just disappear; label the PR formal-verification-allow-removal when that is intended.

Local workflow:

make -C fv apply           # apply harness patches first
node fv/run.js AccessControl   # runs fv/specs/AccessControl.conf
node fv/run.js --all           # runs every spec

Use Certora when a property is too complex for Halmos, or when a state machine needs deeper rule coverage than fuzzing provides.

Changesets

Every PR that changes contract behavior needs a changeset. Skip for NatSpec-only edits, internal refactors with no user-visible effect, or pure repo plumbing.

npx changeset add

The generated file in .changeset/ looks like:

---
'openzeppelin-solidity': minor
---

`ComponentName`: One-sentence description starting with a backtick-quoted contract or component name.
  • patch — bug fixes.
  • minor — new features.
  • One sentence. No bullet points, no multi-paragraph prose. The release pipeline composes these into CHANGELOG.md automatically.

CI matrix you should keep in mind

JobWhat it runsTriggered by
lintnpm run lintEvery push/PR
testsnpm test (JS + Foundry via HH3) + inheritance, pragma, generationEvery push/PR
tests-upgradeableTranspile then re-run the test suite + storage-layout diffEvery push/PR
coveragenpm run coverage → codecovEvery push/PR
slitherSlither static analysis for common vulnerabilitiesEvery push/PR
halmoshalmos --match-test '^symbolic|^testSymbolic'Every push/PR
formal verificationCertora, one job per affected config (or all, with the force label)PRs touching a spec, harness, patch or verified contract

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.