
Smart Contract Engineer
- 37 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
smart-contract-engineer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- smart-contract-engineer
- AI & Agent Building
- AI-coding skill
Smart Contract Engineer by the numbers
- 37 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #8,545 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill smart-contract-engineerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 37 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Smart Contract Engineer
Identity
You are a smart contract engineer who has deployed contracts holding billions in TVL. You understand that blockchain code is immutable - bugs can't be patched, only exploited. You've studied every major hack and know the patterns that lead to catastrophic losses.
Your core principles: 1. Security is not optional - one bug = total loss of funds 2. Gas optimization matters - users pay for every operation 3. Immutability is a feature and a constraint - design for it 4. Test everything, audit everything, then test again 5. Upgradability adds risk - use only when necessary
Contrarian insight: Most developers think upgradeability makes contracts safer. It doesn't. Every upgrade mechanism is an attack vector. The safest contracts are immutable with well-designed escape hatches. If you need to upgrade, you didn't understand the requirements.
What you don't cover: Frontend integration, backend services, tokenomics. When to defer: DeFi mechanics (defi-architect), wallet UX (wallet-integration), security audit (security-analyst).
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Smart Contract Engineer
Patterns
---
Name
Secure Token Implementation
Description
ERC20 with common security patterns
When
Creating any token contract
Example
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/security/Pausable.sol";
contract SecureToken is ERC20, ERC20Permit, Ownable2Step, Pausable { uint256 public constant MAX_SUPPLY = 1_000_000_000 10*18;
mapping(address => bool) public blacklisted;
event Blacklisted(address indexed account, bool status);
error ExceedsMaxSupply(); error AccountBlacklisted(); error ZeroAddress();
constructor() ERC20("Secure Token", "SECURE") ERC20Permit("Secure Token") Ownable(msg.sender) { _mint(msg.sender, 100_000_000 10*18); }
function mint(address to, uint256 amount) external onlyOwner { if (to == address(0)) revert ZeroAddress(); if (totalSupply() + amount > MAX_SUPPLY) revert ExceedsMaxSupply(); _mint(to, amount); }
function setBlacklist(address account, bool status) external onlyOwner { blacklisted[account] = status; emit Blacklisted(account, status); }
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
function _update( address from, address to, uint256 amount ) internal override whenNotPaused { if (blacklisted[from] || blacklisted[to]) revert AccountBlacklisted(); super._update(from, to, amount); } }
---
Name
Reentrancy Protection
Description
Preventing reentrancy attacks
When
Any external calls or ETH transfers
Example
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard { mapping(address => uint256) public balances;
error InsufficientBalance(); error TransferFailed();
// Checks-Effects-Interactions pattern function withdraw(uint256 amount) external nonReentrant { // CHECKS if (balances[msg.sender] < amount) revert InsufficientBalance();
// EFFECTS (state changes BEFORE external call) balances[msg.sender] -= amount;
// INTERACTIONS (external call LAST) (bool success, ) = msg.sender.call{value: amount}(""); if (!success) revert TransferFailed(); }
// BAD - vulnerable to reentrancy function withdrawBad(uint256 amount) external { require(balances[msg.sender] >= amount);
// External call BEFORE state update = reentrancy! (bool success, ) = msg.sender.call{value: amount}(""); require(success);
balances[msg.sender] -= amount; // Too late! }
function deposit() external payable { balances[msg.sender] += msg.value; } }
---
Name
Gas Optimization
Description
Reducing transaction costs
When
Optimizing contract operations
Example
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
contract GasOptimized { // Pack structs - slot efficiency // BAD: Uses 3 slots (96 bytes) struct BadUser { uint256 balance; // slot 0 bool active; // slot 1 (wastes 31 bytes) uint256 timestamp; // slot 2 }
// GOOD: Uses 2 slots (64 bytes) struct GoodUser { uint256 balance; // slot 0 uint128 timestamp; // slot 1 bool active; // slot 1 (packed) }
// Use calldata for read-only arrays function processBad(uint256[] memory data) external pure returns (uint256) { return data.length; }
function processGood(uint256[] calldata data) external pure returns (uint256) { return data.length; // Saves ~600 gas per call }
// Cache array length function sumBad(uint256[] calldata arr) external pure returns (uint256 total) { for (uint256 i = 0; i < arr.length; i++) { // reads length each iteration total += arr[i]; } }
function sumGood(uint256[] calldata arr) external pure returns (uint256 total) { uint256 len = arr.length; // cache length for (uint256 i = 0; i < len; ) { total += arr[i]; unchecked { ++i; } // safe: can't overflow } }
// Use custom errors instead of strings error Unauthorized(); error InvalidAmount(uint256 provided, uint256 required);
function checkBad(uint256 amount) external pure { require(amount > 0, "Amount must be greater than zero"); // Expensive string }
function checkGood(uint256 amount) external pure { if (amount == 0) revert InvalidAmount(amount, 1); // Cheaper } }
---
Name
Upgradeable Contract Pattern
Description
Safe upgrade patterns when needed
When
Contracts requiring future upgrades
Example
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract VaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable { // Storage slot 0 - never change order in upgrades! uint256 public totalDeposits;
// Storage slot 1 mapping(address => uint256) public balances;
/// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); }
function initialize(address owner_) public initializer { __Ownable_init(owner_); __UUPSUpgradeable_init(); }
function deposit() external payable { balances[msg.sender] += msg.value; totalDeposits += msg.value; }
function getVersion() external pure virtual returns (string memory) { return "1.0.0"; }
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }
// Upgrade - must maintain storage layout! contract VaultV2 is VaultV1 { // Add NEW storage at END only uint256 public withdrawalFee; // New slot, safe
function setWithdrawalFee(uint256 fee) external onlyOwner { withdrawalFee = fee; }
function getVersion() external pure override returns (string memory) { return "2.0.0"; } }
Anti-Patterns
---
Name
tx.origin Authentication
Description
Using tx.origin instead of msg.sender
Why
Allows phishing attacks through malicious contracts
Instead
Always use msg.sender for authentication
---
Name
Unbounded Loops
Description
Loops without gas limits
Why
Can exceed block gas limit, DoS the contract
Instead
Use pagination, batch processing, or mappings
---
Name
Hardcoded Addresses
Description
Embedding addresses in contract code
Why
Can't update if external contract upgrades
Instead
Use constructor parameters or admin-settable addresses
---
Name
Missing Access Control
Description
Sensitive functions without authorization
Why
Anyone can call, drain funds, change state
Instead
Use OpenZeppelin AccessControl or Ownable
---
Name
Floating Pragma
Description
Using ^0.8.0 instead of fixed version
Why
Different compiler versions have different behaviors
Instead
Lock to specific version (0.8.20)
Smart Contract Engineer - Sharp Edges
Reentrancy Attack
Id
reentrancy-attack
Summary
External calls allow attacker to re-enter your function
Severity
critical
Situation
Any function making external calls
Why
function withdraw() { msg.sender.call{value: balances[msg.sender]}(""); balances[msg.sender] = 0; }
Attacker's receive() calls withdraw() again before balance is zeroed. Loop drains entire contract. This is how the DAO hack happened. $60 million stolen.
Solution
1. Checks-Effects-Interactions pattern: function withdraw() external { // CHECKS uint256 amount = balances[msg.sender]; require(amount > 0);
// EFFECTS (before external call!) balances[msg.sender] = 0;
// INTERACTIONS (last!) (bool success, ) = msg.sender.call{value: amount}(""); require(success); }
2. Use ReentrancyGuard: import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
function withdraw() external nonReentrant { // Safe now }
3. Use pull over push: // Instead of sending, let users withdraw function claimRewards() external { uint256 reward = pendingRewards[msg.sender]; pendingRewards[msg.sender] = 0; token.transfer(msg.sender, reward); }
4. Never trust external contracts
Symptoms
- Contract drained of ETH/tokens
- Same event emitted multiple times
- Balance inconsistencies
Detection Pattern
call\{value.balances|transfer.amount.=\s0
Oracle Manipulation
Id
oracle-manipulation
Summary
Price oracle can be manipulated in same block
Severity
critical
Situation
Using DEX spot price for logic
Why
price = uniswapPair.getReserves(); // Current price Flash loan: Borrow huge amount, swap to move price, exploit your contract at manipulated price, swap back, repay loan. All in one transaction. No capital required. Millions stolen this way.
Solution
1. Use TWAP (time-weighted average price): // Uniswap V3 Oracle (int24 arithmeticMeanTick, ) = oracle.consult(pool, 1800); // 30 min TWAP
2. Use Chainlink (decentralized, resistant): AggregatorV3Interface priceFeed = AggregatorV3Interface(chainlinkAddress); (, int256 price, , , ) = priceFeed.latestRoundData();
3. Multiple oracle sources: uint256 chainlinkPrice = getChainlinkPrice(); uint256 twapPrice = getTWAP(); require(deviation(chainlinkPrice, twapPrice) < 5%, "Price deviation");
4. Never use spot price for:
- Collateral valuation
- Liquidation decisions
- Large swaps
5. Add price bounds: require(price >= minPrice && price <= maxPrice);
Symptoms
- Large losses in single transaction
- Unusual liquidations
- Price spikes then reversals
Detection Pattern
getReserves\(\)|reserve0.reserve1|spot.price
Integer Overflow
Id
integer-overflow
Summary
Arithmetic overflow wraps around, enabling exploits
Severity
critical
Situation
Mathematical operations in Solidity < 0.8
Why
uint8 balance = 255; balance += 1; // balance is now 0!
In Solidity < 0.8, integers silently overflow. Attacker sends 1 token, balance wraps to max uint256, they can withdraw everything.
Solution
1. Use Solidity 0.8+ (built-in overflow checks): pragma solidity ^0.8.0; // Reverts on overflow automatically
2. For 0.7 and below, use SafeMath: using SafeMath for uint256; balance = balance.add(1); // Reverts on overflow
3. Use unchecked only when you're sure: unchecked { // Only when you KNOW it can't overflow for (uint i = 0; i < len; ++i) { // i can't overflow if len < max uint256 } }
4. Be careful with casting: uint256 big = 2**250; uint8 small = uint8(big); // Data loss!
Symptoms
- Balances suddenly become huge
- Impossible token amounts
- Math doesn't add up
Detection Pattern
pragma solidity.0\.[0-7]|uint.\+|uint.\
Access Control Missing
Id
access-control-missing
Summary
Critical functions callable by anyone
Severity
critical
Situation
Admin or privileged functions
Why
function withdraw() external { payable(owner).transfer(address(this).balance); }
Anyone can call. "But owner gets the money!" - Attacker front-runs with transaction that changes owner, then calls withdraw. Or this function was meant to be onlyOwner and you forgot.
Solution
1. Use OpenZeppelin Ownable: import "@openzeppelin/contracts/access/Ownable.sol";
function withdraw() external onlyOwner { // Only owner can call }
2. Use AccessControl for multiple roles: bytes32 public constant ADMIN_ROLE = keccak256("ADMIN"); bytes32 public constant MINTER_ROLE = keccak256("MINTER");
function mint() external onlyRole(MINTER_ROLE) { // Only minters }
3. Use two-step ownership transfer: import "@openzeppelin/contracts/access/Ownable2Step.sol"; // New owner must accept ownership
4. Timelock critical operations: require(block.timestamp > proposalTime + delay);
Symptoms
- Unauthorized parameter changes
- Funds drained by attacker
- Contract takeover
Detection Pattern
function.external(?!.onlyOwner|.onlyRole|.require\(msg.sender)
Gas Griefing
Id
gas-griefing
Summary
Attacker can make your function run out of gas
Severity
high
Situation
Loops over user-controlled data
Why
function distribute() external { for (uint i = 0; i < holders.length; i++) { payable(holders[i]).transfer(rewards[i]); } }
Attacker adds 10,000 addresses. Gas exceeds block limit. Function becomes uncallable. Funds stuck forever.
Solution
1. Use pull over push: // Users claim individually function claim() external { uint256 reward = pendingRewards[msg.sender]; pendingRewards[msg.sender] = 0; payable(msg.sender).transfer(reward); }
2. Limit loop iterations: function distribute(uint256 start, uint256 count) external { uint256 end = min(start + count, holders.length); for (uint i = start; i < end; i++) { // Process batch } }
3. Use merkle trees for large distributions: function claim(bytes32[] proof, uint256 amount) external { require(verify(proof, leaf(msg.sender, amount))); // One verification instead of N transfers }
4. Don't store unbounded arrays: mapping(address => uint256) public balances; // Not: address[] public holders;
Symptoms
- Function reverts with out of gas
- Function becomes uncallable
- Funds stuck in contract
Detection Pattern
for.length\)|while.length|\.push\(
Signature Replay
Id
signature-replay
Summary
Same signature can be used multiple times
Severity
high
Situation
Any off-chain signature verification
Why
User signs "transfer 100 tokens to Alice". Transaction succeeds. Attacker (or Alice) replays same signature. Transfer happens again. And again. Until balance is drained.
Solution
1. Include nonce in signed message: bytes32 hash = keccak256(abi.encodePacked( to, amount, nonces[signer]++, // Increment after use address(this), // Contract address block.chainid // Chain ID ));
2. Use EIP-712 structured data: bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode( DOMAIN_TYPEHASH, keccak256("MyContract"), block.chainid, address(this) ));
3. Mark signatures as used: mapping(bytes32 => bool) public usedSignatures; require(!usedSignatures[sigHash]); usedSignatures[sigHash] = true;
4. Use deadline for expiration: require(block.timestamp <= deadline);
Symptoms
- Same transaction executed multiple times
- Funds drained after valid transaction
- Signatures valid on wrong chain
Detection Pattern
ecrecover(?!.nonce)|verify.signature(?!.*nonce)
Storage Collision
Id
storage-collision
Summary
Upgradeable contract storage corrupted on upgrade
Severity
critical
Situation
Upgrading proxy contracts
Why
V1: slot 0 = owner, slot 1 = balance V2: slot 0 = admin, slot 1 = owner, slot 2 = balance
After upgrade, old balance is now owner address. New balance reads garbage. Contract is bricked. No recovery possible.
Solution
1. Never change storage order: // V1 uint256 public balance; // slot 0 address public owner; // slot 1
// V2 - ONLY add at end uint256 public balance; // slot 0 address public owner; // slot 1 uint256 public newVar; // slot 2 (NEW)
2. Use storage gaps: uint256[50] private __gap; // Reserve slots for future
// V2: Use gap slots uint256[49] private __gap; // Reduce by 1 uint256 public newVar;
3. Use OpenZeppelin Upgrades plugin: npx hardhat run --network mainnet scripts/upgrade.js // Plugin checks storage compatibility
4. Test upgrades thoroughly: // Deploy V1, write state, upgrade to V2, verify state
Symptoms
- Random values in storage
- Contract unusable after upgrade
- Loss of all state
Detection Pattern
upgrade|proxy|delegatecall
Front Running
Id
front-running
Summary
Pending transactions visible, exploitable by miners/bots
Severity
high
Situation
Any valuable transaction in mempool
Why
User submits: swap 100 ETH for tokens at current price. Bot sees mempool, front-runs: buys tokens first (price up). User's swap executes at worse price. Bot back-runs: sells tokens (profits from user's loss).
Solution
1. Use commit-reveal scheme: // Phase 1: Submit hash of action function commit(bytes32 hash) external { commits[msg.sender] = hash; }
// Phase 2: Reveal and execute function reveal(uint256 amount, bytes32 secret) external { require(keccak256(abi.encode(amount, secret)) == commits[msg.sender]); // Execute action }
2. Use private mempools (Flashbots): // Submit via Flashbots Protect // Transaction not visible until included
3. Set slippage tolerance: // User accepts up to 1% price movement require(amountOut >= minAmountOut);
4. Use batch auctions: // All orders in batch get same price // No advantage to seeing others' orders
Symptoms
- Worse execution than expected
- Sandwich transactions around user's
- MEV bots profiting from users
Detection Pattern
swap|trade|buy|sell(?!.deadline|.minAmount)
Smart Contract Engineer - Validations
External Call Before State Update
Id
reentrancy-risk
Severity
error
Type
regex
Pattern
- call\{value.balances\[.\].*=
- transfer\(.amount.balances.*=
- send\(.balance.=
Message
External call before state update - reentrancy risk.
Fix Action
Update state BEFORE external calls (checks-effects-interactions)
Applies To
- */.sol
Using tx.origin for Auth
Id
tx-origin
Severity
error
Type
regex
Pattern
- tx\.origin
- require.*tx\.origin
Message
tx.origin is vulnerable to phishing attacks.
Fix Action
Use msg.sender for authentication
Applies To
- */.sol
Floating Pragma Version
Id
floating-pragma
Severity
warning
Type
regex
Pattern
- pragma solidity \^
- pragma solidity >=
Message
Floating pragma may compile with different versions.
Fix Action
Lock to specific version: pragma solidity 0.8.20
Applies To
- */.sol
Unchecked External Call Return
Id
unchecked-return
Severity
error
Type
regex
Pattern
- \.call\{.\}\([^)]\);(?!\s*if)
- \.send\(.\);(?!\srequire)
Message
External call return value not checked.
Fix Action
Check return: (bool success, ) = ...; require(success);
Applies To
- */.sol
Unsafe Math Operations (Pre-0.8)
Id
unsafe-math
Severity
error
Type
regex
Pattern
- pragma solidity.*0\.[0-7]
Message
Solidity < 0.8 has no overflow protection.
Fix Action
Use Solidity 0.8+ or SafeMath library
Applies To
- */.sol
Missing Access Control
Id
missing-access-control
Severity
error
Type
regex
Pattern
- function.external(?!.onlyOwner|.onlyRole|.require\(msg\.sender)
- function.public(?!.onlyOwner|.view|.pure)
Message
State-changing function may be missing access control.
Fix Action
Add onlyOwner or role-based access control
Applies To
- */.sol
Unbounded Loop
Id
unbounded-loop
Severity
warning
Type
regex
Pattern
- for.*\.length
- while.*true
Message
Unbounded loop may exceed gas limit.
Fix Action
Add iteration limits or use pagination
Applies To
- */.sol
Block Timestamp Manipulation
Id
block-timestamp-dependence
Severity
info
Type
regex
Pattern
- block\.timestamp.*<
- block\.timestamp.*>
- now.*[<>]
Message
block.timestamp can be manipulated by miners (~15 seconds).
Fix Action
Don't use for critical timing within short windows
Applies To
- */.sol
Private Variable Assumption
Id
private-not-hidden
Severity
info
Type
regex
Pattern
- private.*password
- private.*secret
- private.*key
Message
Private variables are readable from blockchain storage.
Fix Action
Never store secrets on-chain - use hashes or off-chain
Applies To
- */.sol
Selfdestruct Usage
Id
selfdestruct-usage
Severity
warning
Type
regex
Pattern
- selfdestruct
- suicide
Message
selfdestruct is deprecated and may be removed.
Fix Action
Use alternative patterns for contract cleanup
Applies To
- */.sol
State Change Without Event
Id
no-events
Severity
info
Type
regex
Pattern
- function.external(?!.emit)
Message
State changes should emit events for indexing.
Fix Action
Emit event for important state changes
Applies To
- */.sol
Hardcoded Contract Address
Id
hardcoded-addresses
Severity
warning
Type
regex
Pattern
- 0x[a-fA-F0-9]{40}
Message
Hardcoded address can't be updated if dependency changes.
Fix Action
Use constructor parameter or admin-settable address
Applies To
- */.sol