Agent Skills Framework Extension
Blockchain Development Patterns Skill
When to Use This Skill
Use this skill when implementing blockchain development patterns patterns in your codebase.
How to Use This Skill
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
Smart contracts, Web3 integration, DeFi patterns, and blockchain security.
Core Capabilities
- Smart Contracts - Solidity development patterns
- Web3 Integration - Frontend connection to blockchain
- DeFi Patterns - Token, staking, lending protocols
- Security - Common vulnerabilities and prevention
- Gas Optimization - Efficient contract design
Smart Contract Patterns
// contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MyToken is ERC20, ERC20Burnable, ERC20Permit, AccessControl, Pausable, ReentrancyGuard {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18; // 1 billion tokens
event TokensMinted(address indexed to, uint256 amount);
event TokensBurned(address indexed from, uint256 amount);
constructor(address defaultAdmin)
ERC20("MyToken", "MTK")
ERC20Permit("MyToken")
{
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
_grantRole(MINTER_ROLE, defaultAdmin);
_grantRole(PAUSER_ROLE, defaultAdmin);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) whenNotPaused {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
emit TokensMinted(to, amount);
}
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
}
function unpause() external onlyRole(PAUSER_ROLE) {
_unpause();
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override whenNotPaused {
super._beforeTokenTransfer(from, to, amount);
}
}
// contracts/Staking.sol
contract Staking is ReentrancyGuard, Pausable, AccessControl {
IERC20 public immutable stakingToken;
IERC20 public immutable rewardToken;
uint256 public rewardRate; // Rewards per second
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
uint256 public totalStaked;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
mapping(address => uint256) public stakedBalance;
mapping(address => uint256) public stakingTime;
uint256 public constant MINIMUM_STAKE_DURATION = 7 days;
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 reward);
event RewardRateUpdated(uint256 newRate);
constructor(
address _stakingToken,
address _rewardToken,
uint256 _rewardRate
) {
stakingToken = IERC20(_stakingToken);
rewardToken = IERC20(_rewardToken);
rewardRate = _rewardRate;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
(block.timestamp - lastUpdateTime) * rewardRate * 1e18 / totalStaked
);
}
function earned(address account) public view returns (uint256) {
return (
stakedBalance[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18
) + rewards[account];
}
function stake(uint256 amount) external nonReentrant whenNotPaused updateReward(msg.sender) {
require(amount > 0, "Cannot stake 0");
totalStaked += amount;
stakedBalance[msg.sender] += amount;
stakingTime[msg.sender] = block.timestamp;
stakingToken.transferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Cannot withdraw 0");
require(stakedBalance[msg.sender] >= amount, "Insufficient balance");
require(
block.timestamp >= stakingTime[msg.sender] + MINIMUM_STAKE_DURATION,
"Minimum stake duration not met"
);
totalStaked -= amount;
stakedBalance[msg.sender] -= amount;
stakingToken.transfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
function claimReward() external nonReentrant updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardToken.transfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
function exit() external {
withdraw(stakedBalance[msg.sender]);
claimReward();
}
// Admin functions
function setRewardRate(uint256 _rewardRate) external onlyRole(DEFAULT_ADMIN_ROLE) updateReward(address(0)) {
rewardRate = _rewardRate;
emit RewardRateUpdated(_rewardRate);
}
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_pause();
}
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
}
Web3 Frontend Integration
// src/lib/web3/provider.ts
import { ethers, BrowserProvider, Contract } from 'ethers';
import { create } from 'zustand';
interface Web3State {
provider: BrowserProvider | null;
signer: ethers.Signer | null;
address: string | null;
chainId: number | null;
isConnecting: boolean;
error: string | null;
connect: () => Promise<void>;
disconnect: () => void;
switchNetwork: (chainId: number) => Promise<void>;
}
export const useWeb3Store = create<Web3State>((set, get) => ({
provider: null,
signer: null,
address: null,
chainId: null,
isConnecting: false,
error: null,
connect: async () => {
set({ isConnecting: true, error: null });
try {
if (!window.ethereum) {
throw new Error('Please install MetaMask');
}
const provider = new BrowserProvider(window.ethereum);
const accounts = await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const network = await provider.getNetwork();
set({
provider,
signer,
address: accounts[0],
chainId: Number(network.chainId),
isConnecting: false,
});
// Listen for account changes
window.ethereum.on('accountsChanged', (accounts: string[]) => {
if (accounts.length === 0) {
get().disconnect();
} else {
set({ address: accounts[0] });
}
});
// Listen for chain changes
window.ethereum.on('chainChanged', (chainId: string) => {
set({ chainId: parseInt(chainId, 16) });
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Connection failed',
isConnecting: false,
});
}
},
disconnect: () => {
set({
provider: null,
signer: null,
address: null,
chainId: null,
});
},
switchNetwork: async (chainId: number) => {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${chainId.toString(16)}` }],
});
} catch (error: any) {
// Chain not added, try to add it
if (error.code === 4902) {
const chainConfig = CHAIN_CONFIGS[chainId];
if (chainConfig) {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [chainConfig],
});
}
}
throw error;
}
},
}));
// src/lib/web3/contracts.ts
import { Contract, parseEther, formatEther } from 'ethers';
import StakingABI from '../abis/Staking.json';
import TokenABI from '../abis/Token.json';
const CONTRACTS = {
mainnet: {
staking: '0x...',
token: '0x...',
},
sepolia: {
staking: '0x...',
token: '0x...',
},
};
export function useStakingContract() {
const { signer, chainId } = useWeb3Store();
if (!signer || !chainId) return null;
const network = chainId === 1 ? 'mainnet' : 'sepolia';
const address = CONTRACTS[network]?.staking;
if (!address) return null;
return new Contract(address, StakingABI, signer);
}
// Hooks for contract interactions
export function useStake() {
const contract = useStakingContract();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const stake = async (amount: string) => {
if (!contract) throw new Error('Contract not available');
setIsLoading(true);
setError(null);
try {
const amountWei = parseEther(amount);
// First approve the staking contract
const tokenContract = useTokenContract();
const approveTx = await tokenContract.approve(
await contract.getAddress(),
amountWei
);
await approveTx.wait();
// Then stake
const stakeTx = await contract.stake(amountWei);
await stakeTx.wait();
return stakeTx.hash;
} catch (err) {
const message = err instanceof Error ? err.message : 'Stake failed';
setError(message);
throw err;
} finally {
setIsLoading(false);
}
};
return { stake, isLoading, error };
}
export function useEarned(address: string) {
const contract = useStakingContract();
const [earned, setEarned] = useState<string>('0');
useEffect(() => {
if (!contract || !address) return;
const fetchEarned = async () => {
const earnedWei = await contract.earned(address);
setEarned(formatEther(earnedWei));
};
fetchEarned();
const interval = setInterval(fetchEarned, 10000); // Update every 10s
return () => clearInterval(interval);
}, [contract, address]);
return earned;
}
Security Patterns
// contracts/security/SafeContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title SafeContract
* @dev Demonstrates common security patterns
*/
contract SafeContract is ReentrancyGuard, Pausable, Ownable {
mapping(address => uint256) public balances;
mapping(address => bool) public authorized;
// Events for transparency
event Deposit(address indexed user, uint256 amount);
event Withdrawal(address indexed user, uint256 amount);
// Check-Effects-Interactions pattern
function withdraw(uint256 amount) external nonReentrant whenNotPaused {
// Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
require(amount > 0, "Amount must be positive");
// Effects (update state before external call)
balances[msg.sender] -= amount;
// Interactions (external call last)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawal(msg.sender, amount);
}
// Pull over push pattern
function claimRewards() external nonReentrant {
uint256 reward = calculateReward(msg.sender);
require(reward > 0, "No rewards to claim");
// Update state first
lastClaimTime[msg.sender] = block.timestamp;
// Then transfer
(bool success, ) = msg.sender.call{value: reward}("");
require(success, "Reward transfer failed");
}
// Rate limiting
mapping(address => uint256) public lastActionTime;
uint256 public constant MIN_ACTION_DELAY = 1 hours;
modifier rateLimited() {
require(
block.timestamp >= lastActionTime[msg.sender] + MIN_ACTION_DELAY,
"Action rate limited"
);
lastActionTime[msg.sender] = block.timestamp;
_;
}
// Access control with timelock
mapping(bytes32 => uint256) public timelocks;
uint256 public constant TIMELOCK_DELAY = 2 days;
function proposeAction(bytes32 actionId) external onlyOwner {
timelocks[actionId] = block.timestamp + TIMELOCK_DELAY;
}
function executeAction(bytes32 actionId) external onlyOwner {
require(timelocks[actionId] != 0, "Action not proposed");
require(block.timestamp >= timelocks[actionId], "Timelock not expired");
delete timelocks[actionId];
// Execute action...
}
// Emergency stop
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// Receive ETH
receive() external payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
}
Testing
// test/Staking.test.ts
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { time, loadFixture } from '@nomicfoundation/hardhat-network-helpers';
describe('Staking', function () {
async function deployFixture() {
const [owner, user1, user2] = await ethers.getSigners();
// Deploy tokens
const Token = await ethers.getContractFactory('MyToken');
const stakingToken = await Token.deploy(owner.address);
const rewardToken = await Token.deploy(owner.address);
// Deploy staking contract
const Staking = await ethers.getContractFactory('Staking');
const rewardRate = ethers.parseEther('0.1'); // 0.1 tokens per second
const staking = await Staking.deploy(
await stakingToken.getAddress(),
await rewardToken.getAddress(),
rewardRate
);
// Mint tokens
await stakingToken.mint(user1.address, ethers.parseEther('10000'));
await rewardToken.mint(await staking.getAddress(), ethers.parseEther('1000000'));
return { staking, stakingToken, rewardToken, owner, user1, user2 };
}
describe('Staking', function () {
it('should allow staking tokens', async function () {
const { staking, stakingToken, user1 } = await loadFixture(deployFixture);
const amount = ethers.parseEther('100');
await stakingToken.connect(user1).approve(await staking.getAddress(), amount);
await staking.connect(user1).stake(amount);
expect(await staking.stakedBalance(user1.address)).to.equal(amount);
expect(await staking.totalStaked()).to.equal(amount);
});
it('should accrue rewards over time', async function () {
const { staking, stakingToken, user1 } = await loadFixture(deployFixture);
const amount = ethers.parseEther('100');
await stakingToken.connect(user1).approve(await staking.getAddress(), amount);
await staking.connect(user1).stake(amount);
// Fast forward 1 day
await time.increase(86400);
const earned = await staking.earned(user1.address);
expect(earned).to.be.gt(0);
});
it('should prevent withdrawal before minimum duration', async function () {
const { staking, stakingToken, user1 } = await loadFixture(deployFixture);
const amount = ethers.parseEther('100');
await stakingToken.connect(user1).approve(await staking.getAddress(), amount);
await staking.connect(user1).stake(amount);
await expect(
staking.connect(user1).withdraw(amount)
).to.be.revertedWith('Minimum stake duration not met');
});
it('should allow withdrawal after minimum duration', async function () {
const { staking, stakingToken, user1 } = await loadFixture(deployFixture);
const amount = ethers.parseEther('100');
await stakingToken.connect(user1).approve(await staking.getAddress(), amount);
await staking.connect(user1).stake(amount);
// Fast forward 7 days
await time.increase(7 * 86400);
await staking.connect(user1).withdraw(amount);
expect(await staking.stakedBalance(user1.address)).to.equal(0);
});
});
describe('Security', function () {
it('should prevent reentrancy', async function () {
// Test reentrancy protection
});
it('should pause/unpause correctly', async function () {
const { staking, stakingToken, owner, user1 } = await loadFixture(deployFixture);
const amount = ethers.parseEther('100');
await staking.connect(owner).pause();
await stakingToken.connect(user1).approve(await staking.getAddress(), amount);
await expect(
staking.connect(user1).stake(amount)
).to.be.revertedWith('Pausable: paused');
await staking.connect(owner).unpause();
await staking.connect(user1).stake(amount);
});
});
});
Usage Examples
Create ERC20 Token
Apply blockchain-development-patterns skill to create ERC20 token with minting, burning, and access control
Build Staking Contract
Apply blockchain-development-patterns skill to implement staking contract with rewards distribution
Web3 Frontend
Apply blockchain-development-patterns skill to integrate Web3 wallet connection with React hooks
Success Output
When successful, this skill MUST output:
✅ SKILL COMPLETE: blockchain-development-patterns
Completed:
- [x] Smart contract implemented with security patterns
- [x] Web3 integration tested with MetaMask
- [x] Test suite passing with >80% coverage
- [x] Gas optimization validated
Outputs:
- contracts/Token.sol (ERC20 token with access control)
- contracts/Staking.sol (Staking contract with rewards)
- src/lib/web3/provider.ts (Web3 provider hooks)
- test/Staking.test.ts (Contract test suite)
Completion Checklist
Before marking this skill as complete, verify:
- Smart contracts compiled without errors
- All OpenZeppelin imports resolved
- ReentrancyGuard applied to state-changing functions
- Access control modifiers implemented
- Events emitted for all state changes
- Test suite includes security test cases
- Web3 provider handles network switching
- MetaMask connection tested in browser
- Gas estimates within acceptable limits
- All contract interactions properly typed
Failure Indicators
This skill has FAILED if:
- ❌ Solidity compilation errors unresolved
- ❌ Reentrancy vulnerabilities detected in contracts
- ❌ Web3 provider fails to connect to MetaMask
- ❌ Test suite has failing tests
- ❌ Gas costs exceed network limits
- ❌ Contract deployment reverts
- ❌ Missing access control on privileged functions
- ❌ Events not emitted for state changes
When NOT to Use
Do NOT use this skill when:
- Building traditional web2 applications (use
backend-api-patternsinstead) - Working with centralized databases only (use
database-design-patternsinstead) - Simple data storage needs without decentralization (use
backend-api-security-patternsinstead) - Project has no cryptocurrency or token requirements
- Team lacks blockchain security expertise (engage security audit first)
- Gas costs are prohibitive for use case
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| No ReentrancyGuard | Vulnerable to reentrancy attacks | Always use OpenZeppelin's ReentrancyGuard |
| State changes after external calls | Check-Effects-Interactions violation | Update state before external calls |
| Missing access control | Unauthorized users can call privileged functions | Use Ownable or AccessControl |
| No event emissions | Difficult to track contract state changes | Emit events for all state changes |
| Hardcoded addresses | Not portable across networks | Use constructor parameters or config |
| Skipping test coverage | Bugs reach production | Require >80% test coverage |
| No gas optimization | High transaction costs | Use gas reporter and optimize loops |
| Missing pause mechanism | Cannot stop contract in emergency | Implement Pausable pattern |
Principles
This skill embodies:
- #2 Security First - ReentrancyGuard, access control, and pausable patterns prevent common vulnerabilities
- #3 Separation of Concerns - Token, staking, and frontend logic clearly separated
- #5 Eliminate Ambiguity - Explicit state management with clear function names
- #8 No Assumptions - All external calls protected, all inputs validated
- #10 Test Everything - Comprehensive test suite with fixtures and edge cases
Full Standard: CODITECT-STANDARD-AUTOMATION.md
Integration Points
- testing-strategies - Smart contract testing
- backend-api-security-patterns - API for blockchain indexing
- frontend-react-patterns - Web3 UI components