Aller au contenu principal

Développement blockchain et Web3 dans Cursor : Solidity, Hardhat et ethers.js

Développement blockchain et Web3 dans Cursor

Le développement blockchain combine la programmation de contrats intelligents avec l'intégration frontend pour créer des applications décentralisées (dApps). Cursor, un éditeur IDE alimenté par IA, accélère chaque couche du développement Web3 : écriture et audit de contrats Solidity, configuration des chaînes d'outils Hardhat, génération de couches d'interaction ethers.js, construction de frontends React propulsés par wagmi et exécution de suites de tests complètes. Ce guide couvre les contrats intelligents Solidity, l'environnement de développement Hardhat, l'intégration ethers.js et wagmi, les stratégies de test de contrats, et la génération de contrats et l'audit de sécurité assistés par IA.

Prérequis​

Avant de commencer, assurez-vous de disposer des éléments suivants :

  • Cursor (version la plus récente recommandée) comme éditeur IDE
  • Node.js 18 ou supérieur
  • Une compréhension de base de JavaScript, TypeScript et React
  • MetaMask ou un autre portefeuille Web3 installé dans votre navigateur
  • Des ETH de test sur un testnet (Sepolia ou Holesky) pour la pratique de déploiement
astuce

Le développement blockchain a une courbe d'apprentissage raide. Avant d'écrire votre premier contrat, demandez à Cursor d'expliquer la Machine Virtuelle Ethereum (EVM), les coûts de gas et le cycle de vie des transactions. Comprendre ces fondamentaux évite des erreurs coûteuses lors du déploiement sur le mainnet.

Contrats intelligents Solidity​

Solidity est le langage le plus largement utilisé pour écrire des contrats intelligents sur les blockchains compatibles EVM. Cursor vous aide à écrire, réviser et optimiser le code Solidity avec des suggestions et des analyses de sécurité alimentées par IA.

Écriture d'un contrat de jeton​

Demandez à Cursor de générer un contrat de jeton 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 génère :

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

Utilisez toujours les bibliothèques de contrats auditées d'OpenZeppelin au lieu d'écrire la logique cryptographique ou de standard de jeton à partir de zéro. Demandez à Cursor d'importer les derniers contrats OpenZeppelin et de vérifier que la version correspond à votre version de compilateur Solidity. Les versions inadéquates provoquent des erreurs de compilation et peuvent introduire des bugs de sécurité subtils.

attention

La protection contre le dépassement d'entier Solidity a changé dans la version 0.8.0. Dans les versions antérieures à 0.8.0, les opérations arithmétiques pouvaient silencieusement déborder, entraînant des vulnérabilités catastrophiques. Si vous révisez d'anciens contrats, demandez à Cursor de vérifier l'utilisation de SafeMath et de signaler toute arithmétique reposant sur le comportement pré-0.8.0.

L'environnement de développement Hardhat​

Hardhat est l'environnement de développement le plus populaire pour Ethereum. Il fournit une simulation de blockchain locale, la compilation de contrats, les tests, les scripts de déploiement et l'extensibilité via plugins.

Configuration du projet​

Demandez à Cursor d'échafauder un projet 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 génère la structure du projet :

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

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

Script de déploiement​

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

Demandez à Cursor de générer des scripts de déploiement pour plusieurs réseaux avec vérification automatique des contrats sur Etherscan. Une invite comme « Créez un script de déploiement qui détecte le réseau, déploie le contrat et appelle verify sur Etherscan si pas sur localhost » fait économiser des heures de travail de vérification manuelle.

Tests de contrats​

Tester les contrats intelligents est critique car les contrats déployés sont immuables. Cursor génère des suites de tests complètes qui couvrent les chemins nominaux, les cas limites et les scénarios de sécurité.

Écriture de tests avec Hardhat et 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');
});
});
});
astuce

Demandez à Cursor de générer des tests de fuzzing qui randomisent les entrées. Une invite comme « Créez un test de fuzz qui tente de frapper avec des montants aléatoires entre 0 et 100 000 pour trouver les cas limites dans la logique de plafond par transaction et de plafond d'approvisionnement total » peut révéler des bugs de dépassement et de condition limite que les tests à valeur fixe manquent.

attention

Ne déployez jamais un contrat sans une couverture de tests complète. Les contrats immuables ne peuvent pas être corrigés après déploiement. Un seul cas limite manqué peut entraîner une perte permanente de fonds. Exécutez votre suite de tests complète et demandez également à Cursor d'auditer le contrat pour les modèles de vulnérabilités courants avant tout déploiement sur le mainnet.

Intégration ethers.js​

ethers.js est la bibliothèque JavaScript la plus populaire pour interagir avec Ethereum. Cursor génère des couches d'interaction sécurisées par typage qui encapsulent vos ABI de contrats.

Couche d'interaction de contrat​

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

Utilisation d'ethers.js avec un portefeuille de navigateur​

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

Vérifiez toujours que l'utilisateur est connecté au réseau attendu avant d'envoyer des transactions. Envoyer une transaction mainnet tout en étant connecté à un testnet, ou vice versa, provoque des erreurs confuses. Vérifiez l'ID de chaîne et invitez l'utilisateur à changer de réseau s'il ne correspond pas.

Construction d'un frontend wagmi​

wagmi est une collection de hooks React pour Ethereum qui simplifie la connexion du portefeuille, les lectures de contrats et les écritures de contrats. Cursor génère des composants React propulsés par wagmi avec une sécurité de type complète.

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

Composant tableau de bord des jetons​

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

Demandez à Cursor de générer un hook wagmi Read pour chaque fonction view de votre contrat et un hook Write pour chaque fonction modifiant l'état. Une invite comme « Créez des hooks React personnalisés encapsulant wagmi useReadContract et useWriteContract pour chaque fonction de mon ABI TutorialToken » vous donne une couche d'interaction propre et réutilisable.

Génération et audit de contrats assistés par IA​

L'IA de Cursor peut générer des systèmes de contrats entiers à partir d'une description et auditer les contrats existants pour les modèles de vulnérabilités courantes.

Génération d'un système de contrats complet​

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 des vulnérabilités​

Exécutez cette invite d'audit contre votre contrat :

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

Les audits de sécurité assistés par IA sont une première ligne de défense, pas un remplacement des audits professionnels. Avant de déployer tout contrat qui gère une valeur réelle, commandez un audit formel auprès d'une entreprise réputée. L'IA peut manquer des modèles de vulnérabilités subtils que des auditeurs expérimentés détectent.

Résumé​

Le développement blockchain et Web3 dans Cursor combine la puissance de la génération de code assistée par IA avec la rigueur de l'ingénierie de contrats intelligents. En suivant les modèles de ce guide — contrats basés sur OpenZeppelin, suites de tests Hardhat complètes, couches d'interaction ethers.js sécurisées par typage, frontends React propulsés par wagmi et audit de contrats piloté par IA — vous pouvez créer et livrer des applications décentralisées en toute confiance. Testez toujours de manière exhaustive, auditez les vulnérabilités avant le déploiement sur le mainnet, et ne déployez jamais de contrats que vous ne comprenez pas pleinement.