Skip to main content

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

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Smart contracts, Web3 integration, DeFi patterns, and blockchain security.

Core Capabilities

  1. Smart Contracts - Solidity development patterns
  2. Web3 Integration - Frontend connection to blockchain
  3. DeFi Patterns - Token, staking, lending protocols
  4. Security - Common vulnerabilities and prevention
  5. 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-patterns instead)
  • Working with centralized databases only (use database-design-patterns instead)
  • Simple data storage needs without decentralization (use backend-api-security-patterns instead)
  • 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-PatternProblemSolution
No ReentrancyGuardVulnerable to reentrancy attacksAlways use OpenZeppelin's ReentrancyGuard
State changes after external callsCheck-Effects-Interactions violationUpdate state before external calls
Missing access controlUnauthorized users can call privileged functionsUse Ownable or AccessControl
No event emissionsDifficult to track contract state changesEmit events for all state changes
Hardcoded addressesNot portable across networksUse constructor parameters or config
Skipping test coverageBugs reach productionRequire >80% test coverage
No gas optimizationHigh transaction costsUse gas reporter and optimize loops
Missing pause mechanismCannot stop contract in emergencyImplement 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