Solidity Ethereum Agent Rules
Project Context
You are developing Ethereum smart contracts with Solidity 0.8+. Contracts handle real value — correctness and security are non-negotiable. Use OpenZeppelin for standard patterns and Hardhat for testing and deployment.
Code Style
- Pin the compiler version: `pragma solidity 0.8.24;` — avoid floating pragmas like `^0.8.0`.
- Follow Solidity naming: contracts `PascalCase`, functions and variables `camelCase`, constants `UPPER_SNAKE_CASE`, events `PascalCase`.
- Order contract members: type declarations → state variables → events → errors → modifiers → constructor → external → public → internal → private.
- Write NatSpec comments (`///` or `/** */`) for every public/external function, event, error, and state variable.
- Use custom errors instead of revert strings: `error InsufficientBalance(uint256 available, uint256 required)` — they cost less gas and carry structured data.
- Keep contracts under 300 lines; extract reusable logic into libraries and base contracts.
- Use `immutable` for values set once in the constructor; use `constant` for compile-time known values.
Security: Checks-Effects-Interactions
- Always follow the CEI pattern: perform all `require`/`revert` checks first, update state second, make external calls last.
- Use `ReentrancyGuard.nonReentrant` from OpenZeppelin on every function that transfers ETH or calls external contracts.
- Use `SafeERC20` from OpenZeppelin for all ERC20 token transfers to handle non-standard return values.
- Never use `tx.origin` for authentication — use `msg.sender` exclusively.
- Validate all external inputs: check for zero addresses, zero amounts, array length mismatches.
- Use access control on every state-changing function: `onlyOwner`, `onlyRole(MINTER_ROLE)`, or custom modifiers.
- Be aware of front-running: use commit-reveal schemes or time-locks for sensitive operations (auctions, randomness).
- Set explicit upper bounds on loops: never iterate over user-controlled or unbounded arrays in a single transaction.
Access Control
- Use OpenZeppelin `AccessControl` for role-based permissions: `grantRole(MINTER_ROLE, address)`.
- Define granular roles: `MINTER_ROLE`, `PAUSER_ROLE`, `UPGRADER_ROLE`, `TREASURY_ROLE`.
- Use a timelocked multisig (Safe + Timelock) for admin roles in production.
- Implement `Pausable` on contracts handling user funds for emergency stops.
- Emit events on every role grant/revoke — on-chain auditability matters.
Upgradeable Contracts
- Use UUPS proxy pattern over TransparentProxy for lower per-call gas cost.
- Use `initializer` modifier in place of constructors for upgradeable contracts.
- Call `_disableInitializers()` in the implementation constructor to prevent direct initialization.
- Use structured storage slots (EIP-7201) to prevent storage collisions across upgrades.
- Test every upgrade path: deploy V1, upgrade to V2, verify state is preserved and new logic is correct.
Gas Optimization
- Pack storage variables: group `uint128 a; uint128 b;` into one 32-byte slot rather than two.
- Use `calldata` for function parameters that are only read, not modified.
- Cache storage reads in local memory variables when a storage variable is accessed multiple times in a function.
- Use `unchecked { ++i; }` in loop increments after validating the operation cannot overflow.
- Use mappings over arrays for lookups; use `EnumerableSet` only when enumeration is genuinely required.
- Use `bytes32` instead of `string` for fixed-length identifiers.
- Minimize event data — only index fields used for off-chain filtering; log only what indexers need.
Events & Errors
- Emit events for every state change external systems need to track: transfers, role changes, parameter updates.
- Index up to 3 fields per event for efficient filtering: addresses, IDs, topics — not large value fields.
- Define errors at the interface level so callers can decode them without the full implementation ABI.
- Name events in past tense: `Deposited`, `Withdrawn`, `OwnershipTransferred`, `ConfigUpdated`.
- Include both old and new values in update events: `event PriceUpdated(uint256 oldPrice, uint256 newPrice)`.
Testing
- Write unit tests in TypeScript with Hardhat, ethers.js, and Chai — test every public and external function.
- Test all revert conditions: unauthorized calls, invalid inputs, paused state, reentrancy attempts.
- Use `loadFixture` from `@nomicfoundation/hardhat-network-helpers` for efficient snapshot/restore between tests.
- Write fuzz tests with Foundry (`forge test`) or Echidna for mathematical invariants and boundary conditions.
- Measure gas with `hardhat-gas-reporter` — set gas budgets per function and fail CI if exceeded.
- Target 100% branch coverage on security-critical contracts: access control, fund handling, state transitions.
- Fork mainnet state for integration tests: `--fork-url $MAINNET_RPC_URL` to test against real deployed protocols.
Deployment & Verification
- Write deterministic deployment scripts in TypeScript — verify constructor arguments match expected values.
- Deploy to testnets (Sepolia, Holesky) first; run the full test suite against the testnet deployment.
- Verify contracts on Etherscan immediately post-deployment using `hardhat-verify`.
- Store deployment addresses, transaction hashes, and ABIs in version-controlled JSON files per network.
- Use Safe multisig for all contract ownership and admin roles in production — no single EOA owner.
- Complete a security audit before mainnet deployment for any contract handling user funds.