Skip to content

Instantly share code, notes, and snippets.

@Theo6890
Last active March 6, 2026 20:08
Show Gist options
  • Select an option

  • Save Theo6890/a2b3469817d8175b8ad9538e79e35aa2 to your computer and use it in GitHub Desktop.

Select an option

Save Theo6890/a2b3469817d8175b8ad9538e79e35aa2 to your computer and use it in GitHub Desktop.
Best practices to follow by AI agents in solidity - based on Foundry and Secureum best practices

Copilot Instructions — Org-Wide Conventions

This is the routing index for all instruction modules. It is automatically loaded as Tier 1 context. Detailed rules live in the module files under .github/instructions/.


Instruction Modules

File Scope Rule IDs Primary Consumers
instructions/solidity-style.md Naming, formatting, NatSpec, NatSpec tag grouping, struct field documentation, ordering, visibility, error handling, service boundaries STYLE-01STYLE-13 Contract agents, refactoring agents
instructions/testing.md Test patterns, setup, mocks, fuzz, fork, assertions TEST-01TEST-14 Test agents, fuzz agents
instructions/git-workflow.md Commits, branches, PRs, formatting hooks GIT-01GIT-04 All agents that commit/push
instructions/architecture.md Design principles: composition, state machines, accounting, auth ARCH-01ARCH-07 Master agents, architecture agents
instructions/agent-delivery.md Agent planning, delegation, compliance, context loading AGENT-01AGENT-07 Master agents, orchestration agents

Documentation Tiers

Tier File Purpose
1 This file + instructions/*.md Org-wide conventions (style, testing, git, architecture, delivery)
2 docs/v1/PLAN.md (if exists) Project-wide architecture, design decisions, glossary
3 docs/v1/ITERATION_N_PLAN.md (if exists) Scope, tasks, acceptance criteria for current iteration

Load context in tier order. Never duplicate higher-tier content into lower tiers — reference by filename.

Tier 2 and Tier 3 files may not exist in newly initialised repos or repos that were not bootstrapped by AI agents. When absent, rely solely on Tier 1 conventions and the codebase itself for context. Do not create these files unless explicitly requested.

Context Loading Quick Reference

Task Load These Modules
Planning / orchestration architecture.md + agent-delivery.md + git-workflow.md
Writing Solidity contracts solidity-style.md + architecture.md
Writing tests testing.md + solidity-style.md
Committing / branching git-workflow.md
Full review / audit All modules

Rule Severity Tags

All rules use severity tags. See AGENT-07 in agent-delivery.md for definitions.

Tag Meaning
[MUST] Correctness/security. Never skip.
[SHOULD] Strong convention. Deviate only with justification.
[MAY] Preference. Either choice acceptable.

Universal Rules

These apply to all agents regardless of task:

  • All Solidity files must pass forge fmt --check before commit.
  • All non-Solidity files must pass npx prettier --check . before commit.
  • Every commit must compile (forge build) and pass tests (forge test).

Agent Delivery Workflow

Consumers: Master agents, orchestration agents. Load when: Planning iterations, decomposing features into PRs, delegating tasks to subagents, or validating delivery compliance.


AGENT-01 · Documentation Tier System [MUST]

This org uses a three-tier documentation hierarchy. Agents must load context in this order:

Tier File Purpose Loaded By
1 .github/copilot-instructions.md Routing index to all instruction modules All agents (auto-loaded)
1 .github/instructions/*.md Org-wide conventions (style, testing, git, architecture) Per-task (see AGENT-03)
2 docs/v1/PLAN.md (if exists) Project-wide architecture, design decisions, glossary Master agents, architecture subagents
3 docs/v1/ITERATION_N_PLAN.md (if exists) Scope, tasks, acceptance criteria for current iteration Master agents, executing subagents

Rules:

  • Never duplicate content from a higher tier into a lower tier — reference by filename.
  • Completed iteration files are historical context; only the active iteration file is the operative execution plan.
  • If a convention conflicts with a project-specific decision in PLAN.md, the project-specific decision takes precedence within that project.
  • Tier 2/3 files may not exist in newly initialised repos or repos not bootstrapped by AI agents. When absent, rely solely on Tier 1 conventions and the codebase itself for context. Do not create these files unless explicitly requested.

AGENT-02 · PR Decomposition [MUST]

Plan implementation as dependency-ordered, atomic pull requests where each PR compiles and tests independently.

  • Follow the build order in ARCH-01 (architecture.md) for sequencing.
  • Each PR should contain one logical unit of work (e.g., "add types + storage + interfaces", "add core module X", "add tests for module X").
  • Shared dependencies (types, storage, interfaces, test setup) must be merged before PRs that consume them.
  • Independent PRs (e.g., separate test files for separate modules) may be parallelized only after shared dependencies are merged.

AGENT-03 · Context Loading per Task Type [SHOULD]

Load only the instruction files relevant to the task. This minimizes context-window waste and improves agent focus.

Task Type Instruction Files to Load
Planning / orchestration architecture.md + agent-delivery.md + git-workflow.md
Writing Solidity contracts solidity-style.md + architecture.md
Writing tests testing.md + solidity-style.md
Committing / branching git-workflow.md
Full review / audit All instruction files

When delegating to a subagent, include only the relevant files in the subagent's prompt context.

AGENT-04 · Commit Discipline [MUST]

  • One logical change per commit (see GIT-02 in git-workflow.md).
  • Align commit scope with touched modules — a commit that modifies MarketBetting.sol should not also modify MarketAdmin.sol unless the change is inherently cross-cutting.
  • Every commit must compile (forge build) and pass tests (forge test).
  • Run forge fmt and npx prettier --check . before each commit.

AGENT-05 · Convention Compliance Validation [SHOULD]

Before finalizing any PR, verify:

  1. Naming: Contracts, functions, variables, errors, events follow solidity-style.md rules.
  2. Ordering: Function/declaration ordering per STYLE-12.
  3. NatSpec: Contract-level + function-level documentation per STYLE-10.
  4. Tests: Naming, setup, section organization per testing.md.
  5. Formatting: forge fmt --check passes. npx prettier --check . passes.
  6. Commits: Each commit message follows GIT-01. Each commit is atomic per GIT-02.
  7. Storage slots: Any new ERC-7201 storage library has a corresponding TEST-14 verification test.

AGENT-06 · Handling Ambiguity [SHOULD]

When conventions are unclear or conflicting:

  1. Check whether the project's PLAN.md documents a project-specific decision that resolves the ambiguity (skip if the file does not exist).
  2. If not (or no PLAN.md exists), prefer the stricter interpretation (e.g., private over internal, external over public).
  3. If genuine ambiguity remains, ask one narrow question and proceed with minimal assumptions.
  4. Never invent conventions — if a pattern is not documented, flag the gap rather than guessing.

AGENT-07 · Rule Severity Guide [MUST]

All rules across instruction files are tagged with severity:

Tag Meaning Agent Behavior
[MUST] Correctness, security, or hard constraint. Violation causes bugs or blocks review. Always comply. Never skip or defer.
[SHOULD] Strong convention. Deviation requires explicit justification. Comply by default. Deviate only with stated reason.
[MAY] Preference or stylistic option. Either choice is acceptable. Follow when practical. No justification needed to deviate.

Architecture & Design Principles

Consumers: Master agents, planning agents, architecture-review agents. Load when: Planning new systems, designing modules, reviewing architectural decisions, or making any design choice that affects state, storage, accounting, or authorization.

These are durable design principles that govern all implementation decisions across iterations. They are not project-specific — they apply to any Solidity system built under this org's standards.


ARCH-01 · Build Order for New Systems [MUST]

When implementing a new protocol or module family, follow this dependency order:

  1. Shared domain types + centralized errors + storage layout + interfaces
  2. Minimal reusable primitives (e.g., token/adapter/mock implementations)
  3. Core modules grouped by responsibility
  4. Main composition/orchestrator contract
  5. Test setup + mocks
  6. Feature tests (lifecycle, settlement, claims, access)
  7. Fuzz/invariant tests
  8. Deployment script

This sequencing minimizes refactors and keeps each step independently reviewable.

ARCH-02 · Modular Composition [MUST]

  • Decompose behavior into focused modules (admin, lifecycle, execution, claims, view) and compose them in a thin orchestrator.
  • Keep shared cross-module logic in one base module/library to avoid drift and duplicate validation.
  • Interfaces are the canonical location for events and externally visible errors.
  • Storage libraries (namespaced via ERC-7201) are the single source of truth for shared state access.

ARCH-03 · State Machine Design [MUST]

  • Separate stored state (persisted lifecycle checkpoints) from effective state (runtime-derived from stored state + block.timestamp).
  • Compute time-dependent transitions at read/validation time — do not require maintenance transactions to advance state.
  • Gate each mutative function by the strict state required for that action.
  • For optional delay/grace mechanisms, support both paths explicitly:
    • Zero-delay path: single-step transition to final state.
    • Delayed path: pending state + explicit finalization step.

ARCH-04 · Accounting & Parameter Safety [MUST]

  • Use a single canonical internal unit for accounting; convert only at external transfer boundaries.
  • Snapshot mutable config (fees, rates, signer-dependent params) at irreversible state transitions — later admin updates must not retroactively affect settled outcomes.
  • Persist original aggregate accounting values when derivative balances may decrease over time (e.g., burn-on-claim). Do not derive final accounting from mutable balances.
  • Define deterministic fallback behavior for terminal edge cases (e.g., no valid winner → cancel/refund path).

ARCH-05 · Off-chain Authorization & Finality [MUST]

  • For signed reports/actions, require EIP-712 typed structured data with domain separation, nonce, and expiry deadline.
  • Reject stale signatures and enforce monotonic/non-reusable nonces per context.
  • If corrections are allowed, confine them to an explicit pending window and reset pending timers/versions on update.

ARCH-06 · Test Strategy for Lifecycle Systems [MUST]

  • Use setup contracts with deterministic actors and helper functions for repeatable scenario construction.
  • Validate full lifecycle transitions (including timestamp-driven) without relying on implicit/manual steps.
  • Cover both revert and happy-path cases for every mutative function.
  • Add invariant/fuzz tests for conservation properties (e.g., payouts + fees never exceed pool; refunds match pool).
  • Include storage-slot correctness tests for all namespaced storage libraries.

ARCH-07 · Deployment & Environment [SHOULD]

Deploy scripts use run(string memory network, string memory environment) as the entry point. See STYLE-04 in solidity-style.md for the function signature convention. Further deployment conventions are pending documentation.

Git Workflow

Consumers: All agents that commit or push. Master agents for PR planning. Load when: Creating commits, branches, or pull requests.


GIT-01 · Commit Message Format [MUST]

All commits follow Conventional Commits enforced by commitlint.

type(scope): concise imperative description
Element Rule
type Required. One of the types below, lowercase.
scope Optional. Module or area affected, e.g. market, staking, ci.
Description Imperative mood ("add X", not "added X"), no trailing period, max ~72 characters.
Body Optional. Separated by blank line. Explains why, not what.
Breaking Append ! after type/scope: refactor(market)!: remove deprecated hook.

Allowed types:

Type Purpose
feat New feature or capability
fix Bug fix
refactor Code restructuring, no behavior change
test Adding or updating tests
style Formatting, linting, whitespace — no logic change
chore Maintenance tasks (deps, configs, tooling)
ci CI/CD pipeline changes
docs Documentation only
perf Performance improvement
build Build system or external dependency changes

GIT-02 · Atomic Commits [MUST]

Each commit contains exactly one logical change matching its message.

Rule Violation Example
No unrelated changes A feat commit that also fixes formatting in another file
No bundling across types Tests + refactor + feature in a single commit
Commit must compile and tests pass A refactor commit that breaks compilation, fixed in the next one
Scope matches message Message says "add whitelist" but commit also modifies staking logic

If a feature requires a preparatory refactor, that refactor is a separate commit (refactor: ...) before the feature (feat: ...).

GIT-03 · Branching & Pull Requests [MUST]

Rule Details
One branch per work item Each feature, fix, or refactor gets its own branch
Branch naming type/short-kebab-description — e.g. feat/whitelist, fix/double-claim
Branch from main Always branch off the latest main
Multiple focused commits per branch A branch contains several atomic commits forming a reviewable unit
PR for review Every branch merged via PR — no direct pushes to main
Each commit independently reviewable Reviewers can step through commits one by one

GIT-04 · Formatting & Pre-commit Hooks [MUST]

Husky pre-commit hooks enforce formatting on every commit.

Pre-commit checks (.husky/pre-commit):

forge fmt --check
npx prettier --check .

Commit message lint (.husky/commit-msg):

npx --no -- commitlint --edit "$1"
Tool Scope Config Fix command
forge fmt Solidity (*.sol) foundry.toml [fmt] forge fmt
prettier Non-Solidity (JS, JSON…) .prettierrc npx prettier --write .
commitlint Commit messages commitlint.config.js Amend the message

Rules:

  • Never commit with --no-verify.
  • If forge fmt --check or prettier --check fails, fix and include formatting in the same commit — do not create a separate style: commit for code you are already touching.
  • style: commits are reserved for standalone formatting sweeps on unrelated files.
  • Run both checks locally before opening a PR.

Solidity Style Guide

Consumers: Contract-writing agents, refactoring agents, review agents. Load when: Writing, editing, or reviewing any .sol production file.


STYLE-01 · Contract Naming [MUST]

Category Convention Examples
Main contracts PascalCase, domain-prefixed MyContract, MyContractFactory
Modules PascalCase, functional-role suffix MyAdmin, MyLifecycle, MyView
Abstract contracts No special prefix/suffix abstract contract MyLifecycle
Interfaces I prefix + PascalCase IMyContract, IMyAdmin
Libraries PascalCase matching purpose MyContractStorage, StateChecker
Errors library Service-prefixed PascalCase MyContractErrors, VaultErrors

STYLE-02 · File Naming [MUST]

Type Pattern Examples
Contracts ContractName.sol MyContract.sol
Tests ContractName.t.sol MyContract.t.sol
Sub-feature tests ContractName.feature.t.sol MyContract.claims.t.sol
Deploy scripts N_ContractName.deploy.s.sol (numbered) 1_MyContract.deploy.s.sol
Upgrade scripts ContractName.upgrade.s.sol MyContract.upgrade.s.sol
Storage libs ContractNameStorage.sol MyContractStorage.sol
Types ContractNameTypes.sol MyContractTypes.sol
Errors ContractNameErrors.sol MyContractErrors.sol
Mocks ContractName_Mock.sol MyContract_Mock.sol
Test setup ContractName.setup.sol MyContract.setup.sol
Storage tests ContractName.storages.t.sol MyContract.storages.t.sol
Fuzzing No .t.sol suffix Fuzz.sol, FuzzSetup.sol

STYLE-03 · Import Ordering [SHOULD]

Separated by blank lines, in this order:

  1. External dependencies — grouped by publisher/package, alphabetical within each group
  2. Custom interfaces — project-local I-prefixed interfaces
  3. Custom contracts — project-local contracts and abstract contracts
  4. Custom libraries / types / errors — storage libraries, struct definitions, error libraries
// 1. External dependencies (grouped by publisher)
import {Math} from "oz/utils/math/Math.sol";
import {SafeERC20} from "oz/token/ERC20/utils/SafeERC20.sol";

// 2. Custom interfaces
import {IMyContract} from "./interfaces/IMyContract.sol";

// 3. Custom contracts (including abstract)
import {MyBase} from "./MyBase.sol";

// 4. Custom libraries, storage, types, errors
import {MyStorage} from "./libs/MyStorage.sol";
import {MyContractErrors} from "./libs/MyContractErrors.sol";

STYLE-04 · Function Naming [MUST]

Visibility Convention Examples
External/public camelCase claimTokens(), depositFunds()
Internal _camelCase (single underscore prefix) _checkAuth(), _setMerkleRoot()
Private _camelCase (single underscore prefix preferred) _validateInput(), _computeSlot()
View/getters get prefix getState(), getTotalDeposited()
Initializers initialize() top-level; initX() for sub-init initialize(), initFundraise()
Storage load Always load() MyStorage.load()
Script entry Always run(string memory network, string memory environment)

STYLE-05 · Variable Naming [MUST]

Type Convention Examples
Constants UPPER_CASE ADMIN_ROLE, MAX_AMOUNT
Storage slot constants STORAGE_SLOT or _UPPER_CASE STORAGE_SLOT, _STORAGE_LOCATION
State variables camelCase (internal: _camelCase, private: __camelCase) rewardToken, _rewardToken, __rewardToken
Storage layout ref $ (single) or $$ (secondary) Layout storage $, SubLayout storage $$
Function params camelCase when no state-var name conflict; camelCase_ when a same-name state variable exists amount, staker or amount_, staker_
Local variables camelCase totalBalance, expectedRatio

STYLE-06 · Events [MUST]

  • PascalCase, past-tense or descriptive.
  • Declared in interfaces, not implementations.

STYLE-07 · Errors [MUST]

  • Errors must be scoped per service, not shared across unrelated services.
  • Use a dedicated ContractNameErrors library for each top-level service or contract family.
  • Pattern: ContractName__PascalCaseDescription with double-underscore separator.
MyContractErrors.MyContract__ZeroAmount()
MyContractErrors.MyContract__InvalidState(CurrentState)
MyFactoryErrors.MyFactory__ZeroAdminAddress()
  • Storage and types follow the same service boundary rule: prefer MyContractStorage.sol and MyContractTypes.sol over cross-service shared files unless the shared type is intentionally cross-domain.
  • Cross-references are allowed when logically needed, but ownership remains service-local.
  • Standalone module errors may be declared in interfaces or contracts directly.
  • OZ-inherited errors follow OZ style: AccessManagerUnauthorizedAccount(address, uint64).

STYLE-08 · Error Handling [MUST]

Pattern When to Use
require(condition, CustomError()) When the error is immediately obvious without application context (e.g., zero-address, zero-amount, simple access checks).
if (!condition) revert CustomError() When the revert condition involves domain logic, multiple parameters, or benefits from the explicit if block for readability.
try/catch Rarely used. No established best practices yet — avoid unless handling external-call failures where silent failure is unacceptable.
// ✅ require — self-explanatory error
require(amount > 0, Errors.MyContract__ZeroAmount());

// ✅ if...revert — domain logic with context
if (getEffectiveState(id) != EffectiveState.Open) {
    revert Errors.MyContract__InvalidState(getEffectiveState(id));
}

STYLE-09 · Storage — ERC-7201 Namespaced [MUST]

Element Convention
Namespace "my-protocol.ContractName" or "myorg.module.Name"
Slot computation keccak256(abi.encode(uint256(keccak256("namespace")) - 1)) & ~bytes32(uint256(0xff))
Layout struct Layout (or XLayout when multiple exist, e.g. FundraiseLayout)
Accessor function load() internal pure returns (Layout storage $)

STYLE-10 · NatSpec Comments [SHOULD]

Line length: All comments must respect the same line-length limit as code (foundry.toml [fmt] settings; Foundry default is 120). When a /// @dev one-liner exceeds the limit, convert to a /** @dev ... */ block.

Multi-line NatSpec: Always use collapsible block NatSpec for multi-line comments:

/**
 * @title MyContract
 * @notice Short description.
 */

Do not use stacked multi-line /// comments for contract-, library-, struct-, or function-level multi-line NatSpec.

Tag grouping order: When a NatSpec block contains multiple tag categories, keep them in this order:

  1. @title
  2. @notice and @dev together
  3. @param entries together
  4. @return entries together
  5. @custom:* entries together

Leave exactly one blank line between each populated group.

/**
 * @title MyContract
 *
 * @notice Short description.
 * @dev Extra implementation context.
 *
 * @param account Account being updated.
 * @param amount Amount in asset units.
 *
 * @return success Whether the update succeeded.
 *
 * @custom:audit Uses pull-based settlement.
 */

Contract-level: Every contract/abstract/library (not interfaces) has @title + @notice. Add @custom:audit when deviating from upstream.

Structs: Put the NatSpec block immediately before the struct keyword.

  • Do not place routine field-by-field NatSpec comments inside the struct body.
  • When a field is not obvious without broader protocol context, document it in the struct-level NatSpec block.
  • Prefer an explicit field list in the block comment for non-obvious members, for example:
/**
 * @dev Persisted market data.
 *
 * @param state Stored lifecycle checkpoint used to derive the effective state.
 * @param pendingReportedAt Timestamp of the latest pending report inside the correction window.
 * @param feeAmount Fee amount snapshotted when the market resolves.
 */
struct MarketData {
    MarketState state;
    uint40 pendingReportedAt;
    uint256 feeAmount;
}
  • Use @param entries selectively for the fields that need extra context; obvious fields do not need redundant explanations.

Interfaces: No @title/@notice — the interface keyword is self-documenting.

Events / Errors: No NatSpec when self-documenting. /// @dev one-liner when extra context is needed.

Functions — Interfaces: No NatSpec when self-documenting. /// @dev for quick clarifications. /** @dev ... */ for complex behavior, reentrancy notes, or non-obvious side effects.

Functions — Implementations:

  • /// @inheritdoc IInterfaceName only when the interface function has NatSpec. Omit if there is nothing to inherit.
  • /// @dev one-liner for internal/private functions with non-obvious logic.
  • No NatSpec for trivial internals whose name is self-documenting.

STYLE-11 · Visibility & Mutability [MUST]

Principle Rule
Visibility external > public (if never called internally). private > internal (if only used in declaring contract).
Mutability pure if no state access. view if read-only. Unmarked only when writing state.
virtual hooks Use strictest mutability for the current implementation.

Modifiers: Keep modifiers as thin wrappers — call a private helper, then _;.

modifier restricted() {
    __restricted();
    _;
}

function __restricted() private view {
    if (msg.sender != authority) revert Unauthorized(msg.sender);
}

STYLE-12 · Function & Declaration Ordering [SHOULD]

Section Headers

Major visibility groups use banner-style comments:

//////////////////////////////////////////////////////////////////////////////
////////////////////////////////// EXTERNAL //////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

Sub-sections use lightweight inline headers:

//////////////// VIEW (internal) ////////////////

// ---- Config ---- //

Top-Level Order

  1. using directives
  2. Type declarations (structs, enums)
  3. State variables (contract only)
  4. Constants
  5. Events
  6. Errors
  7. Modifiers (contract only)
  8. Constructor / Initializer (contract only)

Contract Function Order

Functions ordered by visibility → mutability → alphabetical (A → Z):

# Group
1 External — mutative (A → Z)
2 External — view (A → Z)
3 External — pure (A → Z)
4 Public — mutative (A → Z)
5 Public — view (A → Z)
6 Public — pure (A → Z)
7 Internal — mutative (A → Z)
8 Internal — view (A → Z)
9 Internal — pure (A → Z)
10 Private — mutative (A → Z)
11 Private — view (A → Z)
12 Private — pure (A → Z)

Sub-categories (e.g., // ---- Config ---- //) are allowed within a group. Each sub-category is ordered A → Z internally.

Interface Order

Four-section structure with matching banners:

  1. EVENTS — A → Z
  2. ERRORS — A → Z
  3. MUTATIVE — A → Z within optional sub-categories
  4. GETTERS — A → Z within optional sub-categories

STYLE-13 · Foundry Formatter Configuration [SHOULD]

forge fmt uses Foundry default settings unless explicitly overridden in foundry.toml [fmt]. Only settings present in [fmt] are non-default. Common defaults (not repeated in config):

  • line_length = 120
  • tab_width = 4
  • bracket_spacing = false

Check the project's foundry.toml for active overrides. Always run forge fmt --check before committing.

Test Patterns

Consumers: Test-writing agents, fuzz agents, review agents. Load when: Writing, editing, or reviewing any test file. Also load solidity-style.md for naming rules.


TEST-01 · Directory Structure [SHOULD]

Tests mirror the src/ hierarchy. For single-domain projects a flat test/ layout is acceptable. For multi-domain projects, use subdirectories mirroring src/:

test/
├── ContractName.setup.sol         # Shared test setup
├── ContractName.t.sol             # Core lifecycle tests
├── ContractName.feature.t.sol     # Per-feature test files
├── ContractName.storages.t.sol    # ERC-7201 slot verification
├── ContractName.fuzz.t.sol        # Fuzz tests
└── mocks/
    └── ERC20Mock.sol              # Shared utility mocks

TEST-02 · Test Contract Inheritance [SHOULD]

Pattern When to Use Example
Standalone setup Self-contained test suites MyContract_Test is MyContract_Setup
Shared abstract base Multiple related test contracts MyContract_BaseMyContract_Feature_Test
Plain Test Simple contracts, storage tests MyContract_Storages_Test is Test

TEST-03 · Test Function Naming [MUST]

Pattern Usage Examples
test_feature_Description Happy path test_createMarket_SetsState, test_claim_ProRataPayout
test_RevertWhen_function_Scenario Revert on condition test_RevertWhen_claim_DeadlinePassed
test_RevertIf_function_Scenario Revert if guard fails test_RevertIf_deposit_NotAuthorized
testDifferential_description Differential tests testDifferential_payout_SolidityVsTypescript
testFuzz_function Fuzz tests testFuzz_claim(uint256, uint256)
testFuzzDifferential_description Fuzz differential testFuzzDifferential_claim_withWhitelist
testFork_description Fork tests testFork_redeem_User

Legacy tests may not follow these patterns. All new or updated tests must comply.

TEST-04 · Setup Patterns [SHOULD]

All test contracts override setUp() and call super.setUp():

function setUp() public override {
    super.setUp();
    // Additional test-specific setup
}

Decompose setup into named internal helpers for readability:

function setUp() public override {
    super.setUp();
    _createUserWallets();
    _setUpTestParams();
    _deployContracts();
}

TEST-05 · Helper Functions [SHOULD]

  • Internal helpers: _camelCase_deployContracts(), _fundUser(address, uint256)
  • Per-user repeatable actions extracted into named helpers: _aliceDeposits(uint128 amount)
  • Defined in both base test contracts and individual test contracts

TEST-06 · Section Organization [SHOULD]

Tests within a contract are grouped by visual separator comments:

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////// REVERT ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////// HAPPY ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

No givenX/whenY modifier pattern — use separate test contracts per feature instead.

TEST-07 · Mock Patterns [SHOULD]

  • Named ContractName_Mock
  • exposed_functionName() to surface internal functions
  • Direct state setters allowed for test convenience
contract MyContract_Mock is MyContract {
    function exposed_computeLeaf(...) external view returns (bytes32) { ... }
    function setToken(address t_) external { ... }
}

TEST-08 · Event Testing [MUST]

Always use vm.expectEmit(true, true, true, true) to assert all event parameters (indexed and non-indexed). Only omit a parameter check (by setting the corresponding bool to false) when the value cannot be controlled or predicted in the test context (e.g., depends on third-party logic, block-level randomness, or is prohibitively complex to reproduce).

Reference events via their interface:

// ✅ Default — check all params
vm.expectEmit(true, true, true, true);
emit IMyContract.ActionCompleted(user, amount);
myContract.doAction(amount);

// ✅ Named parameter syntax is acceptable
vm.expectEmit(true, true, true, true);
emit IMyContract.ActionCompleted({user: alice, amount: 100});
myContract.doAction(100);

// ✅ Skipping a param — only when justified (e.g., third-party-generated value)
vm.expectEmit(true, true, false, true);
emit IMyContract.ActionCompleted(user, 0 /* unknown amount from oracle */);
myContract.doAction(oracleData);

TEST-09 · Error/Revert Testing [MUST]

Selector only (no args):

vm.expectRevert(Errors.MyContract__NothingToRecover.selector);

Encoded with args:

vm.expectRevert(abi.encodeWithSelector(Errors.MyContract__InvalidState.selector, CurrentState.Open));

Bare revert:

vm.expectRevert();

TEST-10 · Fuzzing [SHOULD]

bound() over assume() [MUST]: bound() clamps the value into range; assume() discards the run. Use assume() only when the valid set cannot be expressed as a contiguous range.

function testFuzz_claim(uint256 amount, uint256 shares) public {
    amount = bound(amount, 1, MAX_REALISTIC_AMOUNT);
    shares = bound(shares, 1, amount);
    // ...
}

Stateful fuzzing suite (Guardian Audits pattern):

  • Inheritance chain: FuzzSetup → FunctionCalls → ... → FuzzGuided → Fuzz
  • Functions: fuzz_functionName(...) with invariant_NAME() checks
  • Named invariants: INV_01, INV_02, etc.
  • Actor-based: maps address → uint256 private keys
  • Run: forge test --mc Fuzz --show-progress

TEST-11 · Fork Testing [SHOULD]

  • Test function prefix: testFork_
  • Use vm.createSelectFork() with a target chain RPC
  • Load real deployment addresses from JSON configs

TEST-12 · Test Contract Naming [MUST]

ContractName_featureOrScope_Test

Examples: MyContract_Claims_Test, MyContract_Fuzz_Test, MyContract_Fork_Test

TEST-13 · Assertion Reason Strings [SHOULD]

When a test has more than one or two assertions, add short reason strings (2–4 words):

assertEq(pendingAdmin, user1, "pending admin");
assertEq(pendingSchedule, expected, "pending schedule");

Single-assertion test functions do not need a reason string.

TEST-14 · ERC-7201 Storage Slot Verification [MUST]

Every ERC-7201 storage library must have a test re-deriving the slot from the namespace string. A wrong slot silently reads/writes to wrong storage.

contract MyContract_Storages_Test is Test {
    function test_myStorage_CorrectlyComputed() public pure {
        bytes32 expected = _computeStorageLocation("my-protocol.MyContract");
        bytes32 actual = MyStorage.STORAGE_SLOT;
        assertEq(actual, expected);
    }

    function _computeStorageLocation(string memory name) internal pure returns (bytes32) {
        return keccak256(abi.encode(uint256(keccak256(bytes(name))) - 1)) & ~bytes32(uint256(0xff));
    }
}
  • One test per storage library — file named ContractName.storages.t.sol.
  • Namespace in test must match the @custom:storage-location erc7201:<namespace> annotation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment