Rule
Org pragma convention: pragma solidity ^X.Y.Z for files whose top-level declarations are only library / interface / abstract contract; pragma solidity =X.Y.Z for files declaring a concrete contract (concrete test mocks/utils included). Libs/abstracts float so downstream soldeer consumers can compile published packages' src/; concretes pin.
Today this is convention-by-discipline only, and it drifts: audit passes flag the ^/= mix as "inconsistent" and produce mass-pinning PRs in the wrong direction (e.g. rain.erc4626.words#225, human-rejected for pinning 6 src lib/abstract files; its issue rain.erc4626.words#55 shows the confusion the unenforced rule causes).
Proposal
Enforce it mechanically in rainix, as a sibling of the one-contract-per-file rule:
lib/sol-pragma-convention.sh following the lib/sol-single-contract.sh pattern (comment-stripping + top-level declaration scan is already written there and can be shared/reused):
- file declares a top-level non-abstract
contract → pragma must match ^pragma solidity =;
- file declares only
library/interface/abstract contract (or just file-scope errors/constants/types) → pragma must match ^pragma solidity \^;
- version number itself stays repo-defined — the check enforces the operator, not the version;
- skip generated (
src/generated/) and vendored dirs, matching the existing checks' scoping.
rainix-sol-pragma-convention mkTask in flake.nix, wired into the rainix-sol-static reusable next to rainix-sol-single-contract.
- bats coverage mirroring
test/bats/task/sol-single-contract.test.bats.
Edge cases to decide during implementation
Rule
Org pragma convention:
pragma solidity ^X.Y.Zfor files whose top-level declarations are onlylibrary/interface/abstract contract;pragma solidity =X.Y.Zfor files declaring a concretecontract(concrete test mocks/utils included). Libs/abstracts float so downstream soldeer consumers can compile published packages'src/; concretes pin.Today this is convention-by-discipline only, and it drifts: audit passes flag the
^/=mix as "inconsistent" and produce mass-pinning PRs in the wrong direction (e.g. rain.erc4626.words#225, human-rejected for pinning 6 src lib/abstract files; its issue rain.erc4626.words#55 shows the confusion the unenforced rule causes).Proposal
Enforce it mechanically in rainix, as a sibling of the one-contract-per-file rule:
lib/sol-pragma-convention.shfollowing thelib/sol-single-contract.shpattern (comment-stripping + top-level declaration scan is already written there and can be shared/reused):contract→ pragma must match^pragma solidity =;library/interface/abstract contract(or just file-scope errors/constants/types) → pragma must match^pragma solidity \^;src/generated/) and vendored dirs, matching the existing checks' scoping.rainix-sol-pragma-conventionmkTask inflake.nix, wired into therainix-sol-staticreusable next torainix-sol-single-contract.test/bats/task/sol-single-contract.test.bats.Edge cases to decide during implementation
contract ErrFtso {}for bug(forge script):failed to read artifact sourcewith lib dependencies that include files without a matching contract name foundry-rs/foundry#6572 inside an otherwise file-scope-errors-only file currently at^0.8.19): exempt empty contracts, or accept=there?>=pragmas or multiple pragma lines: reject outright.