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.
This skill provides expertise for writing production-quality Solidity contracts following industry best practices.
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.)
| 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 |
@inheritdoc in implementations; full docs live in interfacessafeTransfer and safeTransferFromuint40 for all timestampsuint128 for token amountssrc/
├── 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
Inherit in alphabetical order:
contract TokenVault is Batch, ERC721, ITokenVault, VaultDynamic, VaultLinear { ... }
When writing a new contract or function:
@inheritdoc for interface implementationsuint40 for timestamps, uint128 for amountsContracts must stay under the 24kb bytecode limit. Verify with the optimized profile:
forge build --sizes
If a contract exceeds the limit:
public/external library functions instead of internalUse 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.
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.
libraries/Errors.sol{ContractName}_{ErrorDescription}/// @notice Thrown when.../// @notice Thrown when trying to withdraw more than available.
error TokenVault_Overdraw(uint256 id, uint128 amount, uint128 withdrawableAmount);
@inheritdocusing SafeERC20 for IERC20;
// Safe transfers handle non-standard ERC20s
token.safeTransfer(to, amount);
token.safeTransferFrom(from, to, amount);
@inheritdoc InterfaceNamenotNull(id) if accessing resource state)@inheritdocnoDelegateCall, notNull(id), etc.libraries/Errors.sol with section comment/// @notice Thrown when...revert Errors.ContractName_ErrorDesc(...)types/ directory.claude/skills/solidity-coding/references/coding-patterns.md)@param for each field in struct NatSpecTest this skill with these prompts:
getStreamBalance view function to the ISablierFlow interface"Flow_InsufficientBalance error with debugging parameters"_withdraw function using storage caching"TokenVesting contract with deposit, withdraw, and claim
functions"