name: solidity-coding user-invocable: true description: Write production-quality Solidity contracts. Trigger phrases - write contract, implement function, add feature, add error, gas optimization, event design, contract architecture, or when working in src/ directories.
Solidity Contract Development Skill
This skill provides expertise for writing production-quality Solidity contracts following industry best practices.
Bundled References
For detailed patterns and code examples, read these reference files:
| Reference | Content | When to Read |
|---|---|---|
.claude/skills/solidity-coding/references/coding-patterns.md |
NatSpec, errors, modifiers, imports, section headers | Before writing any contract code |
.claude/skills/solidity-coding/references/security-practices.md |
CEI, access control, EIP-7702, upgrades | When implementing state changes |
.claude/skills/solidity-coding/references/gas-optimization.md |
EIP-1153, L2 patterns, Solady, storage caching | When optimizing contract efficiency |
.claude/skills/solidity-coding/references/event-design.md |
Event design for The Graph and indexers | When adding events to contracts |
.claude/skills/solidity-coding/references/versioning-migration.md |
Interface versioning, storage migration, deprecation | When releasing new contract versions |
.claude/skills/solidity-coding/references/sablier-conventions.md |
Sablier-specific naming, patterns, and examples | When working in Sablier repos |
.claude/skills/solidity-coding/references/nft-descriptor.md |
Onchain NFT metadata and SVG generation | When implementing tokenURI |
Note: Your repo's agent will provide repo-specific structure (package locations, inheritance hierarchies, etc.)
Quick Conventions Reference
Naming
| Element | Convention | Example |
|---|---|---|
| Contract / Library | PascalCase | TokenVault |
| Interface | I + PascalCase | ITokenVault |
| Function | camelCase | withdrawableAmountOf |
| Variable | camelCase | streamId |
| Constant | SCREAMING_SNAKE | MAX_SEGMENT_COUNT |
| Private/Internal | _underscore prefix | _balances |
| Error | {Contract}_{Description} |
TokenVault_Overdraw |
Import Ordering
- Alphabetical order
- External imports first, local imports second
Function Ordering
- Constructor
- Receive/Fallback (if any)
- External functions (view/pure first, then state-changing)
- Public functions (view/pure first, then state-changing)
- Internal functions
- Private functions
Key Principles
- NatSpec: Use
@inheritdocin implementations; full docs live in interfaces - Errors: One specific error per failure mode; never use generic catch-all errors
- CEI Pattern: Always checks → effects → interactions
- SafeERC20: Always use
safeTransferandsafeTransferFrom - Timestamps: Use
uint40for all timestamps - Amounts: Use
uint128for token amounts
Contract Structure Pattern
Standard Directory Layout
src/
├── MainContract.sol # Entry point
├── abstracts/ # Inheritance chain (state, features, base contracts)
├── interfaces/ # Public APIs - NatSpec lives here (use @inheritdoc in impl)
├── libraries/ # Errors.sol, Helpers.sol, Math libraries
└── types/ # Structs, enums, namespace libraries
Inheritance Pattern
Inherit in alphabetical order:
contract TokenVault is Batch, ERC721, ITokenVault, VaultDynamic, VaultLinear { ... }
Writing New Contract Code
Contract Checklist
When writing a new contract or function:
- Correct license and pragma
- Imports ordered: external → internal → local
- Named imports only (use curly braces)
- Section comments for code organization
-
@inheritdocfor interface implementations - Specific errors for each failure mode
- Checks-effects-interactions ordering
- SafeERC20 for token transfers
-
uint40for timestamps,uint128for amounts - Storage packing considered for new structs
- Contract size under 24kb limit
Contract Size Limit
Contracts must stay under the 24kb bytecode limit. Verify with the optimized profile:
forge build --sizes
If a contract exceeds the limit:
- Extract logic into external libraries - Use
public/externallibrary functions instead ofinternal - Split into multiple contracts via inheritance
- Remove unused functions
- Use shorter error messages or custom errors
External Libraries Pattern
Use public library functions to reduce contract size (called via DELEGATECALL rather than inlined):
// Library with PUBLIC functions (not inlined, reduces contract size)
library VaultMath {
function calculateAmount(...) public pure returns (uint128) { ... }
}
library Helpers {
function validateParams(...) public view { ... }
}
// Main contract calls library functions (DELEGATECALL, not inlined)
contract TokenVault {
function _computeAmount(uint256 id) private view returns (uint128) {
return VaultMath.calculateAmount(...);
}
}
Key insight: internal library functions are inlined into the contract bytecode. public/external library
functions are called via DELEGATECALL, keeping bytecode smaller but costing slightly more gas per call.
Stack Too Deep
When you encounter "Stack Too Deep" errors, use an in-memory struct to bundle local variables:
/// @dev Needed to avoid Stack Too Deep.
struct ComputeVars {
address token;
string tokenSymbol;
uint128 depositedAmount;
string json;
ITokenVault vault;
string status;
}
function compute(uint256 id) external view returns (string memory result) {
ComputeVars memory vars;
vars.vault = ITokenVault(address(this));
vars.depositedAmount = vars.vault.getDepositedAmount(id);
vars.token = address(vars.vault.getToken(id));
// ... use vars.field instead of separate local variables
}
Place *Vars structs in types/DataTypes.sol if reused, or inline in the contract if function-specific.
Adding Errors
- Add error to package's
libraries/Errors.sol - Use naming:
{ContractName}_{ErrorDescription} - Add NatSpec:
/// @notice Thrown when... - Include relevant parameters for debugging
/// @notice Thrown when trying to withdraw more than available.
error TokenVault_Overdraw(uint256 id, uint128 amount, uint128 withdrawableAmount);
Adding Functions
- Add signature and full NatSpec to interface
- Implement in contract with
@inheritdoc - Place in correct section (external/public/internal/private)
- Order modifiers: visibility → mutability → override → custom
Common Patterns
Safe Token Transfers
using SafeERC20 for IERC20;
// Safe transfers handle non-standard ERC20s
token.safeTransfer(to, amount);
token.safeTransferFrom(from, to, amount);
Common Tasks
Add a view function
- Add to interface with full NatSpec (notice, dev, param, return)
- Implement with
@inheritdoc InterfaceName - Add appropriate modifiers (e.g.,
notNull(id)if accessing resource state)
Add a state-changing function
- Add to interface with NatSpec (include Notes and Requirements sections)
- Implement with
@inheritdoc - Add modifiers:
noDelegateCall,notNull(id), etc. - Follow CEI pattern
- Emit events after state changes
Add a new error
- Define in
libraries/Errors.solwith section comment - Add NatSpec:
/// @notice Thrown when... - Include debugging parameters
- Use in contract:
revert Errors.ContractName_ErrorDesc(...)
Add a new struct
- Define in
types/directory - Pack fields for gas efficiency (see
.claude/skills/solidity-coding/references/coding-patterns.md) - Add NatSpec for struct and each field
- Use
@paramfor each field in struct NatSpec
Example Invocations
Test this skill with these prompts:
- New function: "Add a
getStreamBalanceview function to the ISablierFlow interface" - Error handling: "Add a
Flow_InsufficientBalanceerror with debugging parameters" - Gas optimization: "Optimize the
_withdrawfunction using storage caching" - Contract structure: "Create the interface for a new
TokenVestingcontract with deposit, withdraw, and claim functions"