メインコンテンツまでスキップ

Cursor でのブロックチェーンと Web3 開発:Solidity、Hardhat、ethers.js

Cursor でのブロックチェーンと Web3 開発

ブロックチェーン開発は、スマートコントラクトプログラミングとフロントエンド統合を組み合わせて分散型アプリ(dApp)を作成します。Cursor は AI 駆動の IDE エディターとして、Solidity コントラクトの作成と監査、Hardhat ツールチェーンの設定、ethers.js インタラクションレイヤーの生成、wagmi による React フロントエンドの構築、包括的なテストスイートの実行など、Web3 開発のすべての層を加速します。本ガイドでは、Solidity スマートコントラクト、Hardhat 開発環境、ethers.js と wagmi の統合、コントラクトテスト戦略、AI 支援によるコントラクト生成とセキュリティ監査をカバーします。

前提条件​

開始する前に、以下の条件を満たしていることを確認してください:

  • Cursor(最新バージョン推奨)を IDE エディターとして使用
  • Node.js 18 以上
  • JavaScript、TypeScript、React の基本的な理解
  • ブラウザに MetaMask またはその他の Web3 ウォレットがインストール済み
  • デプロイ練習用のテストネット(Sepolia または Holesky)のテスト ETH
ヒント

ブロックチェーン開発は学習曲線が急です。最初のコントラクトを書く前に、Cursor に Ethereum Virtual Machine(EVM)、ガスコスト、トランザクションライフサイクルの説明を依頼してください。これらの基礎を理解することで、メインネットへのデプロイ時にコストのかかる間違いを防ぐことができます。

Solidity スマートコントラクト​

Solidity は EVM 互換ブロックチェーン上でスマートコントラクトを記述するために最も広く使用されている言語です。Cursor は AI 駆動の提案とセキュリティ分析により、Solidity コードの作成、レビュー、最適化を支援します。

トークンコントラクトの作成​

Cursor に ERC-20 トークンコントラクトの生成を依頼します:

Create an ERC-20 token contract in Solidity using OpenZeppelin.
The token should be named "TutorialToken" with symbol "TUT",
18 decimals, and a total supply of 1,000,000 tokens minted to
the deployer. Include a public minting function with a per-transaction
cap and an emergency pause mechanism.

Cursor が生成します:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract TutorialToken is ERC20, AccessControl, Pausable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

// Maximum mint per transaction: 10,000 tokens (with 18 decimals)
uint256 public constant MAX_MINT_PER_TX = 10_000 * 10 ** 18;
// Total supply cap: 1,000,000 tokens
uint256 public constant TOTAL_SUPPLY_CAP = 1_000_000 * 10 ** 18;
uint256 public currentSupply;

event Minted(address indexed to, uint256 amount);
event EmergencyPaused(address indexed by);

constructor() ERC20("TutorialToken", "TUT") {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);

uint256 initialSupply = 1_000_000 * 10 ** 18;
_mint(msg.sender, initialSupply);
currentSupply = initialSupply;
}

function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) whenNotPaused {
require(amount <= MAX_MINT_PER_TX, "Exceeds max mint per transaction");
require(currentSupply + amount <= TOTAL_SUPPLY_CAP, "Exceeds total supply cap");

_mint(to, amount);
currentSupply += amount;

emit Minted(to, amount);
}

function pause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_pause();
emit EmergencyPaused(msg.sender);
}

function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}

// Override required by Solidity for multiple inheritance
function _update(address from, address to, uint256 amount) internal override(ERC20, Pausable) {
Pausable._update(from, to, amount);
}

function supportsInterface(bytes4 interfaceId)
public view override(AccessControl) returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
ヒント

暗号ロジックやトークン標準ロジックをゼロから書く代わりに、常に OpenZeppelin の監査済みコントラクトライブラリを使用してください。Cursor に最新の OpenZeppelin コントラクトをインポートし、バージョンが Solidity コンパイラのバージョンと一致することを確認するよう依頼してください。バージョンの不一致はコンパイルエラーを引き起こし、微妙なセキュリティバグを招く可能性があります。

警告

Solidity の整数オーバーフロー保護はバージョン 0.8.0 で変更されました。0.8.0 より前のバージョンでは、算術演算がサイレントにオーバーフローする可能性があり、致命的な脆弱性につながりました。古いコントラクトをレビューする場合は、Cursor に SafeMath の使用を確認し、0.8.0 以前の動作に依存する算術演算をフラグ付けするよう依頼してください。

Hardhat 開発環境​

Hardhat は Ethereum 向けの最も人気のある開発環境です。ローカルブロックチェーンシミュレーション、コントラクトコンパイル、テスト、デプロイスクリプト、プラグイン拡張性を提供します。

プロジェクトセットアップ​

Cursor に Hardhat プロジェクトのスキャフォールディングを依頼します:

Create a Hardhat TypeScript project with the following configuration:
- Solidity compiler version 0.8.24
- Hardhat toolbox plugin for testing and deployment
- A deployment script for the TutorialToken contract
- Environment variables for RPC URL and private key
- TypeChain for TypeScript contract bindings

Cursor はプロジェクト構造を生成します:

my-dapp/
contracts/
TutorialToken.sol
scripts/
deploy.ts
test/
TutorialToken.test.ts
hardhat.config.ts
package.json
.env.example

Hardhat 設定​

// hardhat.config.ts
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import * as dotenv from 'dotenv';

dotenv.config();

const config: HardhatUserConfig = {
solidity: {
version: '0.8.24',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
hardhat: {
chainId: 31337,
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || '',
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
mainnet: {
url: process.env.MAINNET_RPC_URL || '',
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
typechain: {
target: 'ethers-v6',
outDir: 'typechain-types',
},
gasReporter: {
enabled: true,
currency: 'USD',
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
token: 'ETH',
},
};

export default config;

デプロイスクリプト​

// scripts/deploy.ts
import { ethers } from 'hardhat';

async function main() {
const [deployer] = await ethers.getSigners();
console.log('Deploying contracts with account:', deployer.address);
console.log('Account balance:', ethers.formatEther(await ethers.provider.getBalance(deployer.address)), 'ETH');

const TutorialToken = await ethers.getContractFactory('TutorialToken');
const token = await TutorialToken.deploy();
await token.waitForDeployment();

const tokenAddress = await token.getAddress();
console.log('TutorialToken deployed to:', tokenAddress);

// Verify the deployment
const name = await token.name();
const symbol = await token.symbol();
const totalSupply = await token.totalSupply();
console.log('Name:', name);
console.log('Symbol:', symbol);
console.log('Total Supply:', ethers.formatEther(totalSupply));
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
ヒント

Cursor に Etherscan での自動コントラクト検証を含む複数ネットワーク向けデプロイスクリプトの生成を依頼してください。「ネットワークを検出し、コントラクトをデプロイし、localhost 以外の場合は Etherscan で verify を呼び出すデプロイスクリプトを作成して」というプロンプトで、手動検証作業の時間を節約できます。

コントラクトテスト​

デプロイされたコントラクトは不変であるため、スマートコントラクトのテストは非常に重要です。Cursor は正常パス、エッジケース、セキュリティシナリオをカバーする包括的なテストスイートを生成します。

Hardhat と Chai によるテスト​

// test/TutorialToken.test.ts
import { expect } from 'chai';
import { ethers, network } from 'hardhat';
import { TutorialToken } from '../typechain-types';
import { SignerWithAddress } from '@nomicfoundation/hardhat-network-signers';

describe('TutorialToken', () => {
let token: TutorialToken;
let owner: SignerWithAddress;
let minter: SignerWithAddress;
let alice: SignerWithAddress;
let bob: SignerWithAddress;

const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('MINTER_ROLE'));
const MAX_MINT = ethers.parseEther('10000');
const TOTAL_CAP = ethers.parseEther('1000000');

beforeEach(async () => {
[owner, minter, alice, bob] = await ethers.getSigners();

const TutorialToken = await ethers.getContractFactory('TutorialToken');
token = await TutorialToken.deploy();
await token.waitForDeployment();
});

describe('Deployment', () => {
it('sets the correct name and symbol', async () => {
expect(await token.name()).to.equal('TutorialToken');
expect(await token.symbol()).to.equal('TUT');
});

it('mints the total supply to the deployer', async () => {
const balance = await token.balanceOf(owner.address);
expect(balance).to.equal(TOTAL_CAP);
});

it('grants DEFAULT_ADMIN_ROLE and MINTER_ROLE to deployer', async () => {
expect(await token.hasRole(ethers.ZeroHash, owner.address)).to.be.true;
expect(await token.hasRole(MINTER_ROLE, owner.address)).to.be.true;
});
});

describe('Minting', () => {
beforeEach(async () => {
await token.grantRole(MINTER_ROLE, minter.address);
});

it('allows minter to mint within per-transaction cap', async () => {
const mintAmount = ethers.parseEther('5000');
await token.connect(minter).mint(alice.address, mintAmount);
expect(await token.balanceOf(alice.address)).to.equal(mintAmount);
});

it('reverts when minting exceeds per-transaction cap', async () => {
const mintAmount = ethers.parseEther('10001');
await expect(
token.connect(minter).mint(alice.address, mintAmount)
).to.be.revertedWith('Exceeds max mint per transaction');
});

it('reverts when non-minter tries to mint', async () => {
await expect(
token.connect(alice).mint(bob.address, ethers.parseEther('100'))
).to.be.revertedWithCustomError(token, 'AccessControlUnauthorizedAccount');
});

it('reverts when minting exceeds total supply cap', async () => {
// Already minted 1,000,000 on deployment, so any additional mint should fail
await expect(
token.connect(minter).mint(alice.address, ethers.parseEther('1'))
).to.be.revertedWith('Exceeds total supply cap');
});
});

describe('Pause Mechanism', () => {
it('allows admin to pause and unpause', async () => {
await token.pause();
expect(await token.paused()).to.be.true;

await token.unpause();
expect(await token.paused()).to.be.false;
});

it('reverts minting when paused', async () => {
await token.grantRole(MINTER_ROLE, minter.address);
await token.pause();

await expect(
token.connect(minter).mint(alice.address, ethers.parseEther('100'))
).to.be.revertedWithCustomError(token, 'EnforcedPause');
});

it('reverts when non-admin tries to pause', async () => {
await expect(token.connect(alice).pause())
.to.be.revertedWithCustomError(token, 'AccessControlUnauthorizedAccount');
});
});

describe('Transfers', () => {
it('allows token transfers and reverts when paused', async () => {
const amount = ethers.parseEther('100');
await token.transfer(alice.address, amount);
expect(await token.balanceOf(alice.address)).to.equal(amount);

// Now pause and verify transfer is blocked
await token.pause();
await expect(token.transfer(alice.address, amount))
.to.be.revertedWithCustomError(token, 'EnforcedPause');
});
});
});
ヒント

Cursor に入力をランダム化するファジングテストの生成を依頼してください。「0 から 100,000 の間のランダム量でミントを試行し、1トランザクション上限と総供給上限ロジックのエッジケースを見つけるファズテストを作成して」というプロンプトで、固定値テストが見逃すオーバーフローや境界条件のバグを発見できます。

警告

完全なテストカバレッジなしにコントラクトをデプロイしないでください。不変のコントラクトはデプロイ後にパッチできません。見逃した1つのエッジケースが永続的な資金損失につながる可能性があります。フルテストスイートを実行し、メインネットデプロイ前に Cursor に一般的な脆弱性パターンのコントラクト監査を依頼してください。

ethers.js 統合​

ethers.js は Ethereum との対話に最も人気のある JavaScript ライブラリです。Cursor はコントラクト ABI をラップする型安全なインタラクションレイヤーを生成します。

コントラクトインタラクションレイヤー​

// src/lib/token.ts
import { ethers, Contract, formatEther, parseEther } from 'ethers';
import type { TutorialToken } from '../typechain-types';

export interface TokenInfo {
name: string;
symbol: string;
decimals: number;
totalSupply: string;
}

export class TokenService {
private contract: TutorialToken;
private provider: ethers.Provider;
private signer?: ethers.Signer;

constructor(
address: string,
abi: ethers.InterfaceAbi,
provider: ethers.Provider,
signer?: ethers.Signer
) {
this.provider = provider;
this.signer = signer;
this.contract = (signer
? new Contract(address, abi, signer)
: new Contract(address, abi, provider)) as unknown as TutorialToken;
}

async getInfo(): Promise<TokenInfo> {
const [name, symbol, decimals, totalSupply] = await Promise.all([
this.contract.name(),
this.contract.symbol(),
this.contract.decimals(),
this.contract.totalSupply(),
]);

return {
name,
symbol,
decimals: Number(decimals),
totalSupply: formatEther(totalSupply),
};
}

async balanceOf(address: string): Promise<string> {
const balance = await this.contract.balanceOf(address);
return formatEther(balance);
}

async mint(to: string, amount: string): Promise<string> {
if (!this.signer) throw new Error('Signer required for minting');
const tx = await this.contract.mint(to, parseEther(amount));
await tx.wait();
return tx.hash;
}

async transfer(to: string, amount: string): Promise<string> {
if (!this.signer) throw new Error('Signer required for transfers');
const tx = await this.contract.transfer(to, parseEther(amount));
await tx.wait();
return tx.hash;
}

async isPaused(): Promise<boolean> {
return this.contract.paused();
}

async hasMinterRole(address: string): Promise<boolean> {
const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('MINTER_ROLE'));
return this.contract.hasRole(MINTER_ROLE, address);
}
}

ブラウザウォレットとの ethers.js 使用​

// src/lib/wallet.ts
import { BrowserProvider } from 'ethers';

let provider: BrowserProvider | null = null;

export async function getProvider(): Promise<BrowserProvider> {
if (!provider) {
if (!window.ethereum) {
throw new Error('No Web3 wallet found. Please install MetaMask.');
}
provider = new BrowserProvider(window.ethereum);
}
return provider;
}

export async function connectWallet(): Promise<string> {
const provider = await getProvider();
const accounts = await provider.send('eth_requestAccounts', []);
return accounts[0];
}

export async function getSigner() {
const provider = await getProvider();
return provider.getSigner();
}

export function onAccountsChanged(callback: (accounts: string[]) => void): void {
if (window.ethereum) {
window.ethereum.on('accountsChanged', callback);
}
}

export function onChainChanged(callback: (chainId: string) => void): void {
if (window.ethereum) {
window.ethereum.on('chainChanged', callback);
}
}
警告

トランザクションを送信する前に、ユーザーが期待するネットワークに接続していることを常に確認してください。テストネットに接続中にメインネットトランザクションを送信(またはその逆)すると、不可解なエラーが発生します。チェーン ID を確認し、一致しない場合はネットワークの切り替えをユーザーに促してください。

wagmi フロントエンドの構築​

wagmi は Ethereum 向けの React Hooks のコレクションで、ウォレット接続、コントラクト読み取り、コントラクト書き込みを簡素化します。Cursor は完全な型安全性を持つ wagmi ベースの React コンポーネントを生成します。

wagmi 設定​

// src/lib/wagmi.ts
import { createConfig, http } from 'wagmi';
import { sepolia, mainnet } from 'wagmi/chains';
import { injected } from 'wagmi/connectors';

export const config = createConfig({
chains: [sepolia, mainnet],
connectors: [injected()],
transports: {
[sepolia.id]: http('https://rpc.sepolia.org'),
[mainnet.id]: http(),
},
});

// Contract address and ABI
export const TOKEN_ADDRESS = '0x...'; // Replace with your deployed address
export const TOKEN_ABI = [
// ABI generated by TypeChain or hardhat
] as const;

トークンダッシュボードコンポーネント​

// src/components/TokenDashboard.tsx
import { useAccount, useReadContract, useWriteContract, useWatchContractEvent } from 'wagmi';
import { parseEther, formatEther } from 'viem';
import { TOKEN_ADDRESS, TOKEN_ABI } from '../lib/wagmi';
import { useState } from 'react';

export function TokenDashboard() {
const { address, isConnected } = useAccount();
const { writeContractAsync } = useWriteContract();
const [mintAmount, setMintAmount] = useState('100');
const [transferTo, setTransferTo] = useState('');
const [transferAmount, setTransferAmount] = useState('10');

// Batch-read contract state
const { data: name } = useReadContract({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, functionName: 'name' });
const { data: symbol } = useReadContract({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, functionName: 'symbol' });
const { data: balance } = useReadContract({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, functionName: 'balanceOf', args: [address] });
const { data: isPaused } = useReadContract({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, functionName: 'paused' });

// Listen for mint events
useWatchContractEvent({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, eventName: 'Minted', onLogs: (logs) => console.log('Mint:', logs) });

const handleMint = () => writeContractAsync({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, functionName: 'mint', args: [address, parseEther(mintAmount)] });
const handleTransfer = () => writeContractAsync({ address: TOKEN_ADDRESS, abi: TOKEN_ABI, functionName: 'transfer', args: [transferTo, parseEther(transferAmount)] });

if (!isConnected) {
return <div className="p-8 text-center text-gray-500">Connect your wallet to view your token balance.</div>;
}

return (
<div className="space-y-6 p-4">
<div className="rounded-lg bg-gray-100 p-4">
<h2 className="text-xl font-bold">{name} ({symbol})</h2>
<p className="mt-1 text-sm">Balance: {balance ? formatEther(balance as bigint) : '0'} {symbol}</p>
<p className="text-sm">Status: {isPaused ? 'Paused' : 'Active'}</p>
</div>

<div className="rounded-lg border p-4">
<h3 className="mb-2 font-semibold">Mint Tokens</h3>
<input type="number" value={mintAmount} onChange={(e) => setMintAmount(e.target.value)} className="mb-2 w-full rounded border px-3 py-2" disabled={isPaused} />
<button onClick={handleMint} disabled={isPaused} className="w-full rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50">Mint</button>
</div>

<div className="rounded-lg border p-4">
<h3 className="mb-2 font-semibold">Transfer Tokens</h3>
<input type="text" placeholder="Recipient address (0x...)" value={transferTo} onChange={(e) => setTransferTo(e.target.value)} className="mb-2 w-full rounded border px-3 py-2" />
<input type="number" placeholder="Amount" value={transferAmount} onChange={(e) => setTransferAmount(e.target.value)} className="mb-2 w-full rounded border px-3 py-2" disabled={isPaused} />
<button onClick={handleTransfer} disabled={isPaused} className="w-full rounded bg-green-600 px-4 py-2 text-white disabled:opacity-50">Transfer</button>
</div>
</div>
);
}
ヒント

Cursor にコントラクトの各 view 関数の wagmi Read hook と、各状態変更関数の Write hook の生成を依頼してください。「TutorialToken ABI の各関数で wagmi useReadContract と useWriteContract をラップするカスタム React hooks を作成して」というプロンプトで、クリーンで再利用可能なインタラクションレイヤーが得られます。

AI 支援によるコントラクト生成と監査​

Cursor の AI は、説明からコントラクトシステム全体を生成し、既存のコントラクトの一般的な脆弱性パターンを監査できます。

完全なコントラクトシステムの生成​

Create a decentralized marketplace contract system with:
1. An ERC-721 NFT contract for listing items
2. A marketplace contract that accepts ETH and the NFT
3. Royalty enforcement using ERC-2981
4. A 2.5% platform fee sent to a treasury address
5. Reentrancy protection on all withdrawal functions
6. Events for every state change
Use OpenZeppelin libraries and include full NatSpec comments.

脆弱性の監査​

コントラクトに対してこの監査プロンプトを実行します:

Audit my Solidity contract for the following vulnerability classes:
1. Reentrancy attacks (check for CEI pattern and ReentrancyGuard)
2. Integer overflow/underflow
3. Access control bypass
4. Front-running susceptibility
5. Denial of service via gas limit
6. Timestamp dependence
7. Delegate call injection
8. Unchecked external calls
Rate the severity of each finding and provide a fix.
警告

AI 支援のセキュリティ監査は第一線の防御であり、専門監査の代わりにはなりません。実際の価値を扱うコントラクトをデプロイする前に、信頼できる企業に正式な監査を依頼してください。AI は経験豊富な監査人が見つける微妙な脆弱性パターンを見逃す可能性があります。

まとめ​

Cursor でのブロックチェーンと Web3 開発は、AI 支援によるコード生成の力とスマートコントラクトエンジニアリングの厳格さを組み合わせます。本ガイドのパターンに従うことで、OpenZeppelin ベースのコントラクト、包括的な Hardhat テストスイート、型安全な ethers.js インタラクションレイヤー、wagmi による React フロントエンド、AI 駆動のコントラクト監査により、自信を持って分散型アプリを構築・出荷できます。常に徹底的にテストし、メインネットデプロイ前に脆弱性を監査し、完全に理解していないコントラクトは絶対にデプロイしないでください。