Install
$ agentstack add skill-kaynetik-skills-solidity-security ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Solidity Security
Vulnerability prevention, secure patterns, gas-safe optimizations, audit preparation.
Code Style Rules
No Unicode Separator Comments
Never use Unicode box-drawing characters (─, ━, ═, etc.) as comment decorators or section separators in generated code. This includes patterns like:
// ── State ─────────────────────────────────────────
// ══ Errors ═════════════════════════════════════════
These are AI slop. They carry no semantic value, are invisible noise in diffs, and mark generated code as low-quality. Use plain labels or nothing at all:
// State
mapping(address => uint256) public balances;
// Errors
error InsufficientBalance();
Vulnerabilities & Secure Patterns
1. Reentrancy
External call before state update lets an attacker re-enter mid-execution.
Vulnerable:
function withdraw() public {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0; // state update after call
}
Secure - CEI + ReentrancyGuard:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
// Errors
error InsufficientBalance();
error TransferFailed();
function withdraw(uint256 amount) external nonReentrant {
if (balances[msg.sender] = 0.8.0 has checked arithmetic by default. For `unchecked` blocks, the surrounding logic must prove bounds:
```solidity
uint256 len = arr.length;
for (uint256 i; i = minOutput, "Slippage");
}
Secure - Commit-Reveal:
// State
mapping(bytes32 => uint256) public commitBlock;
uint256 public constant REVEAL_DELAY = 1;
// Errors
error NoCommitment();
error RevealTooEarly();
function commit(bytes32 hash) external {
commitBlock[hash] = block.number;
}
function reveal(uint256 amount, uint256 minOutput, bytes32 secret) external {
bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount, minOutput, secret));
if (commitBlock[hash] == 0) revert NoCommitment();
if (block.number MAX_STALENESS) revert StaleOracle();
return uint256(price);
}
7. Proxy / Upgrade Pitfalls
| Risk | Prevention | |------|-----------| | Storage collision | EIP-1967 slots, OZ upgrades plugin | | Uninitialized proxy | initialize() in same tx as deploy | | Selector clash | TransparentUpgradeableProxy or UUPS | | Re-initialization | _disableInitializers() in constructor |
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
8. Signature Replay
error InvalidSignature();
error NonceAlreadyUsed();
mapping(bytes32 => bool) public usedNonces;
function executeWithSig(
address signer, uint256 amount, bytes32 nonce, bytes calldata sig
) external {
if (usedNonces[nonce]) revert NonceAlreadyUsed();
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(signer, amount, nonce))
));
if (ECDSA.recover(digest, sig) != signer) revert InvalidSignature();
usedNonces[nonce] = true;
}
Use EIP-712 typed data + nonce + block.chainid in the domain separator.
Design Patterns
Pull Over Push
// State
mapping(address => uint256) public pending;
// Errors
error NothingToWithdraw();
error TransferFailed();
function recordPayment(address recipient, uint256 amount) internal {
pending[recipient] += amount;
}
function withdraw() external {
uint256 amount = pending[msg.sender];
if (amount == 0) revert NothingToWithdraw();
pending[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed();
}
Emergency Stop
import {PausableUpgradeable} from
"@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
contract Protocol is PausableUpgradeable, OwnableUpgradeable {
function deposit() external payable whenNotPaused { /* ... */ }
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
}
Input Validation
error ZeroAddress();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
function transfer(address to, uint256 amount) external {
if (to == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
if (balances[msg.sender] = 0.8.4) are cheaper than string reverts and encode structured data.
```solidity
error WithdrawalExceedsBalance(uint256 requested, uint256 available);
function withdraw(uint256 amount) external {
if (amount > address(this).balance) {
revert WithdrawalExceedsBalance(amount, address(this).balance);
}
}
Events for Off-Chain Data
event DataStored(address indexed user, uint256 indexed id, bytes data);
function storeData(uint256 id, bytes calldata data) external {
emit DataStored(msg.sender, id, data);
}
Only persist to storage what on-chain logic actually reads.
Security Tooling
| Category | Tool | Purpose | |----------|------|---------| | Static analysis | Slither | Detector suite for common vulns | | Static analysis | Aderyn | Rust-based, Foundry-native | | Fuzzing | Echidna | Property-based Solidity fuzzer | | Fuzzing | Medusa | Go-based alternative to Echidna | | Formal verification | Certora | Prover for critical invariants | | Formal verification | Halmos | Symbolic execution for Foundry | | SMT | SMTChecker | Built-in bounded model checker |
Minimum CI Pipeline
slither . --filter-paths "node_modules|lib"
forge test --fuzz-runs 10000
forge snapshot --check
Testing for Security (Foundry)
import "forge-std/Test.sol";
contract SecurityTest is Test {
Vault vault;
address attacker = makeAddr("attacker");
function setUp() public {
vault = new Vault();
vm.deal(address(vault), 10 ether);
}
function test_RevertWhen_ReentrancyAttempted() public {
ReentrancyAttacker exploit = new ReentrancyAttacker(address(vault));
vm.deal(address(exploit), 1 ether);
vm.expectRevert();
exploit.attack();
}
function test_RevertWhen_UnauthorizedWithdraw() public {
vm.prank(attacker);
vm.expectRevert(Vault.Unauthorized.selector);
vault.emergencyWithdraw();
}
function testFuzz_TransferNeverExceedsBalance(uint256 amount) public {
vm.assume(amount > 0 && amount 0).
/// @return shares Vault shares minted.
function deposit(address token, uint256 amount) external returns (uint256 shares) {
// ...
}
}
Quick Reference
| Vulnerability | Fix | |---------------|-----| | Reentrancy | CEI + ReentrancyGuard | | Missing access control | Ownable2Step / AccessControl | | Unchecked ERC20 return | SafeERC20 | | Oracle manipulation | TWAP + freshness check + sanity bounds | | Frontrunning | Commit-reveal, slippage + deadline params | | Proxy storage collision | EIP-1967, OZ upgrades plugin | | tx.origin auth | msg.sender | | On-chain randomness | Chainlink VRF | | Unbounded loop DoS | Pagination or pull pattern | | Signature replay | EIP-712 + nonce + block.chainid | | Flash loan price manipulation | TWAP, multiple oracles | | Push-payment DoS | Pull-over-push | | Delegatecall to untrusted | Never; or restrict target via allowlist |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kaynetik
- Source: kaynetik/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.