solidity-coding — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited solidity-coding (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
pragma solidity ^0.8.19; — keep consistent across all files in the projectremappings.txtrequire strings — saves gas and is more expressiveerror InsufficientBalance(uint256 available, uint256 required);if (balance < amount) revert InsufficientBalance(balance, amount);public / external functions must have NatSpec (@notice, @param, @return)indexed to address type parameters — add comment if indexing other typesimmutable / constant / unchecked / assembly must have inline comment explaining whyWhen a contract involves state initialization (external addresses, business parameters, role assignments), DO NOT default to any single approach. Follow this workflow:
| Scheme A: Constructor Only | Scheme B: Initializer Only | Scheme C: Hybrid | |
|---|---|---|---|
| Approach | All init in constructor params | Deploy empty, call initialize() after | Constructor handles known params; initialize() handles post-deploy dependencies |
| Atomicity | Deploy fails = nothing exists, no dangerous half-init state | Risk of "deployed but not initialized" window | Partially atomic — constructor part is safe, initializer part has the window |
| Front-run risk | None — constructor cannot be front-run | initialize() can be front-run if unprotected | Only the initializer portion has risk |
| Gas | One tx for deploy + init | Deploy tx + N init txs | Deploy tx + 1 init tx |
| Flexibility | Low — all params must be known at deploy time | High — can set dependencies after other contracts deploy | Medium — split between deploy-time and post-deploy |
| Upgradeable | Not compatible with proxy patterns | Required for UUPS / Transparent Proxy | Partial — constructor sets proxy-safe params, initializer for the rest |
| Scenario | Suggested Scheme |
|---|---|
| Non-upgradeable, all dependencies known at deploy | A |
| Upgradeable contract (UUPS / Transparent Proxy) | B |
| Non-upgradeable, but some dependencies deploy later (e.g., LP pair created after token deploy) | C |
| Multi-contract system with circular references | C |
| Simple utility / library-style contract | A |
| Contract needs post-deploy configuration by multisig / DAO | B or C |
require(currentValue == address(0)) or a bool initialized flag — prevent re-initializationinitialize() functions must have access control (onlyOwner, initializer modifier, or deployer-only check)immutable over regular storage — saves gas on every readinitialize() in the same transaction batch or immediately after — document the required call sequenceWhen an event represents a conditional/branching outcome (the same action can happen for different reasons), add a string reason parameter to make on-chain data self-documenting:
// GOOD — on-chain data is self-explanatory
event WithdrawalFailed(address indexed user, uint256 amount, string reason);
emit WithdrawalFailed(user, amount, "insufficient balance");
emit WithdrawalFailed(user, amount, "cooldown not expired");
event ProposalRejected(uint256 indexed proposalId, string reason);
emit ProposalRejected(id, "quorum not reached");
emit ProposalRejected(id, "voting period ended");
// BAD — must read contract source to understand why
event WithdrawalFailed(address indexed user, uint256 amount);适用场景:
"security incident", "parameter tuning")不适用场景:
uint8 reasonCode 代替| 场景 | 方式 | Gas 成本 |
|---|---|---|
| 原因种类少且固定(≤5种) | string reason 字面量 | 低(编译器优化短字符串) |
| 原因种类多或动态 | uint8 reasonCode + 前端映射表 | 最低 |
| 需要附带动态数据 | string reason 拼接 | 较高,慎用 |
// 方式 A:string literal(推荐,可读性最好)
emit WithdrawalFailed(user, amount, "cooldown not expired");
// 方式 B:uint8 code(高频场景省 gas)
event SwapRejected(address indexed user, uint256 amount, uint8 reasonCode);
// 0 = slippage, 1 = insufficient liquidity, 2 = paused
emit SwapRejected(user, amount, 0);external / public 函数必须 emit 至少一个事件@param 必须为 reason 参数列出所有可能的值| Element | Convention | Example |
|---|---|---|
| Contract / Library | PascalCase | MyToken, StakingPool |
| Interface | I + PascalCase | IMyToken, IStakingPool |
| State variable / Function | lowerCamelCase | totalSupply, claimDividend |
| Constant / Immutable | UPPER_SNAKE_CASE | MAX_SUPPLY, ROUTER_ADDRESS |
| Event | PascalCase (past tense) | TokenTransferred, PoolCreated |
| Custom Error | PascalCase | InsufficientBalance, Unauthorized |
| Function parameter | prefix _ for setter | function setFee(uint256 _fee) |
i/j/k in loops), excessive abbreviations| Situation | Rule |
|---|---|
| Cross-contract constants | Place in src/common/Const.sol |
| Interface definitions | Place in src/interfaces/I<Name>.sol, separate from implementation |
| Simple on-chain queries | Use Foundry cast CLI (call / send) |
| Complex multi-step operations | Use Foundry script (*.s.sol) |
| Import style | Use named imports: import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; |
src/ — Contract source code
interfaces/ — Interface definitions (I*.sol)
common/ — Shared constants, types, errors (Const.sol, Types.sol)
test/ — Test files (*.t.sol)
script/ — Deployment & interaction scripts (*.s.sol)
config/ — Network config, parameters (*.json)
deployments/ — Deployment records (latest.env)
docs/ — Documentation, changelogs
lib/ — Dependencies (managed by Foundry)config/*.json — network RPC URLs, contract addresses, business parametersdeployments/latest.env — latest deployed contract addresses, must update after each deploymentfoundry.toml — compiler version, optimizer settings, remappingsWhen writing Solidity contracts, prioritize using battle-tested OpenZeppelin libraries over custom implementations. Select the appropriate library based on the scenario:
| Scenario | Library | Import Path |
|---|---|---|
| Single owner management | Ownable | @openzeppelin/contracts/access/Ownable.sol |
| Owner transfer needs safety | Ownable2Step | @openzeppelin/contracts/access/Ownable2Step.sol |
| Multi-role permission (admin/operator/minter) | AccessControl | @openzeppelin/contracts/access/AccessControl.sol |
| Need to enumerate role members | AccessControlEnumerable | @openzeppelin/contracts/access/AccessControlEnumerable.sol |
| Governance with timelock delay | TimelockController | @openzeppelin/contracts/governance/TimelockController.sol |
Rule: Single owner → Ownable2Step; 2+ roles → AccessControl; governance/DAO → TimelockController
| Scenario | Library | Usage |
|---|---|---|
| External call / token transfer | ReentrancyGuard | Add nonReentrant modifier |
| Emergency pause needed | Pausable | Add whenNotPaused to user-facing functions; keep admin functions unpaused |
| ERC20 token interaction | SafeERC20 | Use safeTransfer / safeTransferFrom / safeApprove instead of raw calls |
Rule: Any contract that transfers tokens or ETH MUST use ReentrancyGuard + SafeERC20
| Scenario | Library | Notes |
|---|---|---|
| Fungible token | ERC20 | Base standard |
| Token with burn mechanism | ERC20Burnable | Adds burn() and burnFrom() |
| Token with max supply cap | ERC20Capped | Enforces totalSupply <= cap |
| Gasless approval (EIP-2612) | ERC20Permit | Saves users approve tx gas |
| Governance voting token | ERC20Votes | Snapshot-based voting power |
| NFT | ERC721 | Base NFT standard |
| NFT with enumeration | ERC721Enumerable | Supports tokenOfOwnerByIndex queries |
| Multi-token (FT + NFT mixed) | ERC1155 | Game items, batch operations |
| Scenario | Library | Usage |
|---|---|---|
| Whitelist / airdrop verification | MerkleProof | Gas-efficient Merkle tree verification |
| Signature verification | ECDSA + EIP712 | Off-chain sign + on-chain verify |
| Auto-increment IDs | Counters | Token ID, order ID generation |
| Batch function calls | Multicall | Multiple operations in one tx |
| Address set / uint set | EnumerableSet | Iterable sets with O(1) add/remove/contains |
| Revenue sharing | PaymentSplitter | Split ETH/token payments by shares |
| Standardized yield vault | ERC4626 | DeFi vault standard |
| Scenario | Library | Notes |
|---|---|---|
| Upgradeable contract (gas efficient) | UUPSUpgradeable | Upgrade logic in implementation contract |
| Upgradeable contract (admin separated) | TransparentUpgradeableProxy | Upgrade logic in proxy, higher gas |
| Initializer (replace constructor) | Initializable | Use initializer modifier instead of constructor |
Rule: New projects prefer UUPSUpgradeable; always use Initializable for upgradeable contracts
| Scenario | Library | Notes |
|---|---|---|
| Token price data | AggregatorV3Interface | Only for tokens with supported oracle data feeds |
| Verifiable randomness (lottery/NFT) | VRFConsumerBaseV2 | On-chain provably fair random numbers |
| Automated execution (cron jobs) | AutomationCompatible | Replace centralized keepers |
| Cross-chain messaging | CCIP | Cross-chain token/message transfer |
Does contract handle user funds/tokens?
├── YES → Add ReentrancyGuard + SafeERC20
│ Does it need emergency stop?
│ ├── YES → Add Pausable
│ └── NO → Skip
└── NO → Skip
How many admin roles needed?
├── 1 role → Ownable2Step
├── 2+ roles → AccessControl
└── DAO/governance → TimelockController
Does contract need price data?
├── Token has oracle feed → AggregatorV3Interface
├── No oracle feed → Custom TWAP with min-liquidity check
└── No price needed → Skip
Will contract need upgrades?
├── YES → UUPSUpgradeable + Initializable
└── NO → Standard deployment (immutable)transfer wrappers — use SafeERC20Ownable / AccessControlPausableSafeMath on Solidity >= 0.8.0 — overflow checks are built-inrequire(token.transfer(...)) — use token.safeTransfer(...) via SafeERC20tx.origin for auth — use msg.sender with Ownable / AccessControlWhen OpenZeppelinContracts MCP is configured, prefer using it to generate base contracts instead of writing from scratch:
| Contract Type | MCP Tool | When to Use |
|---|---|---|
| Fungible token | solidity-erc20 | Any new ERC20 token contract |
| NFT | solidity-erc721 | Any new NFT contract |
| Multi-token | solidity-erc1155 | Game items, batch operations |
| Stablecoin | solidity-stablecoin | Stablecoin with ERC20 compliance |
| Real-world assets | solidity-rwa | Asset tokenization |
| Smart account | solidity-account | ERC-4337 account abstraction |
| Governance | solidity-governor | DAO voting and proposals |
| Custom | solidity-custom | Non-standard contracts with OZ patterns |
Workflow: MCP generates base → apply this skill's naming/structure rules → customize business logic → apply /solidity-security rules
Why MCP over manual: MCP output is validated against the same rule-set as OZ Contracts Wizard — imports, modifiers, security checks are guaranteed correct. Manual coding risks missing imports or using wrong OZ versions.
When NOT to use MCP: Heavily custom contracts with non-standard patterns, contracts that don't fit any OZ template, or when you need fine-grained control from line 1.
Graceful degradation: If MCP is not configured, fall back to the Library Selection Standards above and write contracts manually following all rules in this skill.
| Operation | Command |
|---|---|
| Create new project | forge init <project-name> |
| Install dependency | forge install openzeppelin-contracts |
| Build contracts | forge build |
| Format code | forge fmt |
| Update remappings | forge remappings |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.