Zum Hauptinhalt springen

Blockchain- und Web3-Entwicklung in Cursor: Solidity, Hardhat und ethers.js

Blockchain- und Web3-Entwicklung in Cursor

Blockchain-Entwicklung kombiniert Smart-Contract-Programmierung mit Frontend-Integration, um dezentrale Anwendungen (dApps) zu erstellen. Cursor, ein KI-gestützter IDE-Editor, beschleunigt jede Schicht der Web3-Entwicklung: Schreiben und Prüfen von Solidity-Contracts, Konfigurieren von Hardhat-Toolchains, Generieren von ethers.js-Interaktionsschichten, Erstellen von wagmi-basierten React-Frontends und Ausführen umfassender Test-Suiten. Dieser Leitfaden behandelt Solidity-Smart-Contracts, die Hardhat-Entwicklungsumgebung, ethers.js- und wagmi-Integration, Contract-Testing-Strategien und KI-gestützte Contract-Generierung und Sicherheitsprüfung.

Voraussetzungen​

Stellen Sie vor dem Start sicher, dass Sie Folgendes haben:

  • Cursor (aktuellste Version empfohlen) als IDE-Editor
  • Node.js 18 oder höher
  • Grundlegendes Verständnis von JavaScript, TypeScript und React
  • MetaMask oder ein anderes Web3-Wallet in Ihrem Browser installiert
  • Test-ETH auf einem Testnet (Sepolia oder Holesky) für Deployments-Übung
tipp

Blockchain-Entwicklung hat eine steile Lernkurve. Bevor Sie Ihren ersten Contract schreiben, bitten Sie Cursor, die Ethereum Virtual Machine (EVM), Gas-Kosten und den Transaktions-Lebenszyklus zu erklären. Das Verständnis dieser Grundlagen verhindert kostspielige Fehler bei Deployments auf dem Mainnet.

Solidity-Smart-Contracts​

Solidity ist die am weitesten verbreitete Sprache zum Schreiben von Smart-Contracts auf EVM-kompatiblen Blockchains. Cursor hilft Ihnen, Solidity-Code mit KI-gestützten Vorschlägen und Sicherheitsanalysen zu schreiben, zu prüfen und zu optimieren.

Einen Token-Contract schreiben​

Bitten Sie Cursor, einen ERC-20-Token-Contract zu generieren:

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 generiert:

// 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);
}
}
tipp

Verwenden Sie immer die geprüften Contract-Bibliotheken von OpenZeppelin, anstatt kryptografische oder Token-Standard-Logik von Grund auf zu schreiben. Bitten Sie Cursor, die neuesten OpenZeppelin-Contracts zu importieren und zu überprüfen, ob die Version zu Ihrer Solidity-Compiler-Version passt. Nicht übereinstimmende Versionen verursachen Kompilierungsfehler und können subtile Sicherheitsfehler einführen.

warnung

Der Schutz vor Solidity-Integer-Overflow wurde in Version 0.8.0 geändert. In Versionen vor 0.8.0 konnten arithmetische Operationen stillschweigend überlaufen, was zu katastrophalen Schwachstellen führte. Wenn Sie alte Contracts prüfen, bitten Sie Cursor, die Verwendung von SafeMath zu überprüfen und alle Arithmetik zu markieren, die sich auf Pre-0.8.0-Verhalten verlässt.

Die Hardhat-Entwicklungsumgebung​

Hardhat ist die beliebteste Entwicklungsumgebung für Ethereum. Sie bietet lokale Blockchain-Simulation, Contract-Kompilierung, Testing, Deployment-Skripte und Plugin-Erweiterbarkeit.

Projekt-Setup​

Bitten Sie Cursor, ein Hardhat-Projekt zu scaffolden:

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 generiert die Projektstruktur:

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

Hardhat-Konfiguration​

// 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;

Deployment-Skript​

// 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;
});
tipp

Bitten Sie Cursor, Deployment-Skripte für mehrere Netzwerke mit automatischer Contract-Verifizierung auf Etherscan zu generieren. Ein Prompt wie „Erstelle ein Deploy-Skript, das das Netzwerk erkennt, den Contract deployt und bei Nicht-Lokalhost auf Etherscan verifiziert" spart Stunden manueller Verifizierungsarbeit.

Contract-Testing​

Das Testen von Smart-Contracts ist kritisch, da deployte Contracts unveränderlich sind. Cursor generiert umfassende Test-Suiten, die Happy Paths, Edge Cases und Sicherheitsszenarien abdecken.

Tests mit Hardhat und Chai schreiben​

// 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');
});
});
});
tipp

Bitten Sie Cursor, Fuzzing-Tests zu generieren, die Eingaben randomisieren. Ein Prompt wie „Erstelle einen Fuzz-Test, der Minting mit zufälligen Beträgen zwischen 0 und 100.000 versucht, um Edge Cases in der Per-Transaction-Cap- und Total-Supply-Cap-Logik zu finden" kann Overflow- und Boundary-Condition-Fehler aufdecken, die Tests mit festen Werten übersehen.

warnung

Deployen Sie niemals einen Contract ohne volle Testabdeckung. Unveränderliche Contracts können nach dem Deployment nicht gepatched werden. Ein einzelner übersehener Edge Case kann zu dauerhaftem Geldverlust führen. Führen Sie Ihre vollständige Test-Suite aus und bitten Sie Cursor zusätzlich, den Contract auf häufige Schwachstellenmuster zu prüfen, vor jedem Mainnet-Deployment.

ethers.js-Integration​

ethers.js ist die beliebteste JavaScript-Bibliothek für die Interaktion mit Ethereum. Cursor generiert typsichere Interaktionsschichten, die Ihre Contract-ABIs wrappen.

Contract-Interaktionsschicht​

// 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 mit einem Browser-Wallet verwenden​

// 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);
}
}
warnung

Überprüfen Sie immer, ob der Benutzer mit dem erwarteten Netzwerk verbunden ist, bevor Sie Transaktionen senden. Das Senden einer Mainnet-Transaktion während der Verbindung mit einem Testnet oder umgekehrt verursacht verwirrende Fehler. Prüfen Sie die Chain-ID und fordern Sie den Benutzer auf, das Netzwerk zu wechseln, wenn es nicht übereinstimmt.

Ein wagmi-Frontend erstellen​

wagmi ist eine Sammlung von React-Hooks für Ethereum, die Wallet-Verbindung, Contract-Reads und Contract-Writes vereinfacht. Cursor generiert wagmi-basierte React-Komponenten mit voller Typsicherheit.

wagmi-Konfiguration​

// 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;

Token-Dashboard-Komponente​

// 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>
);
}
tipp

Bitten Sie Cursor, einen wagmi-Read-Hook für jede View-Funktion in Ihrem Contract und einen Write-Hook für jede zustandsverändernde Funktion zu generieren. Ein Prompt wie „Erstelle benutzerdefinierte React-Hooks, die wagmi useReadContract und useWriteContract für jede Funktion in meinem TutorialToken-ABI wrappen" gibt Ihnen eine saubere, wiederverwendbare Interaktionsschicht.

KI-gestützte Contract-Generierung und Prüfung​

Cursor KI kann gesamte Contract-Systeme aus einer Beschreibung generieren und bestehende Contracts auf häufige Schwachstellenmuster prüfen.

Generierung eines vollständigen Contract-Systems​

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.

Prüfung auf Schwachstellen​

Führen Sie diesen Prüfungsprompt gegen Ihren Contract aus:

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.
warnung

KI-gestützte Sicherheitsprüfungen sind eine erste Verteidigungslinie, kein Ersatz für professionelle Audits. Bevor Sie einen Contract deployen, der echten Wert verwaltet, beauftragen Sie einen formalen Audit von einer reputablen Firma. KI kann subtile Schwachstellenmuster übersehen, die erfahrene Auditoren erkennen.

Zusammenfassung​

Blockchain- und Web3-Entwicklung in Cursor kombiniert die Kraft KI-gestützter Code-Generierung mit der Strenge des Smart-Contract-Engineerings. Mit den Mustern dieses Leitfadens — OpenZeppelin-basierte Contracts, umfassende Hardhat-Test-Suiten, typsichere ethers.js-Interaktionsschichten, wagmi-basierte React-Frontends und KI-gesteuerte Contract-Prüfung — können Sie dezentrale Anwendungen zuversichtlich bauen und ausliefern. Testen Sie immer erschöpfend, prüfen Sie auf Schwachstellen vor dem Mainnet-Deployment, und deployen Sie niemals Contracts, die Sie nicht vollständig verstehen.